magic-agent 2.6.0 → 2.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLIENT-CONTRACT.md +188 -16
- package/client-ui/pc/app.css +42 -0
- package/client-ui/pc/js/90-glue.js +3 -0
- package/client-ui/shared/conv/03-chat-message.js +25 -19
- package/client-ui/shared/conv/09-stream-apply.js +9 -0
- package/client-ui/shared/conv/29-msg-variants.js +136 -0
- package/client-ui/shared/conv/conv.css +253 -158
- package/client-ui/shared/conversation.js +39 -2
- package/client-ui-mobile/apps.css +16 -0
- package/client-ui-mobile/apps.js +406 -0
- package/client-ui-mobile/index.html +2695 -379
- package/client-ui-mobile/reqs.css +11 -4
- package/client-ui-mobile/reqs.js +49 -5
- package/client-ui-mobile/scripts.css +46 -0
- package/client-ui-mobile/scripts.js +66 -6
- package/cmd/magic-client/browsectl.go +1 -1
- package/cmd/magic-client/clientctl.go +37 -2
- package/cmd/magic-client/main.go +159 -12
- package/internal/agent/dsh_caps_cache_test.go +184 -0
- package/internal/agent/dsh_resume.go +144 -5
- package/internal/agent/env.go +173 -1
- package/internal/agent/env_test.go +114 -0
- package/internal/agent/limitnotice.go +207 -1
- package/internal/agent/limitnotice_test.go +140 -0
- package/internal/agent/stream.go +30 -0
- package/internal/agent/streamjson.go +11 -1
- package/internal/cli/ask.go +8 -0
- package/internal/cli/engines_test.go +103 -0
- package/internal/cli/events.go +11 -0
- package/internal/cli/events_test.go +50 -0
- package/internal/cli/judge.go +104 -0
- package/internal/cli/root.go +35 -6
- package/internal/cli/session.go +69 -3
- package/internal/cli/session_test.go +82 -3
- package/internal/cli/sessionlog.go +6 -0
- package/internal/cli/sessionlog_test.go +22 -2
- package/internal/client/apk.go +8 -0
- package/internal/client/apk_test.go +59 -0
- package/internal/client/apps.go +1078 -0
- package/internal/client/apps_test.go +516 -0
- package/internal/client/coreexe.go +196 -0
- package/internal/client/coreexe_test.go +161 -0
- package/internal/client/coreupgrade.go +866 -0
- package/internal/client/coreupgrade_test.go +561 -0
- package/internal/client/daemon.go +209 -18
- package/internal/client/feishu.go +1259 -0
- package/internal/client/feishu_test.go +649 -0
- package/internal/client/health_identity_test.go +84 -0
- package/internal/client/hostenv.go +68 -0
- package/internal/client/hostenv_test.go +37 -0
- package/internal/client/jobs.go +233 -12
- package/internal/client/jobs_auto.go +571 -0
- package/internal/client/jobs_auto_test.go +431 -0
- package/internal/client/plugins.go +5 -1
- package/internal/client/prefs.go +67 -0
- package/internal/client/procstart_linux.go +59 -0
- package/internal/client/publicurl.go +82 -0
- package/internal/client/requirements.go +9 -1
- package/internal/client/requirements_test.go +103 -0
- package/internal/client/restart.go +167 -13
- package/internal/client/restart_handoff_test.go +230 -0
- package/internal/client/restart_test.go +140 -1
- package/internal/client/scripts_decode_implausible.go +38 -0
- package/internal/client/scripts_decode_other.go +4 -0
- package/internal/client/scripts_decode_test.go +16 -11
- package/internal/client/scripts_decode_windows.go +0 -24
- package/internal/client/scripts_test.go +14 -1
- package/internal/client/service.go +403 -87
- package/internal/client/service_test.go +166 -1
- package/internal/client/sessions_cache.go +148 -0
- package/internal/client/sessions_cache_test.go +163 -0
- package/internal/client/sessions_fold.go +165 -0
- package/internal/client/sessions_fold_test.go +204 -0
- package/internal/client/singleinstance.go +470 -0
- package/internal/client/singleinstance_test.go +607 -0
- package/internal/client/testmain_test.go +88 -0
- package/internal/client/workspace_default_test.go +158 -0
- package/internal/client/workspacelockenv_test.go +54 -0
- package/internal/config/config.go +48 -0
- package/internal/judge/judge.go +315 -0
- package/internal/session/multiclient_test.go +368 -0
- package/internal/session/session.go +83 -11
- package/internal/session/session_test.go +7 -0
- package/internal/session/status.go +21 -0
- package/internal/session/status_test.go +28 -0
- package/internal/session/title.go +16 -0
- package/internal/session/title_test.go +26 -0
- package/internal/session/workspace_lock.go +265 -28
- package/npm/apk/magic-agent-mobile-release.apk +0 -0
- package/npm/apk/version.json +8 -1
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/npm/install.js +11 -0
- package/package.json +8 -3
|
@@ -29,6 +29,7 @@ import (
|
|
|
29
29
|
"os"
|
|
30
30
|
"os/exec"
|
|
31
31
|
"path/filepath"
|
|
32
|
+
"strconv"
|
|
32
33
|
"strings"
|
|
33
34
|
"time"
|
|
34
35
|
|
|
@@ -59,6 +60,48 @@ const (
|
|
|
59
60
|
probeTimeout = 1500 * time.Millisecond
|
|
60
61
|
)
|
|
61
62
|
|
|
63
|
+
/* SelfStateName 前台 `serve` 自己登记的那份状态文件名(落在 profile 的 DataDir 下)。
|
|
64
|
+
*
|
|
65
|
+
* ── 为什么前台 serve 也要落状态(2026-10-26 用户:「通过 magic-test 启动的无法在移动端
|
|
66
|
+
* 点击重启服务重启」)──
|
|
67
|
+
* `POST /desk/restart` 原本只认 `magic-client start` 写的那份 client.json,前台 serve
|
|
68
|
+
* 什么都不写,于是手机点重启一律 409「当前实例不是后台启动的」。可是 magic-test 的桥接
|
|
69
|
+
* (desktop/magic-client-bridge.cjs)正是用 `spawn(bin, ['serve', ...])` 起的实例 ——
|
|
70
|
+
* 用户手机上连的就是它,按钮却永远点不动。
|
|
71
|
+
*
|
|
72
|
+
* 修法不是把前台硬改成后台(那会丢掉「壳退出即服务退出」的托管语义),而是让 serve
|
|
73
|
+
* **如实登记自己是谁、怎么起的**:重启助手照这份记录停旧起新即可。
|
|
74
|
+
*
|
|
75
|
+
* ⚠️ 必须是**另一个文件名**,不能复用 client.json:那份是 `start` 的唯一实例记录,
|
|
76
|
+
* 两者混写会出现「start 起的实例被前台 serve 的登记覆盖」——重启时按它重建参数,
|
|
77
|
+
* 就会用一个根本不是 start 参数拼出来的命令行去起新实例。 */
|
|
78
|
+
const SelfStateName = "serve.json"
|
|
79
|
+
|
|
80
|
+
// SelfStatePath 前台 serve 的状态文件路径:<profile 数据目录>/serve.json。
|
|
81
|
+
//
|
|
82
|
+
// 按 DataDir 分而不是按 ~/.magic-client 分,理由与单实例锁一致:同机可能同时跑着
|
|
83
|
+
// 好几个不同应用(magic-test 的 8977、magic-agent 自己的 9100),数据目录不同、
|
|
84
|
+
// 令牌不同,各登记各的,谁也认领不到谁。
|
|
85
|
+
func SelfStatePath(dataDir string) string {
|
|
86
|
+
d := strings.TrimSpace(expandHome(dataDir))
|
|
87
|
+
if d == "" {
|
|
88
|
+
return ""
|
|
89
|
+
}
|
|
90
|
+
return filepath.Join(d, SelfStateName)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// HostAlive 托管父进程是否还在(pid <= 0 = 没人托管,恒 true)。
|
|
94
|
+
//
|
|
95
|
+
// 重启后的新实例是 detach 出去的(不然它会被旧实例的退出带走),但它的「主人」仍然是
|
|
96
|
+
// 拉起这台服务的桌面壳:壳退出了服务就该跟着退,否则会留下一个孤儿占着端口与单实例锁,
|
|
97
|
+
// 下次开壳直接「插件启动即退出」。见 service.go 的 WatchHost。
|
|
98
|
+
func HostAlive(pid int) bool {
|
|
99
|
+
if pid <= 0 {
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
return agent.PIDAlive(pid)
|
|
103
|
+
}
|
|
104
|
+
|
|
62
105
|
// DefaultStatePath 状态文件默认位置:~/.magic-client/client.json。
|
|
63
106
|
func DefaultStatePath() string {
|
|
64
107
|
if p := strings.TrimSpace(os.Getenv(EnvState)); p != "" {
|
|
@@ -178,6 +221,17 @@ type DaemonState struct {
|
|
|
178
221
|
Addr string `json:"addr,omitempty"`
|
|
179
222
|
BrowseUA string `json:"browseUA,omitempty"`
|
|
180
223
|
MagicAgent string `json:"magicAgent,omitempty"`
|
|
224
|
+
// AnyOrigin serve 那次是否对任意 Origin 回 CORS 头(`--allow-any-origin` 或 `--lan`)。
|
|
225
|
+
// 重启 / 升级后原地拉起时要原样带上,否则升级完手机页面突然跨域失败。
|
|
226
|
+
AnyOrigin bool `json:"anyOrigin,omitempty"`
|
|
227
|
+
/* HostPID 托管这台服务的父进程(桌面壳 / 测试宿主)。
|
|
228
|
+
*
|
|
229
|
+
* 2026-10-26 加:重启后的新实例必须 detach(否则旧实例退出会把它一起带走),可
|
|
230
|
+
* detach 之后它就不在壳的进程组里了 —— 壳退出时 `r.stop()` 杀不到它,留下一个
|
|
231
|
+
* 孤儿占着端口与单实例锁,下次开壳就是「插件启动即退出」。
|
|
232
|
+
* 所以把「主人是谁」记下来,由服务自己盯着:主人没了就优雅退出(见 WatchHost)。
|
|
233
|
+
* 0 = 没人托管(`magic-client start` 起的常驻实例),不盯。 */
|
|
234
|
+
HostPID int `json:"hostPid,omitempty"`
|
|
181
235
|
}
|
|
182
236
|
|
|
183
237
|
// Base 本机根地址(不带令牌)。
|
|
@@ -204,6 +258,60 @@ func writeDaemonState(path string, st *DaemonState) error {
|
|
|
204
258
|
return os.Rename(tmp, path)
|
|
205
259
|
}
|
|
206
260
|
|
|
261
|
+
// WriteSelfState 前台 `serve` 监听成功后,把自己是谁、怎么起的登记下来。
|
|
262
|
+
//
|
|
263
|
+
// 与 writeDaemonState 同一套落盘规则(0600 原子写),但**写到 serve.json**,
|
|
264
|
+
// 绝不碰 `start` 独占的 client.json(理由见 SelfStateName)。
|
|
265
|
+
//
|
|
266
|
+
// 记的东西要和 `start` 那份一样全 —— 重启助手会拿它当**唯一的启动参数来源**重建
|
|
267
|
+
// 命令行(RestartDaemon → StartDaemon),少记一项新实例就会少一个 flag:
|
|
268
|
+
// 少了 Profile ⇒ 数据目录/业务模块全变;少了 Token ⇒ 手机里存的地址立刻作废;
|
|
269
|
+
// 少了 Addr ⇒ 换端口,手机再也连不上。
|
|
270
|
+
//
|
|
271
|
+
// 返回路径,调用方负责在退出时删掉(见 RemoveSelfState)。
|
|
272
|
+
func WriteSelfState(dataDir string, st *DaemonState) (string, error) {
|
|
273
|
+
return WriteSelfStateAt("", dataDir, st)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// WriteSelfStateAt 同上,但允许显式指定完整路径(`--state`)。
|
|
277
|
+
// explicit 空 → 用 dataDir 推出来的 SelfStatePath。
|
|
278
|
+
func WriteSelfStateAt(explicit, dataDir string, st *DaemonState) (string, error) {
|
|
279
|
+
path := strings.TrimSpace(expandHome(explicit))
|
|
280
|
+
if path == "" {
|
|
281
|
+
path = SelfStatePath(dataDir)
|
|
282
|
+
}
|
|
283
|
+
if path == "" {
|
|
284
|
+
return "", fmt.Errorf("没有数据目录:无法登记前台实例状态")
|
|
285
|
+
}
|
|
286
|
+
if st == nil {
|
|
287
|
+
return "", fmt.Errorf("状态为空")
|
|
288
|
+
}
|
|
289
|
+
if st.PID == 0 {
|
|
290
|
+
st.PID = os.Getpid()
|
|
291
|
+
}
|
|
292
|
+
// 状态文件自己那份路径要记对:重启助手按它找回来(见 selfDaemonState)。
|
|
293
|
+
st.StatePath = path
|
|
294
|
+
if err := writeDaemonState(path, st); err != nil {
|
|
295
|
+
return "", err
|
|
296
|
+
}
|
|
297
|
+
return path, nil
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// RemoveSelfState 退出时删掉自己那份登记。
|
|
301
|
+
//
|
|
302
|
+
// 只删「pid 还是我」的那份:重启链路里旧实例退出时,新实例可能**已经**登记好了
|
|
303
|
+
// (它先监听成功才轮到我们收尾),这时删掉就等于把新实例弄成"没有状态"的孤儿 ——
|
|
304
|
+
// 手机再点重启又变成 409。判断错了会留下陈旧登记,但那比删掉活人的准。
|
|
305
|
+
func RemoveSelfState(path string) {
|
|
306
|
+
if strings.TrimSpace(path) == "" {
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
if st, err := ReadDaemonState(path); err == nil && st != nil && st.PID != os.Getpid() {
|
|
310
|
+
return // 已经不是我的了(新实例接管),别动
|
|
311
|
+
}
|
|
312
|
+
_ = os.Remove(path)
|
|
313
|
+
}
|
|
314
|
+
|
|
207
315
|
// ReadDaemonState 读状态文件:不存在 → (nil, nil)。
|
|
208
316
|
func ReadDaemonState(path string) (*DaemonState, error) {
|
|
209
317
|
if strings.TrimSpace(path) == "" {
|
|
@@ -313,6 +421,12 @@ type StartOptions struct {
|
|
|
313
421
|
Token string
|
|
314
422
|
ProxyAddr string
|
|
315
423
|
BrowseUA string
|
|
424
|
+
/* AnyOrigin 新实例要不要对任意 Origin 回 CORS 头。由 `restart` / 升级后重启按状态
|
|
425
|
+
文件原样带回(`--lan` 时 create 自己也会开,这里不重复造条件,只是别丢)。 */
|
|
426
|
+
AnyOrigin bool
|
|
427
|
+
/* HostPID 托管父进程(见 DaemonState.HostPID)。0 = 没人托管。
|
|
428
|
+
`restart` 按状态文件原样带回,于是重启出来的新实例继续盯着同一个主人。 */
|
|
429
|
+
HostPID int
|
|
316
430
|
Lan bool
|
|
317
431
|
StatePath string
|
|
318
432
|
LogPath string
|
|
@@ -382,6 +496,14 @@ func StartDaemon(o StartOptions) (*DaemonState, error) {
|
|
|
382
496
|
defer logf.Close()
|
|
383
497
|
|
|
384
498
|
args := []string{"serve", "--ready-file", readyPath, "--token", tok}
|
|
499
|
+
/* --state(2026-10-26):告诉子进程「哪份状态文件是你自己的」。
|
|
500
|
+
两个用处,缺一不可:
|
|
501
|
+
· 退出时清理 —— 重启出来的实例由 WatchHost 收摊(宿主退出),它不走 StopDaemon,
|
|
502
|
+
没人删这份登记,留着就是一个指向死 pid 的陈旧文件;
|
|
503
|
+
· 认领自己 —— selfDaemonState 按这条路径找回参数,重启/升级才能落到自己身上。
|
|
504
|
+
不传的话子进程只知道 ready 文件,对「我那份登记在哪」一无所知(实测:重启后
|
|
505
|
+
serve.json 残留 pid 已死的旧记录)。 */
|
|
506
|
+
args = append(args, "--state", statePath)
|
|
385
507
|
appendIf := func(flag, v string) {
|
|
386
508
|
if v = strings.TrimSpace(v); v != "" {
|
|
387
509
|
args = append(args, flag, v)
|
|
@@ -394,6 +516,12 @@ func StartDaemon(o StartOptions) (*DaemonState, error) {
|
|
|
394
516
|
appendIf("--proxy-addr", o.ProxyAddr)
|
|
395
517
|
appendIf("--browse-ua", o.BrowseUA)
|
|
396
518
|
appendIf("--magic-agent", o.MagicAgent)
|
|
519
|
+
if o.AnyOrigin {
|
|
520
|
+
args = append(args, "--allow-any-origin")
|
|
521
|
+
}
|
|
522
|
+
if o.HostPID > 0 {
|
|
523
|
+
args = append(args, "--host-pid", strconv.Itoa(o.HostPID))
|
|
524
|
+
}
|
|
397
525
|
if o.Lan {
|
|
398
526
|
args = append(args, "--lan")
|
|
399
527
|
} else {
|
|
@@ -456,6 +584,8 @@ func StartDaemon(o StartOptions) (*DaemonState, error) {
|
|
|
456
584
|
Addr: o.Addr,
|
|
457
585
|
BrowseUA: o.BrowseUA,
|
|
458
586
|
MagicAgent: o.MagicAgent,
|
|
587
|
+
AnyOrigin: o.AnyOrigin,
|
|
588
|
+
HostPID: o.HostPID,
|
|
459
589
|
}
|
|
460
590
|
if st.PID == 0 {
|
|
461
591
|
st.PID = cmd.Process.Pid
|
|
@@ -469,6 +599,40 @@ func StartDaemon(o StartOptions) (*DaemonState, error) {
|
|
|
469
599
|
// StopOptions `stop` 的参数。
|
|
470
600
|
type StopOptions struct {
|
|
471
601
|
StatePath string
|
|
602
|
+
/* AlsoLockHolder(2026-10-26):状态文件里没有 / 指向的进程已死时,再按**单实例锁**
|
|
603
|
+
里记的 pid 试一次。
|
|
604
|
+
|
|
605
|
+
为什么必须有这一步:`magic-client serve`(前台)与旧版(不认锁、可执行文件已变成
|
|
606
|
+
*.stale 的那些)都**不写状态文件**,于是 `stop` 只读状态文件就永远「本来就没在跑」,
|
|
607
|
+
而 8977 / 54428 上实实在在还有两个进程占着隧道 —— 用户此刻正是这个现场:
|
|
608
|
+
手机连着旧的、新版起不来、`stop` 又停不掉,只能手工 kill。
|
|
609
|
+
|
|
610
|
+
`restart` 不传这个(它必须严格按状态文件重建参数,顺手停掉一个前台实例会把
|
|
611
|
+
用户手上的调试会话一起干掉)。 */
|
|
612
|
+
AlsoLockHolder bool
|
|
613
|
+
/* DataDir(2026-10-26):单实例锁的位置。空 → 从状态文件所在目录推(老调用方行为)。
|
|
614
|
+
与 profile 同源 —— 锁是按数据目录分的,不知道是哪份目录就不知道该停谁。 */
|
|
615
|
+
DataDir string
|
|
616
|
+
/* AppID(2026-10-26):AppID 级占用登记的位置。有了它才能停掉「同一应用换了
|
|
617
|
+
数据目录在跑」的那一份 —— 那种情况下 DataDir 锁查不到它(在另一个目录)。 */
|
|
618
|
+
AppID string
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// stopPID 发 SIGTERM,等 stopGrace,还在就 SIGKILL。返回是否真的停了。
|
|
622
|
+
func stopPID(pid int) bool {
|
|
623
|
+
if !agent.PIDAlive(pid) {
|
|
624
|
+
return false
|
|
625
|
+
}
|
|
626
|
+
_ = agent.TerminatePID(pid, false)
|
|
627
|
+
deadline := time.Now().Add(stopGrace)
|
|
628
|
+
for time.Now().Before(deadline) {
|
|
629
|
+
if !agent.PIDAlive(pid) {
|
|
630
|
+
return true
|
|
631
|
+
}
|
|
632
|
+
time.Sleep(50 * time.Millisecond)
|
|
633
|
+
}
|
|
634
|
+
_ = agent.KillPID(pid, false)
|
|
635
|
+
return true
|
|
472
636
|
}
|
|
473
637
|
|
|
474
638
|
// StopDaemon 停掉后台实例。
|
|
@@ -484,26 +648,49 @@ func StopDaemon(o StopOptions) (bool, *DaemonState, error) {
|
|
|
484
648
|
if err != nil {
|
|
485
649
|
return false, nil, err
|
|
486
650
|
}
|
|
487
|
-
if st == nil {
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
if !agent.PIDAlive(st.PID) {
|
|
491
|
-
_ = os.Remove(statePath)
|
|
492
|
-
return false, st, nil
|
|
493
|
-
}
|
|
494
|
-
// SIGTERM 先礼:serve 收到后会 srv.Close(),正在跑的请求有机会收尾。
|
|
495
|
-
_ = agent.TerminatePID(st.PID, false)
|
|
496
|
-
deadline := time.Now().Add(stopGrace)
|
|
497
|
-
for time.Now().Before(deadline) {
|
|
498
|
-
if !agent.PIDAlive(st.PID) {
|
|
651
|
+
if st == nil || !agent.PIDAlive(st.PID) {
|
|
652
|
+
if st != nil {
|
|
499
653
|
_ = os.Remove(statePath)
|
|
500
|
-
return true, st, nil
|
|
501
654
|
}
|
|
502
|
-
|
|
655
|
+
/* 状态文件这条路走不通:还有一份不写状态文件的实例(前台 serve / 旧版 .stale)
|
|
656
|
+
占着锁的话,用户唯一的自救方式就是它。停掉并清锁 —— 不清的话下一次启动
|
|
657
|
+
会因为「锁的持有者已死」走回收路径,虽然也能起来,但日志里会多一段困惑。 */
|
|
658
|
+
if o.AlsoLockHolder {
|
|
659
|
+
/* 两条路都要查:先按数据目录(最常见),再按 AppID 登记(治「同一应用
|
|
660
|
+
换了目录在跑」—— 那一份的 DataDir 锁根本不在我们要找的目录里)。 */
|
|
661
|
+
type lockTarget struct{ path string }
|
|
662
|
+
var targets []lockTarget
|
|
663
|
+
if p := DefaultLockPath(o.DataDir); p != "" {
|
|
664
|
+
targets = append(targets, lockTarget{p})
|
|
665
|
+
}
|
|
666
|
+
if p := appIDLockPath(o.AppID); p != "" && p != DefaultLockPath(o.DataDir) {
|
|
667
|
+
targets = append(targets, lockTarget{p})
|
|
668
|
+
}
|
|
669
|
+
for _, t := range targets {
|
|
670
|
+
h := ReadSingleInstanceHolder(t.path)
|
|
671
|
+
if h == nil || h.PID == os.Getpid() || !stopPID(h.PID) {
|
|
672
|
+
continue
|
|
673
|
+
}
|
|
674
|
+
_ = os.Remove(t.path)
|
|
675
|
+
// 顺手把它的锁也清掉:不清的话它那个目录下次启动会撞「持有者已死」。
|
|
676
|
+
if h.DataDir != "" {
|
|
677
|
+
_ = os.Remove(DefaultLockPath(h.DataDir))
|
|
678
|
+
}
|
|
679
|
+
return true, &DaemonState{
|
|
680
|
+
PID: h.PID, Port: h.Port, Exe: h.Exe,
|
|
681
|
+
}, nil
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if st == nil {
|
|
685
|
+
return false, nil, nil
|
|
686
|
+
}
|
|
687
|
+
return false, st, nil
|
|
688
|
+
}
|
|
689
|
+
if stopPID(st.PID) {
|
|
690
|
+
_ = os.Remove(statePath)
|
|
691
|
+
return true, st, nil
|
|
503
692
|
}
|
|
504
|
-
|
|
505
|
-
_ = os.Remove(statePath)
|
|
506
|
-
return true, st, nil
|
|
693
|
+
return false, st, nil
|
|
507
694
|
}
|
|
508
695
|
|
|
509
696
|
// RestartOptions `restart` 的参数。
|
|
@@ -512,6 +699,8 @@ type RestartOptions struct {
|
|
|
512
699
|
// Delay 停旧实例之前等多久。服务端「自重启」用它先把 HTTP 响应写回手机
|
|
513
700
|
// (响应一发完,本进程就会在 Delay 之后被 TerminatePID 带走)。
|
|
514
701
|
Delay time.Duration
|
|
702
|
+
// DataDir 该实例的数据目录(决定单实例锁的位置)。
|
|
703
|
+
DataDir string
|
|
515
704
|
}
|
|
516
705
|
|
|
517
706
|
// RestartDaemon 按**当前实例自己的启动参数**重启:读状态文件 → 停旧实例 → 拉起新实例。
|
|
@@ -537,7 +726,7 @@ func RestartDaemon(o RestartOptions) (*DaemonState, error) {
|
|
|
537
726
|
if o.Delay > 0 {
|
|
538
727
|
time.Sleep(o.Delay)
|
|
539
728
|
}
|
|
540
|
-
if _, _, err := StopDaemon(StopOptions{StatePath: statePath}); err != nil {
|
|
729
|
+
if _, _, err := StopDaemon(StopOptions{StatePath: statePath, DataDir: o.DataDir}); err != nil {
|
|
541
730
|
return nil, err
|
|
542
731
|
}
|
|
543
732
|
/* 旧进程退出到端口真正释放之间还有一小段窗口(Windows 上关句柄不是瞬时),
|
|
@@ -553,6 +742,8 @@ func RestartDaemon(o RestartOptions) (*DaemonState, error) {
|
|
|
553
742
|
Token: prev.Token,
|
|
554
743
|
ProxyAddr: prev.ProxyAddr,
|
|
555
744
|
BrowseUA: prev.BrowseUA,
|
|
745
|
+
AnyOrigin: prev.AnyOrigin,
|
|
746
|
+
HostPID: prev.HostPID,
|
|
556
747
|
Lan: prev.Lan,
|
|
557
748
|
StatePath: statePath,
|
|
558
749
|
LogPath: prev.LogPath,
|