From 500bafba0de2a152aa59c311f10c1d496583ebdd Mon Sep 17 00:00:00 2001 From: _kmz Date: Sun, 5 May 2024 16:49:15 +0800 Subject: [PATCH] :pill: added systemd support --- README.md | 22 +++--------- cmd/kd.go | 16 ++++----- config/config.go | 17 ++++++--- internal/daemon/process.go | 62 ++++++++++++++++++++------------- internal/server.go | 1 + logger/logger.go | 41 ++++++++++++---------- pkg/proc/proc.go | 9 ++++- pkg/systemd/systemd.go | 70 ++++++++++++++++++++++++++++++++++++++ plan.md | 4 ++- scripts/build.sh | 2 +- scripts/kd-server.service | 10 ++++++ 11 files changed, 177 insertions(+), 77 deletions(-) create mode 100644 pkg/systemd/systemd.go create mode 100644 scripts/kd-server.service diff --git a/README.md b/README.md index ccb1403..fd87d67 100644 --- a/README.md +++ b/README.md @@ -107,13 +107,13 @@ Invoke-WebRequest -uri 'https://github.com/Karmenzind/kd/releases/latest/downloa ### 卸载
🖱️ 点击展开
-
 1. 删除kd可执行文件(Linux/Mac:/usr/local/bin/kd,Win:C:\bin\kd.exe)
 2. 删除配置文件和缓存目录
     - Linux: `rm -rfv ~/.config/kd.toml ~/.cache/kdcache`
     - MacOS: `rm -rfv ~/.config/kd.toml ~/Library/Caches/kdcache`
     - Win: `rm ~\kd.toml ~\kdcache`
 
+如果通过AUR安装,则直接通过AUR管理工具卸载,例如:`yay -Rs kd`
 
## :gear: 用法和配置 @@ -168,8 +168,6 @@ theme = "temp" http_proxy = "" # 输出内容前自动清空终端,适合强迫症 -clear_screen = false - # 是否开启频率提醒:本月第X次查询xxx freq_alert = false @@ -184,7 +182,6 @@ enable_emoji = true path = "" # 日志级别,支持:DEBUG/INFO/WARN/PANIC/FATAL level = "WARN" - stderr = false ``` ## 🎈 提升体验技巧 @@ -208,20 +205,9 @@ fi ### 通过systemd管理daemon进程 -为避免每次开机后第一次查询都要等待守护进程启动,可以创建service文件`/usr/lib/systemd/user/kd-server.service`,然后执行`systemctl enable --user kd-server`,daemon进程将随系统自动启动: - -``` -[Unit] -Description=kd the command-line dictionary's server - -[Service] -Type=simple -ExecStart=/usr/bin/kd --server -Restart=always +为避免每次开机后第一次查询都要等待守护进程启动,可以创建service文件`/usr/lib/systemd/user/kd-server.service`,然后执行`systemctl enable --user kd-server`,daemon进程将随系统自动启动 -[Install] -WantedBy=default.target -``` +内容参考[kd-server.service](./scripts/kd-server.service) ## 🎨 颜色主题 @@ -273,7 +259,7 @@ sudo xattr -r -d com.apple.quarantine - 增加多种主题,包含常见配色如Gruvbox/Molokai,仿照bat实现 - 支持全模块自定义显示配置 - 引入多种查询源和词库,如stardict、bing等 -- 增加服务端 +- 增加远程服务端 - 支持通过fzf补全 - Vim插件,浮窗显示查词结果 - 离线词库周期更新 diff --git a/cmd/kd.go b/cmd/kd.go index 22562c9..12143e7 100644 --- a/cmd/kd.go +++ b/cmd/kd.go @@ -24,7 +24,7 @@ import ( "go.uber.org/zap" ) -var VERSION = "v0.0.8" +var VERSION = "v0.0.9" func showPrompt() { exename, err := pkg.GetExecutableBasename() @@ -32,6 +32,7 @@ func showPrompt() { d.EchoFatal(err.Error()) } fmt.Printf(`%[1]s 查单词、词组 +%[1]s -t 查长句 %[1]s -h 查看详细帮助 `, exename) } @@ -49,6 +50,7 @@ var um = map[string]string{ "generate-config": "generate config sample 生成配置文件,Linux/Mac默认地址为~/.config/kd.toml,Win为~\\kd.toml", "edit-config": "edit configuration file with the default editor 用默认编辑器打开配置文件", "status": "show running status 展示运行信息", + "log-to-stream": "redirect logging output to stdout&stderr (for debugging or server mode)", } // ----------------------------------------------------------------------------- @@ -86,16 +88,12 @@ func flagStop(*cli.Context, bool) error { } func flagRestart(*cli.Context, bool) error { - err := daemon.KillDaemonIfRunning() - if err == nil { - err = daemon.StartDaemonProcess() - } - return err + return daemon.RestartDaemon() } func flagUpdate(ctx *cli.Context, _ bool) (err error) { var ver string - if pkg.GetLinuxDistro() == "arch" { + if runtime.GOOS == "linux" && pkg.GetLinuxDistro() == "arch" { d.EchoFine("您在使用ArchLinux,推荐直接通过AUR安装/升级(例如`yay -S kd`),更便于维护") } force := ctx.Bool("force") @@ -241,9 +239,6 @@ func main() { } defer cache.LiteDB.Close() defer core.WG.Wait() - // emoji.Println(":beer: Beer!!!") - // pizzaMessage := emoji.Sprint("I like a :pizza: and :sushi:!!") - // fmt.Println(pizzaMessage) app := &cli.App{ Suggest: true, // XXX @@ -271,6 +266,7 @@ func main() { &cli.BoolFlag{Name: "generate-config", DisableDefaultText: true, Action: flagGenerateConfig, Usage: um["generate-config"]}, &cli.BoolFlag{Name: "edit-config", DisableDefaultText: true, Action: flagEditConfig, Usage: um["edit-config"]}, &cli.BoolFlag{Name: "status", DisableDefaultText: true, Hidden: true, Action: flagStatus, Usage: um["status"]}, + &cli.BoolFlag{Name: "log-to-stream", DisableDefaultText: true, Hidden: true, Action: flagStatus, Usage: um["log-to-stream"]}, }, Action: func(cCtx *cli.Context) error { // 除了--text外,其他的BoolFlag都当subcommand用 diff --git a/config/config.go b/config/config.go index cc1bdc7..3d2e6e1 100644 --- a/config/config.go +++ b/config/config.go @@ -16,10 +16,11 @@ import ( var CONFIG_PATH string type LoggerConfig struct { - Enable bool `default:"true" toml:"enable"` - Path string `toml:"path"` - Level string `default:"warn" toml:"level"` - Stderr bool `default:"false" toml:"stderr"` + Enable bool `default:"true" toml:"enable"` + Path string `toml:"path"` + Level string `default:"warn" toml:"level"` + Stderr bool `default:"false" toml:"stderr"` + RedirectToStream bool `default:"false" toml:"redirect_to_stream"` } type Config struct { @@ -58,9 +59,15 @@ func (c *Config) CheckAndApply() (err error) { c.Logging.Level = "warn" } else if !str.InSlice(c.Logging.Level, []string{"debug", "info", "warn", "panic", "fatal"}) { return fmt.Errorf("[logging.level] 不支持的日志等级:%s", c.Logging.Level) - } } + for _, arg := range os.Args { + if arg == "--log-to-stream" { + c.Logging.Enable = true + c.Logging.RedirectToStream = true + break + } + } return } diff --git a/internal/daemon/process.go b/internal/daemon/process.go index 7a97c36..16f4931 100644 --- a/internal/daemon/process.go +++ b/internal/daemon/process.go @@ -13,30 +13,15 @@ import ( "github.com/Karmenzind/kd/pkg" d "github.com/Karmenzind/kd/pkg/decorate" "github.com/Karmenzind/kd/pkg/proc" + "github.com/Karmenzind/kd/pkg/systemd" "github.com/shirou/gopsutil/v3/process" "go.uber.org/zap" ) -// type model.RunInfo struct { -// *proc.ProcInfo -// Port string -// Version string -// } - +var SYSTEMD_UNIT_NAME = "kd-server" var DaemonInfo = &model.RunInfo{} -// func RecordRunInfo(port string) { -// run.Info.Port = port - -// err := pkg.SaveJson(filepath.Join(run.CACHE_RUN_PATH, "daemon.json"), run.Info) -// if err == nil { -// zap.S().Infof("Recorded running information of daemon %+v", DaemonInfo) -// } else { -// zap.S().Warnf("Failed to record running info of daemon %+v", err) -// } -// } - func GetDaemonInfoPath() string { return filepath.Join(run.CACHE_RUN_PATH, "daemon.json") } @@ -103,12 +88,7 @@ func FindServerProcess() (*process.Process, error) { if n == "kd" || (runtime.GOOS == "windows" && n == "kd.exe") { cmd, _ := p.Cmdline() - if p.Pid == 13328 { - name, _ := p.Name() - cmdslice, _ := p.CmdlineSlice() - zap.S().Debugf("13328:Name: `%s` Cmd: `%s` cmdslice: `%+v`", name, cmd, cmdslice) - } - zap.S().Debugf("Found process kd.exe with CMD: %s", cmd) + // zap.S().Debugf("Found process kd with CMD: %s", cmd) if strings.Contains(cmd, " --server") { zap.S().Debugf("Found process %+v Cmd: `%s`", p, cmd) return p, nil @@ -154,6 +134,16 @@ func StartDaemonProcess() error { } func KillDaemonIfRunning() error { + if runtime.GOOS == "linux" { + if yes, _ := systemd.ServiceIsActive(SYSTEMD_UNIT_NAME, true); yes { + d.EchoWarn("检测到daemon作为systemd unit运行,将使用systemctl停止,再次启动需执行systemctl start --user %s", SYSTEMD_UNIT_NAME) + _, err := systemd.StopService(SYSTEMD_UNIT_NAME, true) + if err == nil { + d.EchoOkay("已经通过systemd停止kd-server服务") + } + return err + } + } p, err := FindServerProcess() if err == nil { if p == nil { @@ -170,6 +160,32 @@ func KillDaemonIfRunning() error { if err == nil { zap.S().Info("Terminated daemon process.") d.EchoOkay("守护进程已经停止") + } else { + zap.S().Warnf("Failed to terminate daemon process: %s", err) + } + return err +} + +// TODO (k): <2024-05-05 15:56> +func SendHUP2Daemon() error { + return nil +} + +func RestartDaemon() error { + if runtime.GOOS == "linux" { + if yes, _ := systemd.ServiceIsActive(SYSTEMD_UNIT_NAME, true); yes { + zap.S().Debugf("Found systemd unit: %s", SYSTEMD_UNIT_NAME) + d.EchoWarn("检测到daemon存在相应systemd unit,将使用systemctl重启") + _, err := systemd.RestartService(SYSTEMD_UNIT_NAME, true) + if err == nil { + d.EchoOkay("已经通过systemctl重启daemon服务") + } + return err + } + } + err := KillDaemonIfRunning() + if err == nil { + err = StartDaemonProcess() } return err } diff --git a/internal/server.go b/internal/server.go index faaf93a..52a151f 100644 --- a/internal/server.go +++ b/internal/server.go @@ -38,6 +38,7 @@ func StartServer() (err error) { run.Info.SetPort(port) go run.Info.SaveToFile(filepath.Join(run.CACHE_RUN_PATH, "daemon.json")) d.EchoOkay("Listening on host: %s, port: %s\n", host, port) + zap.S().Info("Started kd server") daemon.InitCron() diff --git a/logger/logger.go b/logger/logger.go index def1243..c5c4af5 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -15,27 +15,32 @@ var LOG_FILE string func buildLogger(logCfg *config.LoggerConfig, options ...zap.Option) (*zap.Logger, error) { cfg := zap.NewDevelopmentConfig() - var f string - if logCfg.Path == "" { - u, err := user.Current() - if err != nil { - f = filepath.Join(os.TempDir(), "kd.log") + if logCfg.RedirectToStream { + LOG_FILE = "[stream]" + cfg.OutputPaths = []string{"stdout"} + cfg.ErrorOutputPaths = []string{"stderr"} + } else { + var f string + if logCfg.Path == "" { + u, err := user.Current() + if err != nil { + f = filepath.Join(os.TempDir(), "kd.log") + } else { + name := strings.ReplaceAll(u.Username, " ", "_") + name = strings.ReplaceAll(name, "\\", "_") + f = filepath.Join(os.TempDir(), fmt.Sprintf("kd_%s.log", name)) + } } else { - name := strings.ReplaceAll(u.Username, " ", "_") - name = strings.ReplaceAll(name, "\\", "_") - f = filepath.Join(os.TempDir(), fmt.Sprintf("kd_%s.log", name)) + f = logCfg.Path } - } else { - f = logCfg.Path - } - if _, err := os.Stat(f); err == nil { - os.Chmod(f, 0o666) - } - LOG_FILE = f - - cfg.OutputPaths = []string{f} - cfg.ErrorOutputPaths = []string{f} + if _, err := os.Stat(f); err == nil { + os.Chmod(f, 0o666) + } + LOG_FILE = f + cfg.OutputPaths = []string{f} + cfg.ErrorOutputPaths = []string{f} + } level, err := zap.ParseAtomicLevel(logCfg.Level) if err != nil { return nil, err diff --git a/pkg/proc/proc.go b/pkg/proc/proc.go index 14a1d16..ec15286 100644 --- a/pkg/proc/proc.go +++ b/pkg/proc/proc.go @@ -15,7 +15,9 @@ func KillProcess(p *process.Process) (err error) { if runtime.GOOS != "windows" { errSig := p.SendSignal(syscall.SIGINT) if errSig == nil { - if yes, errCheck := p.IsRunning(); errCheck == nil && !yes { + yes, errCheck := p.IsRunning() + zap.S().Infof("Checking if running (pid %v): %v err: %v", p.Pid, yes, errCheck) + if errCheck == nil && !yes { zap.S().Infof("Stopped process %v with SIGINT.", p.Pid) return } @@ -49,3 +51,8 @@ func SysKillPID(pid int32) (err error) { } return } + +func SendSignalToProcess(pid int32, signal syscall.Signal) error { + return nil +} + diff --git a/pkg/systemd/systemd.go b/pkg/systemd/systemd.go new file mode 100644 index 0000000..1bac85a --- /dev/null +++ b/pkg/systemd/systemd.go @@ -0,0 +1,70 @@ +package systemd + +import ( + "bytes" + "os/exec" + "strings" + + "go.uber.org/zap" +) + +func systemdAction(unit string, subcommand string, isUser bool) (string, error) { + var cmd *exec.Cmd + if isUser { + cmd = exec.Command("systemctl", subcommand, "--user", unit) + } else { + cmd = exec.Command("systemctl", subcommand, unit) + } + output, err := cmd.CombinedOutput() + outputStr := strings.Trim(string(output), "\n") + zap.S().Infof("Executed: `%s` Result: `%s` Error: %v", cmd, output, err) + return outputStr, err +} + +func ServiceIsActive(unit string, isUser bool) (bool, error) { + out, err := systemdAction(unit, "is-active", isUser) + return out == "active", err +} + +func ServiceIsEnabled(unit string, isUser bool) (bool, error) { + out, err := systemdAction(unit, "is-enabled", isUser) + return out == "enabled", err +} + +func ServiceIsActiveOrEnabled(unit string, isUser bool) bool { + out1, _ := systemdAction(unit, "is-active", isUser) + out2, _ := systemdAction(unit, "is-enabled", isUser) + return out1 == "active" || out2 == "enabled" +} + +func UnitExists(unitName string, isUser bool) (bool, error) { + var cmd *exec.Cmd + if isUser { + cmd = exec.Command("systemctl", "list-unit-files", "--full", "--no-pager", "--user") + } else { + cmd = exec.Command("systemctl", "list-unit-files", "--full", "--no-pager") + } + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + zap.S().Warnf("Failed to fetch unit-files. Error: %s", err) + return false, err + } + output := out.String() + + zap.S().Debugf("Fetched unit-files: %s", output) + return strings.Contains(output, unitName), err +} + +func StopService(unit string, isUser bool) (string, error) { + return systemdAction(unit, "stop", isUser) +} + +func StartService(unit string, isUser bool) (string, error) { + return systemdAction(unit, "start", isUser) +} + +func RestartService(unit string, isUser bool) (string, error) { + return systemdAction(unit, "restart", isUser) +} diff --git a/plan.md b/plan.md index b63b5a5..541c58e 100644 --- a/plan.md +++ b/plan.md @@ -8,6 +8,7 @@ - gitea镜像 ## short-term +- 重启改为signal - 多source直接嵌套进列表 - 记录pid/port,先检查这两个 - 预留一个接口调用,获取重要信息 @@ -18,7 +19,6 @@ ## Long-term - 引入stardict源 - 增加服务端 -- 更新自动提醒 - 自动更新UA数据 - 加入词库设置,供选择词库大小 @@ -36,6 +36,8 @@ # BUG +- `--status` shows port and pid of dead daemon + ## Risk - 实际文件名 不改的时候的process_name,增加同时校验kd和当前文件名 diff --git a/scripts/build.sh b/scripts/build.sh index 481d3b6..5410f08 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -73,7 +73,7 @@ case $1 in "") echo ">>> Building for current workspace..." # do_build '' '' ${PROJECT_DIR}/kd - do_build '' '' /usr/local/bin/kd + do_build '' '' /usr/bin/kd exit ;; -a) diff --git a/scripts/kd-server.service b/scripts/kd-server.service new file mode 100644 index 0000000..2f46948 --- /dev/null +++ b/scripts/kd-server.service @@ -0,0 +1,10 @@ +[Unit] +Description=kd the command-line dictionary's server + +[Service] +Type=simple +ExecStart=/usr/bin/kd --server --log-to-stream +Restart=always + +[Install] +WantedBy=default.target