|
|
|
@ -1,13 +1,14 @@
|
|
|
|
package main
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
import (
|
|
|
|
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"fmt"
|
|
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
|
|
"github.com/towgo/towgo/lib/jsonrpc"
|
|
|
|
|
|
|
|
"github.com/towgo/towgo/lib/system"
|
|
|
|
"github.com/towgo/towgo/lib/system"
|
|
|
|
"github.com/towgo/towgo/lib/www"
|
|
|
|
"github.com/towgo/towgo/lib/www"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"log"
|
|
|
|
"log"
|
|
|
|
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"os/exec"
|
|
|
|
"os/user"
|
|
|
|
"os/user"
|
|
|
|
@ -16,7 +17,6 @@ import (
|
|
|
|
"tgk-touch/internal/core"
|
|
|
|
"tgk-touch/internal/core"
|
|
|
|
g "tgk-touch/internal/global"
|
|
|
|
g "tgk-touch/internal/global"
|
|
|
|
"tgk-touch/internal/initialize"
|
|
|
|
"tgk-touch/internal/initialize"
|
|
|
|
licenseterminal "tgk-touch/internal/module/license_terminal"
|
|
|
|
|
|
|
|
"tgk-touch/internal/module/maincontrollerClient"
|
|
|
|
"tgk-touch/internal/module/maincontrollerClient"
|
|
|
|
"time"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
@ -28,6 +28,47 @@ var (
|
|
|
|
productNumber string = "LampServer"
|
|
|
|
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() {
|
|
|
|
func main() {
|
|
|
|
log.SetFlags(log.Lshortfile | log.LstdFlags)
|
|
|
|
log.SetFlags(log.Lshortfile | log.LstdFlags)
|
|
|
|
defer func() {
|
|
|
|
defer func() {
|
|
|
|
@ -45,9 +86,10 @@ func main() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
func appInit() {
|
|
|
|
func appInit() {
|
|
|
|
initialize.Init()
|
|
|
|
initialize.Init()
|
|
|
|
initLicense()
|
|
|
|
kioskInfof("app initialized")
|
|
|
|
|
|
|
|
//initLicense()
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
} /*
|
|
|
|
func initLicense() {
|
|
|
|
func initLicense() {
|
|
|
|
licenseterminal.SetProductNumber(productNumber)
|
|
|
|
licenseterminal.SetProductNumber(productNumber)
|
|
|
|
licenseterminal.AutoFristActive()
|
|
|
|
licenseterminal.AutoFristActive()
|
|
|
|
@ -68,11 +110,13 @@ func initLicense() {
|
|
|
|
log.Println("产品:" + productNumber + "许可证续存")
|
|
|
|
log.Println("产品:" + productNumber + "许可证续存")
|
|
|
|
jsonrpc.MethodUnlockAll()
|
|
|
|
jsonrpc.MethodUnlockAll()
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}*/
|
|
|
|
func start() {
|
|
|
|
func start() {
|
|
|
|
|
|
|
|
kioskInfof("service start begin")
|
|
|
|
var err error
|
|
|
|
var err error
|
|
|
|
var serialPortAddress string = g.Config().Tty.SerialPortAddress
|
|
|
|
var serialPortAddress string = g.Config().Tty.SerialPortAddress
|
|
|
|
if serialPortAddress == "" {
|
|
|
|
if serialPortAddress == "" {
|
|
|
|
|
|
|
|
kioskErrorf("tty serialPortAddress is empty")
|
|
|
|
panic(gerror.Wrap(err, "串口地址未配置 tty.serialPortAddress"))
|
|
|
|
panic(gerror.Wrap(err, "串口地址未配置 tty.serialPortAddress"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var baudRateint int = g.Config().Tty.BaudRate
|
|
|
|
var baudRateint int = g.Config().Tty.BaudRate
|
|
|
|
@ -80,14 +124,18 @@ func start() {
|
|
|
|
zap.S().Info("波特率未配置 tty.baudRate , 使用 默认 9600")
|
|
|
|
zap.S().Info("波特率未配置 tty.baudRate , 使用 默认 9600")
|
|
|
|
baudRateint = 9600
|
|
|
|
baudRateint = 9600
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
kioskInfof("tty config loaded serial=%s baud=%d", serialPortAddress, baudRateint)
|
|
|
|
towgo.SetFunc("/getMessageInterval", getMessageInterval)
|
|
|
|
towgo.SetFunc("/getMessageInterval", getMessageInterval)
|
|
|
|
err = maincontrollerClient.UseSerialPort(serialPortAddress, baudRateint)
|
|
|
|
err = maincontrollerClient.UseSerialPort(serialPortAddress, baudRateint)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
kioskErrorf("serial port start failed serial=%s baud=%d err=%v", serialPortAddress, baudRateint, err)
|
|
|
|
panic(gerror.Wrap(err, "串口启动失败"))
|
|
|
|
panic(gerror.Wrap(err, "串口启动失败"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
kioskInfof("serial port started serial=%s baud=%d", serialPortAddress, baudRateint)
|
|
|
|
webServer := www.WebServer{}
|
|
|
|
webServer := www.WebServer{}
|
|
|
|
webServer.Wwwroot = system.GetPathOfProgram() + "wwwroot"
|
|
|
|
webServer.Wwwroot = system.GetPathOfProgram() + "wwwroot"
|
|
|
|
webServer.Index = []string{"index.html"}
|
|
|
|
webServer.Index = []string{"index.html"}
|
|
|
|
|
|
|
|
kioskInfof("web server configured wwwroot=%s index=%v", webServer.Wwwroot, webServer.Index)
|
|
|
|
g.HttpServerMux().HandleFunc("/",
|
|
|
|
g.HttpServerMux().HandleFunc("/",
|
|
|
|
webServer.WebServerHandller,
|
|
|
|
webServer.WebServerHandller,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
@ -110,7 +158,9 @@ func start() {
|
|
|
|
rpcConn.WriteError(500, rpcConn.GetRpcRequest().Method+":"+msg)
|
|
|
|
rpcConn.WriteError(500, rpcConn.GetRpcRequest().Method+":"+msg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
kioskInfof("install checks begin")
|
|
|
|
install()
|
|
|
|
install()
|
|
|
|
|
|
|
|
kioskInfof("install checks done; starting firefox client goroutine port=%d", g.Config().Server.Port)
|
|
|
|
go runClient(g.Config().Server.Port)
|
|
|
|
go runClient(g.Config().Server.Port)
|
|
|
|
core.RunServer()
|
|
|
|
core.RunServer()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -119,28 +169,37 @@ func getMessageInterval(rpcConn towgo.JsonRpcConnection) {
|
|
|
|
rpcConn.WriteResult(g.Config().MessageInterval)
|
|
|
|
rpcConn.WriteResult(g.Config().MessageInterval)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
func install() {
|
|
|
|
func install() {
|
|
|
|
// 1. 检查并安装Firefox
|
|
|
|
kioskInfof("checking firefox usability")
|
|
|
|
if !isFirefoxInstalled() {
|
|
|
|
// 1. 检查并修复 Firefox
|
|
|
|
fmt.Println("Firefox未安装,正在安装...")
|
|
|
|
if !isFirefoxUsable() {
|
|
|
|
|
|
|
|
kioskWarnf("firefox unusable; starting install/repair")
|
|
|
|
|
|
|
|
fmt.Println("Firefox missing or unusable, installing/repairing...")
|
|
|
|
if err := installFirefox(); err != nil {
|
|
|
|
if err := installFirefox(); err != nil {
|
|
|
|
fmt.Printf("安装Firefox失败: %v\n", err)
|
|
|
|
kioskErrorf("firefox install/repair failed err=%v", err)
|
|
|
|
|
|
|
|
fmt.Printf("install/repair Firefox failed: %v\n", err)
|
|
|
|
os.Exit(1)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fmt.Println("Firefox安装成功")
|
|
|
|
kioskInfof("firefox install/repair success")
|
|
|
|
|
|
|
|
fmt.Println("Firefox install/repair success")
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
fmt.Println("Firefox已安装")
|
|
|
|
kioskInfof("firefox is usable")
|
|
|
|
|
|
|
|
fmt.Println("Firefox is installed and usable")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 设置当前应用开机自启动
|
|
|
|
// 2. 设置当前应用开机自启动
|
|
|
|
appPath, err := os.Executable()
|
|
|
|
appPath, err := os.Executable()
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
kioskErrorf("read executable path failed err=%v", err)
|
|
|
|
fmt.Printf("读取应用路径失败: %v\n", err)
|
|
|
|
fmt.Printf("读取应用路径失败: %v\n", err)
|
|
|
|
os.Exit(1)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
kioskInfof("current executable path=%s workdir=%s", appPath, filepath.Dir(appPath))
|
|
|
|
if !isAutoStartEnabled(appPath) {
|
|
|
|
if !isAutoStartEnabled(appPath) {
|
|
|
|
|
|
|
|
kioskWarnf("autostart service missing or mismatched; reinstalling service=%s", autoStartServiceFile(appPath))
|
|
|
|
fmt.Println("正在设置开机自启动...")
|
|
|
|
fmt.Println("正在设置开机自启动...")
|
|
|
|
if err := enableAutoStart(appPath); err != nil {
|
|
|
|
if err := enableAutoStart(appPath); err != nil {
|
|
|
|
|
|
|
|
kioskErrorf("autostart setup failed service=%s err=%v", autoStartServiceFile(appPath), err)
|
|
|
|
fmt.Printf("设置开机自启动失败: %v\n", err)
|
|
|
|
fmt.Printf("设置开机自启动失败: %v\n", err)
|
|
|
|
os.Exit(1)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -153,36 +212,58 @@ func install() {
|
|
|
|
func runClient(port int) {
|
|
|
|
func runClient(port int) {
|
|
|
|
// 配置显示器和Xauthority路径
|
|
|
|
// 配置显示器和Xauthority路径
|
|
|
|
display := ":0"
|
|
|
|
display := ":0"
|
|
|
|
|
|
|
|
kioskInfof("firefox client loop init display=%s port=%d", display, port)
|
|
|
|
currentUser, err := user.Current()
|
|
|
|
currentUser, err := user.Current()
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
kioskErrorf("read current user failed err=%v", err)
|
|
|
|
fmt.Printf("读取当前用户失败: %v\n", err)
|
|
|
|
fmt.Printf("读取当前用户失败: %v\n", err)
|
|
|
|
os.Exit(1)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
xauthPath := filepath.Join(currentUser.HomeDir, ".Xauthority")
|
|
|
|
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 {
|
|
|
|
for {
|
|
|
|
// 设置环境变量
|
|
|
|
// 设置环境变量
|
|
|
|
os.Setenv("DISPLAY", display)
|
|
|
|
os.Setenv("DISPLAY", display)
|
|
|
|
|
|
|
|
if xauthPath := findXAuthorityPath(currentUser, display); xauthPath != "" {
|
|
|
|
os.Setenv("XAUTHORITY", 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{
|
|
|
|
args := []string{
|
|
|
|
"--kiosk",
|
|
|
|
|
|
|
|
"--noerrdialogs",
|
|
|
|
|
|
|
|
"--no-first-run",
|
|
|
|
|
|
|
|
"--new-instance",
|
|
|
|
"--new-instance",
|
|
|
|
// 👇 下面这 3 个就是禁密码弹窗的核心
|
|
|
|
"--profile",
|
|
|
|
"--disable-features=PasswordManager",
|
|
|
|
profileDir,
|
|
|
|
"--pref=signon.rememberSignons=false",
|
|
|
|
"--remote-debugging-port",
|
|
|
|
"--pref=signon.autofillForms=false",
|
|
|
|
fmt.Sprintf("%d", remoteDebugPort),
|
|
|
|
"http://127.0.0.1:" + fmt.Sprintf("%d", port),
|
|
|
|
"--kiosk",
|
|
|
|
|
|
|
|
clientURL,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 启动Firefox
|
|
|
|
// 启动Firefox
|
|
|
|
cmd := exec.Command("firefox", args...)
|
|
|
|
cmd := exec.Command("firefox", args...)
|
|
|
|
|
|
|
|
cmd.Env = firefoxKioskEnv()
|
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
cmd.Stderr = os.Stderr
|
|
|
|
cmd.Stderr = os.Stderr
|
|
|
|
|
|
|
|
kioskInfof("starting firefox args=%v", args)
|
|
|
|
|
|
|
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
if err := runFirefoxWithWatchdog(cmd, clientURL, remoteDebugPort); err != nil {
|
|
|
|
g.Log().Error(err.Error())
|
|
|
|
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) // 等待后重试
|
|
|
|
time.Sleep(5 * time.Second) // 等待后重试
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -190,10 +271,423 @@ func runClient(port int) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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是否安装
|
|
|
|
// 检查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 {
|
|
|
|
func isFirefoxInstalled() bool {
|
|
|
|
cmd := exec.Command("which", "firefox")
|
|
|
|
cmd := exec.Command("which", "firefox")
|
|
|
|
return cmd.Run() == nil
|
|
|
|
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
|
|
|
|
// 安装Firefox
|
|
|
|
@ -206,6 +700,7 @@ func installFirefox() error {
|
|
|
|
p.DebPkgPath = g.Config().Firefox.DebPkgPath
|
|
|
|
p.DebPkgPath = g.Config().Firefox.DebPkgPath
|
|
|
|
p.DebPkgName = g.Config().Firefox.DebPkgName
|
|
|
|
p.DebPkgName = g.Config().Firefox.DebPkgName
|
|
|
|
p.InstallShellPath = g.Config().Firefox.InstallShellPath
|
|
|
|
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(`
|
|
|
|
command := exec.Command("sudo", "sh", "-c", fmt.Sprintf(`
|
|
|
|
echo "=== 当前工作目录 ==="
|
|
|
|
echo "=== 当前工作目录 ==="
|
|
|
|
@ -243,20 +738,72 @@ echo "=== Firefox 安装完成 ==="
|
|
|
|
command.Stdout = os.Stdout
|
|
|
|
command.Stdout = os.Stdout
|
|
|
|
command.Stderr = os.Stderr
|
|
|
|
command.Stderr = os.Stderr
|
|
|
|
|
|
|
|
|
|
|
|
return command.Run()
|
|
|
|
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 {
|
|
|
|
func isAutoStartEnabled(appPath string) bool {
|
|
|
|
desktopFile := fmt.Sprintf("/etc/systemd/system/%s.service", strings.TrimSuffix(filepath.Base(appPath), filepath.Ext(appPath)))
|
|
|
|
serviceFile := autoStartServiceFile(appPath)
|
|
|
|
|
|
|
|
kioskInfof("autostart check begin service=%s expectedExec=%s expectedWorkdir=%s", serviceFile, appPath, filepath.Dir(appPath))
|
|
|
|
|
|
|
|
|
|
|
|
_, err := os.Stat(desktopFile)
|
|
|
|
content, err := os.ReadFile(serviceFile)
|
|
|
|
return !os.IsNotExist(err)
|
|
|
|
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 {
|
|
|
|
func enableAutoStart(appPath string) error {
|
|
|
|
appName := strings.TrimSuffix(filepath.Base(appPath), filepath.Ext(appPath))
|
|
|
|
serviceFile := autoStartServiceFile(appPath)
|
|
|
|
serviceFile := fmt.Sprintf("/etc/systemd/system/%s.service", appName)
|
|
|
|
kioskInfof("autostart setup begin service=%s exec=%s workdir=%s", serviceFile, appPath, filepath.Dir(appPath))
|
|
|
|
|
|
|
|
|
|
|
|
// 创建systemd服务文件内容
|
|
|
|
// 创建systemd服务文件内容
|
|
|
|
content := fmt.Sprintf(`[Unit]
|
|
|
|
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
|
|
|
|
Description=%s Background Service
|
|
|
|
After=network.target
|
|
|
|
After=network.target
|
|
|
|
|
|
|
|
|
|
|
|
@ -274,21 +821,33 @@ WantedBy=multi-user.target
|
|
|
|
appName,
|
|
|
|
appName,
|
|
|
|
appPath,
|
|
|
|
appPath,
|
|
|
|
filepath.Dir(appPath))
|
|
|
|
filepath.Dir(appPath))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 写入服务文件
|
|
|
|
func autoStartServiceMatches(content string, appPath string) bool {
|
|
|
|
cmd := exec.Command("sudo", "bash", "-c",
|
|
|
|
execStart := normalizeSystemdValue(systemdUnitValue(content, "ExecStart"))
|
|
|
|
fmt.Sprintf("echo '%s' > %s", content, serviceFile))
|
|
|
|
workingDirectory := normalizeSystemdValue(systemdUnitValue(content, "WorkingDirectory"))
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
expectedWorkdir := filepath.Dir(appPath)
|
|
|
|
return err
|
|
|
|
matches := execStart == appPath && workingDirectory == expectedWorkdir
|
|
|
|
}
|
|
|
|
kioskInfof("autostart service fields execStart=%s expectedExec=%s workingDirectory=%s expectedWorkdir=%s matches=%v", execStart, appPath, workingDirectory, expectedWorkdir, matches)
|
|
|
|
|
|
|
|
|
|
|
|
// 重新加载systemd配置
|
|
|
|
return matches
|
|
|
|
cmd = exec.Command("sudo", "systemctl", "daemon-reload")
|
|
|
|
}
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
|
|
|
return err
|
|
|
|
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 {
|
|
|
|
cmd = exec.Command("sudo", "systemctl", "enable", appName+".service")
|
|
|
|
return strings.Trim(strings.TrimSpace(value), `"'`)
|
|
|
|
return cmd.Run()
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|