-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain.go
More file actions
486 lines (447 loc) · 11.9 KB
/
main.go
File metadata and controls
486 lines (447 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
package main
import (
"fmt"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
"time"
"github.com/getlantern/systray"
"github.com/sqweek/dialog"
"gopkg.in/yaml.v3"
)
// 处理打开目录菜单项的点击
func openDirHandler(cfg *Config, app App) {
go func() {
for range menuOpenDir.ClickedCh {
dir := app.Path
if idx := strings.LastIndexAny(dir, "\\/"); idx != -1 {
dir = dir[:idx]
}
if dir == "" {
showError("未能获取目录")
continue
}
cmd := exec.Command("explorer", dir)
err := cmd.Start()
if err != nil {
showError("打开文件夹失败: " + err.Error())
}
}
}()
}
type App struct {
Path string `yaml:"path"`
Args []string `yaml:"args"`
}
type Config struct {
Activate string `yaml:"activate"`
Apps map[string]App `yaml:"apps"`
}
const configFile = "config.yaml"
func loadConfig() (*Config, error) {
data, err := os.ReadFile(configFile)
if err != nil {
return nil, err
}
var cfg Config
err = yaml.Unmarshal(data, &cfg)
if err != nil {
return nil, err
}
if cfg.Apps == nil {
cfg.Apps = make(map[string]App)
}
return &cfg, nil
}
func saveConfig(cfg *Config) error {
data, err := yaml.Marshal(cfg)
if err != nil {
return err
}
return os.WriteFile(configFile, data, 0644)
}
func listApps(cfg *Config) {
fmt.Println("已配置应用:")
for name, app := range cfg.Apps {
current := ""
if cfg.Activate == name {
current = " (当前)"
}
fmt.Printf("- %s: %s%s\n", name, app.Path, current)
}
}
func addApp(cfg *Config, args []string) {
if len(args) < 2 {
fmt.Println("用法: add <appname> <path> [args...]")
return
}
name := args[0]
path := args[1]
appArgs := []string{}
if len(args) > 2 {
appArgs = args[2:]
}
cfg.Apps[name] = App{Path: path, Args: appArgs}
fmt.Printf("添加应用 %s 成功\n", name)
}
func removeApp(cfg *Config, args []string) {
if len(args) < 1 {
fmt.Println("用法: remove <appname>")
return
}
name := args[0]
if _, ok := cfg.Apps[name]; ok {
delete(cfg.Apps, name)
fmt.Printf("已删除应用 %s\n", name)
if cfg.Activate == name {
cfg.Activate = ""
}
} else {
fmt.Println("应用不存在")
}
}
func switchApp(cfg *Config, args []string) {
if len(args) < 1 {
fmt.Println("用法: switch <appname>")
return
}
name := args[0]
if _, ok := cfg.Apps[name]; ok {
cfg.Activate = name
fmt.Printf("已切换当前应用为 %s\n", name)
} else {
fmt.Println("应用不存在")
}
}
func parseArgs(args []string) map[string]string {
result := make(map[string]string)
var lastKey string
for i := 0; i < len(args); i++ {
arg := args[i]
if strings.HasPrefix(arg, "--") {
if eq := strings.Index(arg, "="); eq != -1 {
// --key=value
key := arg[:eq]
val := arg[eq+1:]
result[key] = val
lastKey = ""
} else if i+1 < len(args) && !strings.HasPrefix(args[i+1], "--") {
// --key value
result[arg] = args[i+1]
lastKey = ""
i++
} else {
// --flag(布尔)
result[arg] = ""
lastKey = arg
}
} else if lastKey != "" {
result[lastKey] = arg
lastKey = ""
}
}
return result
}
func mergeArgs(defaults, overrides []string) []string {
defMap := parseArgs(defaults)
overMap := parseArgs(overrides)
// 覆盖默认参数
for k, v := range overMap {
defMap[k] = v
}
// 保证顺序:先输出覆盖后的默认参数,再输出命令行中未出现在默认参数里的参数
used := make(map[string]bool)
result := []string{}
for _, arg := range defaults {
if strings.HasPrefix(arg, "--") {
key := arg
if eq := strings.Index(arg, "="); eq != -1 {
key = arg[:eq]
}
if val, ok := defMap[key]; ok {
if val == "" {
result = append(result, key)
} else {
result = append(result, key+"="+val)
}
used[key] = true
}
} else {
result = append(result, arg)
}
}
// 添加命令行中新增的参数
for _, arg := range overrides {
if strings.HasPrefix(arg, "--") {
key := arg
if eq := strings.Index(arg, "="); eq != -1 {
key = arg[:eq]
}
if !used[key] {
if val, ok := overMap[key]; ok {
if val == "" {
result = append(result, key)
} else {
result = append(result, key+"="+val)
}
used[key] = true
}
}
} else {
result = append(result, arg)
}
}
return result
}
// 终止进程树
func killProcessTree(pid int) error {
// 使用 taskkill 命令终止进程树
cmd := exec.Command("taskkill", "/F", "/T", "/PID", fmt.Sprintf("%d", pid))
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("终止进程树失败: %v, 输出: %s", err, output)
}
return nil
}
var trayQuitCh = make(chan struct{})
var switchAppCh = make(chan string)
// 全局:切换应用主菜单及子菜单项列表
var menuSwitch *systray.MenuItem
var menuSwitchSubs []*systray.MenuItem
var menuAppInfo *systray.MenuItem
var menuPath *systray.MenuItem
var menuArgs *systray.MenuItem
var menuOpenDir *systray.MenuItem
func onReady(cfg *Config, app App, allArgs []string) {
// 设置托盘图标和标题
systray.SetIcon(getIcon())
systray.SetTitle("EVS: " + cfg.Activate)
systray.SetTooltip(fmt.Sprintf("当前应用: %s", cfg.Activate))
// 添加切换应用父菜单
menuSwitch = systray.AddMenuItem("切换应用", "切换到其他应用")
menuSwitchSubs = []*systray.MenuItem{}
for name := range cfg.Apps {
sub := menuSwitch.AddSubMenuItem(name, fmt.Sprintf("切换到 %s", name))
if name == cfg.Activate {
sub.Check()
} else {
sub.Uncheck()
}
menuSwitchSubs = append(menuSwitchSubs, sub)
go func(n string, m *systray.MenuItem) {
for {
<-m.ClickedCh
if n != cfg.Activate {
switchAppCh <- n
}
}
}(name, sub)
}
// 添加“打开目录”菜单项(全局变量)
menuOpenDir = systray.AddMenuItem("打开目录", "在文件资源管理器中打开当前应用所在文件夹")
go openDirHandler(cfg, app)
systray.AddSeparator()
// 添加应用信息
menuAppInfo = systray.AddMenuItem(fmt.Sprintf("应用: %s", cfg.Activate), "当前运行的应用")
menuAppInfo.Disable()
menuPath = systray.AddMenuItem(fmt.Sprintf("路径: %s", app.Path), "可执行文件路径")
menuPath.Disable()
menuArgs = systray.AddMenuItem(fmt.Sprintf("参数: %s", strings.Join(allArgs, " ")), "启动参数")
menuArgs.Disable()
systray.AddSeparator()
// 添加退出菜单
menuQuit := systray.AddMenuItem("退出", "退出应用")
go func() {
<-menuQuit.ClickedCh
close(trayQuitCh)
}()
// 不再在 onReady 监听 switchAppCh,菜单内容更新全部在 runApp 中处理
}
func onExit() {
// 清理托盘图标
systray.Quit()
// 保险起见也通知主goroutine
select {
case <-trayQuitCh:
// 已经通知过了
default:
close(trayQuitCh)
}
}
// 获取托盘图标数据
func getIcon() []byte {
data, err := os.ReadFile("resources/icon.ico")
if err != nil {
fmt.Printf("读取托盘图标失败: %v\n", err)
return []byte{}
}
return data
}
func showError(msg string) {
dialog.Message("%s", msg).Title("错误").Error()
}
var childPid int
func runApp(cfg *Config, extraArgs []string) {
if cfg.Activate == "" {
showError("未设置当前应用,请先用 switch 命令切换")
return
}
// 启动托盘(只调用一次)
go systray.Run(func() {
onReady(cfg, cfg.Apps[cfg.Activate], mergeArgs(cfg.Apps[cfg.Activate].Args, extraArgs))
}, onExit)
// 管理子进程及菜单切换
for {
app := cfg.Apps[cfg.Activate]
allArgs := mergeArgs(app.Args, extraArgs)
// 打印应用启动信息
fmt.Printf("\n启动应用配置信息:\n")
fmt.Printf("- 应用名称: %s\n", cfg.Activate)
fmt.Printf("- 可执行文件: %s\n", app.Path)
fmt.Printf("- 默认参数: %v\n", app.Args)
fmt.Printf("- 附加参数: %v\n", extraArgs)
cmdLine := fmt.Sprintf("\"%s\" %s", app.Path, strings.Join(allArgs, " "))
fmt.Printf("- 最终命令行: %s\n", cmdLine)
fmt.Println()
cmd := exec.Command(app.Path, allArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP}
if err := cmd.Start(); err != nil {
errMsg := fmt.Sprintf("启动应用失败:\n%v", err)
fmt.Println(errMsg)
showError(errMsg)
return
}
done := make(chan error, 1)
go func() { done <- cmd.Wait() }()
select {
case newApp := <-switchAppCh:
fmt.Printf("切换到应用: %s\n", newApp)
// 先优雅退出当前进程
cmd.Process.Signal(syscall.SIGTERM)
time.Sleep(time.Second)
killProcessTree(cmd.Process.Pid)
killProcessTree(childPid)
cfg.Activate = newApp
saveConfig(cfg)
systray.SetTitle("EVS: " + cfg.Activate)
systray.SetTooltip(fmt.Sprintf("当前应用: %s", cfg.Activate))
// 更新应用信息菜单项内容
appNew := cfg.Apps[cfg.Activate]
allArgsNew := mergeArgs(appNew.Args, extraArgs)
if menuAppInfo != nil {
menuAppInfo.SetTitle(fmt.Sprintf("应用: %s", cfg.Activate))
}
if menuPath != nil {
menuPath.SetTitle(fmt.Sprintf("路径: %s", appNew.Path))
}
if menuArgs != nil {
menuArgs.SetTitle(fmt.Sprintf("参数: %s", strings.Join(allArgsNew, " ")))
}
// 更新切换应用子菜单的 Checked 状态
for i, name := range getAppNames(cfg) {
if name == cfg.Activate {
menuSwitchSubs[i].Check()
} else {
menuSwitchSubs[i].Uncheck()
}
}
continue
case <-trayQuitCh:
// 处理托盘退出,终止进程树并输出日志
fmt.Printf("\n收到托盘退出,正在关闭应用...\n")
if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
fmt.Printf("发送 SIGTERM 失败: %v,尝试终止进程树\n", err)
}
time.Sleep(time.Second)
killProcessTree(cmd.Process.Pid)
killProcessTree(childPid)
// 等待子进程退出
select {
case <-done:
fmt.Println("应用已成功终止")
case <-time.After(5 * time.Second):
fmt.Println("应用未在预期时间内终止,强制退出")
}
os.Exit(0)
case err := <-done:
if err != nil {
fmt.Printf("应用退出,错误: %v\n", err)
}
killProcessTree(cmd.Process.Pid)
killProcessTree(childPid)
return
}
}
}
func printHelp() {
fmt.Println("使用方法:")
fmt.Println(" exe-version-selector <command> [args...]")
fmt.Println("\n可用命令:")
fmt.Printf(" %-26s %s\n", "list", "列出所有已配置的应用")
fmt.Printf(" %-26s %s\n", "add <name> <path> [args]", "添加新应用,可选指定默认启动参数")
fmt.Printf(" %-26s %s\n", "remove <name>", "删除指定应用")
fmt.Printf(" %-26s %s\n", "switch <name>", "切换到指定应用")
fmt.Printf(" %-26s %s\n", "help", "显示此帮助信息")
fmt.Println("\n如果不指定命令,将直接运行当前选中的应用")
}
// 返回所有应用名,顺序与 menuSwitchSubs 保持一致
func getAppNames(cfg *Config) []string {
names := make([]string, 0, len(cfg.Apps))
for name := range cfg.Apps {
names = append(names, name)
}
return names
}
func main() {
// 捕获 Ctrl+C、关闭窗口等信号,确保退出时清理子进程
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
if childPid != 0 {
killProcessTree(childPid)
}
os.Exit(0)
}()
if len(os.Args) >= 2 {
cmd := os.Args[1]
if cmd == "help" || cmd == "-h" || cmd == "--help" {
printHelp()
return
}
if cmd == "list" || cmd == "add" || cmd == "remove" || cmd == "switch" {
cfg, err := loadConfig()
if err != nil {
fmt.Printf("读取配置文件失败: %v\n", err)
return
}
switch cmd {
case "list":
listApps(cfg)
case "add":
addApp(cfg, os.Args[2:])
saveConfig(cfg)
case "remove":
removeApp(cfg, os.Args[2:])
saveConfig(cfg)
case "switch":
switchApp(cfg, os.Args[2:])
saveConfig(cfg)
}
return
}
}
// 默认行为:代理当前激活应用,并转发所有参数
cfg, err := loadConfig()
if err != nil {
fmt.Printf("读取配置文件失败: %v\n", err)
return
}
extraArgs := os.Args[1:]
runApp(cfg, extraArgs)
}