diff --git a/cmd/kd.go b/cmd/kd.go index 690c178..2cf24e4 100644 --- a/cmd/kd.go +++ b/cmd/kd.go @@ -13,6 +13,8 @@ import ( "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" @@ -42,6 +44,7 @@ var um = map[string]string{ "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 用默认编辑器打开配置文件", @@ -57,13 +60,13 @@ func flagServer(*cli.Context, bool) error { if strings.Contains(err.Error(), "address already in use") { return fmt.Errorf("端口已经被占用(%s)", err) } - return nil + return err } func flagDaemon(*cli.Context, bool) error { p, _ := daemon.FindServerProcess() if p != nil { - d.EchoWrong(fmt.Sprintf("已存在运行中的守护进程,PID:%d。请先执行`kd --stop`停止该进程", p.Pid)) + d.EchoWrong("已存在运行中的守护进程,PID:%d。请先执行`kd --stop`停止该进程", p.Pid) return nil } @@ -79,7 +82,15 @@ func flagStop(*cli.Context, bool) error { if err != nil { d.EchoFatal(err.Error()) } - return nil + return err +} + +func flagRestart(*cli.Context, bool) error { + err := daemon.KillDaemonIfRunning() + if err == nil { + err = daemon.StartDaemonProcess() + } + return err } func flagUpdate(ctx *cli.Context, _ bool) (err error) { @@ -114,7 +125,7 @@ func flagUpdate(ctx *cli.Context, _ bool) (err error) { if doUpdate { emoji.Println(":lightning: Let's update now") - go daemon.KillDaemonIfRunning() + go daemon.KillDaemonIfRunning() err = update.UpdateBinary(VERSION) } return err @@ -175,7 +186,19 @@ func flagStatus(*cli.Context, bool) error { fmt.Printf(" Binary地址:%s\n", kdpath) } - return nil + return err +} + +func checkAndNoticeUpdate() { + if ltag := update.GetCachedLatestTag(); ltag != "" { + if update.CompareVersions(ltag, VERSION) == 1 { + prompt := fmt.Sprintf("发现新版本%s,请执行`kd --update`更新", ltag) + if pkg.GetLinuxDistro() == "arch" { + prompt += "。ArchLinux推荐通过AUR安装/升级" + } + d.EchoWeakNotice(prompt) + } + } } func basicCheck() { @@ -203,6 +226,8 @@ func main() { cfg := config.Cfg d.ApplyConfig(cfg.EnableEmoji) + run.Info.Version = VERSION + if cfg.Logging.Enable { l, err := logger.InitLogger(&cfg.Logging) if err != nil { @@ -242,6 +267,7 @@ func main() { &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"]}, @@ -249,7 +275,10 @@ func main() { }, Action: func(cCtx *cli.Context) error { // 除了--text外,其他的BoolFlag都当subcommand用 - for _, flag := range []string{"init", "server", "daemon", "stop", "update", "generate-config", "edit-config", "status"} { + 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 } @@ -257,8 +286,7 @@ func main() { if cfg.FileExists && cfg.ModTime > daemon.GetDaemonInfo().StartTime { d.EchoWarn("检测到配置文件发生修改,正在重启守护进程") - flagStop(cCtx, true) - flagDaemon(cCtx, true) + flagRestart(cCtx, true) } if cCtx.String("theme") != "" { @@ -268,10 +296,6 @@ func main() { if cCtx.Args().Len() > 0 { zap.S().Debugf("Recieved Arguments (len: %d): %+v \n", cCtx.Args().Len(), cCtx.Args().Slice()) - // emoji.Printf(":eyes: Arguments are: %+v \n", cCtx.Args().Slice()) - // emoji.Printf(":eyes: Flat --update %+v \n", cCtx.Bool("update")) - // emoji.Printf(":eyes: Flat --nocache %+v \n", cCtx.Bool("nocache")) - // emoji.Printf(":eyes: flags are: %+v \n", cCtx.App.VisibleFlags) // emoji.Printf("Test emoji:\n:accept: :inbox_tray: :information: :us: :uk: 🗣 :lips: :eyes: :balloon: \n") qstr := strings.Join(cCtx.Args().Slice(), " ") @@ -284,7 +308,7 @@ func main() { } if err == nil { if r.Found { - err = pkg.OutputResult(r.PrettyFormat(cfg.EnglishOnly), cfg.Paging, cfg.PagerCommand, cfg.ClearScreen) + err = pkg.OutputResult(query.PrettyFormat(r, cfg.EnglishOnly), cfg.Paging, cfg.PagerCommand, cfg.ClearScreen) if err != nil { d.EchoFatal(err.Error()) } @@ -310,14 +334,4 @@ func main() { zap.S().Errorf("APP stopped: %s", err) d.EchoError(err.Error()) } - - if ltag := update.GetCachedLatestTag(); ltag != "" { - if update.CompareVersions(ltag, VERSION) == 1 { - prompt := fmt.Sprintf("发现新版本%s,请执行`kd --update`更新", ltag) - if pkg.GetLinuxDistro() == "arch" { - prompt+= "。ArchLinux推荐通过AUR安装/升级" - } - d.EchoWeakNotice(prompt) - } - } } diff --git a/internal/cache/cache.go b/internal/cache/cache.go index d9da5a0..0ec1e85 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -10,6 +10,7 @@ import ( "path/filepath" "github.com/Karmenzind/kd/internal/model" + "github.com/Karmenzind/kd/pkg" "go.uber.org/zap" ) @@ -33,7 +34,7 @@ func GetCachedQuery(r *model.Result) (err error) { } c.Close() j := jb.Bytes() - zap.S().Debugf("Got cached json %s", j) + zap.S().Debugf("Got cached json %s", j) if len(j) > 0 { err = json.Unmarshal(j, r) @@ -55,9 +56,9 @@ func UpdateQueryCache(r *model.Result) (err error) { j, err := json.Marshal(r) if err != nil { zap.S().Warnf("Failed to marshal %+v: %s", r, err) - return + return } - zap.S().Debugf("Got marshalled json to save: %s", j) + zap.S().Debugf("Got marshalled json to save: %s", j) var zb bytes.Buffer jw := zlib.NewWriter(&zb) @@ -104,7 +105,7 @@ func UpdateQueryCacheJson(r *model.Result) (err error) { if !r.Found { return } - err = saveJson(getQueryCacheFilePath(r.Query), r) + err = pkg.SaveJson(getQueryCacheFilePath(r.Query), r) if err != nil { zap.S().Errorf("Failed to update cache for '%s'. Error: %s", r.Query, err) } diff --git a/internal/cache/counter.go b/internal/cache/counter.go index 8ae2d81..71b9034 100644 --- a/internal/cache/counter.go +++ b/internal/cache/counter.go @@ -26,7 +26,7 @@ func CounterIncr(query string, history chan int) { 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 := loadJson(counterPath, &c) + err := pkg.LoadJson(counterPath, &c) if err != nil { zap.S().Warnf("Failed to load counter") return @@ -35,5 +35,5 @@ func CounterIncr(query string, history chan int) { } c[query] += 1 history <- c[query] - saveJson(counterPath, &c) + pkg.SaveJson(counterPath, &c) } diff --git a/internal/cache/utils.go b/internal/cache/utils.go index a0749f7..14ea62a 100644 --- a/internal/cache/utils.go +++ b/internal/cache/utils.go @@ -1,46 +1,28 @@ package cache import ( + "fmt" "os" - "path/filepath" - "runtime" - "github.com/Karmenzind/kd/pkg" + "github.com/Karmenzind/kd/internal/run" + d "github.com/Karmenzind/kd/pkg/decorate" ) -var CACHE_ROOT_DIRNAME = "kdcache" -var CACHE_ROOT_PATH string // kdCache目录完整路径 -var CACHE_WORDS_PATH string // 放word缓存文件的目录完整路径 -var CACHE_RUN_PATH string // 存放运行信息 -var CACHE_STAT_DIR_PATH string - -var saveJson = pkg.SaveJson -var loadJson = pkg.LoadJson - -func ensureCacheDir() string { - userdir, _ := os.UserHomeDir() - var target string - switch runtime.GOOS { - case "linux": - target = filepath.Join(userdir, ".cache", CACHE_ROOT_DIRNAME) - case "darwin": - target = filepath.Join(userdir, "Library/Caches", CACHE_ROOT_DIRNAME) - case "windows": - target = filepath.Join(userdir, ".cache", CACHE_ROOT_DIRNAME) - } - err := os.MkdirAll(target, os.ModePerm) - if err != nil { - panic("Failed to create cache dir") - } - return target -} +var CACHE_ROOT_PATH = run.CACHE_ROOT_PATH +var CACHE_WORDS_PATH = run.CACHE_WORDS_PATH +var CACHE_RUN_PATH = run.CACHE_RUN_PATH +var CACHE_STAT_DIR_PATH = run.CACHE_STAT_DIR_PATH func init() { - CACHE_ROOT_PATH = ensureCacheDir() - 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") - os.Mkdir(CACHE_WORDS_PATH, os.ModePerm) - os.Mkdir(CACHE_STAT_DIR_PATH, os.ModePerm) - os.Mkdir(CACHE_RUN_PATH, os.ModePerm) + 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)) + } + } } diff --git a/internal/client.go b/internal/client.go index cb7f478..cd0385e 100644 --- a/internal/client.go +++ b/internal/client.go @@ -1,16 +1,7 @@ package internal -/* - -功能: - -- 查询 -- 更新 -*/ - import ( "fmt" - "os" "regexp" "strings" @@ -19,9 +10,8 @@ import ( "github.com/Karmenzind/kd/internal/daemon" "github.com/Karmenzind/kd/internal/model" q "github.com/Karmenzind/kd/internal/query" - "github.com/Karmenzind/kd/pkg" + "github.com/Karmenzind/kd/internal/run" d "github.com/Karmenzind/kd/pkg/decorate" - "github.com/Karmenzind/kd/pkg/proc" "github.com/Karmenzind/kd/pkg/str" "go.uber.org/zap" ) @@ -36,35 +26,30 @@ func ensureDaemon(running chan bool) { d.EchoFatal(err.Error()) } } else { - exename, err := pkg.GetExecutablePath() - if err == nil { - runningExename, _ := p.Exe() - // TODO (k): <2024-01-03> 增加检查版本 - if exename != runningExename { - d.EchoWarn(fmt.Sprintf("正在运行的守护程序(%s)与当前程序(%s)文件信息或版本不一致,将尝试重新启动守护进程", runningExename, exename)) - err := proc.KillProcess(p) - if err != nil { - cmd := proc.GetKillCMD(p.Pid) - d.EchoError(fmt.Sprintf("停止进程%v失败,请手动执行:", p.Pid)) - fmt.Println(cmd.String()) - os.Exit(1) - } - d.EchoRun("已终止,正在启动新的守护进程...") - err = daemon.StartDaemonProcess() - if err != nil { - d.EchoFatal(err.Error()) - } - } + var warn string + // recorded daemon info + recDi := daemon.GetDaemonInfo() + if run.Info.Version != recDi.Version { + warn = fmt.Sprintf("正在运行的守护程序版本(%s)与当前程序(%s)不一致", recDi.Version, run.Info.Version) + } else if daemonExepath, _ := p.Exe(); run.Info.ExePath != daemonExepath { + warn = fmt.Sprintf("正在运行的守护程序(%s)与当前程序(%s)文件路径不一致", daemonExepath, run.Info.ExePath) + // err := proc.KillProcess(p) + // if err != nil { + // cmd := proc.GetKillCMD(p.Pid) + // d.EchoError("停止进程%v失败,请手动执行:", p.Pid) + // fmt.Println(cmd.String()) + // os.Exit(1) + // } + // d.EchoRun("已终止,正在启动新的守护进程...") + // err = daemon.StartDaemonProcess() + // if err != nil { + // d.EchoFatal(err.Error()) + // } + } + if warn != "" { + d.EchoWarn(warn + ",建议执行`kd --restart`重启") } } - // if !daemon.ServerIsRunning() { - // err := daemon.StartDaemonProcess() - // if err != nil { - // d.EchoRun("未找到守护进程,正在启动...") - // d.EchoFatal(err.Error()) - // } - // running <- true - // } running <- true } @@ -74,8 +59,6 @@ func Query(query string, noCache bool, longText bool) (r *model.Result, err erro if !longText { query = strings.ToLower(query) } - // query = strings.ToLower(strings.Trim(query, " ")) - // query = strings.ReplaceAll(query, "\n", " ") r = buildResult(query, longText) r.History = make(chan int, 1) @@ -93,12 +76,6 @@ func Query(query string, noCache bool, longText bool) (r *model.Result, err erro return } - // if longText { - // r.Found = false - // r.Prompt = "暂不支持长句翻译" - // return - // } - var inNotFound bool var line int if !longText { @@ -137,17 +114,11 @@ func Query(query string, noCache bool, longText bool) (r *model.Result, err erro d.EchoFatal("守护进程未启动,请手动执行`kd --daemon`") } - // FIXME move to server - // if !r.Found { - // err = q.FetchOnline(r) - // // 判断时间 - // cache.UpdateQueryCache(r) - // } return r, err } func QueryDaemon(r *model.Result) error { - addr := fmt.Sprintf("localhost:%d", SERVER_PORT) + addr := fmt.Sprintf("localhost:%d", run.SERVER_PORT) err := q.QueryDaemon(addr, r) return err } diff --git a/internal/daemon/process.go b/internal/daemon/process.go index fe566b3..3c81129 100644 --- a/internal/daemon/process.go +++ b/internal/daemon/process.go @@ -2,7 +2,6 @@ package daemon import ( "fmt" - "os" "os/exec" "path/filepath" "runtime" @@ -10,6 +9,7 @@ import ( "time" "github.com/Karmenzind/kd/internal/cache" + "github.com/Karmenzind/kd/internal/model" "github.com/Karmenzind/kd/pkg" d "github.com/Karmenzind/kd/pkg/decorate" "github.com/Karmenzind/kd/pkg/proc" @@ -18,27 +18,27 @@ import ( "go.uber.org/zap" ) -type DaemonInfoType struct { - StartTime int64 - Port string - PID int -} +// type model.RunInfo struct { +// *proc.ProcInfo +// Port string +// Version string +// } -var DaemonInfo = &DaemonInfoType{} - -func RecordRunInfo(port string) { - DaemonInfo.StartTime = time.Now().Unix() - DaemonInfo.PID = os.Getpid() - DaemonInfo.Port = port - pkg.SaveJson( - filepath.Join(cache.CACHE_RUN_PATH, "daemon.json"), - DaemonInfo, - ) - zap.S().Infof("Recorded running information of daemon %+v", DaemonInfo) -} +var DaemonInfo = &model.RunInfo{} + +// func RecordRunInfo(port string) { +// run.Info.Port = port + +// err := pkg.SaveJson(filepath.Join(run.CACHE_RUN_PATH, "daemon.json"), run.Info) +// if err == nil { +// zap.S().Infof("Recorded running information of daemon %+v", DaemonInfo) +// } else { +// zap.S().Warnf("Failed to record running info of daemon %+v", err) +// } +// } -func GetDaemonInfo() *DaemonInfoType { - if *DaemonInfo == (DaemonInfoType{}) { +func GetDaemonInfo() *model.RunInfo { + if *DaemonInfo == (model.RunInfo{}) { err := pkg.LoadJson(filepath.Join(cache.CACHE_RUN_PATH, "daemon.json"), DaemonInfo) if err != nil { d.EchoFatal("获取守护进程信息失败,请执行`kd --stop && kd --daemon`") @@ -76,7 +76,7 @@ func FindServerProcess() (*process.Process, error) { // XXX err n, _ := p.Name() if p.Pid == int32(di.PID) { - zap.S().Debugf("Got daemon process %v via daemon info", 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 @@ -147,7 +147,7 @@ func KillDaemonIfRunning() error { return err } - err = proc.KillProcess(p) + err = proc.KillProcess(p) if err == nil { zap.S().Info("Terminated daemon process.") diff --git a/internal/model/dataobj.go b/internal/model/dataobj.go index 42b322d..8798297 100644 --- a/internal/model/dataobj.go +++ b/internal/model/dataobj.go @@ -1,13 +1,9 @@ package model import ( - "fmt" "regexp" "strings" - "testing" - "github.com/Karmenzind/kd/pkg" - d "github.com/Karmenzind/kd/pkg/decorate" "go.uber.org/zap" ) @@ -63,243 +59,3 @@ func (r *Result) Initialize() { zap.S().Debugf("Query: isEn: %v isPhrase: %v\n", r.IsEN, r.IsPhrase) } } - -// func emojifyIfNeeded(str string, enableEmoji bool) { -// } - -// func (r *Result) ToQueryDaemonJSON() ([]byte, error) { -// q := model.QueryDaemon{ -// } -// return json.Marshal(r) -// } - -// func (r *Result) FromDaemonResponseJSON() []byte { -// } - -var p = regexp.MustCompile("^([^\u4e00-\u9fa5]+) ([^ ]*[\u4e00-\u9fa5]+.*)$") -var normalSentence = regexp.MustCompile("^[A-Za-z]+ ") - -// XXX -func cutCollinsTrans(line string) (string, string) { - g := p.FindStringSubmatch(line) - if len(g) == 3 { - return g[1], g[2] - } - return "", "" -} - -func (r *Result) PrettyFormat(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 -} - -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 { - r = fmt.Sprintf("%s %s", d.EgEn(item[0]), d.EgCh(item[1])) - } - 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 := pkg.GetTermSize() - if err != nil { - width = 44 - } - return width - 2 -} - -func TestPrint(t *testing.T) { -} - -// type ResultToSave struct { -// *Result -// } - -// func (rs ResultToSave) MarshalJSON() ([]byte, error) { -// var nonEmptyProps map[string]interface{} -// fmt.Println("Here") -// r := *rs.Result - -// t := reflect.TypeOf(r) -// v := reflect.ValueOf(r) - -// nonEmptyProps = make(map[string]interface{}) -// for i := 0; i < t.NumField(); i++ { -// field := t.Field(i) -// name := field.Name -// fmt.Println("Parsing", name) -// switch name { -// case "MachineTrans", "Found", "IsLongText": -// fmt.Println("Ignored", name) -// continue -// } -// tag := field.Tag.Get("json") -// if tag == "-" { -// fmt.Println("Ignored", name) -// continue -// } -// value := v.Field(i).Interface() -// switch field.Type.Kind() { -// case reflect.Map, reflect.Slice: -// if reflect.ValueOf(value).Len() == 0 { -// fmt.Println("Ignored", name) -// continue -// } -// case reflect.Bool, reflect.String, reflect.Int, reflect.Int8, reflect.Int64, reflect.Int32, reflect.Int16: -// if value != reflect.Zero(field.Type).Interface() { -// nonEmptyProps[tag] = value -// fmt.Println("Saved", name, value) -// } -// continue -// default: -// subj, _ := json.Marshal(value) -// nonEmptyProps[tag] = subj -// } -// } -// fmt.Println("Got %+v\n", nonEmptyProps) -// return json.Marshal(nonEmptyProps) -// } diff --git a/internal/model/others.go b/internal/model/others.go new file mode 100644 index 0000000..200bd54 --- /dev/null +++ b/internal/model/others.go @@ -0,0 +1,55 @@ +package model + +import ( + "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 + + isServer bool + termHeight int + termWidth int +} + +func (r *RunInfo) IsServer() bool { + return r.isServer +} + +func (r *RunInfo) SetServer(v bool) { + r.isServer = v +} + +func (r *RunInfo) SetPort(v string) { + r.Port = v +} + +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 +} + +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 +} diff --git a/internal/query/online.go b/internal/query/online.go index 57779f3..4abe2dd 100644 --- a/internal/query/online.go +++ b/internal/query/online.go @@ -8,6 +8,7 @@ import ( "os" "strconv" "strings" + "time" "github.com/Karmenzind/kd/config" "github.com/Karmenzind/kd/internal/cache" @@ -17,14 +18,14 @@ import ( "go.uber.org/zap" ) -var ydCliLegacy = &http.Client{} -var ydCli = &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} +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 + useNewApi := false q := strings.ReplaceAll(r.Query, " ", "%20") if useNewApi { cli = ydCli @@ -122,6 +123,6 @@ func FetchOnline(r *model.Result) (err error) { return } -func init() { - ydCli = pkg.CreateHTTPClient(5) -} +// func init() { +// ydCli = pkg.CreateHTTPClient(5) +// } diff --git a/internal/query/output.go b/internal/query/output.go new file mode 100644 index 0000000..f984cb6 --- /dev/null +++ b/internal/query/output.go @@ -0,0 +1,187 @@ +package query + +import ( + "fmt" + "regexp" + "strings" + + "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]+ ") + +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 +} + +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 { + r = fmt.Sprintf("%s %s", d.EgEn(item[0]), d.EgCh(item[1])) + } + 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 +} + +// XXX +func cutCollinsTrans(line string) (string, string) { + g := collinsTransPat.FindStringSubmatch(line) + if len(g) == 3 { + return g[1], g[2] + } + return "", "" +} diff --git a/internal/run/info.go b/internal/run/info.go new file mode 100644 index 0000000..cc0b12c --- /dev/null +++ b/internal/run/info.go @@ -0,0 +1,58 @@ +package run + +import ( + "os" + "path/filepath" + "runtime" + "time" + + "github.com/Karmenzind/kd/internal/model" + d "github.com/Karmenzind/kd/pkg/decorate" +) + +var Info *model.RunInfo + +var CACHE_ROOT_PATH string // kdCache目录完整路径 +var CACHE_WORDS_PATH string // 放word缓存文件的目录完整路径 +var CACHE_RUN_PATH string // 存放运行信息 +var CACHE_STAT_DIR_PATH string + +// ----------------------------------------------------------------------------- + +// TODO (k) 支持config +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 + +} + +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") +} diff --git a/internal/run/utils.go b/internal/run/utils.go new file mode 100644 index 0000000..bbe6ec6 --- /dev/null +++ b/internal/run/utils.go @@ -0,0 +1 @@ +package run diff --git a/internal/server.go b/internal/server.go index b7ee0ef..faaf93a 100644 --- a/internal/server.go +++ b/internal/server.go @@ -6,19 +6,24 @@ import ( "fmt" "io" "net" + "path/filepath" "github.com/Karmenzind/kd/internal/daemon" "github.com/Karmenzind/kd/internal/model" "github.com/Karmenzind/kd/internal/query" + "github.com/Karmenzind/kd/internal/run" + "github.com/Karmenzind/kd/pkg" + d "github.com/Karmenzind/kd/pkg/decorate" "go.uber.org/zap" ) -// TODO 支持自定义 -var SERVER_PORT = 19707 - func StartServer() (err error) { - IS_SERVER = true - addr := fmt.Sprintf("localhost:%d", SERVER_PORT) + if !pkg.IsPortOpen(run.SERVER_PORT) { + d.EchoFatal("端口%d已经被占用,请停止占用端口的程序后重试", run.SERVER_PORT) + } + + run.Info.SetServer(true) + addr := fmt.Sprintf("localhost:%d", run.SERVER_PORT) l, err := net.Listen("tcp", addr) if err != nil { zap.S().Errorf("Failed to start server:", err) @@ -30,13 +35,13 @@ func StartServer() (err error) { zap.S().Errorf("Failed to SplitHostPort:", err) return err } - daemon.InitCron() - go daemon.RecordRunInfo(port) + run.Info.SetPort(port) + go run.Info.SaveToFile(filepath.Join(run.CACHE_RUN_PATH, "daemon.json")) + d.EchoOkay("Listening on host: %s, port: %s\n", host, port) - fmt.Printf("Listening on host: %s, port: %s\n", host, port) + daemon.InitCron() for { - // Listen for an incoming connection conn, err := l.Accept() if err != nil { zap.S().Errorf("Failed to accept connection:", err) @@ -52,16 +57,16 @@ func handleClient(conn net.Conn) { recv, err := bufio.NewReader(conn).ReadBytes('\n') if err == io.EOF { zap.S().Debugf("Connection closed by client.") - fmt.Println("Connection closed by client") + d.EchoWarn("Connection closed by client") return } else if err != nil { - fmt.Printf("Error reading: %#v\n", err) + d.EchoWrong(fmt.Sprintf("Error reading: %#v\n", err)) zap.S().Errorf("Error reading: %#v\n", err) // FIXME (k): <2024-01-02> reply - return + return } - fmt.Printf("Received: %s\n", recv) + d.EchoRun("Received: %s\n", recv) q := model.TCPQuery{} err = json.Unmarshal(recv, &q) if err != nil { @@ -79,8 +84,7 @@ func handleClient(conn net.Conn) { reply, _ = json.Marshal(model.DaemonResponse{Error: fmt.Sprintf("序列化查询结果失败:%s", err)}) } - fmt.Printf("Sending to client: %s \n", reply) + d.EchoRun("Sending to client: %s \n", reply) conn.Write(append(reply, '\n')) conn.Close() - } diff --git a/internal/utils.go b/internal/utils.go index 9114221..695c115 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -4,12 +4,11 @@ import "github.com/Karmenzind/kd/internal/model" var IS_SERVER = false - func buildResult(q string, ilt bool) *model.Result { - return &model.Result{ - BaseResult: &model.BaseResult{ - Query: q, - IsLongText: ilt, - }, - } + return &model.Result{ + BaseResult: &model.BaseResult{ + Query: q, + IsLongText: ilt, + }, + } } diff --git a/pkg/decorate/fmt.go b/pkg/decorate/fmt.go index 243bd60..e5d8253 100644 --- a/pkg/decorate/fmt.go +++ b/pkg/decorate/fmt.go @@ -12,36 +12,62 @@ import ( ✖ 2716 Heavy multiplication ✗ 2717 Ballot X ✘ 2718 Heavy ballot X +🉑 📥 ℹ 🇺🇸 🇬🇧 🗣 👄 👀 🎈 */ -func EchoWarn(content string) { + +func EchoWarn(content string, a ...any) { + if len(a) > 0 { + content = fmt.Sprintf(content, a...) + } fmt.Println(WarnBg("⚠ WARNING:"), Warn(content)) } -func EchoError(content string) { +func EchoError(content string, a ...any) { + if len(a) > 0 { + content = fmt.Sprintf(content, a...) + } fmt.Println(ErrorBg("☣ ERROR:"), Error(content)) } -func EchoFatal(content string) { +func EchoFatal(content string, a ...any) { + if len(a) > 0 { + content = fmt.Sprintf(content, a...) + } fmt.Println(ErrorBg("☣ ERROR:"), Error(content)) os.Exit(1) } -func EchoRun(content string) { +func EchoRun(content string, a ...any) { + if len(a) > 0 { + content = fmt.Sprintf(content, a...) + } fmt.Println(Blue("≫ "), Blue(content)) } -func EchoOkay(content string) { +func EchoOkay(content string, a ...any) { + if len(a) > 0 { + content = fmt.Sprintf(content, a...) + } fmt.Println(Green("✔ "), Green(content)) } -func EchoFine(content string) { +func EchoFine(content string, a ...any) { + if len(a) > 0 { + content = fmt.Sprintf(content, a...) + } fmt.Println(Green("☺ "), Green(content)) } -func EchoWrong(content string) { +func EchoWrong(content string, a ...any) { + if len(a) > 0 { + content = fmt.Sprintf(content, a...) + } fmt.Println(Red("✘ "), Red(content)) } -func EchoWeakNotice(content string) { +func EchoWeakNotice(content string, a ...any) { + if len(a) > 0 { + content = fmt.Sprintf(content, a...) + } fmt.Println(Gray("☺ "), Gray(content)) } diff --git a/pkg/net.go b/pkg/net.go new file mode 100644 index 0000000..3b3c6d2 --- /dev/null +++ b/pkg/net.go @@ -0,0 +1,33 @@ +package pkg + +import ( + "fmt" + "net" + "os/exec" + "runtime" + "strings" +) + +func IsPortOpen(port int) bool { + if runtime.GOOS == "windows" { + return !IsPortInUseOnWindows(port) + } + address := fmt.Sprintf(":%d", port) + listener, err := net.Listen("tcp", address) + if err != nil { + return false + } + defer listener.Close() + return true +} + +func IsPortInUseOnWindows(port int) bool { + output, err := exec.Command("netstat", "-ano").CombinedOutput() + if err != nil { + fmt.Println("Error running netstat command:", err) + return false + } + + outputString := string(output) + return strings.Contains(outputString, fmt.Sprintf(":%d", port)) +} diff --git a/plan.md b/plan.md index f39cea8..a439e5d 100644 --- a/plan.md +++ b/plan.md @@ -2,9 +2,11 @@ ## wip -- 升级后还提示new version +- 记录daemon更多信息 + - 运行时候比较server版本 + - check if fits daemon.json +- 指定port - release增加version,aur判断此文件 -- 更新后stop server - 长句查询 (另外缓存) - notfound counter 忽略longtext - gitea镜像