From cf043bd09819733b6384fd38b21520c3b66e011b Mon Sep 17 00:00:00 2001 From: LiangshouX Date: Thu, 29 Oct 2026 01:21:02 +0800 Subject: [PATCH] fix(dsh): close the stale-service and probe follow-ups --- integrations/dsh/docs/environment.md | 5 +- integrations/dsh/docs/environment_ZH.md | 4 +- .../dsh/src/environment/client/locales.ts | 4 + .../dsh/src/environment/client/python.tsx | 2 +- .../dsh/src/environment/client/service.tsx | 26 ++- .../dsh/src/environment/host/manager.ts | 22 +- .../dsh/src/environment/host/service.ts | 10 +- integrations/dsh/tests/env-manager.test.mjs | 4 +- integrations/dsh/tests/env-service.test.mjs | 96 ++++++--- integrations/dsh/tests/env-tab.test.mjs | 204 +++++++++++++++++- 10 files changed, 323 insertions(+), 54 deletions(-) diff --git a/integrations/dsh/docs/environment.md b/integrations/dsh/docs/environment.md index da1e6a32..861764ff 100644 --- a/integrations/dsh/docs/environment.md +++ b/integrations/dsh/docs/environment.md @@ -71,8 +71,9 @@ The installation is a Host-owned job: - Progress is polled by job id, so a second installation for another interpreter cannot replace what you see. - **Cancel** stops pip and its build children, and the interpreter stays locked until the process has actually exited. -- The log is written to `$DSH_HOME/reme-memory/install-.log` and its tail is shown in place; install and - service logs are rotated to `.log.1` once they pass 5 MB, and only the newest 20 install logs are kept. +- The log is written to `$DSH_HOME/reme-memory/install-.log` and its tail is shown in place. Each install + opens a new file and only the newest 20 are kept; the long-lived service log rotates to `service.log.1` once it + passes 5 MB. - When the plugin unloads, running jobs are stopped. ## Workspace `.env` diff --git a/integrations/dsh/docs/environment_ZH.md b/integrations/dsh/docs/environment_ZH.md index 9f09c11e..153c7d5c 100644 --- a/integrations/dsh/docs/environment_ZH.md +++ b/integrations/dsh/docs/environment_ZH.md @@ -61,8 +61,8 @@ ReMe 需要 **Python 3.11 或更新版本**。标签页在 Host 上探测下列 - 关闭配置页后仍在运行;重新打开标签页会为同一个解释器恢复进度。 - 进度按任务 ID 轮询,为另一个解释器发起的安装不会顶替当前显示。 - **取消**会同时终止 pip 及其构建子进程,且在进程真正退出前该解释器保持占用。 -- 日志写入 `$DSH_HOME/reme-memory/install-.log`,页面直接显示尾部;安装与服务日志超过 5 MB 会轮转为 `.log.1`, - 并且目录只保留最近 20 个安装日志。 +- 日志写入 `$DSH_HOME/reme-memory/install-.log`,页面直接显示尾部。每次安装都会新建文件,目录只保留最近 + 20 个;长期运行的服务日志超过 5 MB 后轮转为 `service.log.1`。 - 插件卸载时,仍在运行的安装会被停止。 ## Workspace `.env` diff --git a/integrations/dsh/src/environment/client/locales.ts b/integrations/dsh/src/environment/client/locales.ts index 72733783..f5e2b9cc 100644 --- a/integrations/dsh/src/environment/client/locales.ts +++ b/integrations/dsh/src/environment/client/locales.ts @@ -118,6 +118,8 @@ export const envEn = { "The previous service could not be stopped; check the log.", startErrorUnverifiedRecord: "The recorded service identity could not be verified, so it was left untouched; stop it manually, then retry.", + startErrorUnverifiedRecordPid: + "The recorded service identity could not be verified (PID {pid}), so it was left untouched; stop it manually, then retry.", startErrorNoPython: "Select a Python interpreter first.", startErrorNotInstalled: "reme-ai is not installed in the selected interpreter.", @@ -301,6 +303,8 @@ export const envZh: typeof envEn = { startErrorPreviousNotStopped: "上一次服务未能停止,请查看日志。", startErrorUnverifiedRecord: "无法验证记录中的进程身份,已保持其运行;请手动停止后重试。", + startErrorUnverifiedRecordPid: + "无法验证记录中的进程身份(PID {pid}),已保持其运行;请手动停止后重试。", startErrorNoPython: "请先选择一个 Python 解释器。", startErrorNotInstalled: "所选解释器尚未安装 reme-ai。", startErrorVersionTooOld: "已安装的 reme-ai 版本过低,请先升级。", diff --git a/integrations/dsh/src/environment/client/python.tsx b/integrations/dsh/src/environment/client/python.tsx index a140329e..cd6cfd9d 100644 --- a/integrations/dsh/src/environment/client/python.tsx +++ b/integrations/dsh/src/environment/client/python.tsx @@ -178,7 +178,7 @@ export function PythonSection({ {detection.error ? `: ${detection.error}` : ""}

) : null} - {detection.phase === "error" || + {(detection.phase === "error" && !detected) || (detection.phase === "ready" && envs.length === 0) ? (
{t("emptyTitle")} diff --git a/integrations/dsh/src/environment/client/service.tsx b/integrations/dsh/src/environment/client/service.tsx index ecf8275a..f99080a9 100644 --- a/integrations/dsh/src/environment/client/service.tsx +++ b/integrations/dsh/src/environment/client/service.tsx @@ -38,6 +38,10 @@ const START_ERROR_KEYS: Record = { }; function startErrorText(t: EnvTranslator, result: ServiceStartResult): string { + // The unverified record carries the pid it refused to touch, so the message + // can tell the user what to look for instead of a generic sentence. + if (result.code === "unverified-record" && result.pid !== undefined) + return fill(t("startErrorUnverifiedRecordPid"), { pid: result.pid }); const key = result.code ? START_ERROR_KEYS[result.code] : undefined; return key ? t(key) : result.error || t("serviceError"); } @@ -221,7 +225,11 @@ export function useService( return; } } catch { - // Fall back to the reported start error. + // A switch of endpoint aborts this call; its error belongs to the + // old endpoint and must not be painted (or bump the sequence) on + // the new card. + if (controller.signal.aborted) return; + // Otherwise fall back to the reported start error. } } applyState({ phase: "error", error: startErrorText(t, started) }); @@ -446,6 +454,12 @@ export function ServiceSection({ }): JSX.Element { const { state, stopping, stopError, log } = control; const [confirming, setConfirming] = useState(false); + // The confirmation belongs to one phase and endpoint: keeping it open across + // a transition would re-show "Stop?" on top of a live service later. + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setConfirming(false); + }, [endpoint, state.phase]); // A conflict can still be our own unhealthy service (booting, wrong token); // offer the same Stop affordance instead of leaving the tab without a way // out other than editing the record by hand. `stopped`/`starting` with a @@ -463,6 +477,11 @@ export function ServiceSection({ state.data?.managed === true || ((state.phase === "stopped" || state.phase === "starting") && state.data?.pid !== undefined); + // A stale record may be a recycled pid that now belongs to a foreign + // process. Stop then refuses, so Start must stay available: the Host stops a + // provably owned previous launch, discards a foreign record, and reports an + // unverifiable one. Hiding Start would leave that card without a way out. + const canRestart = state.phase === "stopped" && state.data?.pid !== undefined; return (
@@ -584,7 +603,8 @@ export function ServiceSection({ {t("stopService")} ) - ) : ( + ) : null} + {!canStop || canRestart ? ( - )} + ) : null}