package main import ( "encoding/json" "fmt" "github.com/gogf/gf/v2/errors/gerror" "github.com/towgo/towgo/lib/system" "github.com/towgo/towgo/lib/www" "go.uber.org/zap" "log" "net/http" "os" "os/exec" "os/user" "path/filepath" "strings" "tgk-touch/internal/core" g "tgk-touch/internal/global" "tgk-touch/internal/initialize" "tgk-touch/internal/module/maincontrollerClient" "time" "github.com/towgo/towgo/errors/tcode" "github.com/towgo/towgo/towgo" ) var ( productNumber string = "LampServer" ) const firefoxUserPrefs = `// Managed by tgk-touch kiosk startup. Do not edit by hand. user_pref("app.normandy.first_run", false); user_pref("browser.aboutConfig.showWarning", false); user_pref("browser.crashReports.unsubmittedCheck.autoSubmit2", false); user_pref("browser.crashReports.unsubmittedCheck.enabled", false); user_pref("browser.disableResetPrompt", true); user_pref("browser.download.panel.shown", true); user_pref("browser.rights.3.shown", true); user_pref("browser.sessionstore.max_resumed_crashes", 0); user_pref("browser.sessionstore.resume_from_crash", false); user_pref("browser.shell.checkDefaultBrowser", false); user_pref("browser.startup.homepage", "about:blank"); user_pref("browser.startup.page", 0); user_pref("browser.tabs.warnOnClose", false); user_pref("browser.warnOnQuitShortcut", false); user_pref("datareporting.healthreport.uploadEnabled", false); user_pref("datareporting.policy.dataSubmissionEnabled", false); user_pref("signon.autofillForms", false); user_pref("signon.rememberSignons", false); user_pref("toolkit.startup.max_resumed_crashes", 999999); user_pref("toolkit.telemetry.reportingpolicy.firstRun", false); ` const kioskLogPrefix = "[touch-kiosk]" func kioskInfof(format string, args ...interface{}) { g.Log().Infof(kioskLogPrefix+" "+format, args...) } func kioskDebugf(format string, args ...interface{}) { g.Log().Debugf(kioskLogPrefix+" "+format, args...) } func kioskWarnf(format string, args ...interface{}) { g.Log().Warnf(kioskLogPrefix+" "+format, args...) } func kioskErrorf(format string, args ...interface{}) { g.Log().Errorf(kioskLogPrefix+" "+format, args...) } func main() { log.SetFlags(log.Lshortfile | log.LstdFlags) defer func() { if exception := recover(); exception != nil { if v, ok := exception.(error); ok && gerror.HasStack(v) { zap.S().Errorf("err %+v \n", v) } else { zap.S().Errorf("recover exception %+v\n", gerror.NewCodef(tcode.CodeInternalPanic, "%+v", exception)) } } }() appInit() start() } func appInit() { initialize.Init() kioskInfof("app initialized") //initLicense() } /* func initLicense() { licenseterminal.SetProductNumber(productNumber) licenseterminal.AutoFristActive() licenseterminal.RegExpirationCall(productNumber, func() { log.Println("产品:" + productNumber + "许可证到期") list := []string{ "/account/login", "/license/activecode/get", "/license/activecode/online/request", "/license/getActiveCredential", "/license/active", "/license/list", "/license/accesscode/get", } jsonrpc.MethodLockAll(list...) }) licenseterminal.RegReNewCall(productNumber, func() { log.Println("产品:" + productNumber + "许可证续存") jsonrpc.MethodUnlockAll() }) }*/ func start() { kioskInfof("service start begin") var err error var serialPortAddress string = g.Config().Tty.SerialPortAddress if serialPortAddress == "" { kioskErrorf("tty serialPortAddress is empty") panic(gerror.Wrap(err, "串口地址未配置 tty.serialPortAddress")) } var baudRateint int = g.Config().Tty.BaudRate if baudRateint == 0 { zap.S().Info("波特率未配置 tty.baudRate , 使用 默认 9600") baudRateint = 9600 } kioskInfof("tty config loaded serial=%s baud=%d", serialPortAddress, baudRateint) towgo.SetFunc("/getMessageInterval", getMessageInterval) err = maincontrollerClient.UseSerialPort(serialPortAddress, baudRateint) if err != nil { kioskErrorf("serial port start failed serial=%s baud=%d err=%v", serialPortAddress, baudRateint, err) panic(gerror.Wrap(err, "串口启动失败")) } kioskInfof("serial port started serial=%s baud=%d", serialPortAddress, baudRateint) webServer := www.WebServer{} webServer.Wwwroot = system.GetPathOfProgram() + "wwwroot" webServer.Index = []string{"index.html"} kioskInfof("web server configured wwwroot=%s index=%v", webServer.Wwwroot, webServer.Index) g.HttpServerMux().HandleFunc("/", webServer.WebServerHandller, ) g.HttpServerMux().HandleFunc("/jsonrpc/websocket", towgo.DefaultWebSocketServer.WebsocketServiceHandller.ServeHTTP, ) g.HttpServerMux().HandleFunc("/jsonrpc", towgo.HttpHandller) towgo.DefaultExec = func(rpcConn towgo.JsonRpcConnection) { if exception := recover(); exception != nil { var msg string if v, ok := exception.(error); ok && gerror.HasStack(v) { g.Log().Error("towgo jsonrpc exception \n", v) msg = v.Error() } else { g.Log().Error("towgo jsonrpc recover exception \n", gerror.NewCodef(tcode.CodeInternalPanic, "%+v", exception)) msg = v.Error() } rpcConn.WriteError(500, rpcConn.GetRpcRequest().Method+":"+msg) } } kioskInfof("install checks begin") install() kioskInfof("install checks done; starting firefox client goroutine port=%d", g.Config().Server.Port) go runClient(g.Config().Server.Port) core.RunServer() } func getMessageInterval(rpcConn towgo.JsonRpcConnection) { rpcConn.WriteResult(g.Config().MessageInterval) } func install() { kioskInfof("checking firefox usability") // 1. 检查并修复 Firefox if !isFirefoxUsable() { kioskWarnf("firefox unusable; starting install/repair") fmt.Println("Firefox missing or unusable, installing/repairing...") if err := installFirefox(); err != nil { kioskErrorf("firefox install/repair failed err=%v", err) fmt.Printf("install/repair Firefox failed: %v\n", err) os.Exit(1) } kioskInfof("firefox install/repair success") fmt.Println("Firefox install/repair success") } else { kioskInfof("firefox is usable") fmt.Println("Firefox is installed and usable") } // 2. 设置当前应用开机自启动 appPath, err := os.Executable() if err != nil { kioskErrorf("read executable path failed err=%v", err) fmt.Printf("读取应用路径失败: %v\n", err) os.Exit(1) } kioskInfof("current executable path=%s workdir=%s", appPath, filepath.Dir(appPath)) if !isAutoStartEnabled(appPath) { kioskWarnf("autostart service missing or mismatched; reinstalling service=%s", autoStartServiceFile(appPath)) fmt.Println("正在设置开机自启动...") if err := enableAutoStart(appPath); err != nil { kioskErrorf("autostart setup failed service=%s err=%v", autoStartServiceFile(appPath), err) fmt.Printf("设置开机自启动失败: %v\n", err) os.Exit(1) } fmt.Println("开机自启动设置成功") } else { fmt.Println("已设置开机自启动") } } func runClient(port int) { // 配置显示器和Xauthority路径 display := ":0" kioskInfof("firefox client loop init display=%s port=%d", display, port) currentUser, err := user.Current() if err != nil { kioskErrorf("read current user failed err=%v", err) fmt.Printf("读取当前用户失败: %v\n", err) os.Exit(1) } profileDir := filepath.Join(currentUser.HomeDir, ".tgk-touch-firefox-profile") clientURL := "http://127.0.0.1:" + fmt.Sprintf("%d", port) remoteDebugPort := port + 10000 kioskInfof("firefox client config user=%s home=%s profile=%s url=%s remoteDebugPort=%d", currentUser.Username, currentUser.HomeDir, profileDir, clientURL, remoteDebugPort) for { // 设置环境变量 os.Setenv("DISPLAY", display) if xauthPath := findXAuthorityPath(currentUser, display); xauthPath != "" { os.Setenv("XAUTHORITY", xauthPath) kioskInfof("xauthority selected path=%s", xauthPath) } else { os.Unsetenv("XAUTHORITY") kioskWarnf("xauthority not found; XAUTHORITY unset") } if err := prepareFirefoxKioskProfile(profileDir); err != nil { kioskErrorf("prepare firefox profile failed profile=%s err=%v", profileDir, err) time.Sleep(5 * time.Second) continue } // 使用独立 profile,避免断电后默认 profile 的崩溃恢复状态影响 kiosk 启动。 if err := closeExistingFirefox(); err != nil { kioskErrorf("close existing firefox failed err=%v", err) } args := []string{ "--new-instance", "--profile", profileDir, "--remote-debugging-port", fmt.Sprintf("%d", remoteDebugPort), "--kiosk", clientURL, } // 启动Firefox cmd := exec.Command("firefox", args...) cmd.Env = firefoxKioskEnv() cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr kioskInfof("starting firefox args=%v", args) if err := runFirefoxWithWatchdog(cmd, clientURL, remoteDebugPort); err != nil { kioskErrorf("firefox watchdog returned err=%v", err) if resetErr := resetFirefoxKioskProfile(profileDir); resetErr != nil { kioskErrorf("reset firefox profile after failure failed profile=%s err=%v", profileDir, resetErr) } time.Sleep(5 * time.Second) // 等待后重试 continue } } } func findXAuthorityPath(currentUser *user.User, display string) string { kioskDebugf("xauthority search begin display=%s", display) candidates := []string{ os.Getenv("XAUTHORITY"), filepath.Join("/var/run/lightdm/root", display), filepath.Join("/run/lightdm/root", display), } if currentUser != nil && currentUser.HomeDir != "" { candidates = append(candidates, filepath.Join(currentUser.HomeDir, ".Xauthority")) } seen := make(map[string]struct{}, len(candidates)) for _, candidate := range candidates { if candidate == "" { continue } if _, ok := seen[candidate]; ok { continue } seen[candidate] = struct{}{} if isUsableXAuthority(candidate) { kioskInfof("xauthority candidate usable path=%s", candidate) return candidate } kioskDebugf("xauthority candidate unusable path=%s", candidate) } for _, pattern := range []string{ "/run/user/*/gdm/Xauthority", "/run/user/*/.Xauthority", "/run/user/*/Xauthority", "/home/*/.Xauthority", "/root/.Xauthority", "/var/lib/lightdm/.Xauthority", "/var/lib/gdm/.Xauthority", "/var/lib/gdm3/.Xauthority", "/tmp/xauth_*", "/tmp/.Xauthority*", } { if match := newestUsableXAuthority(pattern); match != "" { kioskInfof("xauthority glob matched pattern=%s path=%s", pattern, match) return match } kioskDebugf("xauthority glob no usable match pattern=%s", pattern) } kioskWarnf("xauthority search exhausted display=%s", display) return "" } func newestUsableXAuthority(pattern string) string { matches, err := filepath.Glob(pattern) if err != nil { kioskDebugf("xauthority glob invalid pattern=%s err=%v", pattern, err) return "" } kioskDebugf("xauthority glob pattern=%s matches=%d", pattern, len(matches)) var newestPath string var newestModTime time.Time for _, match := range matches { info, err := os.Stat(match) if err != nil || info.IsDir() || info.Size() == 0 { continue } if newestPath == "" || info.ModTime().After(newestModTime) { newestPath = match newestModTime = info.ModTime() } } return newestPath } func isUsableXAuthority(path string) bool { info, err := os.Stat(path) return err == nil && !info.IsDir() && info.Size() > 0 } func closeExistingFirefox() error { names := []string{"firefox", "firefox-esr", "firefox-bin"} kioskInfof("closing existing firefox processes names=%v", names) for _, name := range names { err := exec.Command("pkill", "-TERM", "-x", name).Run() kioskDebugf("pkill TERM name=%s err=%v", name, err) } deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { if !isAnyProcessRunning(names) { kioskInfof("existing firefox processes closed after TERM") return nil } kioskDebugf("waiting firefox processes to exit after TERM") time.Sleep(500 * time.Millisecond) } kioskWarnf("firefox still running after TERM; sending KILL") for _, name := range names { err := exec.Command("pkill", "-KILL", "-x", name).Run() kioskDebugf("pkill KILL name=%s err=%v", name, err) } for i := 0; i < 10; i++ { if !isAnyProcessRunning(names) { kioskInfof("existing firefox processes closed after KILL") return nil } kioskDebugf("waiting firefox processes to exit after KILL attempt=%d", i+1) time.Sleep(200 * time.Millisecond) } kioskErrorf("firefox process still running after kill") return fmt.Errorf("firefox process still running after kill") } func isAnyProcessRunning(names []string) bool { for _, name := range names { if exec.Command("pgrep", "-x", name).Run() == nil { kioskDebugf("process running name=%s", name) return true } } return false } func runFirefoxWithWatchdog(cmd *exec.Cmd, clientURL string, remoteDebugPort int) error { kioskInfof("firefox process start begin url=%s remoteDebugPort=%d", clientURL, remoteDebugPort) if err := cmd.Start(); err != nil { kioskErrorf("firefox process start failed err=%v", err) return err } if cmd.Process != nil { kioskInfof("firefox process started pid=%d", cmd.Process.Pid) } done := make(chan error, 1) go func() { done <- cmd.Wait() }() startedAt := time.Now() ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for { select { case err := <-done: kioskWarnf("firefox process exited err=%v", err) return err case <-ticker.C: healthy, err := isFirefoxKioskHealthy(clientURL, remoteDebugPort) if err != nil && time.Since(startedAt) < 30*time.Second { kioskDebugf("firefox health check pending during startup grace elapsed=%s err=%v", time.Since(startedAt).Round(time.Second), err) continue } if err != nil || !healthy { if err == nil { err = fmt.Errorf("firefox is not showing kiosk page in fullscreen") } kioskWarnf("firefox health check failed elapsed=%s healthy=%v err=%v; closing firefox", time.Since(startedAt).Round(time.Second), healthy, err) if closeErr := closeExistingFirefox(); closeErr != nil { kioskErrorf("close firefox after health failure failed err=%v", closeErr) } select { case waitErr := <-done: if waitErr != nil { kioskWarnf("firefox stopped after health failure waitErr=%v", waitErr) return fmt.Errorf("%w; firefox stopped: %v", err, waitErr) } kioskWarnf("firefox stopped after health failure") return err case <-time.After(5 * time.Second): kioskWarnf("firefox did not exit within watchdog close timeout") return err } } kioskDebugf("firefox health check ok elapsed=%s", time.Since(startedAt).Round(time.Second)) } } } func isFirefoxKioskHealthy(clientURL string, remoteDebugPort int) (bool, error) { hasKioskURL, err := firefoxHasKioskURL(clientURL, remoteDebugPort) if err != nil || !hasKioskURL { kioskWarnf("firefox kiosk url check failed hasKioskURL=%v err=%v", hasKioskURL, err) return false, err } fullscreen, err := firefoxHasFullscreenWindow() if err != nil { kioskWarnf("firefox fullscreen check errored err=%v", err) return false, err } if !fullscreen { kioskWarnf("firefox fullscreen check failed") } return fullscreen, nil } type firefoxDebugTarget struct { Type string `json:"type"` URL string `json:"url"` } func firefoxHasKioskURL(clientURL string, remoteDebugPort int) (bool, error) { httpClient := http.Client{Timeout: 2 * time.Second} debugURL := fmt.Sprintf("http://127.0.0.1:%d/json/list", remoteDebugPort) kioskDebugf("firefox remote debug request url=%s", debugURL) resp, err := httpClient.Get(debugURL) if err != nil { kioskDebugf("firefox remote debug request failed url=%s err=%v", debugURL, err) return false, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { kioskWarnf("firefox remote debug bad status url=%s status=%d", debugURL, resp.StatusCode) return false, fmt.Errorf("firefox remote debugging returned status %d", resp.StatusCode) } var targets []firefoxDebugTarget if err := json.NewDecoder(resp.Body).Decode(&targets); err != nil { kioskWarnf("firefox remote debug json decode failed url=%s err=%v", debugURL, err) return false, err } kioskDebugf("firefox remote debug targets=%d expectedURL=%s", len(targets), clientURL) for _, target := range targets { kioskDebugf("firefox target type=%s url=%s", target.Type, target.URL) if target.Type == "page" && isKioskURL(target.URL, clientURL) { kioskDebugf("firefox kiosk url matched targetURL=%s", target.URL) return true, nil } } kioskWarnf("firefox kiosk url not found expectedURL=%s targets=%d", clientURL, len(targets)) return false, nil } func isKioskURL(rawURL string, clientURL string) bool { base := strings.TrimRight(clientURL, "/") url := strings.TrimSpace(rawURL) return url == base || strings.HasPrefix(url, base+"/") || strings.HasPrefix(url, base+"#") || strings.HasPrefix(url, base+"?") } func firefoxHasFullscreenWindow() (bool, error) { if !isCommandAvailable("xdotool") || !isCommandAvailable("xprop") { kioskDebugf("fullscreen check skipped because xdotool/xprop unavailable") return true, nil } windowIDs := make(map[string]struct{}) for _, className := range []string{"firefox", "firefox-esr", "Navigator"} { out, err := exec.Command("xdotool", "search", "--onlyvisible", "--class", className).Output() if err != nil { kioskDebugf("xdotool search failed class=%s err=%v", className, err) continue } for _, id := range strings.Fields(string(out)) { windowIDs[id] = struct{}{} } } if len(windowIDs) == 0 { kioskWarnf("fullscreen check found no visible firefox windows") return false, nil } kioskDebugf("fullscreen check candidate windows=%d", len(windowIDs)) for id := range windowIDs { out, err := exec.Command("xprop", "-id", id, "_NET_WM_STATE").Output() if err != nil { kioskDebugf("xprop failed window=%s err=%v", id, err) continue } if strings.Contains(string(out), "_NET_WM_STATE_FULLSCREEN") { kioskDebugf("fullscreen window matched window=%s", id) return true, nil } } kioskWarnf("no firefox window is fullscreen") return false, nil } func isCommandAvailable(name string) bool { ok := exec.Command("which", name).Run() == nil kioskDebugf("command availability name=%s ok=%v", name, ok) return ok } func firefoxKioskEnv() []string { env := make([]string, 0, len(os.Environ())+4) filteredKeys := make([]string, 0) for _, item := range os.Environ() { key, _, ok := strings.Cut(item, "=") if !ok { continue } if key == "MOZ_SAFE_MODE_RESTART" || key == "MOZ_CRASHREPORTER_RESTART_ARG_0" || strings.HasPrefix(key, "MOZ_CRASHREPORTER_RESTART_ARG_") { filteredKeys = append(filteredKeys, key) continue } env = append(env, item) } env = append(env, "MOZ_DISABLE_SAFE_MODE_KEY=1", "MOZ_CRASHREPORTER_DISABLE=1", "MOZ_CRASHREPORTER_NO_REPORT=1", ) kioskDebugf("firefox env prepared filteredKeys=%v addedKeys=%v total=%d", filteredKeys, []string{"MOZ_DISABLE_SAFE_MODE_KEY", "MOZ_CRASHREPORTER_DISABLE", "MOZ_CRASHREPORTER_NO_REPORT"}, len(env)) return env } // 准备 Firefox kiosk profile。 func prepareFirefoxKioskProfile(profileDir string) error { kioskInfof("prepare firefox kiosk profile begin profile=%s", profileDir) if err := os.MkdirAll(profileDir, 0755); err != nil { kioskErrorf("create firefox kiosk profile failed profile=%s err=%v", profileDir, err) return fmt.Errorf("create firefox kiosk profile: %w", err) } if err := os.WriteFile(filepath.Join(profileDir, "user.js"), []byte(firefoxUserPrefs), 0644); err != nil { kioskErrorf("write firefox kiosk prefs failed profile=%s err=%v", profileDir, err) return fmt.Errorf("write firefox kiosk prefs: %w", err) } kioskInfof("firefox kiosk prefs written profile=%s", profileDir) if err := cleanFirefoxRecoveryState(profileDir); err != nil { kioskErrorf("clean firefox recovery state failed profile=%s err=%v", profileDir, err) return fmt.Errorf("clean firefox recovery state: %w", err) } kioskInfof("prepare firefox kiosk profile done profile=%s", profileDir) return nil } func resetFirefoxKioskProfile(profileDir string) error { kioskWarnf("reset firefox kiosk profile begin profile=%s", profileDir) if profileDir == "" || filepath.Base(profileDir) != ".tgk-touch-firefox-profile" { kioskErrorf("refuse to reset unexpected firefox profile path=%s", profileDir) return fmt.Errorf("refuse to reset unexpected firefox profile path: %s", profileDir) } if err := os.RemoveAll(profileDir); err != nil { kioskErrorf("remove firefox kiosk profile failed profile=%s err=%v", profileDir, err) return fmt.Errorf("reset firefox kiosk profile: %w", err) } kioskInfof("firefox kiosk profile removed profile=%s", profileDir) return prepareFirefoxKioskProfile(profileDir) } func cleanFirefoxRecoveryState(profileDir string) error { kioskInfof("clean firefox recovery state begin profile=%s", profileDir) for _, name := range []string{ ".parentlock", ".startup-incomplete", "lock", "parent.lock", "sessionCheckpoints.json", "sessionstore.js", "sessionstore.jsonlz4", } { if err := removeIfExists(filepath.Join(profileDir, name)); err != nil { kioskErrorf("remove firefox recovery file failed profile=%s name=%s err=%v", profileDir, name, err) return err } } backupDir := filepath.Join(profileDir, "sessionstore-backups") entries, err := os.ReadDir(backupDir) if err != nil { if os.IsNotExist(err) { kioskDebugf("firefox session backup dir not found path=%s", backupDir) return nil } kioskErrorf("read firefox session backup dir failed path=%s err=%v", backupDir, err) return err } kioskDebugf("firefox session backup entries=%d path=%s", len(entries), backupDir) for _, entry := range entries { if entry.IsDir() { continue } if err := removeIfExists(filepath.Join(backupDir, entry.Name())); err != nil { kioskErrorf("remove firefox session backup failed path=%s name=%s err=%v", backupDir, entry.Name(), err) return err } } kioskInfof("clean firefox recovery state done profile=%s", profileDir) return nil } func removeIfExists(path string) error { err := os.Remove(path) if err == nil || os.IsNotExist(err) { if err == nil { kioskDebugf("removed file path=%s", path) } else { kioskDebugf("remove skipped; file not found path=%s", path) } return nil } kioskErrorf("remove file failed path=%s err=%v", path, err) return err } // 检查Firefox是否安装 func isFirefoxUsable() bool { cmd := exec.Command("firefox", "--version") out, err := cmd.CombinedOutput() ok := err == nil kioskInfof("firefox usability check ok=%v output=%q err=%v", ok, strings.TrimSpace(string(out)), err) return ok } func isFirefoxInstalled() bool { cmd := exec.Command("which", "firefox") out, err := cmd.CombinedOutput() ok := err == nil kioskDebugf("firefox install path check ok=%v output=%q err=%v", ok, strings.TrimSpace(string(out)), err) return ok } // 安装Firefox func installFirefox() error { var p struct { DebPkgPath string `json:"DebPkgPath"` DebPkgName string `json:"DebPkgName"` InstallShellPath string `json:"InstallShellPath"` } p.DebPkgPath = g.Config().Firefox.DebPkgPath p.DebPkgName = g.Config().Firefox.DebPkgName p.InstallShellPath = g.Config().Firefox.InstallShellPath kioskInfof("firefox install begin programPath=%s debPath=%s debName=%s installShell=%s", system.GetPathOfProgram(), p.DebPkgPath, p.DebPkgName, p.InstallShellPath) command := exec.Command("sudo", "sh", "-c", fmt.Sprintf(` echo "=== 当前工作目录 ===" pwd echo "" echo "=== 卸载旧版 Firefox ===" dpkg -P firefox || echo "警告:卸载旧版 Firefox 失败(可能未安装)" echo "" echo "=== 清理残留文件 ===" rm -rf /opt/firefox* 2>/dev/null echo "" echo "=== 开始安装 Firefox ===" echo "1. 复制安装包到 /opt 目录..." cp -v %s%s/%s /opt/firefox-deb.tar.gz || { echo "错误:复制安装包失败"; exit 1; } echo "" echo "2. 解压安装包..." cd /opt && tar -xzvf firefox-deb.tar.gz || { echo "错误:解压失败"; exit 1; } echo "" echo "3. 安装依赖和主程序..." cd /opt/firefox-deb && sudo dpkg -i *.deb || { echo "错误:安装 deb 包失败"; } echo "" echo "4. 验证安装..." firefox --version || { echo "错误:Firefox 未正确安装"; exit 1; } echo "" echo "=== Firefox 安装完成 ===" `, system.GetPathOfProgram(), p.DebPkgPath, p.DebPkgName)) // 将命令的 stdout/stderr 直接绑定到当前终端 command.Stdout = os.Stdout command.Stderr = os.Stderr err := command.Run() if err != nil { kioskErrorf("firefox install command failed err=%v", err) return err } kioskInfof("firefox install command finished successfully") return nil } func isAutoStartEnabled(appPath string) bool { serviceFile := autoStartServiceFile(appPath) kioskInfof("autostart check begin service=%s expectedExec=%s expectedWorkdir=%s", serviceFile, appPath, filepath.Dir(appPath)) content, err := os.ReadFile(serviceFile) if err != nil { kioskWarnf("autostart service read failed service=%s err=%v", serviceFile, err) return false } matches := autoStartServiceMatches(string(content), appPath) kioskInfof("autostart service match result service=%s matches=%v", serviceFile, matches) return matches } func enableAutoStart(appPath string) error { serviceFile := autoStartServiceFile(appPath) kioskInfof("autostart setup begin service=%s exec=%s workdir=%s", serviceFile, appPath, filepath.Dir(appPath)) // 创建systemd服务文件内容 content := autoStartServiceContent(appPath) kioskDebugf("autostart service content:\n%s", content) // 写入服务文件 cmd := exec.Command("sudo", "bash", "-c", fmt.Sprintf("echo '%s' > %s", content, serviceFile)) if err := cmd.Run(); err != nil { kioskErrorf("write autostart service failed service=%s err=%v", serviceFile, err) return err } kioskInfof("write autostart service success service=%s", serviceFile) // 重新加载systemd配置 cmd = exec.Command("sudo", "systemctl", "daemon-reload") if err := cmd.Run(); err != nil { kioskErrorf("systemctl daemon-reload failed err=%v", err) return err } kioskInfof("systemctl daemon-reload success") // 启用服务 cmd = exec.Command("sudo", "systemctl", "enable", autoStartServiceName(appPath)) if err := cmd.Run(); err != nil { kioskErrorf("systemctl enable failed service=%s err=%v", autoStartServiceName(appPath), err) return err } kioskInfof("systemctl enable success service=%s", autoStartServiceName(appPath)) return nil } func autoStartServiceName(appPath string) string { return strings.TrimSuffix(filepath.Base(appPath), filepath.Ext(appPath)) + ".service" } func autoStartServiceFile(appPath string) string { return filepath.Join("/etc/systemd/system", autoStartServiceName(appPath)) } func autoStartServiceContent(appPath string) string { appName := strings.TrimSuffix(filepath.Base(appPath), filepath.Ext(appPath)) return fmt.Sprintf(`[Unit] Description=%s Background Service After=network.target [Service] Type=simple ExecStart=%s Restart=on-failure RestartSec=10 User=root WorkingDirectory=%s [Install] WantedBy=multi-user.target `, appName, appPath, filepath.Dir(appPath)) } func autoStartServiceMatches(content string, appPath string) bool { execStart := normalizeSystemdValue(systemdUnitValue(content, "ExecStart")) workingDirectory := normalizeSystemdValue(systemdUnitValue(content, "WorkingDirectory")) expectedWorkdir := filepath.Dir(appPath) matches := execStart == appPath && workingDirectory == expectedWorkdir kioskInfof("autostart service fields execStart=%s expectedExec=%s workingDirectory=%s expectedWorkdir=%s matches=%v", execStart, appPath, workingDirectory, expectedWorkdir, matches) return matches } func systemdUnitValue(content string, key string) string { for _, line := range strings.Split(content, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") { continue } name, value, ok := strings.Cut(line, "=") if !ok || strings.TrimSpace(name) != key { continue } return strings.TrimSpace(value) } return "" } func normalizeSystemdValue(value string) string { return strings.Trim(strings.TrimSpace(value), `"'`) }