Inspired by HNKServer/honoka-chan-apk-server, but implemented as a JNI-based embedded runtime instead of the original approach. - Add an Android Compose controller app that starts and stops honoka-chan through JNI, shows runtime and health status, manages data directory mounting, and supports backup import/export for data.db - Add an embeddable Go server entrypoint plus JNI-exported status, health, and reload hooks so desktop and Android builds share the same startup and shutdown path - Add Android build scripts, runtime packaging, and project documentation, including generated default config.json content for honoka_runtime.zip - Add runtime bundle hash tracking so updated honoka_runtime.zip assets are automatically redeployed while preserving config.json and user data files - Add in-app service settings for unlock_all_special_rotation with immediate config persistence and automatic reload when the service is running - Split SQLite driver selection by platform, using go-sqlite3 on Android and modernc.org/sqlite elsewhere to avoid Android x86_64 seccomp crashes - Update startup, database initialization, and system health/reload handlers to support the embedded runtime and Android control flow Signed-off-by: Sean Du <do4suki@gmail.com>
140 lines
3.3 KiB
Go
140 lines
3.3 KiB
Go
package system
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"honoka-chan/config"
|
|
"honoka-chan/internal/router"
|
|
systemschema "honoka-chan/internal/schema/system"
|
|
"honoka-chan/pkg/db"
|
|
"net/http"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
var (
|
|
startedAt = time.Now()
|
|
lastReloadAt atomic.Int64
|
|
)
|
|
|
|
const dateTimeFormat = "2006-01-02 15:04:05"
|
|
|
|
func MarkStarted(t time.Time) {
|
|
startedAt = t
|
|
}
|
|
|
|
func Health() (systemschema.HealthResp, int) {
|
|
appName := config.Conf.AppName
|
|
if appName == "" {
|
|
appName = "honoka-chan"
|
|
}
|
|
|
|
mainDBStatus := "not initialized"
|
|
if db.MainEng != nil {
|
|
mainDBStatus = "ok"
|
|
if err := db.MainEng.Ping(); err != nil {
|
|
mainDBStatus = err.Error()
|
|
}
|
|
}
|
|
|
|
userDBStatus := "not initialized"
|
|
if db.UserEng != nil {
|
|
userDBStatus = "ok"
|
|
if err := db.UserEng.Ping(); err != nil {
|
|
userDBStatus = err.Error()
|
|
}
|
|
}
|
|
|
|
status := "ok"
|
|
statusCode := http.StatusOK
|
|
if db.MainEng == nil || db.UserEng == nil {
|
|
status = "stopped"
|
|
}
|
|
|
|
resp := systemschema.HealthResp{
|
|
Status: status,
|
|
AppName: appName,
|
|
Version: config.PackageVersion,
|
|
StartedAt: startedAt.Format(dateTimeFormat),
|
|
UptimeSeconds: int64(time.Since(startedAt).Seconds()),
|
|
ListenPort: config.Conf.Settings.ListenPort,
|
|
CdnServer: config.Conf.Settings.CdnServer,
|
|
ReloadTokenConfigured: strings.TrimSpace(config.Conf.Settings.ReloadToken) != "",
|
|
MainDB: mainDBStatus,
|
|
UserDB: userDBStatus,
|
|
}
|
|
|
|
if ts := lastReloadAt.Load(); ts > 0 {
|
|
resp.LastReloadAt = time.Unix(ts, 0).Format(dateTimeFormat)
|
|
}
|
|
|
|
if status == "ok" && (mainDBStatus != "ok" || userDBStatus != "ok") {
|
|
resp.Status = "degraded"
|
|
statusCode = http.StatusServiceUnavailable
|
|
}
|
|
|
|
return resp, statusCode
|
|
}
|
|
|
|
func HealthJSON() string {
|
|
resp, _ := Health()
|
|
data, err := json.Marshal(resp)
|
|
if err != nil {
|
|
return `{"status":"error","app_name":"honoka-chan"}`
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
func Reload(token string, requireToken bool) (systemschema.ReloadResp, int, error) {
|
|
expectedToken := strings.TrimSpace(config.Conf.Settings.ReloadToken)
|
|
if requireToken && expectedToken == "" {
|
|
return systemschema.ReloadResp{
|
|
Status: "error",
|
|
Message: "reload_token is not configured",
|
|
}, http.StatusServiceUnavailable, errors.New("reload_token is not configured")
|
|
}
|
|
|
|
if requireToken && strings.TrimSpace(token) != expectedToken {
|
|
return systemschema.ReloadResp{
|
|
Status: "error",
|
|
Message: "invalid reload token",
|
|
}, http.StatusUnauthorized, errors.New("invalid reload token")
|
|
}
|
|
|
|
config.ReloadConfig()
|
|
now := time.Now()
|
|
lastReloadAt.Store(now.Unix())
|
|
|
|
return systemschema.ReloadResp{
|
|
Status: "ok",
|
|
Message: "configuration reloaded",
|
|
ReloadedAt: now.Format(dateTimeFormat),
|
|
}, http.StatusOK, nil
|
|
}
|
|
|
|
func health(ctx *gin.Context) {
|
|
resp, statusCode := Health()
|
|
ctx.JSON(statusCode, resp)
|
|
}
|
|
|
|
func reload(ctx *gin.Context) {
|
|
token := strings.TrimSpace(ctx.GetHeader("X-Reload-Token"))
|
|
if token == "" {
|
|
token = strings.TrimSpace(ctx.Query("token"))
|
|
}
|
|
if token == "" {
|
|
token = strings.TrimSpace(ctx.PostForm("token"))
|
|
}
|
|
|
|
resp, statusCode, _ := Reload(token, true)
|
|
ctx.JSON(statusCode, resp)
|
|
}
|
|
|
|
func init() {
|
|
router.AddHandler("/", "GET", "/system/health", health)
|
|
router.AddHandler("/", "POST", "/system/reload", reload)
|
|
}
|