Add Android control app and embedded server
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>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user