From 441d704d77eba72aca77eb85ef9ed18c8aea8083 Mon Sep 17 00:00:00 2001 From: _kmz Date: Fri, 18 Oct 2024 23:04:43 +0800 Subject: [PATCH] optimize the logic for --edit/generate-config; pronounce: change nation -> English while only_english is on; add a dummy progress bar to file downloading func; install.sh: change HOME/.local/bin -> /usr/local/bin; formatting; update golang version in github workflow file; test new workflow --- .github/workflows/manual-trigger.yml | 87 ++++ .github/workflows/release-tags.yml | 22 +- cmd/kd.go | 634 ++++++++++++++------------- config/config.go | 177 ++++---- config/output.go | 46 +- go.mod | 47 +- go.sum | 107 +++-- internal/cache/cache.go | 261 ++++++----- internal/cache/counter.go | 52 +-- internal/cache/database.go | 126 +++--- internal/cache/notfound.go | 117 +++-- internal/cache/utils.go | 32 +- internal/daemon/cron.go | 620 +++++++++++++------------- internal/daemon/process.go | 306 ++++++------- internal/model/dataobj.go | 80 ++-- internal/model/model.go | 20 +- internal/model/others.go | 74 ++-- internal/model/request.go | 16 +- internal/query/online.go | 208 ++++----- internal/query/output.go | 355 +++++++-------- internal/query/query.go | 60 +-- internal/query/youdao.go | 392 ++++++++--------- internal/run/info.go | 64 +-- internal/update/update.go | 410 +++++++++-------- logger/logger.go | 90 ++-- pkg/decorate/color.go | 88 ++-- pkg/decorate/emoji.go | 8 +- pkg/decorate/fmt.go | 53 +-- pkg/decorate/init.go | 4 +- pkg/file.go | 85 ++-- pkg/http.go | 74 +++- pkg/proc/proc.go | 2 +- pkg/str/utils.go | 22 +- pkg/systemd/systemd.go | 81 ++-- pkg/terminal.go | 271 ++++++------ scripts/install.sh | 17 +- 36 files changed, 2614 insertions(+), 2494 deletions(-) create mode 100644 .github/workflows/manual-trigger.yml diff --git a/.github/workflows/manual-trigger.yml b/.github/workflows/manual-trigger.yml new file mode 100644 index 0000000..174654b --- /dev/null +++ b/.github/workflows/manual-trigger.yml @@ -0,0 +1,87 @@ +name: Mannual Trigger Workflow + +on: + workflow_dispatch: + # inputs: + # branch_name: + # description: 'Branch name to checkout' + # required: true + # default: 'dev' + # +jobs: + linux-part: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - name: "Build Changelog" + id: build_changelog + uses: mikepenz/release-changelog-builder-action@v5 + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + with: + mode: "COMMIT" + failOnError: true + + - name: echo changelog + run: echo "${{steps.build_changelog.outputs.changelog}}" + + - name: "Set up Go" + uses: actions/setup-go@v5 + with: + check-latest: true + go-version-file: go.mod + + - name: "Install system dependencies" + run: sudo apt-get install -y gcc-mingw-w64-x86-64 gcc-aarch64-linux-gnu + + - name: "Install dependencies" + run: go mod tidy + + - name: "Build" + run: | + bash scripts/build.sh linux amd64 + bash scripts/build.sh linux arm64 + bash scripts/build.sh windows amd64 + + - name: "Save artifacts" + uses: actions/upload-artifact@v4 + with: + name: linux-and-win + path: build + if-no-files-found: error + retention-days: 15 + overwrite: true + + mac-part: + runs-on: macos-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: "Set up Go" + uses: actions/setup-go@v5 + with: + check-latest: true + go-version-file: go.mod + + - name: "Install dependencies" + run: go mod tidy + + - name: "Build" + run: | + bash scripts/build.sh darwin arm64 + bash scripts/build.sh darwin amd64 + + - name: "Save artifacts" + uses: actions/upload-artifact@v4 + with: + name: macos + path: build + if-no-files-found: error + retention-days: 15 + overwrite: true diff --git a/.github/workflows/release-tags.yml b/.github/workflows/release-tags.yml index 753b149..e9cdebb 100644 --- a/.github/workflows/release-tags.yml +++ b/.github/workflows/release-tags.yml @@ -1,6 +1,3 @@ -# This workflow will build a golang project -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go - name: release-tags on: @@ -19,20 +16,22 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: "Build Changelog" id: build_changelog - uses: mikepenz/release-changelog-builder-action@v4 + uses: mikepenz/release-changelog-builder-action@v5 env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} with: - commitMode: true + mode: "COMMIT" + failOnError: true - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: "1.21.x" + check-latest: true + go-version-file: go.mod - name: Install system dependencies run: sudo apt-get install -y gcc-mingw-w64-x86-64 gcc-aarch64-linux-gnu @@ -67,11 +66,12 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: "1.21.x" + check-latest: true + go-version-file: go.mod - name: Install dependencies run: go mod tidy diff --git a/cmd/kd.go b/cmd/kd.go index 6d7542d..844925c 100644 --- a/cmd/kd.go +++ b/cmd/kd.go @@ -1,354 +1,372 @@ package main import ( - "fmt" - "os" - "os/exec" - "os/user" - "runtime" - "strings" - - "github.com/Karmenzind/kd/config" - "github.com/Karmenzind/kd/internal" - "github.com/Karmenzind/kd/internal/cache" - "github.com/Karmenzind/kd/internal/core" - "github.com/Karmenzind/kd/internal/daemon" - "github.com/Karmenzind/kd/internal/query" - "github.com/Karmenzind/kd/internal/run" - "github.com/Karmenzind/kd/internal/update" - "github.com/Karmenzind/kd/logger" - "github.com/Karmenzind/kd/pkg" - d "github.com/Karmenzind/kd/pkg/decorate" - "github.com/kyokomi/emoji/v2" - "github.com/urfave/cli/v2" - "go.uber.org/zap" + "fmt" + "os" + "os/exec" + "os/user" + "runtime" + "strings" + + "github.com/Karmenzind/kd/config" + "github.com/Karmenzind/kd/internal" + "github.com/Karmenzind/kd/internal/cache" + "github.com/Karmenzind/kd/internal/core" + "github.com/Karmenzind/kd/internal/daemon" + "github.com/Karmenzind/kd/internal/query" + "github.com/Karmenzind/kd/internal/run" + "github.com/Karmenzind/kd/internal/update" + "github.com/Karmenzind/kd/logger" + "github.com/Karmenzind/kd/pkg" + d "github.com/Karmenzind/kd/pkg/decorate" + "github.com/urfave/cli/v2" + "go.uber.org/zap" + // "github.com/kyokomi/emoji/v2" ) -var VERSION = "v0.0.11" +var VERSION = "v0.0.12.dev" func showPrompt() { - exename, err := pkg.GetExecutableBasename() - if err != nil { - d.EchoFatal(err.Error()) - } - fmt.Printf(`%[1]s 查单词、词组 + exename, err := pkg.GetExecutableBasename() + if err != nil { + d.EchoFatal(err.Error()) + } + fmt.Printf(`%[1]s 查单词、词组 %[1]s -t 查长句 %[1]s -h 查看详细帮助 `, exename) } var um = map[string]string{ - "text": "translate long query `TEXT` with e.g. --text=\"Long time ago\" 翻译长句", - "nocache": "don't use cached result 不使用本地词库,查询网络结果", - "theme": "choose the color theme for current query 选择颜色主题,仅当前查询生效", - "init": "initialize shell completion 初始化部分设置,例如shell的自动补全", - "server": "start server foreground 在前台启动服务端", - "daemon": "ensure/start the daemon process 启动守护进程", - "stop": "stop the daemon process 停止守护进程", - "restart": "restart the daemon process 重新启动守护进程", - "update": "check and update kd client 更新kd的可执行文件", - "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)", + "text": "translate long query `TEXT` with e.g. --text=\"Long time ago\" 翻译长句", + "nocache": "don't use cached result 不使用本地词库,查询网络结果", + "theme": "choose the color theme for current query 选择颜色主题,仅当前查询生效", + "init": "initialize shell completion 初始化部分设置,例如shell的自动补全", + "server": "start server foreground 在前台启动服务端", + "daemon": "ensure/start the daemon process 启动守护进程", + "stop": "stop the daemon process 停止守护进程", + "restart": "restart the daemon process 重新启动守护进程", + "update": "check and update kd client 更新kd的可执行文件", + "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)", } // ----------------------------------------------------------------------------- // cli flag actions // ----------------------------------------------------------------------------- -func flagServer(*cli.Context, bool) error { - err := internal.StartServer() - if strings.Contains(err.Error(), "address already in use") { - return fmt.Errorf("端口已经被占用(%s)", err) - } - return err +func flagServer(*cli.Context, bool) (err error) { + err = internal.StartServer() + if strings.Contains(err.Error(), "address already in use") { + return fmt.Errorf("端口已经被占用(%s)", err) + } + return } -func flagDaemon(*cli.Context, bool) error { - p, _ := daemon.FindServerProcess() - if p != nil { - d.EchoWrong("已存在运行中的守护进程,PID:%d。请先执行`kd --stop`停止该进程", p.Pid) - return nil - - } - err := daemon.StartDaemonProcess() - if err != nil { - d.EchoFatal(err.Error()) - } - return nil +func flagDaemon(*cli.Context, bool) (err error) { + p, _ := daemon.FindServerProcess() + if p != nil { + d.EchoWrong("已存在运行中的守护进程,PID:%d。请先执行`kd --stop`停止该进程", p.Pid) + return + } + if err := daemon.StartDaemonProcess(); err != nil { + d.EchoFatal(err.Error()) + } + return } -func flagStop(*cli.Context, bool) error { - err := daemon.KillDaemonIfRunning() - if err != nil { - d.EchoFatal(err.Error()) - } - return err +func flagStop(*cli.Context, bool) (err error) { + if err = daemon.KillDaemonIfRunning(); err != nil { + d.EchoFatal(err.Error()) + } + return } func flagRestart(*cli.Context, bool) error { - return daemon.RestartDaemon() + return daemon.RestartDaemon() } func flagUpdate(ctx *cli.Context, _ bool) (err error) { - var ver string - if runtime.GOOS == "linux" && run.Info.GetOSInfo().Distro == "arch" { - d.EchoFine("您在使用ArchLinux,推荐直接通过AUR安装/升级(例如`yay -S kd`),更便于维护") - } - force := ctx.Bool("force") - if force { - d.EchoRun("强制更新") - } - doUpdate := force - if !doUpdate { - ver, err = update.GetNewerVersion(VERSION) - if err != nil { - d.EchoError(err.Error()) - return - } - if ver != "" { - prompt := fmt.Sprintf("Found new version (%s). Update?", ver) - if pkg.AskYN(prompt) { - doUpdate = true - } else { - fmt.Println("Canceled.", d.B(d.Green(":)"))) - return nil - } - } else { - fmt.Println("You're using the latest version.") - return nil - } - } - - if doUpdate { - emoji.Println(":lightning: Let's update now") - go daemon.KillDaemonIfRunning() - err = update.UpdateBinary(VERSION) - } - return err + var ver string + if runtime.GOOS == "linux" && run.Info.GetOSInfo().Distro == "arch" { + d.EchoFine("您在使用ArchLinux,推荐直接通过AUR安装/升级(例如`yay -S kd`),更便于维护") + } + force := ctx.Bool("force") + if force { + d.EchoRun("开始强制更新") + } + doUpdate := force + if !doUpdate { + ver, err = update.GetNewerVersion(VERSION) + if err != nil { + d.EchoError(err.Error()) + return + } + if ver != "" { + prompt := fmt.Sprintf("Found new version (%s). Update?", ver) + if pkg.AskYN(prompt) { + doUpdate = true + } else { + fmt.Println("Canceled.", d.B(d.Green(":)"))) + return nil + } + } else { + fmt.Println("You're using the latest version.") + return nil + } + } + + if doUpdate { + // emoji.Println(":lightning: Let's update now") + err = daemon.KillDaemonIfRunning() + if err != nil { + warnMsg := "可能会影响后续文件替换。如果出现问题,请手动执行`kd --stop`后重试" + d.EchoWarn("停止守护进程出现异常(%s),%s", err, warnMsg) + if p, perr := daemon.FindServerProcess(); perr == nil { + if p == nil { + d.EchoOkay("守护进程已确认停止") + } else { + d.EchoWarn("守护进程(PID %v)未能停止,%s", p.Pid, warnMsg) + } + } + } + err = update.UpdateBinary(VERSION) + } + return err } -func flagGenerateConfig(*cli.Context, bool) error { - conf, err := config.GenerateDefaultConfig() - if err != nil { - d.EchoFatal(err.Error()) - } - d.EchoRun("以下默认配置将会被写入配置文件") - fmt.Println(conf) - if pkg.IsPathExists(config.CONFIG_PATH) { - if !pkg.AskYN(fmt.Sprintf("配置文件%s已经存在,是否覆盖?", config.CONFIG_PATH)) { - d.EchoFine("已取消") - return nil - } - } - os.WriteFile(config.CONFIG_PATH, []byte(conf), os.ModePerm) - d.EchoOkay("已经写入配置文件") - return err +func flagGenerateConfig(*cli.Context, bool) (err error) { + if pkg.IsPathExists(config.CONFIG_PATH) { + if !pkg.AskYN(fmt.Sprintf("配置文件%s已经存在,是否覆盖?", config.CONFIG_PATH)) { + d.EchoFine("已取消") + return + } + } + conf, err := config.GenerateDefaultConfig() + if err != nil { + d.EchoFatal(err.Error()) + } + d.EchoRun("以下默认配置将会被写入配置文件,路径为" + config.CONFIG_PATH) + fmt.Println(conf) + if !pkg.AskYN("是否继续?") { + d.EchoFine("已取消") + return + } + + os.WriteFile(config.CONFIG_PATH, []byte(conf), os.ModePerm) + d.EchoOkay("已经写入配置文件") + return } -func flagEditConfig(*cli.Context, bool) error { - var err error - var cmd *exec.Cmd - p := config.CONFIG_PATH - switch runtime.GOOS { - case "linux", "darwin": - for _, k := range []string{"VISUAL", "EDITOR"} { - if env := os.Getenv(k); env != "" { - d.EchoRun("找到预设%s:%s,正在启动", k, env) - cmd = exec.Command(env, p) - break - } - } - if cmd == nil { - if runtime.GOOS == "darwin" { - cmd = exec.Command("open", "-e", p) - } else { - for _, k := range []string{"nano", "vim", "vi"} { - d.EchoRun("未找到EDITOR或VISUAL环境变量,尝试启动编辑器%s", k) - if pkg.CommandExists(k) { - cmd = exec.Command(k, p) - break - } - } - if cmd == nil { - return fmt.Errorf("未找到nano或vim,请安装至少一种,或者指定环境变量EDITOR/VISUAL") - } - } - } - case "windows": - cmd = exec.Command("notepad", p) - // case "darwin": - // cmd = exec.Command("open", "-e", p) - default: - return fmt.Errorf("暂不支持为当前操作系统%s自动打开编辑器,请提交issue反馈", runtime.GOOS) - // d.EchoRun("找到%s:%s,正在启动", k, env) - } - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - err = cmd.Run() - return err +func flagEditConfig(ctx *cli.Context, b bool) error { + var err error + var cmd *exec.Cmd + p := config.CONFIG_PATH + if !pkg.IsPathExists(p) { + d.EchoRun("检测到配置文件不存在") + err = flagGenerateConfig(ctx, b) + if err != nil || !pkg.IsPathExists(p) { + return err + } + } + switch runtime.GOOS { + case "linux", "darwin": + for _, k := range []string{"VISUAL", "EDITOR"} { + if env := os.Getenv(k); env != "" { + d.EchoRun("找到预设%s:%s,正在启动", k, env) + cmd = exec.Command(env, p) + break + } + } + if cmd == nil { + if runtime.GOOS == "darwin" { + cmd = exec.Command("open", "-e", p) + } else { + for _, k := range []string{"nano", "vim", "vi"} { + d.EchoRun("未找到EDITOR或VISUAL环境变量,尝试启动编辑器%s", k) + if pkg.CommandExists(k) { + cmd = exec.Command(k, p) + break + } + } + if cmd == nil { + return fmt.Errorf("未找到nano或vim,请安装至少一种,或者指定环境变量EDITOR/VISUAL") + } + } + } + case "windows": + cmd = exec.Command("notepad", p) + default: + return fmt.Errorf("暂不支持为当前操作系统%s自动打开编辑器,请提交issue反馈", runtime.GOOS) + } + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + err = cmd.Run() + return err } func flagStatus(*cli.Context, bool) error { - di, _ := daemon.GetDaemonInfo() - d.EchoRun("运行和相关配置信息如下:") - fmt.Printf(" Daemon端口:%s\n", di.Port) - fmt.Printf(" Daemon PID:%d\n", di.PID) - fmt.Printf(" 配置文件地址:%s\n", config.CONFIG_PATH) - fmt.Printf(" 数据文件目录:%s\n", cache.CACHE_ROOT_PATH) - fmt.Printf(" Log地址:%s\n", logger.LOG_FILE) - kdpath, err := pkg.GetExecutablePath() - if err == nil { - fmt.Printf(" Binary地址:%s\n", kdpath) - } - return err + di, _ := daemon.GetDaemonInfo() + d.EchoRun("运行和相关配置信息如下:") + fmt.Printf(" Daemon端口:%s\n", di.Port) + fmt.Printf(" Daemon PID:%d\n", di.PID) + fmt.Printf(" 配置文件地址:%s\n", config.CONFIG_PATH) + fmt.Printf(" 数据文件目录:%s\n", cache.CACHE_ROOT_PATH) + fmt.Printf(" Log地址:%s\n", logger.LOG_FILE) + kdpath, err := pkg.GetExecutablePath() + if err == nil { + fmt.Printf(" Binary地址:%s\n", kdpath) + } + return err } func checkAndNoticeUpdate() { - if ltag := update.GetCachedLatestTag(); ltag != "" { - if update.CompareVersions(ltag, VERSION) == 1 { - prompt := fmt.Sprintf("发现新版本%s,请执行`kd --update`更新", ltag) - if run.Info.GetOSInfo().Distro == "arch" { - prompt += "。ArchLinux推荐通过AUR安装/升级" - } - d.EchoWeakNotice(prompt) - } - } + if ltag := update.GetCachedLatestTag(); ltag != "" { + if update.CompareVersions(ltag, VERSION) == 1 { + prompt := fmt.Sprintf("发现新版本%s,请执行`kd --update`更新", ltag) + if run.Info.GetOSInfo().Distro == "arch" { + prompt += "。ArchLinux推荐通过AUR安装/升级" + } + d.EchoWeakNotice(prompt) + } + } } func basicCheck() { - if runtime.GOOS != "windows" { - if u, _ := user.Current(); u.Username == "root" { - d.EchoWrong("不支持Root用户") - os.Exit(1) - } - } - - // XXX (k): <2024-01-01> - // if exename, err := pkg.GetExecutableBasename(); err == nil { - // if exename != "kd" { - // d.EchoWrong("请将名字改成kd") - // os.Exit(1) - // } - // } else { - // d.EchoError(err.Error()) - // } + if runtime.GOOS != "windows" { + if u, _ := user.Current(); u.Username == "root" { + d.EchoWrong("不支持Root用户") + os.Exit(1) + } + } + + // XXX (k): <2024-01-01> + // if exename, err := pkg.GetExecutableBasename(); err == nil { + // if exename != "kd" { + // d.EchoWrong("请将名字改成kd") + // os.Exit(1) + // } + // } else { + // d.EchoError(err.Error()) + // } } func main() { - basicCheck() - config.InitConfig() - cfg := config.Cfg - d.ApplyConfig(cfg.EnableEmoji) - - run.Info.Version = VERSION - - if cfg.Logging.Enable { - l, err := logger.InitLogger(&cfg.Logging) - if err != nil { - d.EchoFatal(err.Error()) - } - defer l.Sync() - } - - err := cache.InitDB() - if err != nil { - d.EchoFatal(err.Error()) - } - defer cache.LiteDB.Close() - defer core.WG.Wait() - - app := &cli.App{ - Suggest: true, // XXX - Name: "kd", - Version: VERSION, - Usage: "A crystal clean command-line dictionary.", - HideHelpCommand: true, - // EnableBashCompletion: true, - // EnableShellCompletion: true, - - // Authors: []*cli.Author{{Name: "kmz", Email: "valesail7@gmail.com"}}, - Flags: []cli.Flag{ - &cli.BoolFlag{Name: "text", Aliases: []string{"t"}, Hidden: true, Usage: um["text"]}, - &cli.BoolFlag{Name: "nocache", Aliases: []string{"n"}, DisableDefaultText: true, Usage: um["nocache"]}, - &cli.StringFlag{Name: "theme", Aliases: []string{"T"}, DefaultText: "temp", Usage: um["theme"]}, - &cli.BoolFlag{Name: "force", Aliases: []string{"f"}, DisableDefaultText: true, Hidden: true}, - - // BoolFlags as commands - &cli.BoolFlag{Name: "init", DisableDefaultText: true, Hidden: true, Usage: um["init"]}, - &cli.BoolFlag{Name: "server", DisableDefaultText: true, Action: flagServer, Hidden: true, Usage: um["server"]}, - &cli.BoolFlag{Name: "daemon", DisableDefaultText: true, Action: flagDaemon, Usage: um["daemon"]}, - &cli.BoolFlag{Name: "stop", DisableDefaultText: true, Hidden: true, Action: flagStop, Usage: um["stop"]}, - &cli.BoolFlag{Name: "restart", DisableDefaultText: true, Hidden: true, Action: flagRestart, Usage: um["restart"]}, - &cli.BoolFlag{Name: "update", DisableDefaultText: true, Action: flagUpdate, Usage: um["update"]}, - &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用 - if !cCtx.Bool("update") { - defer checkAndNoticeUpdate() - } - for _, flag := range []string{"init", "server", "daemon", "stop", "restart", "update", "generate-config", "edit-config", "status"} { - if cCtx.Bool(flag) { - return nil - } - } - - if cfg.FileExists { - di, err := daemon.GetDaemonInfo() - if err == nil && cfg.ModTime > di.StartTime { - d.EchoWarn("检测到配置文件发生修改,正在重启守护进程") - flagRestart(cCtx, true) - } - } - - if cCtx.String("theme") != "" { - cfg.Theme = cCtx.String("theme") - } - d.ApplyTheme(cfg.Theme) - - if cCtx.Args().Len() > 0 { - zap.S().Debugf("Recieved Arguments (len: %d): %+v \n", cCtx.Args().Len(), cCtx.Args().Slice()) - // emoji.Printf("Test emoji:\n:accept: :inbox_tray: :information: :us: :uk: 🗣 :lips: :eyes: :balloon: \n") - - qstr := strings.Join(cCtx.Args().Slice(), " ") - - r, err := internal.Query(qstr, cCtx.Bool("nocache"), cCtx.Bool("text")) - if cfg.FreqAlert { - if h := <-r.History; h > 3 { - d.EchoWarn(fmt.Sprintf("本月第%d次查询`%s`", h, r.Query)) - } - } - if err == nil { - if r.Found { - err = pkg.OutputResult(query.PrettyFormat(r, cfg.EnglishOnly), cfg.Paging, cfg.PagerCommand, cfg.ClearScreen) - if err != nil { - d.EchoFatal(err.Error()) - } - } else { - if r.Prompt != "" { - d.EchoWrong(r.Prompt) - } else { - fmt.Println("Not found", d.Yellow(":(")) - } - } - } else { - d.EchoError(err.Error()) - zap.S().Errorf("%+v\n", err) - } - } else { - showPrompt() - } - return nil - }, - } - - if err := app.Run(os.Args); err != nil { - zap.S().Errorf("APP stopped: %s", err) - d.EchoError(err.Error()) - } + basicCheck() + if err := config.InitConfig(); err != nil { + if !pkg.HasAnyFlag("status", "edit-config", "generate-config") { // XXX (k): <2024-10-18 22:35> 可能不够 + d.EchoFatal(err.Error()) + } + d.EchoWarn(err.Error()) + } + cfg := config.Cfg + d.ApplyConfig(cfg.EnableEmoji) + run.Info.Version = VERSION + + if cfg.Logging.Enable { + l, err := logger.InitLogger(&cfg.Logging) + if err != nil { + d.EchoFatal(err.Error()) + } + defer l.Sync() + } + + if err := cache.InitDB(); err != nil { + d.EchoFatal(err.Error()) + } + defer cache.LiteDB.Close() + defer core.WG.Wait() + + app := &cli.App{ + Suggest: true, // XXX + Name: "kd", + Version: VERSION, + Usage: "A crystal clean command-line dictionary.", + HideHelpCommand: true, + // EnableBashCompletion: true, + + Authors: []*cli.Author{{Name: "kmz", Email: "valesail7@gmail.com"}}, + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "text", Aliases: []string{"t"}, Hidden: true, Usage: um["text"]}, + &cli.BoolFlag{Name: "nocache", Aliases: []string{"n"}, DisableDefaultText: true, Usage: um["nocache"]}, + &cli.StringFlag{Name: "theme", Aliases: []string{"T"}, DefaultText: "temp", Usage: um["theme"]}, + &cli.BoolFlag{Name: "force", Aliases: []string{"f"}, DisableDefaultText: true, Hidden: true}, + + // BoolFlags as commands + // &cli.BoolFlag{Name: "init", DisableDefaultText: true, Hidden: true, Usage: um["init"]}, + &cli.BoolFlag{Name: "server", DisableDefaultText: true, Action: flagServer, Hidden: true, Usage: um["server"]}, + &cli.BoolFlag{Name: "daemon", DisableDefaultText: true, Action: flagDaemon, Usage: um["daemon"]}, + &cli.BoolFlag{Name: "stop", DisableDefaultText: true, Hidden: true, Action: flagStop, Usage: um["stop"]}, + &cli.BoolFlag{Name: "restart", DisableDefaultText: true, Hidden: true, Action: flagRestart, Usage: um["restart"]}, + &cli.BoolFlag{Name: "update", DisableDefaultText: true, Action: flagUpdate, Usage: um["update"]}, + &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用 + if !cCtx.Bool("update") { + defer checkAndNoticeUpdate() + } + + if pkg.HasAnyFlag("init", "server", "daemon", "stop", "restart", "update", "generate-config", "edit-config", "status") { + return nil + } + + if cfg.FileExists { + di, err := daemon.GetDaemonInfo() + if err == nil && cfg.ModTime > di.StartTime { + d.EchoWarn("检测到配置文件发生修改,正在重启守护进程") + flagRestart(cCtx, true) + } + } + + if cCtx.String("theme") != "" { + cfg.Theme = cCtx.String("theme") + } + d.ApplyTheme(cfg.Theme) + + if cCtx.Args().Len() > 0 { + zap.S().Debugf("Recieved Arguments (len: %d): %+v \n", cCtx.Args().Len(), cCtx.Args().Slice()) + // emoji.Printf("Test emoji:\n:accept: :inbox_tray: :information: :us: :uk: 🗣 :lips: :eyes: :balloon: \n") + + qstr := strings.Join(cCtx.Args().Slice(), " ") + + r, err := internal.Query(qstr, cCtx.Bool("nocache"), cCtx.Bool("text")) + if cfg.FreqAlert { + if h := <-r.History; h > 3 { + d.EchoWarn(fmt.Sprintf("本月第%d次查询`%s`", h, r.Query)) + } + } + if err == nil { + if r.Found { + err = pkg.OutputResult(query.PrettyFormat(r, cfg.EnglishOnly), cfg.Paging, cfg.PagerCommand, cfg.ClearScreen) + if err != nil { + d.EchoFatal(err.Error()) + } + } else { + if r.Prompt != "" { + d.EchoWrong(r.Prompt) + } else { + fmt.Println("Not found", d.Yellow(":(")) + } + } + } else { + d.EchoError(err.Error()) + zap.S().Errorf("%+v\n", err) + } + } else { + showPrompt() + } + return nil + }, + } + + if err := app.Run(os.Args); err != nil { + zap.S().Errorf("APP stopped: %s", err) + d.EchoError(err.Error()) + } } diff --git a/config/config.go b/config/config.go index 226340a..a43d758 100644 --- a/config/config.go +++ b/config/config.go @@ -1,125 +1,122 @@ package config import ( - "fmt" - "net/http" - "net/url" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/Karmenzind/kd/internal/run" - "github.com/Karmenzind/kd/pkg/str" - "github.com/jinzhu/configor" + "fmt" + "net/http" + "net/url" + "os" + "path/filepath" + "runtime" + "slices" + "strings" + + "github.com/Karmenzind/kd/internal/run" + "github.com/Karmenzind/kd/pkg" + "github.com/jinzhu/configor" ) var CONFIG_PATH string type LoggerConfig struct { - Enable bool `default:"true" toml:"enable"` - Path string `toml:"path"` - Level string `default:"info" toml:"level"` - Stderr bool `default:"false" toml:"stderr"` - RedirectToStream bool `default:"false" toml:"redirect_to_stream"` + Enable bool `default:"true" toml:"enable"` + Path string `toml:"path"` + Level string `default:"info" toml:"level"` + Stderr bool `default:"false" toml:"stderr"` + RedirectToStream bool `default:"false" toml:"redirect_to_stream"` } type Config struct { - // FIXME - Debug bool `default:"false" toml:"debug"` - - // Modules []string - EnableEmoji bool `default:"true" toml:"enable_emoji"` - Paging bool `default:"true" toml:"paging"` - PagerCommand string `toml:"pager_command"` - AutoClear bool `default:"false" toml:"auto_clear"` - // MaxCached uint `default:"10000" toml:"max_cached"` - EnglishOnly bool `default:"false" toml:"english_only"` - Theme string `default:"temp" toml:"theme"` - HTTPProxy string `toml:"http_proxy"` - ClearScreen bool `toml:"clear_screen" default:"false"` - FreqAlert bool `toml:"freq_alert" default:"false"` - - Logging LoggerConfig `toml:"logging"` - - FileExists bool `toml:"-"` - ModTime int64 `toml:"-"` + // FIXME + Debug bool `default:"false" toml:"debug"` + + // Modules []string + EnableEmoji bool `default:"true" toml:"enable_emoji"` + Paging bool `default:"true" toml:"paging"` + PagerCommand string `toml:"pager_command"` + AutoClear bool `default:"false" toml:"auto_clear"` + // MaxCached uint `default:"10000" toml:"max_cached"` + EnglishOnly bool `default:"false" toml:"english_only"` + Theme string `default:"temp" toml:"theme"` + HTTPProxy string `toml:"http_proxy"` + ClearScreen bool `toml:"clear_screen" default:"false"` + FreqAlert bool `toml:"freq_alert" default:"false"` + + Logging LoggerConfig `toml:"logging"` + + FileExists bool `toml:"-"` + ModTime int64 `toml:"-"` } func (c *Config) CheckAndApply() (err error) { - if c.HTTPProxy != "" { - proxyUrl, err := url.Parse(c.HTTPProxy) - if err != nil { - return fmt.Errorf("[http_proxy] 代理地址格式不合法") - } - http.DefaultTransport = &http.Transport{Proxy: http.ProxyURL(proxyUrl)} - } - if c.Logging.Level != "" { - c.Logging.Level = strings.ToLower(c.Logging.Level) - if c.Logging.Level == "warning" { - 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 + if c.HTTPProxy != "" { + proxyUrl, err := url.Parse(c.HTTPProxy) + if err != nil { + return fmt.Errorf("[http_proxy] 代理地址格式不合法") + } + http.DefaultTransport = &http.Transport{Proxy: http.ProxyURL(proxyUrl)} + } + + if c.Logging.Level != "" { + c.Logging.Level = strings.ToLower(c.Logging.Level) + if c.Logging.Level == "warning" { + c.Logging.Level = "warn" + } else if !slices.Contains([]string{"debug", "info", "warn", "panic", "fatal"}, c.Logging.Level) { + return fmt.Errorf("[logging.level] 不支持的日志等级:%s", c.Logging.Level) } } - return + + if pkg.HasAnyFlag("log-to-stream") { + c.Logging.Enable = true + c.Logging.RedirectToStream = true + } + return } var Cfg = Config{} func getConfigPath() string { - var p string - dirname, _ := os.UserHomeDir() - if runtime.GOOS == "windows" { - p = filepath.Join(dirname, "kd.toml") - } else { - p = filepath.Join(dirname, ".config", "kd.toml") - } - return p + var p string + dirname, _ := os.UserHomeDir() + if runtime.GOOS == "windows" { + p = filepath.Join(dirname, "kd.toml") + } else { + p = filepath.Join(dirname, ".config", "kd.toml") + } + return p } // func getDaemonCreatetime() int64 { // } func parseConfig() (err error) { - p := CONFIG_PATH - if fileinfo, fileErr := os.Stat(p); fileErr == nil { - Cfg.FileExists = true - Cfg.ModTime = fileinfo.ModTime().Unix() - err = configor.New(&configor.Config{ErrorOnUnmatchedKeys: false}).Load(&Cfg, p) - } else { - // 没有配置文件,部分默认值处理 - err = configor.New(&configor.Config{ErrorOnUnmatchedKeys: false}).Load(&Cfg) - switch runtime.GOOS { - case "darwin": //MacOS - Cfg.Paging = false + p := CONFIG_PATH + if fileinfo, fileErr := os.Stat(p); fileErr == nil { + Cfg.FileExists = true + Cfg.ModTime = fileinfo.ModTime().Unix() + err = configor.New(&configor.Config{ErrorOnUnmatchedKeys: false}).Load(&Cfg, p) + } else { + // 没有配置文件,部分默认值处理 + err = configor.New(&configor.Config{ErrorOnUnmatchedKeys: false}).Load(&Cfg) + switch runtime.GOOS { + case "darwin": // MacOS + Cfg.Paging = false case "linux": if run.Info.GetOSInfo().IsDebianBased { Cfg.Paging = false } - } - } - return err + } + } + return err } func InitConfig() error { - CONFIG_PATH = getConfigPath() - err := parseConfig() - if err != nil { - panic(fmt.Sprintf("Failed to parse configuration file: %s", err)) - } - - err = Cfg.CheckAndApply() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return err + CONFIG_PATH = getConfigPath() + err := parseConfig() + if err != nil { + if strings.HasPrefix(err.Error(), "toml") { + return fmt.Errorf("解析配置文件失败,请检查toml文件语法(%s)", err) + } + return fmt.Errorf("解析配置文件失败(%s)", err) + } + return Cfg.CheckAndApply() } diff --git a/config/output.go b/config/output.go index 6395baa..ab1e9f7 100644 --- a/config/output.go +++ b/config/output.go @@ -1,33 +1,29 @@ package config import ( - "bytes" - "strings" + "bytes" + "strings" - "github.com/BurntSushi/toml" + "github.com/BurntSushi/toml" ) func GenerateDefaultConfig() (string, error) { - var buf = new(bytes.Buffer) - var err error - encoder := toml.NewEncoder(buf) - err = encoder.Encode(Cfg) - if err != nil { - return "", err - } - var validline int - lines := strings.Split(buf.String(), "\n") - for _, line := range lines { - // prefix := strings.SplitN(line, " ", 2)[0] - // switch prefix { - // case "debug", "english_only", "": - // } - if strings.HasPrefix(line, "debug =") || - strings.HasPrefix(line, "enable_emoji =") { - continue - } - lines[validline] = line - validline++ - } - return strings.Join(lines[:validline], "\n"), err + var buf = new(bytes.Buffer) + var err error + encoder := toml.NewEncoder(buf) + err = encoder.Encode(Cfg) + if err != nil { + return "", err + } + var validline int + lines := strings.Split(buf.String(), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "debug =") || + strings.HasPrefix(line, "enable_emoji =") { + continue + } + lines[validline] = line + validline++ + } + return strings.Join(lines[:validline], "\n"), err } diff --git a/go.mod b/go.mod index eabbcc2..09a2711 100644 --- a/go.mod +++ b/go.mod @@ -1,39 +1,38 @@ module github.com/Karmenzind/kd -go 1.21.3 +go 1.23 require ( - github.com/BurntSushi/toml v1.3.2 + github.com/BurntSushi/toml v1.4.0 github.com/anaskhan96/soup v1.2.5 - github.com/fatih/color v1.16.0 + github.com/fatih/color v1.17.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/gookit/color v1.5.4 - github.com/jinzhu/configor v1.2.1 - github.com/kyokomi/emoji/v2 v2.2.12 - github.com/mattn/go-sqlite3 v1.14.18 - github.com/shirou/gopsutil/v3 v3.23.11 - github.com/urfave/cli/v2 v2.26.0 - go.uber.org/zap v1.26.0 - golang.org/x/term v0.15.0 + github.com/jinzhu/configor v1.2.2 + github.com/mattn/go-sqlite3 v1.14.24 + github.com/shirou/gopsutil/v3 v3.24.5 + github.com/urfave/cli/v2 v2.27.5 + go.uber.org/zap v1.27.0 + golang.org/x/term v0.25.0 ) require ( - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - github.com/yusufpapurcu/wmi v1.2.3 // indirect - go.uber.org/multierr v1.10.0 // indirect - golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/text v0.3.0 // indirect - gopkg.in/yaml.v2 v2.2.2 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.9.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 013dace..930cd28 100644 --- a/go.sum +++ b/go.sum @@ -1,97 +1,90 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/anaskhan96/soup v1.2.5 h1:V/FHiusdTrPrdF4iA1YkVxsOpdNcgvqT1hG+YtcZ5hM= github.com/anaskhan96/soup v1.2.5/go.mod h1:6YnEp9A2yywlYdM4EgDz9NEHclocMepEtku7wg6Cq3s= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= -github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko= -github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc= -github.com/kyokomi/emoji/v2 v2.2.12 h1:sSVA5nH9ebR3Zji1o31wu3yOwD1zKXQA2z0zUyeit60= -github.com/kyokomi/emoji/v2 v2.2.12/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/jinzhu/configor v1.2.2 h1:sLgh6KMzpCmaQB4e+9Fu/29VErtBUqsS2t8C9BNIVsA= +github.com/jinzhu/configor v1.2.2/go.mod h1:iFFSfOBKP3kC2Dku0ZGB3t3aulfQgTGJknodhFavsU8= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= -github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shirou/gopsutil/v3 v3.23.11 h1:i3jP9NjCPUz7FiZKxlMnODZkdSIp2gnzfrvsu9CuWEQ= -github.com/shirou/gopsutil/v3 v3.23.11/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI= -github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= -github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= -github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= -github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= +github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cache/cache.go b/internal/cache/cache.go index f662645..bc258da 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -1,80 +1,79 @@ package cache import ( - "bytes" - "compress/zlib" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "time" - - "github.com/Karmenzind/kd/internal/model" - "github.com/Karmenzind/kd/pkg" - "go.uber.org/zap" + "bytes" + "compress/zlib" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "time" + + "github.com/Karmenzind/kd/internal/model" + "github.com/Karmenzind/kd/pkg" + "go.uber.org/zap" ) func GetCachedQuery(r *model.Result) (err error) { - z, err := getCachedRow(r.Query, r.IsEN) - if err != nil { - zap.S().Debugf("Failed to query database for %s: %s", r.Query, err) - return err - } - - zb := bytes.NewBuffer(z) - c, err := zlib.NewReader(zb) - if err != nil { - zap.S().Debugf("Failed to decompress data for %s: %s", r.Query, err) - return err - } - var jb bytes.Buffer - _, err = io.Copy(&jb, c) - if err != nil { - zap.S().Errorf("Failed to read buffer: %s", err) - } - c.Close() - j := jb.Bytes() - zap.S().Debugf("Got cached json %s", j) - - if len(j) > 0 { - err = json.Unmarshal(j, r) - if err != nil { - zap.S().Debugf("Failed to unmarshal for %s: %s", r.Query, err) - - return err - } - } - zap.S().Debugf("Got cached %s. (len: %d)", r.Query, len(j)) - return + z, err := getCachedRow(r.Query, r.IsEN) + if err != nil { + zap.S().Debugf("Failed to query database for %s: %s", r.Query, err) + return + } + + zb := bytes.NewBuffer(z) + c, err := zlib.NewReader(zb) + if err != nil { + zap.S().Debugf("Failed to decompress data for %s: %s", r.Query, err) + return + } + var jb bytes.Buffer + _, err = io.Copy(&jb, c) + if err != nil { + zap.S().Errorf("Failed to read buffer: %s", err) + } + c.Close() + j := jb.Bytes() + zap.S().Debugf("Got cached json %s", j) + + if len(j) > 0 { + err = json.Unmarshal(j, r) + if err != nil { + zap.S().Debugf("Failed to unmarshal for %s: %s", r.Query, err) + return + } + } + zap.S().Debugf("Got cached %s. (len: %d)", r.Query, len(j)) + return } func UpdateQueryCache(r *model.Result) (err error) { - if !r.Found { - return - } - - j, err := json.Marshal(r) - if err != nil { - zap.S().Warnf("Failed to marshal %+v: %s", r, err) - return - } - zap.S().Debugf("Got marshalled json to save: %s", j) - - var zb bytes.Buffer - jw := zlib.NewWriter(&zb) - jw.Write(j) - jw.Close() - detail := zb.Bytes() - - err = saveCachedRow(r.Query, r.IsEN, detail) - - if err != nil { - zap.S().Errorf("Failed to update cache for '%s'. Error: %s", r.Query, err) - } - zap.S().Debugf("Updated cache for '%s'. len: %d", r.Query, len(detail)) - return + if !r.Found { + return + } + + j, err := json.Marshal(r) + if err != nil { + zap.S().Warnf("Failed to marshal %+v: %s", r, err) + return + } + zap.S().Debugf("Got marshalled json to save: %s", j) + + var zb bytes.Buffer + jw := zlib.NewWriter(&zb) + jw.Write(j) + jw.Close() + detail := zb.Bytes() + + err = saveCachedRow(r.Query, r.IsEN, detail) + + if err != nil { + zap.S().Errorf("Failed to update cache for '%s'. Error: %s", r.Query, err) + } + zap.S().Debugf("Updated cache for '%s'. len: %d", r.Query, len(detail)) + return } // ----------------------------------------------------------------------------- @@ -82,46 +81,46 @@ func UpdateQueryCache(r *model.Result) (err error) { // ----------------------------------------------------------------------------- type LongTextData struct { - Result string `json:"r"` - AccessTS int64 `json:"a"` - CreateTS int64 `json:"c"` + Result string `json:"r"` + AccessTS int64 `json:"a"` + CreateTS int64 `json:"c"` } func GetLongTextCache(r *model.Result) (err error) { - var m map[string]LongTextData - if pkg.IsPathExists(LONG_TEXT_CACHE_FILE) { - err = pkg.LoadJson(LONG_TEXT_CACHE_FILE, &m) - if err != nil { - return err - } - if res, ok := m[r.Query]; ok { - r.MachineTrans = res.Result - zap.S().Debugf("Got cached '%s'", r.Query) - (&res).AccessTS = time.Now().Unix() - m[r.Query] = res - go pkg.SaveJson(LONG_TEXT_CACHE_FILE, &m) - return - } else { - return fmt.Errorf("no cache for %s", r.Query) - } - } - return fmt.Errorf("cache file not found") + var m map[string]LongTextData + if pkg.IsPathExists(LONG_TEXT_CACHE_FILE) { + err = pkg.LoadJson(LONG_TEXT_CACHE_FILE, &m) + if err != nil { + return err + } + if res, ok := m[r.Query]; ok { + r.MachineTrans = res.Result + zap.S().Debugf("Got cached '%s'", r.Query) + (&res).AccessTS = time.Now().Unix() + m[r.Query] = res + go pkg.SaveJson(LONG_TEXT_CACHE_FILE, &m) + return + } else { + return fmt.Errorf("no cache for %s", r.Query) + } + } + return fmt.Errorf("cache file not found") } func UpdateLongTextCache(r *model.Result) (err error) { - var m map[string]LongTextData - if pkg.IsPathExists(LONG_TEXT_CACHE_FILE) { - err = pkg.LoadJson(LONG_TEXT_CACHE_FILE, &m) - if err != nil { - return err - } - } else { - m = map[string]LongTextData{} - } - now := time.Now().Unix() - m[r.Query] = LongTextData{r.MachineTrans, now, now} - err = pkg.SaveJson(LONG_TEXT_CACHE_FILE, m) - return err + var m map[string]LongTextData + if pkg.IsPathExists(LONG_TEXT_CACHE_FILE) { + err = pkg.LoadJson(LONG_TEXT_CACHE_FILE, &m) + if err != nil { + return err + } + } else { + m = map[string]LongTextData{} + } + now := time.Now().Unix() + m[r.Query] = LongTextData{r.MachineTrans, now, now} + err = pkg.SaveJson(LONG_TEXT_CACHE_FILE, m) + return err } // ----------------------------------------------------------------------------- @@ -130,39 +129,39 @@ func UpdateLongTextCache(r *model.Result) (err error) { // ----------------------------------------------------------------------------- func GetCachedQueryFromJson(r *model.Result) (err error) { - fpath := getQueryCacheFilePath(r.Query) - if _, err = os.Stat(fpath); errors.Is(err, os.ErrNotExist) { - zap.S().Debugf("Cache file for '%s' doesn't exist.", r.Query) - return err - } - j, err := os.ReadFile(fpath) - if err != nil { - zap.S().Debugf("Failed to read file for %s: %s", r.Query, err) - return err - } - if len(j) > 0 { - err = json.Unmarshal(j, r) - if err != nil { - zap.S().Debugf("Failed to unmarshal for %s: %s", r.Query, err) - return err - } - } - zap.S().Debugf("Got cached %s. (len: %d)", r.Query, len(j)) - return + fpath := getQueryCacheFilePath(r.Query) + if _, err = os.Stat(fpath); errors.Is(err, os.ErrNotExist) { + zap.S().Debugf("Cache file for '%s' doesn't exist.", r.Query) + return err + } + j, err := os.ReadFile(fpath) + if err != nil { + zap.S().Debugf("Failed to read file for %s: %s", r.Query, err) + return err + } + if len(j) > 0 { + err = json.Unmarshal(j, r) + if err != nil { + zap.S().Debugf("Failed to unmarshal for %s: %s", r.Query, err) + return err + } + } + zap.S().Debugf("Got cached %s. (len: %d)", r.Query, len(j)) + return } func UpdateQueryCacheJson(r *model.Result) (err error) { - if !r.Found { - return - } - err = pkg.SaveJson(getQueryCacheFilePath(r.Query), r) - if err != nil { - zap.S().Errorf("Failed to update cache for '%s'. Error: %s", r.Query, err) - } - return + if !r.Found { + return + } + err = pkg.SaveJson(getQueryCacheFilePath(r.Query), r) + if err != nil { + zap.S().Errorf("Failed to update cache for '%s'. Error: %s", r.Query, err) + } + return } func getQueryCacheFilePath(query string) string { - fpath := filepath.Join(CACHE_WORDS_PATH, query) - return fpath + fpath := filepath.Join(CACHE_WORDS_PATH, query) + return fpath } diff --git a/internal/cache/counter.go b/internal/cache/counter.go index 71b9034..ce51caf 100644 --- a/internal/cache/counter.go +++ b/internal/cache/counter.go @@ -1,13 +1,13 @@ package cache import ( - "fmt" - "path/filepath" - "time" + "fmt" + "path/filepath" + "time" - "github.com/Karmenzind/kd/internal/core" - "github.com/Karmenzind/kd/pkg" - "go.uber.org/zap" + "github.com/Karmenzind/kd/internal/core" + "github.com/Karmenzind/kd/pkg" + "go.uber.org/zap" ) type MonthCounter map[string]int @@ -15,25 +15,25 @@ type MonthCounter map[string]int // XXX 后续考虑二级映射 func CounterIncr(query string, history chan int) { - defer func() { - if len(history) == 0 { - history <- 0 - } - core.WG.Done() - }() + defer func() { + if len(history) == 0 { + history <- 0 + } + core.WG.Done() + }() - n := time.Now() - c := make(MonthCounter) - counterPath := filepath.Join(CACHE_STAT_DIR_PATH, fmt.Sprintf("counter-%d%02d.json", n.Year(), int(n.Month()))) - if pkg.IsPathExists(counterPath) { - err := pkg.LoadJson(counterPath, &c) - if err != nil { - zap.S().Warnf("Failed to load counter") - return - } - // zap.S().Debugf("Loaded counter: %+v", c) - } - c[query] += 1 - history <- c[query] - pkg.SaveJson(counterPath, &c) + n := time.Now() + c := make(MonthCounter) + counterPath := filepath.Join(CACHE_STAT_DIR_PATH, fmt.Sprintf("counter-%d%02d.json", n.Year(), int(n.Month()))) + if pkg.IsPathExists(counterPath) { + err := pkg.LoadJson(counterPath, &c) + if err != nil { + zap.S().Warnf("Failed to load counter") + return + } + // zap.S().Debugf("Loaded counter: %+v", c) + } + c[query] += 1 + history <- c[query] + pkg.SaveJson(counterPath, &c) } diff --git a/internal/cache/database.go b/internal/cache/database.go index e646280..ea0f9e4 100644 --- a/internal/cache/database.go +++ b/internal/cache/database.go @@ -1,13 +1,13 @@ package cache import ( - "database/sql" - "fmt" - "path/filepath" - "time" + "database/sql" + "fmt" + "path/filepath" + "time" - _ "github.com/mattn/go-sqlite3" - "go.uber.org/zap" + _ "github.com/mattn/go-sqlite3" + "go.uber.org/zap" ) var DB_FILENAME = "kd_data.db" @@ -15,17 +15,17 @@ var DB_FILENAME = "kd_data.db" var LiteDB *sql.DB func InitDB() error { - var err error + var err error - dbPath := filepath.Join(CACHE_ROOT_PATH, DB_FILENAME) + dbPath := filepath.Join(CACHE_ROOT_PATH, DB_FILENAME) - LiteDB, err = sql.Open("sqlite3", dbPath) - if err != nil { - zap.S().Errorf("Failed to open litedb: %s", err) - return err - } + LiteDB, err = sql.Open("sqlite3", dbPath) + if err != nil { + zap.S().Errorf("Failed to open litedb: %s", err) + return err + } - sqlStmt := ` + sqlStmt := ` CREATE TABLE IF NOT EXISTS en ( query text NOT NULL UNIQUE PRIMARY KEY, detail text NOT NULL, @@ -36,61 +36,61 @@ CREATE TABLE IF NOT EXISTS ch ( detail text NOT NULL, update_time datetime NOT NULL) WITHOUT ROWID; ` - _, err = LiteDB.Exec(sqlStmt) + _, err = LiteDB.Exec(sqlStmt) - if err != nil { - zap.S().Errorf("Failed to create table", err) - return err - } - return err + if err != nil { + zap.S().Errorf("Failed to create table", err) + return err + } + return err } func getCachedRow(query string, isEN bool) ([]byte, error) { - var err error - var sql string - if isEN { - sql = "SELECT detail, update_time FROM en WHERE query = ?" - } else { - sql = "SELECT detail, update_time FROM ch WHERE query = ?" - } - row := LiteDB.QueryRow(sql, query) - if row.Err() != nil { - zap.S().Warnf("Failed to query %s: %s", query, row.Err()) - return []byte{}, row.Err() - } - var updateTime time.Time - var detail []byte + var err error + var sql string + if isEN { + sql = "SELECT detail, update_time FROM en WHERE query = ?" + } else { + sql = "SELECT detail, update_time FROM ch WHERE query = ?" + } + row := LiteDB.QueryRow(sql, query) + if row.Err() != nil { + zap.S().Warnf("Failed to query %s: %s", query, row.Err()) + return []byte{}, row.Err() + } + var updateTime time.Time + var detail []byte - err = row.Scan(&detail, &updateTime) - if err != nil { - return []byte{}, err - } - zap.S().Debugf("Got %s with detail length %d update time: %s", query, len(detail), updateTime) - return detail, nil + err = row.Scan(&detail, &updateTime) + if err != nil { + return []byte{}, err + } + zap.S().Debugf("Got %s with detail length %d update time: %s", query, len(detail), updateTime) + return detail, nil } func saveCachedRow(query string, isEN bool, detail []byte) error { - var table string - var err error - if isEN { - table = "en" - } else { - table = "ch" - } - tx, err := LiteDB.Begin() - if err != nil { - zap.S().Warnf("Failed to open transaction for %s: %s", query, err) - return err - } - sql := fmt.Sprintf("INSERT OR REPLACE INTO %s (query, detail, update_time) VALUES(?, ?, ?)", table) - _, err = tx.Exec(sql, query, detail, time.Now()) - if err != nil { - zap.S().Warnf("Failed to insert %s: %s", query, err) - return err - } - err = tx.Commit() - if err != nil { - zap.S().Warnf("Failed to commit transaction for %s: %s", query, err) - } - return err + var table string + var err error + if isEN { + table = "en" + } else { + table = "ch" + } + tx, err := LiteDB.Begin() + if err != nil { + zap.S().Warnf("Failed to open transaction for %s: %s", query, err) + return err + } + sql := fmt.Sprintf("INSERT OR REPLACE INTO %s (query, detail, update_time) VALUES(?, ?, ?)", table) + _, err = tx.Exec(sql, query, detail, time.Now()) + if err != nil { + zap.S().Warnf("Failed to insert %s: %s", query, err) + return err + } + err = tx.Commit() + if err != nil { + zap.S().Warnf("Failed to commit transaction for %s: %s", query, err) + } + return err } diff --git a/internal/cache/notfound.go b/internal/cache/notfound.go index 89a3c2a..816e56a 100644 --- a/internal/cache/notfound.go +++ b/internal/cache/notfound.go @@ -1,17 +1,17 @@ package cache import ( - "bufio" - "os" - "path/filepath" - "strings" + "bufio" + "os" + "path/filepath" + "strings" - "github.com/Karmenzind/kd/pkg" - "go.uber.org/zap" + "github.com/Karmenzind/kd/pkg" + "go.uber.org/zap" ) func getNotFoundFilePath() string { - return filepath.Join(CACHE_ROOT_PATH, "online_not_found") + return filepath.Join(CACHE_ROOT_PATH, "online_not_found") } func CheckNotFound(query string) (int, error) { @@ -20,49 +20,49 @@ func CheckNotFound(query string) (int, error) { return 0, nil } - f, err := os.Open(getNotFoundFilePath()) - if err != nil { - return 0, err - } - defer f.Close() - - // Splits on newlines by default. - scanner := bufio.NewScanner(f) - - line := 1 - // https://golang.org/pkg/bufio/#Scanner.Scan - for scanner.Scan() { - if scanner.Text() == query { - return line, nil - } - line++ - } - - if err := scanner.Err(); err != nil { - return 0, err - // Handle the error - } - return 0, nil + f, err := os.Open(getNotFoundFilePath()) + if err != nil { + return 0, err + } + defer f.Close() + + // Splits on newlines by default. + scanner := bufio.NewScanner(f) + + line := 1 + // https://golang.org/pkg/bufio/#Scanner.Scan + for scanner.Scan() { + if scanner.Text() == query { + return line, nil + } + line++ + } + + if err := scanner.Err(); err != nil { + return 0, err + // Handle the error + } + return 0, nil } func AppendNotFound(query string) (err error) { p := getNotFoundFilePath() - file, err := os.OpenFile(p, os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModeAppend|os.ModePerm) + file, err := os.OpenFile(p, os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModeAppend|os.ModePerm) - if err != nil { + if err != nil { zap.S().Warnf("Failed to open %s: %s", p, err) - return - } + return + } - defer file.Close() + defer file.Close() - _, err = file.WriteString(query + "\n") + _, err = file.WriteString(query + "\n") - if err != nil { + if err != nil { zap.S().Warnf("Failed to write to %s: %s", p, err) - return - } - return + return + } + return } func RemoveNotFound(query string) error { @@ -70,43 +70,42 @@ func RemoveNotFound(query string) error { if !pkg.IsPathExists(p) { return nil } - f, err := os.Open(getNotFoundFilePath()) - if err != nil { - return err - } + f, err := os.Open(getNotFoundFilePath()) + if err != nil { + return err + } newfile := p + ".new" defer func() { - f.Close() + f.Close() if pkg.IsPathExists(newfile) { os.Rename(newfile, p) zap.S().Infof("Replaced not found list. Removed: %s", query) } }() - scanner := bufio.NewScanner(f) + scanner := bufio.NewScanner(f) newLines := []string{} - // https://golang.org/pkg/bufio/#Scanner.Scan - for scanner.Scan() { + // https://golang.org/pkg/bufio/#Scanner.Scan + for scanner.Scan() { line := scanner.Text() - if line == "" || line == query { + if line == "" || line == query { continue - } + } newLines = append(newLines, line) - } + } - if err = scanner.Err(); err != nil { - return err - // Handle the error - } + if err = scanner.Err(); err != nil { + return err + // Handle the error + } content := strings.Join(newLines, "\n") - err = os.WriteFile(newfile, []byte(content), os.ModePerm) - zap.S().Debugf("Updated json file %s. Error: %v. BodyLength: %d", newfile, err, len(content)) + err = os.WriteFile(newfile, []byte(content), os.ModePerm) + zap.S().Debugf("Updated json file %s. Error: %v. BodyLength: %d", newfile, err, len(content)) if err != nil { return err } - return nil + return nil } - diff --git a/internal/cache/utils.go b/internal/cache/utils.go index acd2898..3c0d31b 100644 --- a/internal/cache/utils.go +++ b/internal/cache/utils.go @@ -1,12 +1,12 @@ package cache import ( - "fmt" - "os" - "path/filepath" + "fmt" + "os" + "path/filepath" - "github.com/Karmenzind/kd/internal/run" - d "github.com/Karmenzind/kd/pkg/decorate" + "github.com/Karmenzind/kd/internal/run" + d "github.com/Karmenzind/kd/pkg/decorate" ) var CACHE_ROOT_PATH = run.CACHE_ROOT_PATH @@ -17,16 +17,16 @@ var CACHE_STAT_DIR_PATH = run.CACHE_STAT_DIR_PATH var LONG_TEXT_CACHE_FILE string func init() { - for _, directory := range []string{ - CACHE_ROOT_PATH, - CACHE_WORDS_PATH, - CACHE_STAT_DIR_PATH, - CACHE_RUN_PATH, - } { - err := os.MkdirAll(directory, os.ModePerm) - if err != nil { - d.EchoFatal(fmt.Sprintf("Failed to create %s", directory)) - } - } + for _, directory := range []string{ + CACHE_ROOT_PATH, + CACHE_WORDS_PATH, + CACHE_STAT_DIR_PATH, + CACHE_RUN_PATH, + } { + err := os.MkdirAll(directory, os.ModePerm) + if err != nil { + d.EchoFatal(fmt.Sprintf("Failed to create %s", directory)) + } + } LONG_TEXT_CACHE_FILE = filepath.Join(CACHE_ROOT_PATH, "long_text_results.json") } diff --git a/internal/daemon/cron.go b/internal/daemon/cron.go index 215fa5a..05b7459 100644 --- a/internal/daemon/cron.go +++ b/internal/daemon/cron.go @@ -1,374 +1,374 @@ package daemon import ( - "archive/zip" - "database/sql" - "fmt" - "io" - "os" - "path/filepath" - "runtime" - "time" + "archive/zip" + "database/sql" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "time" - "github.com/Karmenzind/kd/internal/cache" - "github.com/Karmenzind/kd/internal/run" - "github.com/Karmenzind/kd/internal/update" - "github.com/Karmenzind/kd/pkg" - d "github.com/Karmenzind/kd/pkg/decorate" - _ "github.com/mattn/go-sqlite3" - "go.uber.org/zap" + "github.com/Karmenzind/kd/internal/cache" + "github.com/Karmenzind/kd/internal/run" + "github.com/Karmenzind/kd/internal/update" + "github.com/Karmenzind/kd/pkg" + d "github.com/Karmenzind/kd/pkg/decorate" + _ "github.com/mattn/go-sqlite3" + "go.uber.org/zap" ) func InitCron() { - go cronCheckUpdate() - go cronUpdateDataZip() - go cronDeleteSpam() - go cronEnsureDaemonJsonFile() + go cronCheckUpdate() + go cronUpdateDataZip() + go cronDeleteSpam() + go cronEnsureDaemonJsonFile() } func cronEnsureDaemonJsonFile() { - ticker := time.NewTicker(5 * time.Minute) - for { - <-ticker.C - di, err := GetDaemonInfoFromFile() - // TODO 检查信息与当前是否匹配 - var needUpdate bool - if err != nil { - zap.S().Warnf("Failed to get daemon info from file: %s", err) - needUpdate = true - } else { - ri := run.Info + ticker := time.NewTicker(5 * time.Minute) + for { + <-ticker.C + di, err := GetDaemonInfoFromFile() + // TODO 检查信息与当前是否匹配 + var needUpdate bool + if err != nil { + zap.S().Warnf("Failed to get daemon info from file: %s", err) + needUpdate = true + } else { + ri := run.Info run.Info.SetOSInfo() - if !(ri.StartTime == di.StartTime && - ri.PID == di.PID && - ri.Port == di.Port && - ri.ExeName == di.ExeName && - ri.ExePath == di.ExePath && - ri.Version == di.Version) { - zap.S().Warn("DaemonInfo from json is different from current run.Info") - needUpdate = true - } - } - if needUpdate { - run.Info.SaveToFile(GetDaemonInfoPath()) - d.EchoRun("Update daemon.json") - } - } + if !(ri.StartTime == di.StartTime && + ri.PID == di.PID && + ri.Port == di.Port && + ri.ExeName == di.ExeName && + ri.ExePath == di.ExePath && + ri.Version == di.Version) { + zap.S().Warn("DaemonInfo from json is different from current run.Info") + needUpdate = true + } + } + if needUpdate { + run.Info.SaveToFile(GetDaemonInfoPath()) + d.EchoRun("Update daemon.json") + } + } } func cronDeleteSpam() { - ticker := time.NewTicker(600 * time.Second) - for { - <-ticker.C - zap.S().Debugf("Started clearing spam") - p, err := pkg.GetExecutablePath() - if err == nil { - bakPath := p + ".update_backup" - if pkg.IsPathExists(bakPath) { - err = os.Remove(bakPath) - if err != nil { - zap.S().Warnf("Failed to remove %s: %s", bakPath, err) - } - } - } - } + ticker := time.NewTicker(600 * time.Second) + for { + <-ticker.C + zap.S().Debugf("Started clearing spam") + p, err := pkg.GetExecutablePath() + if err == nil { + bakPath := p + ".update_backup" + if pkg.IsPathExists(bakPath) { + err = os.Remove(bakPath) + if err != nil { + zap.S().Warnf("Failed to remove %s: %s", bakPath, err) + } + } + } + } } func cronCheckUpdate() { - ticker := time.NewTicker(3600 * 12 * time.Second) - for { - // TODO (k): <2024-01-01> 改成检查文件Stat来判断时长 - <-ticker.C + ticker := time.NewTicker(3600 * 12 * time.Second) + for { + // TODO (k): <2024-01-01> 改成检查文件Stat来判断时长 + <-ticker.C - for i := 0; i < 3; i++ { - tag, err := update.GetLatestTag() - if err == nil { - fmt.Println("最新Tag", tag) - break - } - zap.S().Warnf("Failed to get latest tag: %s", err) - time.Sleep(5 * time.Second) - } + for i := 0; i < 3; i++ { + tag, err := update.GetLatestTag() + if err == nil { + fmt.Println("最新Tag", tag) + break + } + zap.S().Warnf("Failed to get latest tag: %s", err) + time.Sleep(5 * time.Second) + } - } + } } const DATA_ZIP_URL = "https://gitee.com/void_kmz/kd/releases/download/v0.0.1/kd_data.zip" func cronUpdateDataZip() { - ticker := time.NewTicker(3 * time.Second) - go func() { - for { - <-ticker.C - zap.S().Infof("Start check updating data zip") + ticker := time.NewTicker(3 * time.Second) + go func() { + for { + <-ticker.C + zap.S().Infof("Start check updating data zip") - dbPath := filepath.Join(cache.CACHE_ROOT_PATH, cache.DB_FILENAME) - tsFile := filepath.Join(cache.CACHE_RUN_PATH, "last_fetch_db") - zipPath := filepath.Join(cache.CACHE_ROOT_PATH, "kd_data.zip") - tempDBPath := dbPath + ".temp" + dbPath := filepath.Join(cache.CACHE_ROOT_PATH, cache.DB_FILENAME) + tsFile := filepath.Join(cache.CACHE_RUN_PATH, "last_fetch_db") + zipPath := filepath.Join(cache.CACHE_ROOT_PATH, "kd_data.zip") + tempDBPath := dbPath + ".temp" - if pkg.IsPathExists(tsFile) { - zap.S().Infof("Found last update record. Nothing to do.") - break - } - var need2Dl bool - dbFileInfo, _ := os.Stat(dbPath) - mb := dbFileInfo.Size() / 1024 / 1024 - zap.S().Debugf("Current db file size: %dMB", mb) + if pkg.IsPathExists(tsFile) { + zap.S().Infof("Found last update record. Nothing to do.") + break + } + var need2Dl bool + dbFileInfo, _ := os.Stat(dbPath) + mb := dbFileInfo.Size() / 1024 / 1024 + zap.S().Debugf("Current db file size: %dMB", mb) - if mb < 50 { - fmt.Println(1) - if pkg.IsPathExists(zipPath) { - // TODO checksum - fmt.Println(2) - if checksumZIP(zipPath) { - fmt.Println(3) - need2Dl = false - } - } else { - need2Dl = true - fmt.Println(":)") - } - } - zap.S().Debugf("Need to dl: %v", need2Dl) - var err error - // if need to download - if need2Dl { - zap.S().Debugf("Start downloading %s", DATA_ZIP_URL) - err = downloadDataZip(zipPath) - if err != nil { - zap.S().Warnf("Failed to download zip file: %s", err) - continue - } - } - err = decompressDBZip(tempDBPath, zipPath) - if err != nil { - zap.S().Warnf("Failed: %s. Current invalid file will be removed.", err) + if mb < 50 { + fmt.Println(1) + if pkg.IsPathExists(zipPath) { + // TODO checksum + fmt.Println(2) + if checksumZIP(zipPath) { + fmt.Println(3) + need2Dl = false + } + } else { + need2Dl = true + fmt.Println(":)") + } + } + zap.S().Debugf("Need to dl: %v", need2Dl) + var err error + // if need to download + if need2Dl { + zap.S().Debugf("Start downloading %s", DATA_ZIP_URL) + err = downloadDataZip(zipPath) + if err != nil { + zap.S().Warnf("Failed to download zip file: %s", err) + continue + } + } + err = decompressDBZip(tempDBPath, zipPath) + if err != nil { + zap.S().Warnf("Failed: %s. Current invalid file will be removed.", err) errDel := os.Remove(zipPath) if errDel != nil { zap.S().Warnf("Failed to remove file: %s", err) } - continue - } - zap.S().Infof("Decompressed DB zip file -> %s", tempDBPath) + continue + } + zap.S().Infof("Decompressed DB zip file -> %s", tempDBPath) - // err = parseDBAndInsertOffsetVersion(tempDBPath) - // os.Remove(tempDBPath) - // if err != nil { - // zap.S().Warnf("[parseDBAndInsert] Failed: %s", err) - // continue - // } + // err = parseDBAndInsertOffsetVersion(tempDBPath) + // os.Remove(tempDBPath) + // if err != nil { + // zap.S().Warnf("[parseDBAndInsert] Failed: %s", err) + // continue + // } - err = applyTempDB(dbPath, tempDBPath) - if err != nil { - zap.S().Warnf("Failed: %s", err) - continue - } + err = applyTempDB(dbPath, tempDBPath) + if err != nil { + zap.S().Warnf("Failed: %s", err) + continue + } - // success - os.WriteFile(tsFile, []byte(fmt.Sprint(time.Now().Unix())), os.ModePerm) + // success + os.WriteFile(tsFile, []byte(fmt.Sprint(time.Now().Unix())), os.ModePerm) - ticker.Stop() - // TODO 以消息通知形式 - zap.S().Info("DB文件发生改变,进程主动退出") - fmt.Println("DB文件发生改变,进程主动退出") - os.Exit(0) - break - // TODO (k): <2023-12-15> 看情况删除 - // os.Remove(zipPath) - } - }() + ticker.Stop() + // TODO 以消息通知形式 + zap.S().Info("DB文件发生改变,进程主动退出") + fmt.Println("DB文件发生改变,进程主动退出") + os.Exit(0) + break + // TODO (k): <2023-12-15> 看情况删除 + // os.Remove(zipPath) + } + }() } func checksumZIP(zipPath string) bool { - return true + return true } // download and checksum func downloadDataZip(savePath string) (err error) { - err = pkg.DownloadFile(savePath, DATA_ZIP_URL) - if err != nil { - zap.S().Warnf("Failed to download %s: %s", DATA_ZIP_URL) - } - zap.S().Info("Downloaded %s: %s", DATA_ZIP_URL) - return + err = pkg.DownloadFile(savePath, DATA_ZIP_URL) + if err != nil { + zap.S().Warnf("Failed to download %s: %s", DATA_ZIP_URL) + } + zap.S().Info("Downloaded %s: %s", DATA_ZIP_URL) + return } func decompressDBZip(tempDBPath, zipPath string) (err error) { - a, err := zip.OpenReader(zipPath) - if err != nil { - zap.S().Errorf("Failed to open archive %s: %s", zipPath, err) - return err - } - defer a.Close() + a, err := zip.OpenReader(zipPath) + if err != nil { + zap.S().Errorf("Failed to open archive %s: %s", zipPath, err) + return err + } + defer a.Close() - f := a.File[0] - dstFile, err := os.OpenFile(tempDBPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) - if err != nil { - zap.S().Warnf("Failed to open file %s to write", tempDBPath, err) - return - } - defer dstFile.Close() + f := a.File[0] + dstFile, err := os.OpenFile(tempDBPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + zap.S().Warnf("Failed to open file %s to write", tempDBPath, err) + return + } + defer dstFile.Close() - srcFile, err := f.Open() - if err != nil { - zap.S().Warnf("Failed to open file %s to read", f.Name, err) - return - } - defer srcFile.Close() + srcFile, err := f.Open() + if err != nil { + zap.S().Warnf("Failed to open file %s to read", f.Name, err) + return + } + defer srcFile.Close() - if _, err = io.Copy(dstFile, srcFile); err != nil { - zap.S().Warnf("Failed to copy file %s to %s", f.Name, tempDBPath, err) - return - } - return + if _, err = io.Copy(dstFile, srcFile); err != nil { + zap.S().Warnf("Failed to copy file %s to %s", f.Name, tempDBPath, err) + return + } + return } // XXX 目前直接替换,后续可能需要插入的方式 func applyTempDB(dbPath, tempDBPath string) error { - var err error - now := time.Now() - if runtime.GOOS == "windows" { - cache.LiteDB.Close() - zap.S().Info("Waiting for 3 sec...") - time.Sleep(3 * time.Second) - } - backupPath := fmt.Sprintf("%s.backup.%d-%d-%d", dbPath, now.Year(), now.Month(), now.Day()) - err = os.Rename(dbPath, backupPath) - if err != nil { - zap.S().Errorf("Failed to backup %s: %s", dbPath, err) - return err - } - zap.S().Info("Backup old dbfile -> ", backupPath) - err = os.Rename(tempDBPath, dbPath) - zap.S().Infof("Move %s -> %s", tempDBPath, dbPath) - if err != nil { - zap.S().Errorf("Failed to apply %s: %s", tempDBPath, err) - return err - } - return err + var err error + now := time.Now() + if runtime.GOOS == "windows" { + cache.LiteDB.Close() + zap.S().Info("Waiting for 3 sec...") + time.Sleep(3 * time.Second) + } + backupPath := fmt.Sprintf("%s.backup.%d-%d-%d", dbPath, now.Year(), now.Month(), now.Day()) + err = os.Rename(dbPath, backupPath) + if err != nil { + zap.S().Errorf("Failed to backup %s: %s", dbPath, err) + return err + } + zap.S().Info("Backup old dbfile -> ", backupPath) + err = os.Rename(tempDBPath, dbPath) + zap.S().Infof("Move %s -> %s", tempDBPath, dbPath) + if err != nil { + zap.S().Errorf("Failed to apply %s: %s", tempDBPath, err) + return err + } + return err } func parseDBAndInsert(tempDBPath string) (err error) { - now := time.Now() - var tempDB *sql.DB - tempDB, err = sql.Open("sqlite3", tempDBPath) - if err != nil { - zap.S().Errorf("Failed to open db %s: %s", tempDBPath, err) - return - } - defer tempDB.Close() - for _, table := range []string{"en", "ch"} { - success := 0 - total := 0 - sql := fmt.Sprintf("SELECT query, detail FROM %s", table) - rows, err := tempDB.Query(sql) - if err != nil { - zap.S().Warnf("Failed to query %s", table, err) - return err - } - defer rows.Close() - // insertSQL := fmt.Sprintf("INSERT OR IGNORE INTO %s (query, detail, update_time) VALUES (?, ?, ?)", table) - // stmt, err := cache.LiteDB.Prepare(insertSQL) - if err != nil { - zap.S().Warnf("Failed to invoke db.Prepare for %s: %s", table, err) - continue - } - for rows.Next() { - total++ - var query []byte - var detail []byte - scanErr := rows.Scan(&query, &detail) - if scanErr != nil { - zap.S().Warnf("Failed to scan row for %s: %s", table, scanErr) - continue - } - // _, insertErr := cache.LiteDB.Exec(insertSQL, query, detail, now) - // _, insertErr := stmt.Exec(query, detail, now) - // if insertErr != nil { - // zap.S().Warnf("Failed to insert for %s: %s", table, insertErr) - // continue - // } - _ = now - fmt.Println(query) - time.Sleep(100 * time.Microsecond) - success++ - // zap.S().Debugf("Inserted into %s: %s", table, success) - } - // stmt.Close() - // TODO (k): <2023-12-14> 计算成功多少,判定 - fmt.Printf("Table %s total %d success %d", table, total, success) - } + now := time.Now() + var tempDB *sql.DB + tempDB, err = sql.Open("sqlite3", tempDBPath) + if err != nil { + zap.S().Errorf("Failed to open db %s: %s", tempDBPath, err) + return + } + defer tempDB.Close() + for _, table := range []string{"en", "ch"} { + success := 0 + total := 0 + sql := fmt.Sprintf("SELECT query, detail FROM %s", table) + rows, err := tempDB.Query(sql) + if err != nil { + zap.S().Warnf("Failed to query %s", table, err) + return err + } + defer rows.Close() + // insertSQL := fmt.Sprintf("INSERT OR IGNORE INTO %s (query, detail, update_time) VALUES (?, ?, ?)", table) + // stmt, err := cache.LiteDB.Prepare(insertSQL) + if err != nil { + zap.S().Warnf("Failed to invoke db.Prepare for %s: %s", table, err) + continue + } + for rows.Next() { + total++ + var query []byte + var detail []byte + scanErr := rows.Scan(&query, &detail) + if scanErr != nil { + zap.S().Warnf("Failed to scan row for %s: %s", table, scanErr) + continue + } + // _, insertErr := cache.LiteDB.Exec(insertSQL, query, detail, now) + // _, insertErr := stmt.Exec(query, detail, now) + // if insertErr != nil { + // zap.S().Warnf("Failed to insert for %s: %s", table, insertErr) + // continue + // } + _ = now + fmt.Println(query) + time.Sleep(100 * time.Microsecond) + success++ + // zap.S().Debugf("Inserted into %s: %s", table, success) + } + // stmt.Close() + // TODO (k): <2023-12-14> 计算成功多少,判定 + fmt.Printf("Table %s total %d success %d", table, total, success) + } - return + return } func parseDBAndInsertOffsetVersion(tempDBPath string) (err error) { - now := time.Now() - var tempDB *sql.DB - tempDB, err = sql.Open("sqlite3", tempDBPath) - if err != nil { - zap.S().Errorf("Failed to open db %s: %s", tempDBPath, err) - return - } - defer tempDB.Close() - for _, table := range []string{"en", "ch"} { - success := 0 - total := 0 - offset := 0 - batch := 100 - for { - sql := fmt.Sprintf("SELECT query, detail FROM %s LIMIT %d, %d", table, offset, batch) - rows, err := tempDB.Query(sql) + now := time.Now() + var tempDB *sql.DB + tempDB, err = sql.Open("sqlite3", tempDBPath) + if err != nil { + zap.S().Errorf("Failed to open db %s: %s", tempDBPath, err) + return + } + defer tempDB.Close() + for _, table := range []string{"en", "ch"} { + success := 0 + total := 0 + offset := 0 + batch := 100 + for { + sql := fmt.Sprintf("SELECT query, detail FROM %s LIMIT %d, %d", table, offset, batch) + rows, err := tempDB.Query(sql) - if err != nil { - zap.S().Warnf("Failed to query %s", table, err) - return err - } - defer rows.Close() - insertSQL := fmt.Sprintf("INSERT OR IGNORE INTO %s (query, detail, update_time) VALUES (?, ?, ?)", table) - stmt, err := cache.LiteDB.Prepare(insertSQL) - if err != nil { - zap.S().Warnf("Failed to invoke db.Prepare for %s: %s", table, err) - continue - } - params := make([][][]byte, 0, 100) - fetched := 0 - for rows.Next() { - total++ - var query []byte - var detail []byte - scanErr := rows.Scan(&query, &detail) - if scanErr != nil { - zap.S().Warnf("Failed to scan row for %s: %s", table, scanErr) - continue - } - params = append(params, [][]byte{query, detail}) + if err != nil { + zap.S().Warnf("Failed to query %s", table, err) + return err + } + defer rows.Close() + insertSQL := fmt.Sprintf("INSERT OR IGNORE INTO %s (query, detail, update_time) VALUES (?, ?, ?)", table) + stmt, err := cache.LiteDB.Prepare(insertSQL) + if err != nil { + zap.S().Warnf("Failed to invoke db.Prepare for %s: %s", table, err) + continue + } + params := make([][][]byte, 0, 100) + fetched := 0 + for rows.Next() { + total++ + var query []byte + var detail []byte + scanErr := rows.Scan(&query, &detail) + if scanErr != nil { + zap.S().Warnf("Failed to scan row for %s: %s", table, scanErr) + continue + } + params = append(params, [][]byte{query, detail}) - _ = now - // fmt.Println(query) - // time.Sleep(100 * time.Microsecond) - fetched += 1 - // zap.S().Debugf("Inserted into %s: %s", table, success) - } - // insertSQL := fmt.Sprintf("INSERT OR IGNORE INTO %s (query, detail, update_time) VALUES (?, ?, ?)", table) + _ = now + // fmt.Println(query) + // time.Sleep(100 * time.Microsecond) + fetched += 1 + // zap.S().Debugf("Inserted into %s: %s", table, success) + } + // insertSQL := fmt.Sprintf("INSERT OR IGNORE INTO %s (query, detail, update_time) VALUES (?, ?, ?)", table) - for _, row := range params { - _, insertErr := stmt.Exec(row[0], row[1], now) - if insertErr == nil { - success++ - } - } - stmt.Close() - if len(params) == 0 { - break - } + for _, row := range params { + _, insertErr := stmt.Exec(row[0], row[1], now) + if insertErr == nil { + success++ + } + } + stmt.Close() + if len(params) == 0 { + break + } - time.Sleep(time.Duration(batch*10) * time.Millisecond) - // TODO (k): <2023-12-14> 计算成功多少,判定 - fmt.Printf("Table %s total %d success %d \n", table, total, success) - offset += batch - } - } - return + time.Sleep(time.Duration(batch*10) * time.Millisecond) + // TODO (k): <2023-12-14> 计算成功多少,判定 + fmt.Printf("Table %s total %d success %d \n", table, total, success) + offset += batch + } + } + return } diff --git a/internal/daemon/process.go b/internal/daemon/process.go index 16f4931..dbb0737 100644 --- a/internal/daemon/process.go +++ b/internal/daemon/process.go @@ -1,191 +1,191 @@ package daemon import ( - "fmt" - "os/exec" - "path/filepath" - "runtime" - "strings" - "time" - - "github.com/Karmenzind/kd/internal/model" - "github.com/Karmenzind/kd/internal/run" - "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" + "fmt" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/Karmenzind/kd/internal/model" + "github.com/Karmenzind/kd/internal/run" + "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" ) var SYSTEMD_UNIT_NAME = "kd-server" var DaemonInfo = &model.RunInfo{} func GetDaemonInfoPath() string { - return filepath.Join(run.CACHE_RUN_PATH, "daemon.json") + return filepath.Join(run.CACHE_RUN_PATH, "daemon.json") } func GetDaemonInfoFromFile() (*model.RunInfo, error) { - dipath := filepath.Join(run.CACHE_RUN_PATH, "daemon.json") - if !pkg.IsPathExists(dipath) { - return DaemonInfo, fmt.Errorf("获取守护进程信息失败,文件不存在") - } - err := pkg.LoadJson(dipath, DaemonInfo) - return DaemonInfo, err + dipath := filepath.Join(run.CACHE_RUN_PATH, "daemon.json") + if !pkg.IsPathExists(dipath) { + return DaemonInfo, fmt.Errorf("获取守护进程信息失败,文件不存在") + } + err := pkg.LoadJson(dipath, DaemonInfo) + return DaemonInfo, err } func GetDaemonInfo() (*model.RunInfo, error) { - var err error - if *DaemonInfo == (model.RunInfo{}) { - dipath := filepath.Join(run.CACHE_RUN_PATH, "daemon.json") - if !pkg.IsPathExists(dipath) { - return DaemonInfo, fmt.Errorf("获取守护进程信息失败,文件不存在") - } - err := pkg.LoadJson(dipath, DaemonInfo) - if err != nil { - return DaemonInfo, err - } - } - return DaemonInfo, err + var err error + if *DaemonInfo == (model.RunInfo{}) { + dipath := filepath.Join(run.CACHE_RUN_PATH, "daemon.json") + if !pkg.IsPathExists(dipath) { + return DaemonInfo, fmt.Errorf("获取守护进程信息失败,文件不存在") + } + err := pkg.LoadJson(dipath, DaemonInfo) + if err != nil { + return DaemonInfo, err + } + } + return DaemonInfo, err } func getKdPIDs() { - var cmd *exec.Cmd - - switch runtime.GOOS { - case "windows": - cmd = exec.Command("taskkill", "/NH", "/FO", "csv") - } - output, err := cmd.Output() - if err != nil { - _ = err - } - strings.Split(string(output), "\n") + var cmd *exec.Cmd + + switch runtime.GOOS { + case "windows": + cmd = exec.Command("taskkill", "/NH", "/FO", "csv") + } + output, err := cmd.Output() + if err != nil { + _ = err + } + strings.Split(string(output), "\n") } func ServerIsRunning() bool { - p, _ := FindServerProcess() - return p != nil + p, _ := FindServerProcess() + return p != nil } func FindServerProcess() (*process.Process, error) { - processes, err := process.Processes() - if err != nil { - return nil, err - } - for _, p := range processes { - // XXX err - n, _ := p.Name() - di, err := GetDaemonInfo() - if err == nil && p.Pid == int32(di.PID) { - zap.S().Debugf("Got daemon process %v via daemon info", di.PID) - cmdslice, _ := p.CmdlineSlice() - if len(cmdslice) > 1 && cmdslice[1] == "--server" { - return p, nil - } - } - - if n == "kd" || (runtime.GOOS == "windows" && n == "kd.exe") { - cmd, _ := p.Cmdline() - // 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 - } - } - } - return nil, nil + processes, err := process.Processes() + if err != nil { + return nil, err + } + for _, p := range processes { + // XXX err + n, _ := p.Name() + di, err := GetDaemonInfo() + if err == nil && p.Pid == int32(di.PID) { + zap.S().Debugf("Got daemon process %v via daemon info", di.PID) + cmdslice, _ := p.CmdlineSlice() + if len(cmdslice) > 1 && cmdslice[1] == "--server" { + return p, nil + } + } + + if n == "kd" || (runtime.GOOS == "windows" && n == "kd.exe") { + cmd, _ := p.Cmdline() + // 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 + } + } + } + return nil, nil } func StartDaemonProcess() error { - kdpath, err := pkg.GetExecutablePath() - if err != nil { - zap.S().Errorf("Failed to get current file path: %s", err) - return err - } - zap.S().Debugf("Got executable path %s", kdpath) - - cmd := exec.Command(kdpath, "--server") - err = cmd.Start() - if err != nil { - zap.S().Errorf("Failed to start daemon with system command: %s", err) - return err - } - var p *process.Process - for i := 0; i < 3; i++ { - time.Sleep(time.Second) - p, err_ := FindServerProcess() - if err_ != nil { - zap.S().Warnf("Failed finding daemon process: %s", err_) - } - if p != nil { - zap.S().Infof("Started daemon process.") - d.EchoOkay(fmt.Sprintf("成功启动守护进程,PID:%d", p.Pid)) - return nil - } - d.EchoRun("正在检查运行结果,稍等...") - } - if p == nil { - err = fmt.Errorf("启动失败,请重试。如果多次启动失败,请创建Issue并提交日志文件") - return err - } - return nil + kdpath, err := pkg.GetExecutablePath() + if err != nil { + zap.S().Errorf("Failed to get current file path: %s", err) + return err + } + zap.S().Debugf("Got executable path %s", kdpath) + + cmd := exec.Command(kdpath, "--server") + err = cmd.Start() + if err != nil { + zap.S().Errorf("Failed to start daemon with system command: %s", err) + return err + } + var p *process.Process + for i := 0; i < 3; i++ { + time.Sleep(time.Second) + p, err_ := FindServerProcess() + if err_ != nil { + zap.S().Warnf("Failed finding daemon process: %s", err_) + } + if p != nil { + zap.S().Infof("Started daemon process.") + d.EchoOkay(fmt.Sprintf("成功启动守护进程,PID:%d", p.Pid)) + return nil + } + d.EchoRun("正在检查运行结果,稍等...") + } + if p == nil { + err = fmt.Errorf("启动失败,请重试。如果多次启动失败,请创建Issue并提交日志文件") + return err + } + return nil } 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 { - d.EchoOkay("未发现守护进程,无需停止") - return nil - } - } else { - zap.S().Warnf("[process] Failed to find daemon: %s", err) - return err - } - - err = proc.KillProcess(p) - - if err == nil { - zap.S().Info("Terminated daemon process.") - d.EchoOkay("守护进程已经停止") - } else { - zap.S().Warnf("Failed to terminate daemon process: %s", err) - } - return err + 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 { + d.EchoOkay("未发现守护进程,无需停止") + return nil + } + } else { + zap.S().Warnf("[process] Failed to find daemon: %s", err) + return err + } + + err = proc.KillProcess(p) + + 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 + 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 + 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/model/dataobj.go b/internal/model/dataobj.go index 408febb..5dfafd4 100644 --- a/internal/model/dataobj.go +++ b/internal/model/dataobj.go @@ -1,60 +1,60 @@ package model import ( - "regexp" - "strings" + "regexp" + "strings" - "go.uber.org/zap" + "go.uber.org/zap" ) type CollinsItem struct { - Additional string `json:"a"` - MajorTrans string `json:"maj"` - ExampleLists [][]string `json:"eg"` - // MajorTransCh string // 备用 + Additional string `json:"a"` + MajorTrans string `json:"maj"` + ExampleLists [][]string `json:"eg"` + // MajorTransCh string // 备用 } type BaseResult struct { - Query string - Prompt string - IsEN bool - IsPhrase bool - Output string - Found bool - IsLongText bool - MachineTrans string - History chan int `json:"-"` + Query string + Prompt string + IsEN bool + IsPhrase bool + Output string + Found bool + IsLongText bool + MachineTrans string + History chan int `json:"-"` } type Result struct { - *BaseResult `json:"-"` - - Keyword string `json:"k"` - Pronounce map[string]string `json:"pron"` - Paraphrase []string `json:"para"` - Examples map[string][][]string `json:"eg"` - Collins struct { // XXX (k): <2023-11-15> 直接提到第一级 - Star int `json:"star"` - ViaRank string `json:"rank"` - AdditionalPattern string `json:"pat"` - - Items []*CollinsItem `json:"li"` - } `json:"co"` + *BaseResult `json:"-"` + + Keyword string `json:"k"` + Pronounce map[string]string `json:"pron"` + Paraphrase []string `json:"para"` + Examples map[string][][]string `json:"eg"` + Collins struct { // XXX (k): <2023-11-15> 直接提到第一级 + Star int `json:"star"` + ViaRank string `json:"rank"` + AdditionalPattern string `json:"pat"` + + Items []*CollinsItem `json:"li"` + } `json:"co"` } func (r *Result) ToDaemonResponse() *DaemonResponse { - return &DaemonResponse{ - R: r, - Base: r.BaseResult, - } + return &DaemonResponse{ + R: r, + Base: r.BaseResult, + } } func (r *Result) Initialize() { - if m, e := regexp.MatchString("^[A-Za-z0-9 -.?]+$", r.Query); e == nil && m { - r.IsEN = true - if strings.Contains(r.Query, " ") { - r.IsPhrase = true - } - zap.S().Debugf("Query: isEn: %v isPhrase: %v\n", r.IsEN, r.IsPhrase) - } + if m, e := regexp.MatchString("^[A-Za-z0-9 -.?]+$", r.Query); e == nil && m { + r.IsEN = true + if strings.Contains(r.Query, " ") { + r.IsPhrase = true + } + zap.S().Debugf("Query: isEn: %v isPhrase: %v\n", r.IsEN, r.IsPhrase) + } } diff --git a/internal/model/model.go b/internal/model/model.go index f6fb61a..7b62aa2 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -3,17 +3,17 @@ package model import "time" type Word struct { - word string - frequency string - pureEn bool - sample string - updateTime time.Time - lastTime time.Time + word string + frequency string + pureEn bool + sample string + updateTime time.Time + lastTime time.Time } type Cache struct { - word string - pronunciation string - paraphrase []string - sentences [][]string + word string + pronunciation string + paraphrase []string + sentences [][]string } diff --git a/internal/model/others.go b/internal/model/others.go index 6d132a4..4a51393 100644 --- a/internal/model/others.go +++ b/internal/model/others.go @@ -1,72 +1,72 @@ package model import ( - "github.com/Karmenzind/kd/pkg" - "go.uber.org/zap" - "golang.org/x/term" + "github.com/Karmenzind/kd/pkg" + "go.uber.org/zap" + "golang.org/x/term" ) type RunInfo struct { - StartTime int64 - PID int - Port string - ExeName string - ExePath string - Version string + StartTime int64 + PID int + Port string + ExeName string + ExePath string + Version string - OS *pkg.OSInfo + OS *pkg.OSInfo - isServer bool - termHeight int - termWidth int + isServer bool + termHeight int + termWidth int } func (r *RunInfo) IsServer() bool { - return r.isServer + return r.isServer } func (r *RunInfo) SetServer(v bool) { - r.isServer = v + r.isServer = v } func (r *RunInfo) SetPort(v string) { - r.Port = v + r.Port = v } func (r *RunInfo) SetOSInfo() { var err error - r.OS, err = pkg.GetOSInfo() + r.OS, err = pkg.GetOSInfo() if err != nil { zap.S().Warn("Failed to fetch os info: %s", err) } } func (r *RunInfo) GetOSInfo() *pkg.OSInfo { - if r.OS == nil { + if r.OS == nil { r.SetOSInfo() - } - return r.OS + } + return r.OS } func (r *RunInfo) GetTermSize() (int, int, error) { - if r.termHeight > 0 && r.termWidth > 0 { - return r.termWidth, r.termHeight, nil - } - w, h, err := term.GetSize(0) - if err != nil { - return 0, 0, err - } - r.termHeight = h - r.termWidth = w - return w, h, nil + if r.termHeight > 0 && r.termWidth > 0 { + return r.termWidth, r.termHeight, nil + } + w, h, err := term.GetSize(0) + if err != nil { + return 0, 0, err + } + r.termHeight = h + r.termWidth = w + return w, h, nil } func (r *RunInfo) SaveToFile(path string) (err error) { - err = pkg.SaveJson(path, r) - if err == nil { - zap.S().Infof("Recorded running information of daemon %+v", r) - } else { - zap.S().Warnf("Failed to record running info of daemon %+v", err) - } - return + err = pkg.SaveJson(path, r) + if err == nil { + zap.S().Infof("Recorded running information of daemon %+v", r) + } else { + zap.S().Warnf("Failed to record running info of daemon %+v", err) + } + return } diff --git a/internal/model/request.go b/internal/model/request.go index 7872806..82c0724 100644 --- a/internal/model/request.go +++ b/internal/model/request.go @@ -1,8 +1,8 @@ package model type TCPQuery struct { - Action string - B *BaseResult + Action string + B *BaseResult } func (q *TCPQuery) GetResult() *Result { @@ -10,14 +10,14 @@ func (q *TCPQuery) GetResult() *Result { } type DaemonResponse struct { - R *Result - Error string + R *Result + Error string - Base *BaseResult + Base *BaseResult } func (dr *DaemonResponse) GetResult() *Result { - // json传递中被抹去,重新赋值 - dr.R.BaseResult = dr.Base - return dr.R + // json传递中被抹去,重新赋值 + dr.R.BaseResult = dr.Base + return dr.R } diff --git a/internal/query/online.go b/internal/query/online.go index f77901d..183790d 100644 --- a/internal/query/online.go +++ b/internal/query/online.go @@ -1,136 +1,136 @@ package query import ( - "crypto/tls" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "strconv" - "strings" - "time" + "crypto/tls" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" - "github.com/Karmenzind/kd/config" - "github.com/Karmenzind/kd/internal/cache" - "github.com/Karmenzind/kd/internal/model" - "github.com/Karmenzind/kd/internal/run" - "github.com/Karmenzind/kd/pkg" - "github.com/anaskhan96/soup" - "go.uber.org/zap" + "github.com/Karmenzind/kd/config" + "github.com/Karmenzind/kd/internal/cache" + "github.com/Karmenzind/kd/internal/model" + "github.com/Karmenzind/kd/internal/run" + "github.com/Karmenzind/kd/pkg" + "github.com/anaskhan96/soup" + "go.uber.org/zap" ) var ydCliLegacy = &http.Client{Timeout: 5 * time.Second} var ydCli = &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}, Timeout: 5 * time.Second} func requestYoudao(r *model.Result) (body []byte, err error) { - var req *http.Request - var url string - var cli *http.Client - useNewApi := false - q := strings.ReplaceAll(r.Query, " ", "%20") - if useNewApi { - cli = ydCli - url = fmt.Sprintf("https://dict.youdao.com/result?word=%s&lang=en", q) - } else { - cli = ydCliLegacy - url = fmt.Sprintf("http://dict.youdao.com/w/%s/#keyfrom=dict2.top", q) - // url = fmt.Sprintf("http://dict.youdao.com/search?q=%s", q) - } - req, err = http.NewRequest("GET", url, nil) - if err != nil { - zap.S().Errorf("Failed to create request: %s", err) - return - } - if r.IsLongText { - req.Header.Set("Upgrade-Insecure-Requests", "1") - } - req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") - req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") - req.Header.Set("Connection", "keep-alive") - req.Header.Set("Host", "dict.youdao.com") - req.Header.Set("User-Agent", pkg.GetRandomUA()) + var req *http.Request + var url string + var cli *http.Client + useNewApi := false + q := strings.ReplaceAll(r.Query, " ", "%20") + if useNewApi { + cli = ydCli + url = fmt.Sprintf("https://dict.youdao.com/result?word=%s&lang=en", q) + } else { + cli = ydCliLegacy + url = fmt.Sprintf("http://dict.youdao.com/w/%s/#keyfrom=dict2.top", q) + // url = fmt.Sprintf("http://dict.youdao.com/search?q=%s", q) + } + req, err = http.NewRequest("GET", url, nil) + if err != nil { + zap.S().Errorf("Failed to create request: %s", err) + return + } + if r.IsLongText { + req.Header.Set("Upgrade-Insecure-Requests", "1") + } + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") + req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") + req.Header.Set("Connection", "keep-alive") + req.Header.Set("Host", "dict.youdao.com") + req.Header.Set("User-Agent", pkg.GetRandomUA()) - resp, err := cli.Do(req) - if err != nil { - zap.S().Infof("[http] Failed to do request: %s", err) - return - } + resp, err := cli.Do(req) + if err != nil { + zap.S().Infof("[http] Failed to do request: %s", err) + return + } - defer resp.Body.Close() - body, err = io.ReadAll(resp.Body) - if err != nil { - zap.S().Infof("[http] Failed to read response: %s", err) - return - } - zap.S().Debugf("[http-get] query '%s' Resp len: %d Status: %v", url, len(body), resp.Status) - if resp.StatusCode != 200 { - zap.S().Debugf("[http-get] detail: header %+v", url, len(body), resp.Header) - } - if config.Cfg.Debug { - htmlDir := filepath.Join(run.CACHE_ROOT_PATH, "html") - errW := os.MkdirAll(htmlDir, os.ModePerm) - if errW == nil { - htmlPath := filepath.Join(htmlDir, fmt.Sprintf("%s.html", r.Query)) - errW = os.WriteFile(htmlPath, body, 0666) - } - if errW == nil { - zap.S().Debugf("Saved '%s.html'", r.Query) - } else { - zap.S().Warnf("Failed to save html data for '%s': %s", r.Query, errW) - } - } - return + defer resp.Body.Close() + body, err = io.ReadAll(resp.Body) + if err != nil { + zap.S().Infof("[http] Failed to read response: %s", err) + return + } + zap.S().Debugf("[http-get] query '%s' Resp len: %d Status: %v", url, len(body), resp.Status) + if resp.StatusCode != 200 { + zap.S().Debugf("[http-get] detail: header %+v", url, len(body), resp.Header) + } + if config.Cfg.Debug { + htmlDir := filepath.Join(run.CACHE_ROOT_PATH, "html") + errW := os.MkdirAll(htmlDir, os.ModePerm) + if errW == nil { + htmlPath := filepath.Join(htmlDir, fmt.Sprintf("%s.html", r.Query)) + errW = os.WriteFile(htmlPath, body, 0666) + } + if errW == nil { + zap.S().Debugf("Saved '%s.html'", r.Query) + } else { + zap.S().Warnf("Failed to save html data for '%s': %s", r.Query, errW) + } + } + return } func parseHtml(resp string, r *model.Result) (err error) { - return + return } // "star star5" func parseCollinsStar(v string) (star int) { - if strings.HasPrefix(v, "star star") && len(v) == 10 { - intChar := v[9] - star, _ = strconv.Atoi(string(intChar)) - } - return + if strings.HasPrefix(v, "star star") && len(v) == 10 { + intChar := v[9] + star, _ = strconv.Atoi(string(intChar)) + } + return } // return html func FetchOnline(r *model.Result) (err error) { - body, err := requestYoudao(r) - if err != nil { - zap.S().Infof("[http-youdao] Failed to request: %s", err) - return - } + body, err := requestYoudao(r) + if err != nil { + zap.S().Infof("[http-youdao] Failed to request: %s", err) + return + } - doc := soup.HTMLParse(string(body)) - yr := YdResult{r, &doc} + doc := soup.HTMLParse(string(body)) + yr := YdResult{r, &doc} - if r.IsLongText { - yr.parseMachineTrans() - if r.MachineTrans != "" { - r.Found = true + if r.IsLongText { + yr.parseMachineTrans() + if r.MachineTrans != "" { + r.Found = true go cache.UpdateLongTextCache(r) - } - return - } + } + return + } - yr.parseParaphrase() - if yr.isNotFound() { - go cache.AppendNotFound(r.Query) - return - } + yr.parseParaphrase() + if yr.isNotFound() { + go cache.AppendNotFound(r.Query) + return + } - // XXX (k): <2024-01-02> long text? - yr.parseKeyword() - yr.parsePronounce() - yr.parseCollins() - yr.parseExamples() + // XXX (k): <2024-01-02> long text? + yr.parseKeyword() + yr.parsePronounce() + yr.parseCollins() + yr.parseExamples() - r.Found = true - go cache.UpdateQueryCache(r) - return + r.Found = true + go cache.UpdateQueryCache(r) + return } // func init() { diff --git a/internal/query/output.go b/internal/query/output.go index 5a1d917..70dffb6 100644 --- a/internal/query/output.go +++ b/internal/query/output.go @@ -1,195 +1,202 @@ package query import ( - "fmt" - "regexp" - "strings" + "fmt" + "regexp" + "strings" - "github.com/Karmenzind/kd/internal/model" - "github.com/Karmenzind/kd/internal/run" - d "github.com/Karmenzind/kd/pkg/decorate" + "github.com/Karmenzind/kd/internal/model" + "github.com/Karmenzind/kd/internal/run" + d "github.com/Karmenzind/kd/pkg/decorate" ) var collinsTransPat = regexp.MustCompile("^([^\u4e00-\u9fa5]+) ([^ ]*[\u4e00-\u9fa5]+.*)$") // collins的释义,英中混合 var normalSentence = regexp.MustCompile("^[A-Za-z]+ ") +var nationMap = map[string]string{"英": "EN", "美": "US"} + func PrettyFormat(r *model.Result, onlyEN bool) string { - egPref := d.EgPref("≫ ") - if r.Output != "" { - return r.Output - } - s := []string{} - - var title string - if r.Keyword == "" || r.IsLongText { - title = r.Query - } else { - title = r.Keyword - } - - header := d.Title(title) - // s = append(s, d.Title(title)) - - pronStr := "" - for nation, v := range r.Pronounce { - // pronStr += d.Na(nation) + d.Pron(v) - v = strings.Trim(v, "[]") - pronStr += fmt.Sprintf("%s %s / ", nation, v) - } - if pronStr != "" { - pronStr = d.Pron(fmt.Sprintf("[%s]", strings.Trim(pronStr, "/ "))) - header = fmt.Sprintf("%s %s", header, pronStr) - } - s = append(s, header) - - if r.IsLongText { - s = append(s, d.Text(r.MachineTrans)) - r.Output = strings.Join(s, "\n") - return r.Output - } - - // TODO wth is de morgan's law - if !(onlyEN && r.IsEN) { - for _, para := range r.Paraphrase { - if para == "" { - // FIXME (k): <2023-12-15> 从收集步骤规避 - continue - } - if normalSentence.MatchString(para) { - s = append(s, d.Para(para)) - } else { - splited := strings.SplitN(para, " ", 2) - if len(splited) == 2 { - s = append(s, fmt.Sprintf("%s %s", d.Property(splited[0]), d.Para(splited[1]))) - } else { - s = append(s, d.Para(para)) - } - } - } - } - - // cutoff := strings.Repeat("–", cutoffLength()) - cutoff := strings.Repeat("⸺", cutoffLength()) - - rankParts := []string{} - if r.Collins.Star > 0 { - rankParts = append(rankParts, d.Star(strings.Repeat("★", r.Collins.Star))) - } - if r.Collins.ViaRank != "" { - rankParts = append(rankParts, d.Rank(r.Collins.ViaRank)) - } - if r.Collins.AdditionalPattern != "" { - rankParts = append(rankParts, d.Rank(r.Collins.AdditionalPattern)) - } - if len(rankParts) > 0 { - s = append(s, strings.Join(rankParts, " ")) - } - - if r.IsEN && len(r.Collins.Items) > 0 { - s = append(s, d.Line(cutoff)) - for idx, i := range r.Collins.Items { - var transExpr string - if onlyEN { - transExpr, _ = cutCollinsTrans(i.MajorTrans) - if transExpr == "" { - transExpr = i.MajorTrans - } - } else { - transExpr = i.MajorTrans - } - - var piece string - piece = fmt.Sprintf("%s. ", d.Idx(idx+1)) - if i.Additional != "" { - if strings.HasPrefix(i.Additional, "[") && strings.HasSuffix(i.Additional, "]") { - piece += d.Addi(i.Additional + " ") - } else { - piece += d.Addi("(" + i.Additional + ") ") - } - } - piece += d.CollinsPara(transExpr) - s = append(s, piece) - - for _, ePair := range i.ExampleLists { - var eRepr string - if onlyEN { - eRepr = ePair[0] - } else { - eRepr = strings.Join(ePair, " ") - } - s = append(s, fmt.Sprintf(" %s %s", egPref, d.Eg(eRepr))) - // s = append(s, d.Eg(fmt.Sprintf(" e.g. %s", eRepr))) - } - } - } - - if (!r.IsEN || (r.IsEN && len(r.Collins.Items) == 0)) && len(r.Examples) > 0 { - s = append(s, d.Line(cutoff)) - for _, tab := range []string{"bi", "or"} { - if exampleList, ok := r.Examples[tab]; ok { - for _, item := range exampleList { - if p := displayExample(item, tab, onlyEN, r.IsEN); p != "" { - // s = append(s, fmt.Sprintf("%d. %s", idx+1, p)) - s = append(s, fmt.Sprintf("%s %s", egPref, p)) - } - } - break - } - } - } - - // s = append(s, r.Pronounce) - r.Output = strings.Join(s, "\n") - return r.Output + egPref := d.EgPref("≫ ") + if r.Output != "" { + return r.Output + } + s := []string{} + + var title string + if r.Keyword == "" || r.IsLongText { + title = r.Query + } else { + title = r.Keyword + } + + header := d.Title(title) + // s = append(s, d.Title(title)) + + pronStr := "" + for nation, v := range r.Pronounce { + // pronStr += d.Na(nation) + d.Pron(v) + if nation != "" && onlyEN { + if v, exists := nationMap[nation]; exists { + nation = v + } + } + v = strings.Trim(v, "[]") + pronStr += fmt.Sprintf("%s %s / ", nation, v) + } + if pronStr != "" { + pronStr = d.Pron(fmt.Sprintf("[%s]", strings.Trim(pronStr, "/ "))) + header = fmt.Sprintf("%s %s", header, pronStr) + } + s = append(s, header) + + if r.IsLongText { + s = append(s, d.Text(r.MachineTrans)) + r.Output = strings.Join(s, "\n") + return r.Output + } + + // TODO wth is de morgan's law + if !(onlyEN && r.IsEN) { + for _, para := range r.Paraphrase { + if para == "" { + // FIXME (k): <2023-12-15> 从收集步骤规避 + continue + } + if normalSentence.MatchString(para) { + s = append(s, d.Para(para)) + } else { + splited := strings.SplitN(para, " ", 2) + if len(splited) == 2 { + s = append(s, fmt.Sprintf("%s %s", d.Property(splited[0]), d.Para(splited[1]))) + } else { + s = append(s, d.Para(para)) + } + } + } + } + + // cutoff := strings.Repeat("–", cutoffLength()) + cutoff := strings.Repeat("⸺", cutoffLength()) + + rankParts := []string{} + if r.Collins.Star > 0 { + rankParts = append(rankParts, d.Star(strings.Repeat("★", r.Collins.Star))) + } + if r.Collins.ViaRank != "" { + rankParts = append(rankParts, d.Rank(r.Collins.ViaRank)) + } + if r.Collins.AdditionalPattern != "" { + rankParts = append(rankParts, d.Rank(r.Collins.AdditionalPattern)) + } + if len(rankParts) > 0 { + s = append(s, strings.Join(rankParts, " ")) + } + + if r.IsEN && len(r.Collins.Items) > 0 { + s = append(s, d.Line(cutoff)) + for idx, i := range r.Collins.Items { + var transExpr string + if onlyEN { + transExpr, _ = cutCollinsTrans(i.MajorTrans) + if transExpr == "" { + transExpr = i.MajorTrans + } + } else { + transExpr = i.MajorTrans + } + + var piece string + piece = fmt.Sprintf("%s. ", d.Idx(idx+1)) + if i.Additional != "" { + if strings.HasPrefix(i.Additional, "[") && strings.HasSuffix(i.Additional, "]") { + piece += d.Addi(i.Additional + " ") + } else { + piece += d.Addi("(" + i.Additional + ") ") + } + } + piece += d.CollinsPara(transExpr) + s = append(s, piece) + + for _, ePair := range i.ExampleLists { + var eRepr string + if onlyEN { + eRepr = ePair[0] + } else { + eRepr = strings.Join(ePair, " ") + } + s = append(s, fmt.Sprintf(" %s %s", egPref, d.Eg(eRepr))) + // s = append(s, d.Eg(fmt.Sprintf(" e.g. %s", eRepr))) + } + } + } + + if (!r.IsEN || (r.IsEN && len(r.Collins.Items) == 0)) && len(r.Examples) > 0 { + s = append(s, d.Line(cutoff)) + for _, tab := range []string{"bi", "or"} { + if exampleList, ok := r.Examples[tab]; ok { + for _, item := range exampleList { + if p := displayExample(item, tab, onlyEN, r.IsEN); p != "" { + // s = append(s, fmt.Sprintf("%d. %s", idx+1, p)) + s = append(s, fmt.Sprintf("%s %s", egPref, p)) + } + } + break + } + } + } + + // s = append(s, r.Pronounce) + r.Output = strings.Join(s, "\n") + return r.Output } func displayExample(item []string, tab string, onlyEN bool, isEN bool) string { - var r string - switch tab { - case "bi": - if onlyEN { - r = d.EgEn(item[0]) - } else { - var rh string - if len(item) >= 3 && strings.ToLower(item[2]) != "youdao" { - rh = d.EgCh(item[1]) + d.EgCh(item[2]) - } - - if rh == "" { - rh = d.EgCh(item[1]) - } - r = fmt.Sprintf("%s %s", d.EgEn(item[0]), rh) - } - case "au": - // TODO 增加来源渲染 - r = fmt.Sprintf("%s (%s)", d.EgEn(item[0]), d.EgCh(item[1])) - case "or": - if onlyEN { - if isEN { - r = d.EgEn(item[0]) - } else { - r = d.EgEn(item[1]) - } - } else { - r = fmt.Sprintf("%s %s", d.EgEn(item[0]), d.EgCh(item[1])) - } - } - return r + var r string + switch tab { + case "bi": + if onlyEN { + r = d.EgEn(item[0]) + } else { + var rh string + if len(item) >= 3 && strings.ToLower(item[2]) != "youdao" { + rh = d.EgCh(item[1]) + d.EgCh(item[2]) + } + + if rh == "" { + rh = d.EgCh(item[1]) + } + r = fmt.Sprintf("%s %s", d.EgEn(item[0]), rh) + } + case "au": + // TODO 增加来源渲染 + r = fmt.Sprintf("%s (%s)", d.EgEn(item[0]), d.EgCh(item[1])) + case "or": + if onlyEN { + if isEN { + r = d.EgEn(item[0]) + } else { + r = d.EgEn(item[1]) + } + } else { + r = fmt.Sprintf("%s %s", d.EgEn(item[0]), d.EgCh(item[1])) + } + } + return r } func cutoffLength() int { - width, _, err := run.Info.GetTermSize() - if err != nil { - width = 44 - } - return width - 2 + width, _, err := run.Info.GetTermSize() + if err != nil { + width = 44 + } + return width - 2 } // XXX func cutCollinsTrans(line string) (string, string) { - g := collinsTransPat.FindStringSubmatch(line) - if len(g) == 3 { - return g[1], g[2] - } - return "", "" + g := collinsTransPat.FindStringSubmatch(line) + if len(g) == 3 { + return g[1], g[2] + } + return "", "" } diff --git a/internal/query/query.go b/internal/query/query.go index 03d951f..36c1ff0 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -3,15 +3,15 @@ package query // query api import ( - "bufio" - "encoding/json" - "fmt" - "net" + "bufio" + "encoding/json" + "fmt" + "net" - "github.com/Karmenzind/kd/internal/cache" - "github.com/Karmenzind/kd/internal/model" - d "github.com/Karmenzind/kd/pkg/decorate" - "go.uber.org/zap" + "github.com/Karmenzind/kd/internal/cache" + "github.com/Karmenzind/kd/internal/model" + d "github.com/Karmenzind/kd/pkg/decorate" + "go.uber.org/zap" ) /* @@ -28,21 +28,21 @@ func FetchCached(r *model.Result) (err error) { } else { err = cache.GetCachedQuery(r) } - if err == nil { - r.Found = true - return - } + if err == nil { + r.Found = true + return + } zap.S().Debugf("[cache] Query error: %s", err) - r.Found = false - return + r.Found = false + return } func QueryDaemon(addr string, r *model.Result) error { - conn, err := net.Dial("tcp", addr) - if err != nil { - d.EchoFatal("与守护进程通信失败,请尝试执行`kd --daemon`,如果无法解决问题,请提交issue并上传日志") + conn, err := net.Dial("tcp", addr) + if err != nil { + d.EchoFatal("与守护进程通信失败,请尝试执行`kd --daemon`,如果无法解决问题,请提交issue并上传日志") return err - } + } q := model.TCPQuery{Action: "query", B: r.BaseResult} var j []byte j, err = json.Marshal(q) @@ -51,19 +51,19 @@ func QueryDaemon(addr string, r *model.Result) error { return err } zap.S().Debugf("Sending msg: %s\n", j) - fmt.Fprint(conn, string(j) + "\n") + fmt.Fprint(conn, string(j)+"\n") - message, _ := bufio.NewReader(conn).ReadBytes('\n') + message, _ := bufio.NewReader(conn).ReadBytes('\n') - dr := r.ToDaemonResponse() - err = json.Unmarshal(message, &dr) - zap.S().Debugf("Message from server: %s", string(message)) - if err != nil { - return fmt.Errorf("解析daemon返回结果失败: %s", err) - } - if dr.Error != "" { - return fmt.Errorf(dr.Error) - } + dr := r.ToDaemonResponse() + err = json.Unmarshal(message, &dr) + zap.S().Debugf("Message from server: %s", string(message)) + if err != nil { + return fmt.Errorf("解析daemon返回结果失败: %s", err) + } + if dr.Error != "" { + return fmt.Errorf(dr.Error) + } dr.GetResult() - return nil + return nil } diff --git a/internal/query/youdao.go b/internal/query/youdao.go index 80fefd1..2ffb015 100644 --- a/internal/query/youdao.go +++ b/internal/query/youdao.go @@ -1,232 +1,220 @@ package query import ( - "regexp" - "strings" + "regexp" + "strings" - "github.com/Karmenzind/kd/internal/model" - "github.com/Karmenzind/kd/pkg/str" - "github.com/anaskhan96/soup" - "go.uber.org/zap" + "github.com/Karmenzind/kd/internal/model" + "github.com/Karmenzind/kd/pkg/str" + "github.com/anaskhan96/soup" + "go.uber.org/zap" ) type YdResult struct { - *model.Result - Doc *soup.Root + *model.Result + Doc *soup.Root } func (r *YdResult) parseParaphrase() { - trans := r.Doc.Find("div", "class", "trans-container") - if trans.Error == nil { - // XXX 此处可以输出warning - var para string - if r.IsEN { - for _, v := range trans.FindAll("li") { - para = str.Simplify(v.Text()) - if para != "" { - r.Paraphrase = append(r.Paraphrase, para) - zap.S().Debugf("Got para: %s\n", para) - } - } - } else { - for _, wg := range trans.FindAll("p", "class", "wordGroup") { - para = str.Simplify(wg.FullText()) - if para != "" { - r.Paraphrase = append(r.Paraphrase, para) - zap.S().Debugf("Got para: %s\n", para) - } - } - } - } else { - zap.S().Debug("div trans-container not found\n") - } + if trans := r.Doc.Find("div", "class", "trans-container"); trans.Error == nil { + // XXX 此处可以输出warning + var para string + if r.IsEN { + for _, v := range trans.FindAll("li") { + para = str.Simplify(v.Text()) + if para != "" { + r.Paraphrase = append(r.Paraphrase, para) + zap.S().Debugf("Got para: %s\n", para) + } + } + } else { + for _, wg := range trans.FindAll("p", "class", "wordGroup") { + para = str.Simplify(wg.FullText()) + if para != "" { + r.Paraphrase = append(r.Paraphrase, para) + zap.S().Debugf("Got para: %s\n", para) + } + } + } + } else { + zap.S().Debug("div trans-container not found\n") + } } func (r *YdResult) parseKeyword() { - kwTag := r.Doc.FindStrict("span", "class", "keyword") - if kwTag.Error == nil { - r.Keyword = kwTag.Text() - } + if kwTag := r.Doc.FindStrict("span", "class", "keyword"); kwTag.Error == nil { + r.Keyword = kwTag.Text() + } } func (r *YdResult) parsePronounce() { - r.Pronounce = make(map[string]string) - for _, pron := range r.Doc.FindAll("span", "class", "pronounce") { - if pron.Error != nil { - continue - } - - phoneticTag := pron.Find("span") - if phoneticTag.Error != nil { - continue - } - nation := strings.Trim(pron.Text(), " \n") - phonetic := strings.Trim(pron.Find("span").Text(), "[]") - r.Pronounce[nation] = phonetic - } + r.Pronounce = make(map[string]string) + for _, pron := range r.Doc.FindAll("span", "class", "pronounce") { + if pron.Error != nil { + continue + } + + if phoneticTag := pron.Find("span"); phoneticTag.Error != nil { + continue + } + nation := strings.Trim(pron.Text(), " \n") + phonetic := strings.Trim(pron.Find("span").Text(), "[]") + r.Pronounce[nation] = phonetic + } } func (r *YdResult) parseCollins() { - collinsRoot := r.Doc.Find("div", "id", "collinsResult") - if collinsRoot.Error == nil { - star := collinsRoot.Find("span", "class", "star") - if star.Error == nil { - if starVal, ok := star.Attrs()["class"]; ok { - r.Collins.Star = parseCollinsStar(starVal) - } - } - - viaRank := collinsRoot.FindStrict("span", "class", "via rank") - if viaRank.Error == nil { - r.Collins.ViaRank = viaRank.Text() - } - - ap := collinsRoot.FindStrict("span", "class", "additional pattern") - if ap.Error == nil { - apText := ap.Text() - if apText != "" { - apText = strings.ReplaceAll(apText, "\n", "") - apText = regexp.MustCompile("[ \t]+").ReplaceAllString(apText, "") - apText = strings.Trim(apText, "()") - r.Collins.AdditionalPattern = apText - } - } - - olRoot := collinsRoot.Find("ul", "class", "ol") - if olRoot.Error == nil { - for _, liTag := range olRoot.FindAll("li") { - cTrans := liTag.Find("div", "class", "collinsMajorTrans") - if cTrans.Error != nil { - continue - } - - adtTag := cTrans.Find("span", "class", "additional") - - transTag := cTrans.Find("p") - if adtTag.Error != nil || transTag.Error != nil { - continue - } - adtStr := adtTag.Text() - transStr := str.Simplify(transTag.FullText()) - - if adtStr != "" { - transStr = transStr[len(adtStr)+1:] - } - // TODO (k): <2023-11-16> 此处如果分割中文,猜测 - // - 找到第一个中文char的index - // - 用 /[a-zA-Z]. / 分割 - // fmt.Println(idx+1, adtStr) - // fmt.Println(transStr) - - cExamples := liTag.FindAll("div", "class", "exampleLists") - i := &model.CollinsItem{ - Additional: adtStr, - MajorTrans: transStr, - ExampleLists: make([][]string, 0, len(cExamples)), - } - r.Collins.Items = append(r.Collins.Items, i) - - for _, example := range cExamples { - if example.Error != nil { - continue - } - ps := example.FindAll("p") - if len(ps) > 0 { - exampleEn := str.Simplify(ps[0].FullText()) - exampleSlice := []string{exampleEn} - if len(ps) > 1 { - exampleCh := str.Simplify(ps[1].FullText()) - exampleSlice = append(exampleSlice, exampleCh) - } - i.ExampleLists = append(i.ExampleLists, exampleSlice) - } - } - } - } - } + if collinsRoot := r.Doc.Find("div", "id", "collinsResult"); collinsRoot.Error == nil { + if star := collinsRoot.Find("span", "class", "star"); star.Error == nil { + if starVal, ok := star.Attrs()["class"]; ok { + r.Collins.Star = parseCollinsStar(starVal) + } + } + + if viaRank := collinsRoot.FindStrict("span", "class", "via rank"); viaRank.Error == nil { + r.Collins.ViaRank = viaRank.Text() + } + + if ap := collinsRoot.FindStrict("span", "class", "additional pattern"); ap.Error == nil { + apText := ap.Text() + if apText != "" { + apText = strings.ReplaceAll(apText, "\n", "") + apText = regexp.MustCompile("[ \t]+").ReplaceAllString(apText, "") + apText = strings.Trim(apText, "()") + r.Collins.AdditionalPattern = apText + } + } + + if olRoot := collinsRoot.Find("ul", "class", "ol"); olRoot.Error == nil { + for _, liTag := range olRoot.FindAll("li") { + cTrans := liTag.Find("div", "class", "collinsMajorTrans") + if cTrans.Error != nil { + continue + } + + adtTag := cTrans.Find("span", "class", "additional") + + transTag := cTrans.Find("p") + if adtTag.Error != nil || transTag.Error != nil { + continue + } + adtStr := adtTag.Text() + transStr := str.Simplify(transTag.FullText()) + + if adtStr != "" { + transStr = transStr[len(adtStr)+1:] + } + // TODO (k): <2023-11-16> 此处如果分割中文,猜测 + // - 找到第一个中文char的index + // - 用 /[a-zA-Z]. / 分割 + // fmt.Println(idx+1, adtStr) + // fmt.Println(transStr) + + cExamples := liTag.FindAll("div", "class", "exampleLists") + i := &model.CollinsItem{ + Additional: adtStr, + MajorTrans: transStr, + ExampleLists: make([][]string, 0, len(cExamples)), + } + r.Collins.Items = append(r.Collins.Items, i) + + for _, example := range cExamples { + if example.Error != nil { + continue + } + ps := example.FindAll("p") + if len(ps) > 0 { + exampleEn := str.Simplify(ps[0].FullText()) + exampleSlice := []string{exampleEn} + if len(ps) > 1 { + exampleCh := str.Simplify(ps[1].FullText()) + exampleSlice = append(exampleSlice, exampleCh) + } + i.ExampleLists = append(i.ExampleLists, exampleSlice) + } + } + } + } + } } func (r *YdResult) parseExamples() { - examplesRoot := r.Doc.Find("div", "id", "examplesToggle") - if examplesRoot.Error == nil { - r.Examples = make(map[string][][]string) - for _, tab := range []string{"bilingual", "authority", "originalSound"} { - egTabDiv := examplesRoot.Find("div", "id", tab) - if egTabDiv.Error != nil { - continue - } - lis := egTabDiv.FindAll("li") - if len(lis) == 0 { - continue - } - egKey := tab[:2] - r.Examples[egKey] = make([][]string, 0, len(lis)) - for _, li := range lis { - pTags := li.FindAll("p") - example := make([]string, 0, 3) - for idx, ptag := range pTags { - if idx > 3 { - break - } - example = append(example, str.Simplify(ptag.FullText())) - } - - if tab == "bilingual" { - if len(example) < 2 { - continue - } - if !r.IsEN { - example[0], example[1] = example[1], example[0] - } - } - zap.S().Debug("Got example", example) - r.Examples[egKey] = append(r.Examples[egKey], example) - } - } - } + examplesRoot := r.Doc.Find("div", "id", "examplesToggle") + if examplesRoot.Error == nil { + r.Examples = make(map[string][][]string) + for _, tab := range []string{"bilingual", "authority", "originalSound"} { + egTabDiv := examplesRoot.Find("div", "id", tab) + if egTabDiv.Error != nil { + continue + } + lis := egTabDiv.FindAll("li") + if len(lis) == 0 { + continue + } + egKey := tab[:2] + r.Examples[egKey] = make([][]string, 0, len(lis)) + for _, li := range lis { + pTags := li.FindAll("p") + example := make([]string, 0, 3) + for idx, ptag := range pTags { + if idx > 3 { + break + } + example = append(example, str.Simplify(ptag.FullText())) + } + + if tab == "bilingual" { + if len(example) < 2 { + continue + } + if !r.IsEN { + example[0], example[1] = example[1], example[0] + } + } + zap.S().Debug("Got example", example) + r.Examples[egKey] = append(r.Examples[egKey], example) + } + } + } } func (r *YdResult) parseMachineTrans() { - if tcRoot := r.Doc.FindStrict("div", "class", "trans-container"); tcRoot.Error == nil { - if prev := tcRoot.FindPrevElementSibling(); prev.Error == nil && prev.Attrs()["class"] == "wordbook-js" { - r.MachineTrans = str.Simplify(tcRoot.FullText()) - if r.MachineTrans != "" { - zap.S().Debug("Got Machine trans from top area: ", r.MachineTrans) - return - } - } - // fmt.Printf("[Prev] Error: %v Attrs: %+v\n", prev.Error, prev.Attrs()) - // fmt.Printf("[Prev] HTML: %+v\n", prev.HTML()) - } - - if fanyiRoot := r.Doc.FindStrict("div", "id", "fanyiToggle"); fanyiRoot.Error == nil { - ps := fanyiRoot.FindAll("p") - if len(ps) >= 2 { - r.MachineTrans = str.Simplify(ps[1].FullText()) - - if r.MachineTrans != "" { - zap.S().Debug("Got Machine trans from fanyiToggle: ", r.MachineTrans) - return - } - } - } - - if tWebRoot := r.Doc.FindStrict("div", "id", "tWebTrans"); tWebRoot.Error == nil { - title := tWebRoot.FindStrict("div", "class", "title") - if title.Error == nil { - r.MachineTrans = str.Simplify(title.FullText()) - if r.MachineTrans != "" { - zap.S().Debug("Got Machine trans from tWebTrans: ", r.MachineTrans) - return - } - } - } + if tcRoot := r.Doc.FindStrict("div", "class", "trans-container"); tcRoot.Error == nil { + if prev := tcRoot.FindPrevElementSibling(); prev.Error == nil && prev.Attrs()["class"] == "wordbook-js" { + r.MachineTrans = str.Simplify(tcRoot.FullText()) + if r.MachineTrans != "" { + zap.S().Debug("Got Machine trans from top area: ", r.MachineTrans) + return + } + } + // fmt.Printf("[Prev] Error: %v Attrs: %+v\n", prev.Error, prev.Attrs()) + // fmt.Printf("[Prev] HTML: %+v\n", prev.HTML()) + } + + if fanyiRoot := r.Doc.FindStrict("div", "id", "fanyiToggle"); fanyiRoot.Error == nil { + ps := fanyiRoot.FindAll("p") + if len(ps) >= 2 { + r.MachineTrans = str.Simplify(ps[1].FullText()) + + if r.MachineTrans != "" { + zap.S().Debug("Got Machine trans from fanyiToggle: ", r.MachineTrans) + return + } + } + } + + if tWebRoot := r.Doc.FindStrict("div", "id", "tWebTrans"); tWebRoot.Error == nil { + if title := tWebRoot.FindStrict("div", "class", "title"); title.Error == nil { + r.MachineTrans = str.Simplify(title.FullText()) + if r.MachineTrans != "" { + zap.S().Debug("Got Machine trans from tWebTrans: ", r.MachineTrans) + return + } + } + } } func (r *YdResult) isNotFound() bool { - if r.Paraphrase == nil || len(r.Paraphrase) == 0 { - return true - } - return false + return r.Paraphrase == nil || len(r.Paraphrase) == 0 } diff --git a/internal/run/info.go b/internal/run/info.go index cc0b12c..ecbb9ce 100644 --- a/internal/run/info.go +++ b/internal/run/info.go @@ -1,13 +1,13 @@ package run import ( - "os" - "path/filepath" - "runtime" - "time" + "os" + "path/filepath" + "runtime" + "time" - "github.com/Karmenzind/kd/internal/model" - d "github.com/Karmenzind/kd/pkg/decorate" + "github.com/Karmenzind/kd/internal/model" + d "github.com/Karmenzind/kd/pkg/decorate" ) var Info *model.RunInfo @@ -25,34 +25,34 @@ var SERVER_PORT = 19707 var cacheDirname = "kdcache" func getCacheRootPath() string { - var target string - userdir, _ := os.UserHomeDir() - switch runtime.GOOS { - case "linux": - target = filepath.Join(userdir, ".cache", cacheDirname) - case "darwin": - target = filepath.Join(userdir, "Library/Caches", cacheDirname) - case "windows": - target = filepath.Join(userdir, ".cache", cacheDirname) - } - return target + var target string + userdir, _ := os.UserHomeDir() + switch runtime.GOOS { + case "linux": + target = filepath.Join(userdir, ".cache", cacheDirname) + case "darwin": + target = filepath.Join(userdir, "Library/Caches", cacheDirname) + case "windows": + target = filepath.Join(userdir, ".cache", cacheDirname) + } + return target } func init() { - exepath, err := os.Executable() - if err != nil { - d.EchoFatal(err.Error()) - } - Info = &model.RunInfo{ - PID: os.Getpid(), - StartTime: time.Now().Unix(), - ExePath: exepath, - ExeName: filepath.Base(exepath), - } - - CACHE_ROOT_PATH = getCacheRootPath() - CACHE_WORDS_PATH = filepath.Join(CACHE_ROOT_PATH, "words") - CACHE_STAT_DIR_PATH = filepath.Join(CACHE_ROOT_PATH, "stat") - CACHE_RUN_PATH = filepath.Join(CACHE_ROOT_PATH, "run") + exepath, err := os.Executable() + if err != nil { + d.EchoFatal(err.Error()) + } + Info = &model.RunInfo{ + PID: os.Getpid(), + StartTime: time.Now().Unix(), + ExePath: exepath, + ExeName: filepath.Base(exepath), + } + + CACHE_ROOT_PATH = getCacheRootPath() + CACHE_WORDS_PATH = filepath.Join(CACHE_ROOT_PATH, "words") + CACHE_STAT_DIR_PATH = filepath.Join(CACHE_ROOT_PATH, "stat") + CACHE_RUN_PATH = filepath.Join(CACHE_ROOT_PATH, "run") } diff --git a/internal/update/update.go b/internal/update/update.go index 5a5f904..e65ceb7 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -1,112 +1,109 @@ package update import ( - "crypto/tls" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "os/exec" - "path/filepath" - "regexp" - "runtime" - "strconv" - "strings" - - "github.com/Karmenzind/kd/internal/cache" - d "github.com/Karmenzind/kd/pkg/decorate" - - "github.com/Karmenzind/kd/pkg" - "go.uber.org/zap" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + + "github.com/Karmenzind/kd/internal/cache" + d "github.com/Karmenzind/kd/pkg/decorate" + + "github.com/Karmenzind/kd/pkg" + "go.uber.org/zap" ) -// var LATEST_RELEASE_URL = "http://192.168.31.228:8901/" - const LATEST_RELEASE_URL = "https://github.com/Karmenzind/kd/releases/latest/download/" const TAGLIST_URL = "https://api.github.com/repos/Karmenzind/kd/tags" type GithubTag struct { - Name string - // zipball_url - // tarball_url + Name string + // zipball_url + // tarball_url } func CompareVersions(v1, v2 string) int { - v1 = strings.TrimPrefix(v1, "v") - v2 = strings.TrimPrefix(v2, "v") - - pattern := regexp.MustCompile(`(\d+).(\d+).(\d+)`) - matches1 := pattern.FindStringSubmatch(v1) - matches2 := pattern.FindStringSubmatch(v2) - - version1 := make([]int, 3) - version2 := make([]int, 3) - - for i := 1; i <= 3; i++ { - version1[i-1], _ = strconv.Atoi(matches1[i]) - version2[i-1], _ = strconv.Atoi(matches2[i]) - } - - for i := 0; i < 3; i++ { - if version1[i] > version2[i] { - return 1 - } else if version1[i] < version2[i] { - return -1 - } - } - return 0 + v1 = strings.TrimPrefix(v1, "v") + v2 = strings.TrimPrefix(v2, "v") + + pattern := regexp.MustCompile(`(\d+).(\d+).(\d+)`) + matches1 := pattern.FindStringSubmatch(v1) + matches2 := pattern.FindStringSubmatch(v2) + + version1 := make([]int, 3) + version2 := make([]int, 3) + + for i := 1; i <= 3; i++ { + version1[i-1], _ = strconv.Atoi(matches1[i]) + version2[i-1], _ = strconv.Atoi(matches2[i]) + } + + for i := 0; i < 3; i++ { + if version1[i] > version2[i] { + return 1 + } else if version1[i] < version2[i] { + return -1 + } + } + return 0 } var LATEST_TAG_FILE = filepath.Join(cache.CACHE_ROOT_PATH, "latest_tag") func GetLatestTag() (tag string, err error) { - req, err := http.NewRequest("GET", TAGLIST_URL, nil) - if err != nil { - zap.S().Infof("Error creating request: %v\n", err) - return - } - - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - - client := &http.Client{Transport: tr} - - resp, err := client.Do(req) - if err != nil { - zap.S().Infof("Error sending request: %v\n", err) - return - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - zap.S().Infof("Error reading response body: %v\n", err) - return - } - zap.S().Debugf("Response Status: %s Body: %s\n", resp.Status, string(body)) - - tags := []*GithubTag{} - err = json.Unmarshal(body, &tags) - if err == nil { - if len(tags) > 0 { - tag = tags[0].Name + req, err := http.NewRequest("GET", TAGLIST_URL, nil) + if err != nil { + zap.S().Infof("Error creating request: %v\n", err) + return + } + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + client := &http.Client{Transport: tr} + + resp, err := client.Do(req) + if err != nil { + zap.S().Infof("Error sending request: %v\n", err) + return + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + zap.S().Infof("Error reading response body: %v\n", err) + return + } + zap.S().Debugf("Response Status: %s Body: %s\n", resp.Status, string(body)) + + tags := []*GithubTag{} + + if err = json.Unmarshal(body, &tags); err == nil { + if len(tags) > 0 { + tag = tags[0].Name p := LATEST_TAG_FILE writeErr := os.WriteFile(p, []byte(tag), os.ModePerm) - if writeErr != nil{ + if writeErr != nil { zap.S().Warnf("Failed to save latest_tag: %s", writeErr) } - } else { - err = fmt.Errorf("empty response list") - } - } - return + } else { + err = fmt.Errorf("empty response list") + } + } + return } - func GetCachedLatestTag() (tag string) { - if !pkg.IsPathExists(LATEST_TAG_FILE){ + if !pkg.IsPathExists(LATEST_TAG_FILE) { return } if b, err := os.ReadFile(LATEST_TAG_FILE); err == nil { @@ -116,143 +113,140 @@ func GetCachedLatestTag() (tag string) { } func getBinaryURL() string { - var os, arch string - if runtime.GOOS == "darwin" { - os = "macos" - } else { - os = runtime.GOOS - } - arch = runtime.GOARCH - url := fmt.Sprintf("%skd_%s_%s", LATEST_RELEASE_URL, os, arch) - if os == "windows" { - url += ".exe" - } - return url + var os, arch string + if runtime.GOOS == "darwin" { + os = "macos" + } else { + os = runtime.GOOS + } + arch = runtime.GOARCH + url := fmt.Sprintf("%skd_%s_%s", LATEST_RELEASE_URL, os, arch) + if os == "windows" { + url += ".exe" + } + return url } func GetNewerVersion(currentTag string) (tag string, err error) { - latestTag, err := GetLatestTag() - if err != nil { - return - } - if CompareVersions(latestTag, currentTag) < 1 { - zap.S().Infof("Current tag %s latest tag: %s. No need to update.", currentTag, latestTag) - return - } - return latestTag, nil + latestTag, err := GetLatestTag() + if err != nil { + return + } + if CompareVersions(latestTag, currentTag) < 1 { + zap.S().Infof("Current tag %s latest tag: %s. No need to update.", currentTag, latestTag) + return + } + return latestTag, nil } func UpdateBinary(currentTag string) (err error) { - _ = currentTag - // emoji.Println(":eyes: 不好意思更新功能没写好,请手动到release下载") - tmpPath := filepath.Join(cache.CACHE_ROOT_PATH, "kd.temp") - url := getBinaryURL() - - var exepath string - exepath, err = pkg.GetExecutablePath() - if err != nil { - return err - } - if strings.Contains(exepath, "go-build") { - fmt.Println("非binary,已忽略") - return nil - } - - d.EchoRun(fmt.Sprintf("Start downloading: %s", url)) - // TODO (k): <2023-12-31> 调用curl - err = pkg.DownloadFile(tmpPath, url) - if err != nil { - zap.S().Errorf("Failed to download binary file: %s", err) - } - d.EchoOkay("已下载完成") - // d.EchoRun(fmt.Sprintf("临时文件保存位置:%s", tmpPath)) - - err = moveFile(tmpPath, exepath) - if err != nil { - return - } else { - d.EchoOkay("已成功替换旧版本,更新完成") - } - if runtime.GOOS != "windows" { - err = pkg.AddExecutablePermission(exepath) - if err != nil { - d.EchoWrong(fmt.Sprintf("修改权限失败,请手动执行`chmod +x %s`", exepath)) - } - } - return - // emoji.Println(":lightning: Now we start updating the binary") - // emoji.Println(":lightning: updating...") - // emoji.Println(":beer: DONE :)") + _ = currentTag + // emoji.Println(":eyes: 不好意思更新功能没写好,请手动到release下载") + tmpPath := filepath.Join(cache.CACHE_ROOT_PATH, "kd.temp") + url := getBinaryURL() + + var exepath string + exepath, err = pkg.GetExecutablePath() + if err != nil { + return err + } + if strings.Contains(exepath, "go-build") { + fmt.Println("非binary,已忽略") + return nil + } + + d.EchoRun(fmt.Sprintf("Start downloading: %s", url)) + // TODO (k): <2023-12-31> 调用curl + err = pkg.DownloadFileWithProgress(tmpPath, url) + if err != nil { + zap.S().Errorf("Failed to download binary file: %s", err) + } + d.EchoOkay("已下载完成") + + err = moveFile(tmpPath, exepath) + if err != nil { + return + } else { + d.EchoOkay("已成功替换旧版本,更新完成") + } + if runtime.GOOS != "windows" { + err = pkg.AddExecutablePermission(exepath) + if err != nil { + d.EchoWrong(fmt.Sprintf("修改权限失败,请手动执行`chmod +x %s`", exepath)) + } + } + return } // try sudo if needed func moveFile(src, tgt string) (err error) { - err = os.Rename(src, tgt) - if err == nil { - return - } - zap.S().Infof("Failed to rename binary file: %s", err) - - if runtime.GOOS == "windows" { - // return fmt.Errorf("文件覆盖失败,遇到权限问题(%s),请到release页面下载", err) - // d.EchoRun("尝试覆盖源文件") - err = replaceExecutable(tgt, src) - } else { - cmd := exec.Command("sudo", "mv", src, tgt) - cmd.Stdin = os.Stdin - d.EchoRun("覆盖文件需root权限,请输入密码") - err = cmd.Run() - } - // if linkErr, ok := err.(*os.LinkError); ok { - // if os.IsPermission(linkErr.Err) { - // zap.S().Infof("Permission denied. Please make sure you have write access to the destination directory.") - // if runtime.GOOS == "windows" { - // return fmt.Errorf("文件覆盖失败,遇到权限问题,请到release页面下载") - // } - // cmd := exec.Command("sudo", "mv", src, tgt) - // cmd.Stdin = os.Stdin - // d.EchoRun("覆盖文件需root权限,请输入密码") - // err = cmd.Run() - // } - // } - return + err = os.Rename(src, tgt) + if err == nil { + return + } + zap.S().Infof("Failed to rename binary file: %s", err) + d.EchoWarn("更改文件名失败(%s),将尝试其他方式", err) + + if runtime.GOOS == "windows" { + // return fmt.Errorf("文件覆盖失败,遇到权限问题(%s),请到release页面下载", err) + // d.EchoRun("尝试覆盖源文件") + err = replaceExecutable(tgt, src) + } else { + cmd := exec.Command("sudo", "mv", src, tgt) + cmd.Stdin = os.Stdin + d.EchoRun("覆盖文件需root权限,请输入密码") + err = cmd.Run() + } + // if linkErr, ok := err.(*os.LinkError); ok { + // if os.IsPermission(linkErr.Err) { + // zap.S().Infof("Permission denied. Please make sure you have write access to the destination directory.") + // if runtime.GOOS == "windows" { + // return fmt.Errorf("文件覆盖失败,遇到权限问题,请到release页面下载") + // } + // cmd := exec.Command("sudo", "mv", src, tgt) + // cmd.Stdin = os.Stdin + // d.EchoRun("覆盖文件需root权限,请输入密码") + // err = cmd.Run() + // } + // } + return } func replaceExecutable(oldPath, newPath string) error { - // 修改时改cron - backupPath := oldPath + ".update_backup" - err := os.Rename(oldPath, backupPath) - if err != nil { - return err - } - - err = copyFile(newPath, oldPath) - if err != nil { - // XXX (k): <2024-01-01> 再挪回来? - return err - } - - removeErr := os.Remove(backupPath) - if removeErr != nil { - zap.S().Warnf("Failed to remove old file (%s): %s", backupPath, removeErr) - } - - return nil + // 修改时改cron + backupPath := oldPath + ".update_backup" + err := os.Rename(oldPath, backupPath) + if err != nil { + return err + } + + err = copyFile(newPath, oldPath) + if err != nil { + // XXX (k): <2024-01-01> 再挪回来? + return err + } + + removeErr := os.Remove(backupPath) + if removeErr != nil { + zap.S().Warnf("Failed to remove old file (%s): %s", backupPath, removeErr) + } + + return nil } func copyFile(src, dest string) error { - source, err := os.Open(src) - if err != nil { - return err - } - defer source.Close() - - destination, err := os.Create(dest) - if err != nil { - return err - } - defer destination.Close() - - _, err = io.Copy(destination, source) - return err + source, err := os.Open(src) + if err != nil { + return err + } + defer source.Close() + + destination, err := os.Create(dest) + if err != nil { + return err + } + defer destination.Close() + + _, err = io.Copy(destination, source) + return err } diff --git a/logger/logger.go b/logger/logger.go index c5c4af5..edc87b5 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -1,60 +1,60 @@ package logger import ( - "fmt" - "os" - "os/user" - "path/filepath" - "strings" + "fmt" + "os" + "os/user" + "path/filepath" + "strings" - "github.com/Karmenzind/kd/config" - "go.uber.org/zap" + "github.com/Karmenzind/kd/config" + "go.uber.org/zap" ) var LOG_FILE string func buildLogger(logCfg *config.LoggerConfig, options ...zap.Option) (*zap.Logger, error) { - cfg := zap.NewDevelopmentConfig() - 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 { - f = logCfg.Path - } - if _, err := os.Stat(f); err == nil { - os.Chmod(f, 0o666) - } - LOG_FILE = f + cfg := zap.NewDevelopmentConfig() + 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 { + 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} - } - level, err := zap.ParseAtomicLevel(logCfg.Level) - if err != nil { - return nil, err - } - cfg.Level = level - return cfg.Build(options...) + cfg.OutputPaths = []string{f} + cfg.ErrorOutputPaths = []string{f} + } + level, err := zap.ParseAtomicLevel(logCfg.Level) + if err != nil { + return nil, err + } + cfg.Level = level + return cfg.Build(options...) } func InitLogger(logCfg *config.LoggerConfig) (*zap.Logger, error) { - l, err := buildLogger(logCfg) - if err != nil { - panic(err) - } - zap.ReplaceGlobals(l) + l, err := buildLogger(logCfg) + if err != nil { + panic(err) + } + zap.ReplaceGlobals(l) - return l, err + return l, err } diff --git a/pkg/decorate/color.go b/pkg/decorate/color.go index 0fa634c..40b3b26 100644 --- a/pkg/decorate/color.go +++ b/pkg/decorate/color.go @@ -1,11 +1,11 @@ package decorate import ( - "fmt" - "os" + "fmt" + "os" - fc "github.com/fatih/color" - gc "github.com/gookit/color" + fc "github.com/fatih/color" + gc "github.com/gookit/color" ) /* @@ -61,48 +61,48 @@ var ErrorBg = fc.New(fc.BgRed, fc.FgBlack, fc.Italic, fc.Faint).SprintFunc() type ColorStringFunc func(a ...interface{}) string var Title, Nation, Line, Pron, Property, Idx, Addi, Para, - CollinsPara, Eg, EgPref, EgEn, EgCh, - Star, Rank ColorStringFunc + CollinsPara, Eg, EgPref, EgEn, EgCh, + Star, Rank ColorStringFunc var Text = fc.New(fc.FgWhite).SprintFunc() func applyTheme(colorscheme string) { - switch colorscheme { - case "", "temp": - Title = fc.New(fc.FgHiMagenta, fc.Italic, fc.Bold, fc.Underline).SprintFunc() - Line = fc.New(fc.FgHiBlack, fc.Faint).SprintFunc() - Pron = fc.New(fc.Faint).SprintFunc() - Property = fc.New(fc.FgGreen).SprintFunc() - Idx = fc.New(fc.FgHiWhite).SprintFunc() - Addi = fc.New(fc.FgCyan, fc.Italic).SprintFunc() - Para = Text - CollinsPara = fc.New(fc.FgYellow).SprintFunc() - Eg = fc.New(fc.FgHiWhite, fc.Faint, fc.Italic).SprintFunc() - EgPref = Eg - // EgEn = Text - EgEn = fc.New(fc.FgHiWhite, fc.BgBlack).SprintFunc() - EgCh = fc.New(fc.FgHiWhite, fc.Faint, fc.Italic).SprintFunc() - Star = fc.New(fc.FgYellow).SprintFunc() - Rank = Eg - - case "wudao": - Title = fc.New(fc.FgRed, fc.Italic, fc.Bold, fc.Underline).SprintFunc() - Line = fc.New(fc.FgHiBlack, fc.Faint).SprintFunc() - Pron = fc.New(fc.FgCyan).SprintFunc() - Property = fmt.Sprint - Idx = fc.New(fc.FgHiWhite).SprintFunc() - Addi = fc.New(fc.FgGreen, fc.Italic).SprintFunc() - Para = Text - CollinsPara = fc.New(fc.FgHiWhite).SprintFunc() - Eg = fc.New(fc.FgHiYellow, fc.Faint, fc.Italic).SprintFunc() - EgPref = Addi - EgEn = fc.New(fc.FgYellow, fc.Italic).SprintFunc() - EgCh = Text - Star = fc.New(fc.FgYellow).SprintFunc() - Rank = fc.New(fc.FgRed, fc.Italic).SprintFunc() - - default: - fc.New(fc.FgRed).Printf("✘ Unknown theme: %s\n", colorscheme) - os.Exit(1) - } + switch colorscheme { + case "", "temp": + Title = fc.New(fc.FgHiMagenta, fc.Italic, fc.Bold, fc.Underline).SprintFunc() + Line = fc.New(fc.FgHiBlack, fc.Faint).SprintFunc() + Pron = fc.New(fc.Faint).SprintFunc() + Property = fc.New(fc.FgGreen).SprintFunc() + Idx = fc.New(fc.FgHiWhite).SprintFunc() + Addi = fc.New(fc.FgCyan, fc.Italic).SprintFunc() + Para = Text + CollinsPara = fc.New(fc.FgYellow).SprintFunc() + Eg = fc.New(fc.FgHiWhite, fc.Faint, fc.Italic).SprintFunc() + EgPref = Eg + // EgEn = Text + EgEn = fc.New(fc.FgHiWhite, fc.BgBlack).SprintFunc() + EgCh = fc.New(fc.FgHiWhite, fc.Faint, fc.Italic).SprintFunc() + Star = fc.New(fc.FgYellow).SprintFunc() + Rank = Eg + + case "wudao": + Title = fc.New(fc.FgRed, fc.Italic, fc.Bold, fc.Underline).SprintFunc() + Line = fc.New(fc.FgHiBlack, fc.Faint).SprintFunc() + Pron = fc.New(fc.FgCyan).SprintFunc() + Property = fmt.Sprint + Idx = fc.New(fc.FgHiWhite).SprintFunc() + Addi = fc.New(fc.FgGreen, fc.Italic).SprintFunc() + Para = Text + CollinsPara = fc.New(fc.FgHiWhite).SprintFunc() + Eg = fc.New(fc.FgHiYellow, fc.Faint, fc.Italic).SprintFunc() + EgPref = Addi + EgEn = fc.New(fc.FgYellow, fc.Italic).SprintFunc() + EgCh = Text + Star = fc.New(fc.FgYellow).SprintFunc() + Rank = fc.New(fc.FgRed, fc.Italic).SprintFunc() + + default: + fc.New(fc.FgRed).Printf("✘ Unknown theme: %s\n", colorscheme) + os.Exit(1) + } } diff --git a/pkg/decorate/emoji.go b/pkg/decorate/emoji.go index 21ff72c..7278826 100644 --- a/pkg/decorate/emoji.go +++ b/pkg/decorate/emoji.go @@ -8,8 +8,8 @@ var fallbackMap = map[string]string{ // emojify sprintf func Emo(format string, a ...any) string { - if emojiEnabled { - return fmt.Sprintf(format, a...) - } - return fmt.Sprintf(format, a...) + if emojiEnabled { + return fmt.Sprintf(format, a...) + } + return fmt.Sprintf(format, a...) } diff --git a/pkg/decorate/fmt.go b/pkg/decorate/fmt.go index e5d8253..a153075 100644 --- a/pkg/decorate/fmt.go +++ b/pkg/decorate/fmt.go @@ -1,8 +1,8 @@ package decorate import ( - "fmt" - "os" + "fmt" + "os" ) /* @@ -15,59 +15,42 @@ import ( 🉑 📥 ℹ 🇺🇸 🇬🇧 🗣 👄 👀 🎈 */ +func f(s string, a ...any) string { + if len(a) > 0 { + return fmt.Sprintf(s, a...) + } + return s +} + func EchoWarn(content string, a ...any) { - if len(a) > 0 { - content = fmt.Sprintf(content, a...) - } - fmt.Println(WarnBg("⚠ WARNING:"), Warn(content)) + fmt.Println(WarnBg("⚠ WARNING:"), Warn(f(content, a...))) } func EchoError(content string, a ...any) { - if len(a) > 0 { - content = fmt.Sprintf(content, a...) - } - fmt.Println(ErrorBg("☣ ERROR:"), Error(content)) + fmt.Println(ErrorBg("☣ ERROR:"), Error(f(content, a...))) } func EchoFatal(content string, a ...any) { - if len(a) > 0 { - content = fmt.Sprintf(content, a...) - } - fmt.Println(ErrorBg("☣ ERROR:"), Error(content)) - os.Exit(1) + fmt.Println(ErrorBg("☣ ERROR:"), Error(f(content, a...))) + os.Exit(1) } func EchoRun(content string, a ...any) { - if len(a) > 0 { - content = fmt.Sprintf(content, a...) - } - fmt.Println(Blue("≫ "), Blue(content)) + fmt.Println(Blue("≫ "), Blue(f(content, a...))) } func EchoOkay(content string, a ...any) { - if len(a) > 0 { - content = fmt.Sprintf(content, a...) - } - fmt.Println(Green("✔ "), Green(content)) + fmt.Println(Green("✔ "), Green(f(content, a...))) } func EchoFine(content string, a ...any) { - if len(a) > 0 { - content = fmt.Sprintf(content, a...) - } - fmt.Println(Green("☺ "), Green(content)) + fmt.Println(Green("☺ "), Green(f(content, a...))) } func EchoWrong(content string, a ...any) { - if len(a) > 0 { - content = fmt.Sprintf(content, a...) - } - fmt.Println(Red("✘ "), Red(content)) + fmt.Println(Red("✘ "), Red(f(content, a...))) } func EchoWeakNotice(content string, a ...any) { - if len(a) > 0 { - content = fmt.Sprintf(content, a...) - } - fmt.Println(Gray("☺ "), Gray(content)) + fmt.Println(Gray("☺ "), Gray(f(content, a...))) } diff --git a/pkg/decorate/init.go b/pkg/decorate/init.go index 2ba7468..e5eb21e 100644 --- a/pkg/decorate/init.go +++ b/pkg/decorate/init.go @@ -3,9 +3,9 @@ package decorate var emojiEnabled = false func ApplyConfig(enableEmoji bool) { - emojiEnabled = enableEmoji + emojiEnabled = enableEmoji } func ApplyTheme(theme string) { - applyTheme(theme) + applyTheme(theme) } diff --git a/pkg/file.go b/pkg/file.go index d6cdcfd..14bc348 100644 --- a/pkg/file.go +++ b/pkg/file.go @@ -1,63 +1,60 @@ package pkg import ( - "encoding/json" - "errors" - "os" + "encoding/json" + "errors" + "os" - "go.uber.org/zap" + "go.uber.org/zap" ) - func IsPathExists(p string) bool { - if _, err := os.Stat(p); errors.Is(err, os.ErrNotExist) { - return false - } - return true + if _, err := os.Stat(p); errors.Is(err, os.ErrNotExist) { + return false + } + return true } // marshal struct and write to file func SaveJson(fpath string, v any) (err error) { - j, err := json.Marshal(v) - if err != nil { - zap.S().Debugf("Failed to marshal: %+v %s", v, err) - } - err = os.WriteFile(fpath, j, os.ModePerm) - zap.S().Debugf("Updated json file %s. Error: %v. BodyLength: %d", fpath, err, len(j)) - return + j, err := json.Marshal(v) + if err != nil { + zap.S().Debugf("Failed to marshal: %+v %s", v, err) + } + err = os.WriteFile(fpath, j, os.ModePerm) + zap.S().Debugf("Updated json file %s. Error: %v. BodyLength: %d", fpath, err, len(j)) + return } func LoadJson(fpath string, v any) (err error) { - if _, err = os.Stat(fpath); errors.Is(err, os.ErrNotExist) { - zap.S().Debugf("Json file for '%s' doesn't exist.", fpath) - return err - } - j, err := os.ReadFile(fpath) - if err != nil { - zap.S().Debugf("Failed to read file for %s: %s", fpath, err) - return err - } - if len(j) > 0 { - err = json.Unmarshal(j, v) - if err != nil { - zap.S().Debugf("Failed to unmarshal : %s", fpath, err) - return err - } - } - return + if _, err = os.Stat(fpath); errors.Is(err, os.ErrNotExist) { + zap.S().Debugf("Json file for '%s' doesn't exist.", fpath) + return err + } + j, err := os.ReadFile(fpath) + if err != nil { + zap.S().Debugf("Failed to read file for %s: %s", fpath, err) + return err + } + if len(j) > 0 { + if err = json.Unmarshal(j, v); err != nil { + zap.S().Debugf("Failed to unmarshal : %s", fpath, err) + return err + } + } + return } func AddExecutablePermission(filePath string) error { - fileInfo, err := os.Stat(filePath) - if err != nil { - return err - } - originalMode := fileInfo.Mode() - newMode := originalMode | 0111 - if err := os.Chmod(filePath, newMode); err != nil { - return err - } + fileInfo, err := os.Stat(filePath) + if err != nil { + return err + } + originalMode := fileInfo.Mode() + newMode := originalMode | 0111 + if err := os.Chmod(filePath, newMode); err != nil { + return err + } zap.S().Infof("Executable permission added to %s\n", filePath) - return nil + return nil } - diff --git a/pkg/http.go b/pkg/http.go index 3fb6c89..636b9e9 100644 --- a/pkg/http.go +++ b/pkg/http.go @@ -93,39 +93,36 @@ func CreateHTTPClient(timeoutsec time.Duration) *http.Client { // return r, err // } -func DownloadFile(filepath string, url string) (err error) { - var client *http.Client +func getHTTPClient(url string) *http.Client { if strings.HasPrefix(url, "https") { - client = &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} - } else { - client = &http.Client{} + return &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} } + return &http.Client{} +} + +func DownloadFile(filepath string, url string) (err error) { + client := getHTTPClient(url) req, err := http.NewRequest("GET", url, nil) if err != nil { return err } - // Get the data resp, err := client.Do(req) - // resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() - // Create the file out, err := os.Create(filepath) if err != nil { return err } defer out.Close() - // Check server response if resp.StatusCode != http.StatusOK { return fmt.Errorf("bad status: %s", resp.Status) } - // Writer the body to file _, err = io.Copy(out, resp.Body) if err != nil { return err @@ -133,3 +130,60 @@ func DownloadFile(filepath string, url string) (err error) { return nil } + +func DownloadFileWithProgress(filepath string, url string) (err error) { + client := getHTTPClient(url) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bad status: %s", resp.Status) + } + + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + + totalBytes := resp.ContentLength + if totalBytes < 0 { + return fmt.Errorf("content length is unknown") + } + + // Create a buffer to hold the data + buffer := make([]byte, 4096) + var downloadedBytes int64 + + for { + n, err := resp.Body.Read(buffer) + if n > 0 { + _, writeErr := out.Write(buffer[:n]) + if writeErr != nil { + return writeErr + } + downloadedBytes += int64(n) + + progress := float64(downloadedBytes) / float64(totalBytes) * 100 + fmt.Printf("\rDownloading... %.2f%%", progress) + } + + if err == io.EOF { + break + } + if err != nil { + return err + } + } + + fmt.Println("\nDownload completed.") + return nil +} diff --git a/pkg/proc/proc.go b/pkg/proc/proc.go index ec15286..bc508dd 100644 --- a/pkg/proc/proc.go +++ b/pkg/proc/proc.go @@ -45,7 +45,7 @@ func GetKillCMD(pid int32) *exec.Cmd { func SysKillPID(pid int32) (err error) { cmd := GetKillCMD(pid) output, err := cmd.Output() - zap.S().Infof("Executed '%s'. Output %s", cmd, output) + zap.S().Infof("Executed '%s'. Output '%s'", cmd, output) if err != nil { zap.S().Warnf("Failed to kill %v with system command. Output: `%s` Error: `%s`", pid, output, err) } diff --git a/pkg/str/utils.go b/pkg/str/utils.go index 9fcead2..c274edb 100644 --- a/pkg/str/utils.go +++ b/pkg/str/utils.go @@ -1,8 +1,8 @@ package str import ( - "regexp" - "strings" + "regexp" + "strings" ) var SPACE_OR_TABLE = regexp.MustCompile("[ \t]+") @@ -10,18 +10,8 @@ var SPACE_OR_TABLE = regexp.MustCompile("[ \t]+") // trim spaces, remove \n and // replace /[ \t]+/ with a single ' ' func Simplify(s string) string { - s = strings.Trim(s, " \n\t ") // 结尾包含不可见unicode - s = strings.ReplaceAll(s, "\n", "") - s = SPACE_OR_TABLE.ReplaceAllString(s, " ") - return s -} - - -func InSlice(s string, l []string) bool { - for i := 0; i < len(s); i++ { - if s == l[i] { - return true - } - } - return false + s = strings.Trim(s, " \n\t ") // 结尾包含不可见unicode + s = strings.ReplaceAll(s, "\n", "") + s = SPACE_OR_TABLE.ReplaceAllString(s, " ") + return s } diff --git a/pkg/systemd/systemd.go b/pkg/systemd/systemd.go index 1bac85a..e09e25f 100644 --- a/pkg/systemd/systemd.go +++ b/pkg/systemd/systemd.go @@ -1,70 +1,73 @@ package systemd import ( - "bytes" - "os/exec" - "strings" + "bytes" + "os/exec" + "strings" - "go.uber.org/zap" + "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 + 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, outputStr, err) + return outputStr, err } func ServiceIsActive(unit string, isUser bool) (bool, error) { - out, err := systemdAction(unit, "is-active", isUser) - return out == "active", err + out, err := systemdAction(unit, "is-active", isUser) + if out == "active" || out == "inactive" { + err = nil + } + return out == "active", err } func ServiceIsEnabled(unit string, isUser bool) (bool, error) { - out, err := systemdAction(unit, "is-enabled", isUser) - return out == "enabled", err + 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" + 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 { + 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() - + return false, err + } + output := out.String() + zap.S().Debugf("Fetched unit-files: %s", output) - return strings.Contains(output, unitName), err + return strings.Contains(output, unitName), err } func StopService(unit string, isUser bool) (string, error) { - return systemdAction(unit, "stop", isUser) + return systemdAction(unit, "stop", isUser) } func StartService(unit string, isUser bool) (string, error) { - return systemdAction(unit, "start", isUser) + return systemdAction(unit, "start", isUser) } func RestartService(unit string, isUser bool) (string, error) { - return systemdAction(unit, "restart", isUser) + return systemdAction(unit, "restart", isUser) } diff --git a/pkg/terminal.go b/pkg/terminal.go index 31e69b5..9207375 100644 --- a/pkg/terminal.go +++ b/pkg/terminal.go @@ -1,17 +1,18 @@ package pkg import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - - d "github.com/Karmenzind/kd/pkg/decorate" - "github.com/google/shlex" - "go.uber.org/zap" - "golang.org/x/term" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "slices" + "strings" + + d "github.com/Karmenzind/kd/pkg/decorate" + "github.com/google/shlex" + "go.uber.org/zap" + "golang.org/x/term" ) var EXECUTABLE_BASENAME string @@ -20,157 +21,167 @@ var term_height int var term_width int func fetchExecutableInfo() error { - kdpath, err := os.Executable() - if err == nil { - EXECUTABLE_PATH = kdpath - EXECUTABLE_BASENAME = filepath.Base(kdpath) + kdpath, err := os.Executable() + if err == nil { + EXECUTABLE_PATH = kdpath + EXECUTABLE_BASENAME = filepath.Base(kdpath) - } - return err + } + return err } func GetExecutablePath() (string, error) { - var err error - if EXECUTABLE_PATH == "" { - err = fetchExecutableInfo() - } - return EXECUTABLE_PATH, err + var err error + if EXECUTABLE_PATH == "" { + err = fetchExecutableInfo() + } + return EXECUTABLE_PATH, err } func GetExecutableBasename() (string, error) { - var err error - if EXECUTABLE_BASENAME == "" { - err = fetchExecutableInfo() - } - return EXECUTABLE_BASENAME, err + var err error + if EXECUTABLE_BASENAME == "" { + err = fetchExecutableInfo() + } + return EXECUTABLE_BASENAME, err } // pager or print // config > $PAGER > less -f // TODO 增加检测pager可用 func OutputResult(out string, paging bool, pagerCmd string, doClear bool) error { - var err error - var logger = zap.S() - if paging { - if pagerCmd == "" { - // XXX (k): <2023-12-31> expandenv? - if sysPager := os.Getenv("PAGER"); sysPager != "" { - logger.Debugf("Using system pager %s", sysPager) - pagerCmd = sysPager - } else if runtime.GOOS != "windows" && CommandExists("less") { - pagerCmd = "less -RF" - logger.Debugf("Using default pager %s", pagerCmd) - } - } else { - logger.Debugf("Using assigned pager `%s`", pagerCmd) - } - - if pagerCmd != "" { - var pager *exec.Cmd - var program string - if strings.Contains(pagerCmd, " ") { - args, serr := shlex.Split(pagerCmd) - program = args[0] - if serr != nil { - return err - } - pager = exec.Command(args[0], args[1:]...) - } else { - program = pagerCmd - pager = exec.Command(pagerCmd) - } - if CommandExists(program) { - // pager.Stdin = strings.NewReader(out) - pager.Stdout = os.Stdout - pager.Stderr = os.Stderr - err = Output2PagerVer2(pager, out) - // err = pager.Run() - return err - } - d.EchoWarn(fmt.Sprintf("pager command `%s` not found", program)) - } - } - if doClear { - _, h, err := GetTermSize() - if err == nil && strings.Count(out, "\n") < h { - ClearScreen() - } - } - fmt.Println(out) - return nil + var err error + var logger = zap.S() + if paging { + if pagerCmd == "" { + // XXX (k): <2023-12-31> expandenv? + if sysPager := os.Getenv("PAGER"); sysPager != "" { + logger.Debugf("Using system pager %s", sysPager) + pagerCmd = sysPager + } else if runtime.GOOS != "windows" && CommandExists("less") { + pagerCmd = "less -RF" + logger.Debugf("Using default pager %s", pagerCmd) + } + } else { + logger.Debugf("Using assigned pager `%s`", pagerCmd) + } + + if pagerCmd != "" { + var pager *exec.Cmd + var program string + if strings.Contains(pagerCmd, " ") { + args, serr := shlex.Split(pagerCmd) + program = args[0] + if serr != nil { + return err + } + pager = exec.Command(args[0], args[1:]...) + } else { + program = pagerCmd + pager = exec.Command(pagerCmd) + } + if CommandExists(program) { + // pager.Stdin = strings.NewReader(out) + pager.Stdout = os.Stdout + pager.Stderr = os.Stderr + err = Output2PagerVer2(pager, out) + // err = pager.Run() + return err + } + d.EchoWarn(fmt.Sprintf("pager command `%s` not found", program)) + } + } + if doClear { + _, h, err := GetTermSize() + if err == nil && strings.Count(out, "\n") < h { + ClearScreen() + } + } + fmt.Println(out) + return nil } func Output2PagerVer1(pager *exec.Cmd, output string) (err error) { - pager.Stdin = strings.NewReader(output) - err = pager.Run() + pager.Stdin = strings.NewReader(output) + err = pager.Run() - return err + return err } func Output2PagerVer2(pager *exec.Cmd, output string) (err error) { - pipe, err := pager.StdinPipe() - if err != nil { - return - } - - if err = pager.Start(); err != nil { - return err - } - - defer func() { - pipe.Close() - pager.Wait() - }() - fmt.Fprintln(pipe, output) - return err + pipe, err := pager.StdinPipe() + if err != nil { + return + } + + if err = pager.Start(); err != nil { + return err + } + + defer func() { + pipe.Close() + pager.Wait() + }() + fmt.Fprintln(pipe, output) + return err } func CommandExists(cmd string) bool { - _, err := exec.LookPath(cmd) - return err == nil + p, err := exec.LookPath(cmd) + zap.S().Debugf("Got path of %s: %v", cmd, p) + return err == nil } // ask yes or no func AskYN(prompt string) bool { - var input string - for { - fmt.Print(d.Blue(":: "), prompt, " [Y/n] ") - fmt.Scanln(&input) - switch input { - case "", "Y", "y": - return true - case "n": - return false - default: - fmt.Println("Please input Y or n.") - continue - } - } + var input string + for { + fmt.Print(d.Blue(":: "), prompt, " [Y/n] ") + fmt.Scanln(&input) + switch input { + case "", "Y", "y": + return true + case "n": + return false + default: + fmt.Println("Please input Y or n.") + continue + } + } } func ClearScreen() { - var c *exec.Cmd - switch runtime.GOOS { - case "linux", "darwin": - c = exec.Command("clear") - case "windows": - c = exec.Command("cls") - } - c.Stdout = os.Stdout - c.Run() - zap.S().Debugf("Cleared screen.") + var c *exec.Cmd + switch runtime.GOOS { + case "linux", "darwin": + c = exec.Command("clear") + case "windows": + c = exec.Command("cls") + } + c.Stdout = os.Stdout + c.Run() + zap.S().Debugf("Cleared screen.") } func GetTermSize() (int, int, error) { - if term_height > 0 && term_width > 0 { - return term_width, term_height, nil - } - w, h, err := term.GetSize(0) - if err != nil { - return 0, 0, err - } - term_height = h - term_width = w - return w, h, nil + if term_height > 0 && term_width > 0 { + return term_width, term_height, nil + } + w, h, err := term.GetSize(0) + if err != nil { + return 0, 0, err + } + term_height = h + term_width = w + return w, h, nil +} + +func HasAnyFlag(flags ...string) bool { + for idx := range flags { + if slices.Index(os.Args, "--"+flags[idx]) > 0 { + return true + } + } + return false } diff --git a/scripts/install.sh b/scripts/install.sh index a2499a8..ba18e29 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -87,12 +87,17 @@ echo "[✔] 已经下载完成,文件临时保存位置:${TEMP_PATH}" if [[ $(whoami) == "root" ]]; then usesudo=0 else - if [[ ":$PATH:" == *":$HOME/.local/bin:"* ]]; then - usesudo=0 - INST_PATH=$HOME/.local/bin/kd - echo "≫ 检测到PATH中包含~/.local/bin,kd将保存到该目录下" - else - usesudo=1 + # if [[ ":$PATH:" == *":$HOME/.local/bin:"* ]]; then + # usesudo=0 + # INST_PATH=$HOME/.local/bin/kd + # echo "≫ 检测到PATH中包含~/.local/bin,kd将保存到该目录下" + # else + # usesudo=1 + # fi + usesudo=1 + if [[ ":$PATH:" == *":/usr/local/bin:"* ]]; then + INST_PATH=/usr/local/bin/kd + echo "≫ 检测到PATH中包含/usr/local/bin,kd将保存到该目录下" fi fi