Skip to content
135 changes: 126 additions & 9 deletions plugins/zcode-advisor/tools/companion/launchd.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,75 @@ function uid() {
try { return typeof process.getuid === 'function' ? process.getuid() : null; } catch (_) { return null; }
}

// launchctl 固定在 /bin/launchctl:这是 macOS 系统自带、不随版本漂移的绝对路径,
// 刻意不走 PATH 查找——PATH 上的同名文件可能被用户环境劫持,而这里的调用会
// bootout 用户真实的自启作业。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style · low
在代码中多处使用了非标准拼音(如 bootout),这可能影响国际化维护。建议统一使用英文术语(如 bootout 可以替换为 unload 或保持英文 bootout),以提高代码的可读性和一致性。

function launchctl(args) {
return execFileSync('/bin/launchctl', args, { stdio: 'pipe', timeout: 20000, encoding: 'utf8' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performance · medium
在 launchctl 函数中,虽然设置了超时(20000ms),但建议考虑是否需要根据不同的操作类型(如 print、bootout、bootstrap)调整超时时间。例如,bootstrap 操作可能需要更长的时间。

}
Comment on lines 117 to 119

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performance · low
在 launchctl 函数中,虽然设置了超时(20000ms),但建议考虑是否需要根据不同的操作类型(如 print、bootout、bootstrap)调整超时时间。例如,bootstrap 操作可能需要更长的时间,而 print 或 bootout 操作可能可以接受更短的超时。这有助于提高响应速度并避免不必要的等待。

Suggestion:

Suggested change
function launchctl(args) {
return execFileSync('/bin/launchctl', args, { stdio: 'pipe', timeout: 20000, encoding: 'utf8' });
}
function launchctl(args) {
// 根据操作类型动态调整超时时间,例如:
// - print/bootout: 5000ms
// - bootstrap: 30000ms
const timeout = args.includes('bootstrap') ? 30000 : 5000;
return execFileSync('/bin/launchctl', args, { stdio: 'pipe', timeout, encoding: 'utf8' });
}


// 用户真实自启作业的 plist 路径——**绕过一切覆盖**(dir 参数 / ZCODE_ADVISOR_LAUNCHD_DIR)。
// 完整安全不变量见 canTouchLaunchd 上方注释;这里只负责算出路径。
function realPlistPath() {
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
}

// 路径规范化:优先 realpath 父目录(plist 可能还没落盘,但父目录一定存在),
// 再退回 realpath 自身,最后退回 path.resolve。
//
// 只做 path.resolve 会把「指向真实目录的符号链接」判成另一个位置:macOS 上
// /var → /private/var、/tmp → /private/tmp 都是这种链接。后果是真实卸载被误判成
// 「不是真实作业」而静默跳过——卸载功能本身失效。realpath 之后两个方向都不会误判:
// 指向真实目录的链接被认成真实(该 bootout),沙箱仍被认成沙箱(不碰 launchd 域)。
//
// realpathSync 可能因目录在两次调用之间被删而抛(TOCTOU)。这里不回退重试而是直接
// 落到 path.resolve:**兜底方向是安全的**——沙箱目录消失时判定结果仍是「不是真实作业」,
// 于是不碰 launchd 域(宁可不操作,也不能误打用户真实作业)。反之若真实目录消失,
// 两条路径都退化成 path.resolve,仍然相等,卸载照常生效。
function canonicalPath(p) {
const dir = path.dirname(p);
const base = path.basename(p);
try { return path.join(fs.realpathSync(dir), base); } catch (_) { /* 父目录不存在 */ }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
canonicalPath 函数在处理 fs.realpathSync 异常时使用了空 catch 块,未记录或处理异常信息。虽然这种方式简化了代码,但可能影响调试和问题排查。建议考虑记录异常信息,以便更好地追踪和处理此类问题。

try { return fs.realpathSync(p); } catch (_) { /* 文件也不存在 */ }
Comment on lines +142 to +143

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performance · high
在 canonicalPath() 函数中,使用 fs.realpathSync() 可能存在文件系统竞争风险。如果在执行期间目录或文件被删除或移动,可能导致后续操作失败或误判。建议考虑使用异步版本并添加重试机制,或在调用后立即验证路径是否有效。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
在 canonicalPath 函数中,fs.realpathSync 和 path.resolve 的异常处理使用了空 catch 块,这会导致调试和问题排查变得困难。建议在异常处理时记录日志或错误信息,以便更好地追踪问题。例如,可以使用 console.warn 或其他日志系统来记录异常信息。

Suggestion:

Suggested change
try { return fs.realpathSync(p); } catch (_) { /* 文件也不存在 */ }
try {
return fs.realpathSync(p);
} catch (err) {
console.warn(`[launchd] 路径解析失败,使用备用方案:${err.message}`);
return path.resolve(p);
}

return path.resolve(p);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · high
在 canonicalPath 函数中,虽然使用了 fs.realpathSync 来处理符号链接,但当其抛出异常时直接退回 path.resolve。然而,path.resolve 在处理 macOS 上的特殊符号链接(如 /var 指向 /private/var)时,可能仍然无法完全避免误判。建议进一步验证 path.resolve 的行为,确保在所有情况下都能正确识别真实路径,避免误操作用户真实作业。

Comment on lines +143 to +144

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · medium
canonicalPath 函数在 fs.realpathSync 失败后直接回退到 path.resolve,但未验证回退后的路径是否仍满足安全要求。例如,沙箱目录被删除时,path.resolve 可能返回不安全路径,导致 managesRealAgent 误判。建议在回退到 path.resolve 时,增加额外的安全验证,确保路径仍符合预期的安全策略。

Suggestion:

Suggested change
try { return fs.realpathSync(p); } catch (_) { /* 文件也不存在 */ }
return path.resolve(p);
try {
return fs.realpathSync(p);
} catch (_) {
const resolvedPath = path.resolve(p);
// 添加额外的安全验证逻辑,确保 resolvedPath 符合安全策略
return resolvedPath;
}

}
Comment on lines +139 to +145

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performance · medium
canonicalPath 函数的注释提到 TOCTOU(Time-of-check to Time-of-use)风险,但未采取额外的同步机制(如文件锁或重试机制)来缓解风险。在高并发或快速变化的文件系统环境下,可能导致路径判断不准确。建议考虑引入文件锁或重试机制,以提高路径判断的可靠性。


// 目标 plist 是否就是用户真实那份(只有这份才允许操作 launchd 域)。
//
// 注意:os.homedir() 在个别环境下会抛(用户记录查不到)。这里**不静默吞掉**:
// 吞掉会让 install 照旧把 plist 落盘、却永远不 bootstrap——正好是本 bug 的另一种形态
// (文件在、作业不在、用户以为装好了)。出错时明确写 stderr 再返回 false。
function managesRealAgent(p) {
let real;
try {
real = realPlistPath();
} catch (err) {
process.stderr.write(`[launchd] 无法定位用户 LaunchAgents 目录,跳过 launchd 域操作:${(err && err.message) || err}\n`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · medium
在 managesRealAgent() 函数中,虽然已经处理了 os.homedir() 抛出的错误,但错误信息直接输出到 stderr,可能包含敏感路径信息。虽然已避免直接泄露详细错误信息,但建议进一步简化错误信息,仅输出通用的提示信息(例如 "无法定位用户 LaunchAgents 目录"),避免任何潜在的信息泄露。

return false;
}
Comment on lines +153 to +159

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · high
managesRealAgent 函数在 os.homedir() 抛出异常时虽然写入了 stderr 日志,但未阻止后续 canonicalPath 的调用。如果 os.homedir() 失败,后续路径比较逻辑可能基于无效路径,导致安全判断错误。建议在 os.homedir() 失败时立即返回,避免后续逻辑执行。

Suggestion:

Suggested change
let real;
try {
real = realPlistPath();
} catch (err) {
process.stderr.write(`[launchd] 无法定位用户 LaunchAgents 目录,跳过 launchd 域操作:${(err && err.message) || err}\n`);
return false;
}
let real;
try {
real = realPlistPath();
} catch (err) {
process.stderr.write(`[launchd] 无法定位用户 LaunchAgents 目录,跳过 launchd 域操作:${(err && err.message) || err}\n`);
return false;
}
// 确保 realPlistPath() 成功后才进行后续比较逻辑
if (!real) {
return false;
}

return canonicalPath(p) === canonicalPath(real);
}

// launchd 域操作仅在 macOS、拿得到 uid、且**目标就是用户真实那份 plist** 时才允许。
//
// 关键不变量:LABEL 是固定常量,重定向只能改 plist 的**路径**,改不掉 launchd
// 域里的作业名。所以一旦目标 plist 不是这一份,任何
// `launchctl print/bootout gui/<uid>/<LABEL>` 都打在用户**真实**的自启作业上:
// - bootout 会终止真实作业的进程并把它从 launchd 卸载。KeepAlive 监督随作业
// 一起消失 → 进程被杀且**永远不会自动重启**,直到用户重开一次 App
// (真机故障:在本机跑一次 `npm test`,外挂就被打死,表现为「动不动就连不上
// controller」,而 CI 跑在 Linux 上、launchctl 不存在,ENOENT 被 catch 吞掉,
// 所以结构性看不见)。
// - print 会把真实作业的运行态误报成沙箱的态(装了 agent 的开发机上,
// 「已写入但未加载」这条断言必然变红)。
// 用路径判定而不是「有没有传 dir」:这样显式把 dir 指向真实目录、或经环境变量
// 重定向的情形都能被正确识别。
// 显式判断 platform/uid 而不是靠 execFileSync 抛 ENOENT 再吞掉——「静默吞掉」
// 正是本 bug 在 CI 上无法暴露的原因。
function canTouchLaunchd(p) {
return process.platform === 'darwin' && uid() !== null && managesRealAgent(p);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
canTouchLaunchd() 函数中仅检查 process.platform === 'darwin',但未明确排除 macOS 模拟器或非真实硬件环境。建议添加额外的平台验证逻辑,确保仅在真实 macOS 硬件上执行 launchd 操作。

}
Comment on lines +179 to +181

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · high
uid() 函数在非 darwin 平台或特定环境下可能返回 null 或异常值,而 canTouchLaunchd 函数依赖该返回值进行判断。虽然当前实现已经处理了 uid() 返回 null 的情况,但建议进一步明确 uid() 的异常处理逻辑,并确保在所有平台下都能正确返回有效的用户 ID 或提供明确的错误信息。


// 写 plist(幂等:内容相同则不重写,保持 mtime 稳定,避免触发无谓的重载)。
function writePlist(content, dir) {
const p = plistPath(dir);
Expand All @@ -141,9 +206,24 @@ function isLoaded(label) {
// 幂等加载:已加载且 plist 未变 → 什么都不做(**不能无条件 bootout+bootstrap**,
// 那会把正在跑的实例杀掉再拉起,用户每次点图标都会闪断一次)。
// plist 变了 → 必须重载,否则 launchd 仍按旧配置跑(升级后不生效)。
//
Comment on lines 206 to +209

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

documentation · medium
在 ensureLoaded、install、uninstall 和 status 函数中,虽然返回契约已经明确,但建议在每个函数开头添加更详细的文档注释,以便开发者快速理解各个字段的含义及其区别。例如:

  • skipped:表示是否跳过 launchd 操作(沙箱/非 darwin/无 uid)。
  • warn:跳过或失败的原因说明。
  • loaded:是否已交给 launchd 加载。
    这有助于提高代码的可维护性和理解性。

Suggestion:

Suggested change
// 幂等加载:已加载且 plist 未变 → 什么都不做(**不能无条件 bootout+bootstrap**,
// 那会把正在跑的实例杀掉再拉起,用户每次点图标都会闪断一次)。
// plist 变了 → 必须重载,否则 launchd 仍按旧配置跑(升级后不生效)。
//
/**
* 幂等加载:已加载且 plist 未变 → 什么都不做(不能无条件 bootout+bootstrap,
* 那会把正在跑的实例杀掉再拉起,用户每次点图标都会闪断一次)。
*
* 返回契约:
* - { loaded: true, reloaded: boolean }:已加载(reloaded 表示是否重载过)。
* - { loaded: false, skipped: true, warn }:故意跳过域操作(沙箱/非 darwin/无 uid)。
* - { loaded: false, warn }:真失败(bootstrap 抛错)。
*/

// 注意 plistFile 指向沙箱时**绝不能**走 launchd 域(见 canTouchLaunchd):
// 那里的 bootout 打的是真实 LABEL,会把用户真实的 agent 打死并卸载。
//
// 返回契约(三种形状,靠 skipped / warn 区分「跳过」与「失败」):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

documentation · medium
在 ensureLoaded、install、uninstall 和 status 函数中,虽然返回契约已经明确,但建议在函数开头或文件顶部添加更详细的文档注释,以便开发者快速理解各个字段的含义及其区别。例如,skipped、warn、loaded 的具体含义和使用场景。

// { loaded:true, reloaded:boolean } —— 已加载(reloaded 表示是否重载过)
// { loaded:false, skipped:true, warn } —— 故意跳过域操作(沙箱/非 darwin/无 uid)
// { loaded:false, warn } —— 真失败(bootstrap 抛错)
//
// 注意 skipped 只表示「**没去碰** launchd 域」,与「碰了但作业没加载」是两回事;
// install/uninstall 的返回值沿用同一含义,调用方据此决定是否自行拉起进程。
function ensureLoaded(plistFile, changed, label) {
const l = label || LABEL;
if (uid() === null) return { loaded: false, warn: '无法获取 uid,跳过加载' };
if (uid() === null) return { loaded: false, skipped: true, warn: '无法获取 uid,跳过加载' };
if (!canTouchLaunchd(plistFile)) {
// 只落盘,由调用方决定是否需要别的加载方式;不谎报「已加载」。
return { loaded: false, skipped: true, warn: '目标不是用户真实 LaunchAgent,跳过 launchd 加载(避免影响真实自启作业)' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
在 ensureLoaded 函数中,当 canTouchLaunchd 返回 false 时,直接返回 { loaded: false, skipped: true, warn }。虽然这种设计避免了误操作用户真实作业,但缺乏对 warn 字段的详细说明。建议在函数注释中明确说明 warn 的含义,以及何时会返回 skipped: true,以便调用方更好地理解返回值的含义。

}
Comment on lines +223 to +226

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · medium
ensureLoaded 函数在 canTouchLaunchd 返回 false 时仅设置 skipped: true,但未明确阻止后续 launchctl 调用。如果调用方未正确检查 skipped 状态,仍可能触发 launchctl bootstrap 操作,从而误操作用户真实作业。建议在 ensureLoaded 函数中明确阻止任何 launchctl 操作,并确保调用方严格遵循返回值契约。

Suggestion:

Suggested change
if (!canTouchLaunchd(plistFile)) {
// 只落盘,由调用方决定是否需要别的加载方式;不谎报「已加载」。
return { loaded: false, skipped: true, warn: '目标不是用户真实 LaunchAgent,跳过 launchd 加载(避免影响真实自启作业)' };
}
if (!canTouchLaunchd(plistFile)) {
// 只落盘,由调用方决定是否需要别的加载方式;不谎报「已加载」。
// 确保调用方严格检查 skipped 状态,避免误操作 launchctl
return { loaded: false, skipped: true, warn: '目标不是用户真实 LaunchAgent,跳过 launchd 加载(避免影响真实自启作业)」 };
}
// 确保只有在 canTouchLaunchd 返回 true 时才执行 launchctl 操作
const loaded = isLoaded(l);

const loaded = isLoaded(l);
if (loaded && !changed) return { loaded: true, reloaded: false };
try {
Expand All @@ -155,6 +235,13 @@ function ensureLoaded(plistFile, changed, label) {
}
}

// 返回契约:{ ok, path, changed, loaded, skipped, warn?, reason? }
// ok:false + reason —— 未安装(找不到 controller / 工作树布局 / 写盘失败)
// ok:true + skipped:true —— 只落盘、**没去碰** launchd 域(沙箱/非 darwin/无 uid)
// ok:true + loaded:true —— 已交给 launchd 加载(changed 表示这次是否重写了 plist)
// ok:true + loaded:false + warn —— 碰了域但 bootstrap 失败
// skipped=true 时 warn 说明跳过的原因(两者会同时出现);loaded=true 时才代表真的
// 交给了 launchd。调用方据此决定是否自行拉起进程。
function install(opts) {
const o = opts || {};
Comment on lines 245 to 246

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style · low
在 install、uninstall 和 status 函数中,使用 o 作为参数对象的别名不符合代码规范。建议使用更具语义的名称(如 options 或 config)以提高代码的可读性。例如,可以将 const o = opts || {} 改为 const options = opts || {}。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style · low
在 install、uninstall 和 status 函数中,使用 o 作为参数对象的别名不符合代码规范。建议使用更具语义的名称(如 opts 或 options)以提高代码的可读性和可维护性。

Suggestion:

Suggested change
const o = opts || {};
const opts = opts || {};

const rt = o.nodeBin && o.controllerPath
Expand Down Expand Up @@ -182,43 +269,65 @@ function install(opts) {
// --now 才立即 bootstrap(launchctl bootstrap 会因 RunAtLoad 立刻拉起 controller)。
let loaded = false;
let warn = '';
let skipped = false;
if (o.now) {
const r = ensureLoaded(written.path, written.changed);
loaded = r.loaded;
warn = r.warn || '';
skipped = r.skipped === true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

documentation · medium
install 和 uninstall 函数的返回值中新增了 skipped 字段,但缺乏明确的文档说明或注释。建议在函数注释或 API 文档中明确说明 skipped 的含义及其与其他返回值(如 loaded 或 bootedOut)的关系,以避免调用方误解或误用。

if (!r.loaded && warn) {
return { ok: true, path: written.path, changed: written.changed, loaded: false, warn };
return { ok: true, path: written.path, changed: written.changed, loaded: false, skipped, warn };
}
}
return { ok: true, path: written.path, changed: written.changed, loaded };
return { ok: true, path: written.path, changed: written.changed, loaded, skipped };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test · low
在 install() 和 uninstall() 函数中,虽然增加了 skipped 字段来区分操作是否被跳过,但缺少对 skipped 状态的明确文档说明或示例。建议在函数注释中明确说明 skipped 的含义及其与 loaded、bootedOut 等字段的关系,以便开发者正确理解和使用。

}

function uninstall(opts) {
const o = opts || {};
const p = plistPath(o.dir);
// 只在「目标就是用户真实那份 + macOS」时才 bootout:否则会打掉用户真实的自启作业
// (见 canTouchLaunchd)。重定向调用方(测试)只清理自己沙箱里的 plist 文件。
// 返回 { ok, path, removed, bootedOut, skipped }:skipped=true 表示**没去碰** launchd
// 域(沙箱/非 darwin/无 uid),此时 bootedOut 恒为 false,且与「bootout 试过但作业
// 本就没加载」区分开。
let bootedOut = false;
if (uid() !== null) {
let skipped = false;
if (canTouchLaunchd(p)) {
try { launchctl(['bootout', `gui/${uid()}/${LABEL}`]); bootedOut = true; } catch (_) { /* 未加载:忽略 */ }
} else {
// 非 darwin / 拿不到 uid / 目标是沙箱:bootedOut=false 是**故意**的,
// 用 skipped 与「bootout 试过但作业本就没加载」区分开。
skipped = true;
}
let removed = false;
try { fs.unlinkSync(p); removed = true; } catch (_) { /* 不存在:忽略 */ }
return { ok: true, path: p, removed, bootedOut };
return { ok: true, path: p, removed, bootedOut, skipped };
}

// 返回:{ path, exists, loaded, queried, state }。
// loaded 的含义**始终**是「launchd 报告该作业已加载」;重定向(沙箱)时不去查询,
// 于是 loaded 恒为 false,靠 queried=false 表示「没查过」而不是「查过、没加载」。
// 调用方若要区分这两种情况,看 queried。
Comment on lines +307 to +310

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
status 函数在沙箱环境下直接返回 loaded: false, queried: false,但未明确区分「未查询」与「查询后未加载」的上下文。调用方可能误解状态,导致不必要的重载或卸载操作。建议在文档或注释中明确说明 queried: false 的含义,并确保调用方正确处理此状态。

function status(opts) {
const o = opts || {};
const p = plistPath(o.dir);
const exists = fs.existsSync(p);
// 目标不是真实那份时不去查 launchd 域:作业名仍是真实 LABEL,查出来的是用户真实
// agent 的态,当成沙箱 plist 的态返回会让调用方拿到自相矛盾的结果(沙箱里文件不存在,
// 却报 loaded=true)——装了 agent 的开发机上这条断言必红。
// 返回 loaded=false + queried=false,让「没查过」与「查过、没加载」可区分。
let loaded = false;
let queried = false;
let detail = '';
if (uid() !== null) {
if (canTouchLaunchd(p)) {
queried = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

documentation · medium
status 函数新增了 queried 字段,但缺乏明确的说明,可能导致调用方误解 loaded 和 queried 的关系。建议在函数注释中明确说明 queried 的含义,以及如何与 loaded 结合使用,以避免调用方误解状态查询的结果。

Comment on lines 319 to +323

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
在 status 函数中,queried 变量的初始化和赋值逻辑与 install 和 uninstall 函数中的 skipped 变量类似,但缺少明确的初始化。如果 canTouchLaunchd(p) 返回 false,queried 变量将保持 undefined,这可能导致调用方无法正确理解返回值。

建议明确初始化 queried 变量为 false,并确保其在所有情况下都有明确的值。例如:

let queried = false;
let loaded = false;
let detail = '';
if (canTouchLaunchd(p)) {
  queried = true;
  try {
    detail = launchctl(['print', `gui/${uid()}/${LABEL}`]);
    loaded = true;
  } catch (_) { /* 未加载 */ }
}

Suggestion:

Suggested change
let loaded = false;
let queried = false;
let detail = '';
if (uid() !== null) {
if (canTouchLaunchd(p)) {
queried = true;
let queried = false;
let loaded = false;
let detail = '';
if (canTouchLaunchd(p)) {
queried = true;

try {
detail = launchctl(['print', `gui/${uid()}/${LABEL}`]);
loaded = true;
} catch (_) { /* 未加载 */ }
}
const state = /state = (\w+)/.exec(detail);
return { path: p, exists, loaded, state: state ? state[1] : '' };
return { path: p, exists, loaded, queried, state: state ? state[1] : '' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style · low
在 status() 函数中,虽然增加了 queried 字段来区分是否查询过 launchd 域,但缺少对 queried 字段的明确说明。建议在函数注释中明确说明 queried 的含义,以及其与 loaded 字段的关系,以便开发者正确理解返回结果。

}

function main(argv) {
Expand All @@ -231,11 +340,19 @@ function main(argv) {
}
if (cmd === 'uninstall') {
const r = uninstall();
console.log(`[launchd] uninstall: ${r.removed ? '已移除' : '文件不存在'} ${r.path}`);
// skipped=true 时说明**没去碰** launchd 域(沙箱/非 darwin/无 uid),此时
// 「已移除/文件不存在」只描述 plist 文件,不代表真实作业被卸载——打印出来,
// 免得看日志的人把「文件删了」当成「作业也停了」。
const skip = r.skipped ? '(未操作 launchd 域)' : '';
console.log(`[launchd] uninstall: ${r.removed ? '已移除' : '文件不存在'}${skip} ${r.path}`);
return 0;
}
const s = status();
console.log(`[launchd] status: plist=${s.exists ? '存在' : '不存在'} loaded=${s.loaded ? '是' : '否'}${s.state ? ` state=${s.state}` : ''} path=${s.path}`);
// queried=false 表示**没查** launchd 域(沙箱/非 darwin/无 uid),与「查过、没加载」
// 是两回事。只打 loaded=否 会让调用方(packagers/install-templates 里的
// `grep -q 'loaded=是'`)把「没查过」误读成「作业没在跑」。
const q = s.queried === false ? ' queried=否' : '';
console.log(`[launchd] status: plist=${s.exists ? '存在' : '不存在'} loaded=${s.loaded ? '是' : '否'}${q}${s.state ? ` state=${s.state}` : ''} path=${s.path}`);
return 0;
}

Expand Down
6 changes: 6 additions & 0 deletions test/companion-lock.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ const fs = require('fs');
const os = require('os');
const path = require('path');

// 隔离生产日志:controller.cjs:135 在 require 期固化 LOG_FILE。本文件目前不写日志,
// 但「require controller 就必须隔离日志路径」是这里的统一约束——否则将来新增用例
// 一旦走到 startApi/saveUserConfig,噪音会直接落进用户真实日志。
process.env.ZCODE_ADVISOR_COMPANION_LOG = path.join(
fs.mkdtempSync(path.join(os.tmpdir(), 'zca-lock-log-')), 'companion.log');

const { _internal: I } = require('../tools/companion/controller.cjs');
const LOCK = I.LOCK_FILE;

Expand Down
4 changes: 4 additions & 0 deletions test/companion-zcode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ process.env.ZCODE_ADVISOR_USER_CONFIG = USER_CFG;
process.env.ZCODE_ADVISOR_ZCODE_CONFIG = V2_CFG;
// 隔离第二个数据源(provider_config.json):否则会读到开发机真实配置,断言随环境漂移。
process.env.ZCODE_ADVISOR_ZCODE_PROVIDER_CONFIG = path.join(dir, 'no-such-provider-config.json');
// 隔离生产日志:controller.cjs:135 在 require 期固化 LOG_FILE。本文件的 saveUserConfig
// 用例会触发 0.2.17 迁移日志(controller.cjs:733),不隔离就会写进用户真实日志
// (配置隔离与日志隔离是两条独立的轴)。
process.env.ZCODE_ADVISOR_COMPANION_LOG = path.join(dir, 'companion.log');
fs.writeFileSync(V2_CFG, JSON.stringify({
provider: {
p1: {
Expand Down
9 changes: 9 additions & 0 deletions test/health.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');

// 隔离生产日志:controller.cjs:135 在模块加载期解析
// LOG_FILE = process.env.ZCODE_ADVISOR_COMPANION_LOG || ~/.zcode/advisor-companion.log,
// 而 startApi() 在 listen 时写「本机 API 就绪」(controller.cjs:1391)。
// 下面两个用例会起真实 HTTP server(C.startApi(1, 0, tok)),不隔离就会把测试噪音
// 写进用户真实日志,把真实故障淹没掉。runInChild 不传 env,子进程继承本进程 env,
// 所以这里设一次即可覆盖全部子进程。
process.env.ZCODE_ADVISOR_COMPANION_LOG = path.join(
fs.mkdtempSync(path.join(os.tmpdir(), 'zca-health-log-')), 'companion.log');

const HEALTH = require('../hooks/lib/health');

function tmpDir(prefix) {
Expand Down
3 changes: 3 additions & 0 deletions test/inject.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const path = require('path');
// 读到开发者本机真实的 ~/.zcode/v2/config.json(断言漂移 + 明文 key 进测试输出)。
process.env.ZCODE_ADVISOR_ZCODE_CONFIG = path.join(os.tmpdir(), `zcadv-inject-no-zcode-${Date.now()}.json`);
process.env.ZCODE_ADVISOR_USER_CONFIG = path.join(os.tmpdir(), `zcadv-inject-no-user-${Date.now()}.json`);
// 本文件在端点归一化用例里 require controller.cjs,而 controller.cjs:135 在 require 期
// 固化 LOG_FILE → 一并隔离,避免任何写日志路径落到用户真实的 advisor-companion.log。
process.env.ZCODE_ADVISOR_COMPANION_LOG = path.join(os.tmpdir(), `zcadv-inject-log-${Date.now()}.log`);

const INJECT_SRC = fs.readFileSync(
path.join(__dirname, '..', 'tools', 'companion', 'inject.js'),
Expand Down
Loading
Loading