grix-connector 4.6.4 → 4.6.6

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.
@@ -1,3 +1,3 @@
1
- import{createHash as F}from"node:crypto";import h from"node:fs";import b from"node:os";import o from"node:path";import D from"node:process";import{mkdir as _}from"node:fs/promises";import{setTimeout as B}from"node:timers/promises";import{resolveRuntimePaths as S}from"../core/config/paths.js";import{inspectDaemonState as g}from"../runtime/service-state.js";import{isProcessRunning as H,runCommand as m,terminateProcessTree as T,waitForProcessExit as N}from"./process-control.js";import{isLockHolderIdentityVerified as k,readDaemonLock as P}from"../runtime/daemon-lock.js";import{ServiceInstallStore as q}from"./service-install-store.js";import{getPlatformServiceAdapter as M}from"./platform-adapter.js";import{captureLogCursor as v,formatLogDelta as w}from"./log-diagnostics.js";import{LAUNCHER_FILE as x,bootstrapVersionFromPackage as O,compareVersions as A,ensureLauncher as V,launcherPath as I,packagedLauncherPath as G,readLauncherTemplateVersion as J,readPackageVersionAt as E,readPointer as R,switchCurrent as U,versionEntry as C}from"../core/upgrade/version-store.js";import{buildServiceID as y,resolveWindowsServiceRuntimeDir as W,resolveServiceInstallRecordPath as K,resolveServiceStderrPath as Q,resolveServiceStdoutPath as X}from"./service-paths.js";function Y(L){try{return h.realpathSync(L)}catch{return L}}class Dt{adapter;platform;homeDir;uid;nodePath;cliPath;waitForExit;terminateTree;constructor(e){this.platform=e.platform??D.platform,this.homeDir=e.homeDir??b.homedir(),this.uid=e.uid??D.getuid?.()??0,this.nodePath=e.nodePath??D.execPath,this.cliPath=Y(e.cliPath),this.adapter=M(this.platform),this.waitForExit=e.processControl?.waitForExit??N,this.terminateTree=e.processControl?.terminateTree??T}normalizeRootDir(e){return o.resolve(String(e??"").trim())}resolveLocalServiceBaseDir(){if(this.platform==="darwin")return o.join(this.homeDir,"Library","Application Support","grix-connector");if(this.platform==="win32"){const e=String(D.env.LOCALAPPDATA??"").trim();return o.join(e&&o.isAbsolute(e)?e:o.join(this.homeDir,"AppData","Local"),"grix-connector")}return null}resolveServiceRuntimeDir(e){const t=this.resolveLocalServiceBaseDir();return t?this.platform==="win32"?W(this.normalizeRootDir(e),D.env,this.homeDir):o.join(t,"runtime"):null}resolveServiceCliPath(e){const t=this.resolveServiceRuntimeDir(e);return t?I(t):this.cliPath}resolveSourcePackageDir(){return o.dirname(o.dirname(this.cliPath))}async adoptLauncher(e){const t=this.normalizeRootDir(e),r=this.resolveServiceRuntimeDir(t);if(!r)return!1;const s=await this.createStore(t).load();return!s||s.cli_path===this.resolveServiceCliPath(t)&&this.isLauncherStoreUsable(r)?!1:(await this.refreshDescriptor(t,s),!0)}isLauncherStoreUsable(e){if(!h.existsSync(I(e)))return!1;const t=R(e,"current");return!!t&&h.existsSync(C(e,t))}prepareServiceRuntime(e){const t=this.resolveServiceRuntimeDir(e);if(!t)return;const r=this.resolveSourcePackageDir();h.mkdirSync(t,{recursive:!0});const s=E(r);if(!s)throw new Error(`Cannot read package version at ${r} (entry ${this.cliPath}); refusing to install the service`);if(!V(t,r))throw new Error(`Package at ${r} does not ship ${x}; refusing to point the service at it`);const i=R(t,"current");if((!i||!h.existsSync(C(t,i))||A(s,i)>0)&&(O(t,r,s),U(t,s)),this.platform==="win32"||this.platform==="darwin"){const a=o.join(r,"scripts","upgrade-guardian.mjs");if(h.existsSync(a)){const d=o.join(this.normalizeRootDir(e),"bin"),c=o.join(d,"upgrade-guardian.mjs"),n=o.join(d,"upgrade-guardian.fallback.mjs"),l=`${c}.tmp-${D.pid}`;h.mkdirSync(d,{recursive:!0});try{h.copyFileSync(a,l),h.renameSync(l,c),h.copyFileSync(a,n)}finally{h.rmSync(l,{force:!0})}}if(this.platform==="darwin"){const d=o.join(r,"scripts","upgrade-guardian.sh");if(h.existsSync(d)){const c=o.join(this.normalizeRootDir(e),"bin","upgrade-guardian.sh"),n=`${c}.tmp-${D.pid}`;try{h.copyFileSync(d,n),h.chmodSync(n,493),h.renameSync(n,c)}finally{h.rmSync(n,{force:!0})}}}}}buildDescriptor(e,t,r=""){const s=this.normalizeRootDir(e),i=this.resolveServiceCliPath(s);return{schema_version:1,platform:this.platform,service_id:y(s,this.platform),node_path:this.nodePath,cli_path:i,cli_hash:this.computeServiceHash(i)??"",definition_path:r,config_dir:t??"",installed_at:0,updated_at:0}}createStore(e){return new q(K(this.normalizeRootDir(e)))}toAdapterPayload(e,t){return{serviceID:e.service_id,nodePath:e.node_path,cliPath:e.cli_path,configDir:e.config_dir||void 0,rootDir:this.normalizeRootDir(t),environmentPath:this.buildServiceEnvironmentPath()}}async resolveServiceLogPaths(e){const t=this.resolveLocalServiceBaseDir();if(t){const r=o.join(this.platform==="darwin"?o.join(this.homeDir,"Library","Logs","grix-connector"):o.join(t,"logs"),y(this.normalizeRootDir(e),this.platform));return await _(r,{recursive:!0}),{stdoutPath:o.join(r,"daemon.out.log"),stderrPath:o.join(r,"daemon.err.log")}}return await _(o.join(e,"service"),{recursive:!0}),{stdoutPath:X(e),stderrPath:Q(e)}}buildServiceEnvironmentPath(){const e=[o.dirname(this.nodePath),"/opt/homebrew/bin","/usr/local/bin","/usr/bin","/bin","/usr/sbin","/sbin"],t=String(D.env.PATH??"").split(o.delimiter).filter(i=>i&&o.isAbsolute(i)).filter(i=>!i.includes("/node_modules/.bin")).filter(i=>!i.includes("/node-gyp-bin")),r=[],s=new Set;for(const i of[...e,...t]){if(s.has(i))continue;if(s.add(i),[...r,i].join(o.delimiter).length>2048)break;r.push(i)}return r.join(o.delimiter)}toStartParams(e){return{serviceID:e.service_id,definitionPath:e.definition_path,uid:this.uid,homeDir:this.homeDir,runCommand:m}}toStopParams(e){return{serviceID:e.service_id,uid:this.uid,homeDir:this.homeDir,runCommand:m}}toRestartParams(e){return{serviceID:e.service_id,definitionPath:e.definition_path,uid:this.uid,homeDir:this.homeDir,runCommand:m}}toUninstallParams(e){return{serviceID:e.service_id,definitionPath:e.definition_path,uid:this.uid,homeDir:this.homeDir,runCommand:m}}async loadDescriptor(e){const t=this.createStore(e),r=await t.load();if(!r)throw new Error("\u540E\u53F0\u670D\u52A1\u8FD8\u6CA1\u6709\u5B89\u88C5\uFF0C\u8BF7\u5148\u6267\u884C service install\u3002");return{store:t,descriptor:r}}isDescriptorCurrent(e,t){if(t.node_path!==this.nodePath||t.cli_path!==this.resolveServiceCliPath(e)||!t.cli_hash)return!1;const r=this.computeServiceHash(t.cli_path),s=this.computeServiceHash(this.cliPath);return r!==null&&s!==null&&r===t.cli_hash&&s===t.cli_hash}computeFileHash(e){try{const t=h.readFileSync(e);return F("sha256").update(t).digest("hex")}catch{return null}}computeServiceHash(e){return this.resolveLocalServiceBaseDir()?this.computeLauncherHash(e):this.computeFileHash(e)}computeLauncherHash(e){try{const t=this.resolveSourcePackageDir(),r=E(t)??"",s=o.basename(e)===x,i=s?o.dirname(e):null,a=s?e:G(t);if(!a)return null;const d=J(a);if(d<0)return null;let c=!0;if(i){const n=R(i,"current");c=!!n&&h.existsSync(C(i,n))&&A(n,r)>=0}return c?F("sha256").update(`launcher:${d}`).update(`source:${r}`).digest("hex"):null}catch{return null}}computeRuntimeDirHash(e){try{const t=o.dirname(e),r=F("sha256"),s=a=>{const d=h.readdirSync(a,{withFileTypes:!0}).sort((c,n)=>c.name.localeCompare(n.name));for(const c of d){const n=o.join(a,c.name);r.update(o.relative(t,n)),c.isDirectory()?s(n):c.isFile()&&r.update(h.readFileSync(n))}};s(t);const i=o.join(o.dirname(t),"package.json");return h.existsSync(i)&&(r.update("package.json"),r.update(h.readFileSync(i))),r.digest("hex")}catch{return null}}async refreshDescriptor(e,t){const r=this.normalizeRootDir(e);await _(o.join(r,"service"),{recursive:!0}),this.prepareServiceRuntime(r);const s=this.resolveServiceCliPath(r),i={...t,platform:this.platform,service_id:t.service_id||y(r,this.platform),node_path:this.nodePath,cli_path:s,cli_hash:this.computeServiceHash(s)??"",config_dir:t.config_dir},a=await this.resolveServiceLogPaths(r),d=await this.adapter.install({...this.toAdapterPayload(i,r),stdoutPath:a.stdoutPath,stderrPath:a.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:m}),c={...i,installed_at:t.installed_at,definition_path:d?.definitionPath||i.definition_path,updated_at:Date.now()};return await this.createStore(r).save(c),c}async resolveActiveDescriptor(e){const{descriptor:t}=await this.loadDescriptor(e);return this.isDescriptorCurrent(e,t)?t:this.refreshDescriptor(e,t)}async discoverConflictingServices(e){const t=this.normalizeRootDir(e),r=y(t,this.platform);return(await this.adapter.discoverServices({homeDir:this.homeDir,runCommand:m})).filter(i=>i.serviceID!==r)}async cleanupConflictingServices(e){const t=await this.discoverConflictingServices(e);if(t.length>0){const s=t.map(i=>i.serviceID).join(", ");console.log(`\u53D1\u73B0 ${t.length} \u4E2A\u6B8B\u7559\u670D\u52A1\uFF0C\u6B63\u5728\u68C0\u67E5: ${s}`)}let r=0;for(const s of t){if(await this.adapter.isServiceLoaded({serviceID:s.serviceID,uid:this.uid,homeDir:this.homeDir,runCommand:m})){console.log(`\u8DF3\u8FC7\u6B63\u5728\u8FD0\u884C\u7684\u670D\u52A1: ${s.serviceID}`);continue}try{await this.adapter.stop({serviceID:s.serviceID,uid:this.uid,homeDir:this.homeDir,runCommand:m})}catch{}try{await this.adapter.uninstall({serviceID:s.serviceID,definitionPath:s.definitionPath,uid:this.uid,homeDir:this.homeDir,runCommand:m}),r++}catch{}}return r}captureDaemonBaseline(e){return{status:g(e.daemonStatusFile),lock:P(e.daemonLockFile)}}isDaemonReady(e,t,r){const s=g(e.daemonStatusFile),i=P(e.daemonLockFile);if(!s.running||s.state!=="running"||s.pid<=0||!i||i.pid!==s.pid||!H(i.pid)||typeof i.root_dir!="string"||o.resolve(i.root_dir)!==o.resolve(e.rootDir)||typeof i.instance_id!="string"||i.instance_id.length===0||s.instance_id!==i.instance_id)return!1;const a=s.pid!==t.status.pid||s.state!==t.status.state||s.updated_at!==t.status.updated_at||s.instance_id!==t.status.instance_id,d=!t.lock||i.pid!==t.lock.pid||i.acquired_at!==t.lock.acquired_at||i.instance_id!==t.lock.instance_id,c=a&&d,n=t.status.state==="starting"&&t.status.instance_id===i.instance_id&&t.lock?.pid===i.pid&&t.lock.instance_id===i.instance_id&&s.instance_id===i.instance_id&&s.updated_at!==t.status.updated_at,l=t.lock?.pid===i.pid&&t.lock.instance_id===i.instance_id&&t.lock.acquired_at===i.acquired_at&&(t.status.pid!==i.pid||t.status.instance_id!==i.instance_id||t.status.state!=="running")&&s.pid===i.pid&&s.instance_id===i.instance_id&&s.state==="running"&&s.updated_at!==t.status.updated_at;return!k(i)&&!c&&!n&&!l?!1:r?c||n||l:!0}canForceTerminateDaemon(e,t){const r=P(e.daemonLockFile);return t>0&&r!==null&&r.pid===t&&typeof r.root_dir=="string"&&o.resolve(r.root_dir)===o.resolve(e.rootDir)&&k(r)}defaultDaemonStartTimeoutMs(){return this.platform==="darwin"?5e4:2e4}async waitForDaemonStarted(e,t={}){const r=t.timeoutMs??this.defaultDaemonStartTimeoutMs(),s=t.baseline??this.captureDaemonBaseline(e),i=t.requireNewDaemon??!1,a=t.stdoutLogFile??e.stdoutLogFile,d=t.stderrLogFile??e.stderrLogFile,c=t.stdoutLogCursor??v(a),n=t.stderrLogCursor??v(d),l=Date.now();for(;Date.now()-l<r;){if(this.isDaemonReady(e,s,i))return;const f=g(e.daemonStatusFile);if(f.state==="failed"&&(s.status.state!=="failed"||f.pid!==s.status.pid||f.updated_at!==s.status.updated_at||f.instance_id!==s.status.instance_id)){const z=P(e.daemonLockFile),$=["daemon start failed",`status=${JSON.stringify(f)}`,`lock=${z?JSON.stringify(z):"missing"}`,w("stderr",d,n),w("stdout",a,c)].filter(Boolean);throw new Error($.join(`
1
+ import{createHash as F}from"node:crypto";import h from"node:fs";import b from"node:os";import o from"node:path";import D from"node:process";import{mkdir as _}from"node:fs/promises";import{setTimeout as B}from"node:timers/promises";import{resolveRuntimePaths as S}from"../core/config/paths.js";import{inspectDaemonState as g}from"../runtime/service-state.js";import{isProcessRunning as H,runCommand as m,terminateProcessTree as T,waitForProcessExit as N}from"./process-control.js";import{isLockHolderIdentityVerified as k,readDaemonLock as P}from"../runtime/daemon-lock.js";import{ServiceInstallStore as q}from"./service-install-store.js";import{getPlatformServiceAdapter as M}from"./platform-adapter.js";import{captureLogCursor as v,formatLogDelta as w}from"./log-diagnostics.js";import{LAUNCHER_FILE as z,bootstrapVersionFromPackage as O,compareVersions as A,ensureLauncher as V,launcherPath as I,packagedLauncherPath as G,readLauncherTemplateVersion as J,readPackageVersionAt as E,readPointer as R,switchCurrent as U,versionEntry as C}from"../core/upgrade/version-store.js";import{buildServiceID as y,resolveWindowsServiceRuntimeDir as W,resolveServiceInstallRecordPath as K,resolveServiceStderrPath as Q,resolveServiceStdoutPath as X}from"./service-paths.js";function Y(L){try{return h.realpathSync(L)}catch{return L}}class Dt{adapter;platform;homeDir;uid;nodePath;cliPath;waitForExit;terminateTree;constructor(e){this.platform=e.platform??D.platform,this.homeDir=e.homeDir??b.homedir(),this.uid=e.uid??D.getuid?.()??0,this.nodePath=e.nodePath??D.execPath,this.cliPath=Y(e.cliPath),this.adapter=M(this.platform),this.waitForExit=e.processControl?.waitForExit??N,this.terminateTree=e.processControl?.terminateTree??T}normalizeRootDir(e){return o.resolve(String(e??"").trim())}resolveLocalServiceBaseDir(){if(this.platform==="darwin")return o.join(this.homeDir,"Library","Application Support","grix-connector");if(this.platform==="win32"){const e=String(D.env.LOCALAPPDATA??"").trim();return o.join(e&&o.isAbsolute(e)?e:o.join(this.homeDir,"AppData","Local"),"grix-connector")}return null}resolveServiceRuntimeDir(e){const t=this.resolveLocalServiceBaseDir();return t?this.platform==="win32"?W(this.normalizeRootDir(e),D.env,this.homeDir):o.join(t,"runtime"):null}resolveServiceCliPath(e){const t=this.resolveServiceRuntimeDir(e);return t?I(t):this.cliPath}resolveSourcePackageDir(){return o.dirname(o.dirname(this.cliPath))}async adoptLauncher(e){const t=this.normalizeRootDir(e),r=this.resolveServiceRuntimeDir(t);if(!r)return!1;const s=await this.createStore(t).load();return!s||s.cli_path===this.resolveServiceCliPath(t)&&this.isLauncherStoreUsable(r)?!1:(await this.refreshDescriptor(t,s),!0)}isLauncherStoreUsable(e){if(!h.existsSync(I(e)))return!1;const t=R(e,"current");return!!t&&h.existsSync(C(e,t))}prepareServiceRuntime(e){const t=this.resolveServiceRuntimeDir(e);if(!t)return;const r=this.resolveSourcePackageDir();h.mkdirSync(t,{recursive:!0});const s=E(r);if(!s)throw new Error(`Cannot read package version at ${r} (entry ${this.cliPath}); refusing to install the service`);if(!V(t,r))throw new Error(`Package at ${r} does not ship ${z}; refusing to point the service at it`);const i=R(t,"current");if((!i||!h.existsSync(C(t,i))||A(s,i)>0)&&(O(t,r,s),U(t,s)),this.platform==="win32"||this.platform==="darwin"){const a=o.join(r,"scripts","upgrade-guardian.mjs");if(h.existsSync(a)){const d=o.join(this.normalizeRootDir(e),"bin"),c=o.join(d,"upgrade-guardian.mjs"),n=o.join(d,"upgrade-guardian.fallback.mjs"),l=`${c}.tmp-${D.pid}`;h.mkdirSync(d,{recursive:!0});try{h.copyFileSync(a,l),h.renameSync(l,c),h.copyFileSync(a,n)}finally{h.rmSync(l,{force:!0})}}if(this.platform==="darwin"){const d=o.join(r,"scripts","upgrade-guardian.sh");if(h.existsSync(d)){const c=o.join(this.normalizeRootDir(e),"bin","upgrade-guardian.sh"),n=`${c}.tmp-${D.pid}`;try{h.copyFileSync(d,n),h.chmodSync(n,493),h.renameSync(n,c)}finally{h.rmSync(n,{force:!0})}}}}}buildDescriptor(e,t,r=""){const s=this.normalizeRootDir(e),i=this.resolveServiceCliPath(s);return{schema_version:1,platform:this.platform,service_id:y(s,this.platform),node_path:this.nodePath,cli_path:i,cli_hash:this.computeServiceHash(i)??"",definition_path:r,config_dir:t??"",installed_at:0,updated_at:0}}createStore(e){return new q(K(this.normalizeRootDir(e)))}toAdapterPayload(e,t){return{serviceID:e.service_id,nodePath:e.node_path,cliPath:e.cli_path,configDir:e.config_dir||void 0,rootDir:this.normalizeRootDir(t),environmentPath:this.buildServiceEnvironmentPath()}}async resolveServiceLogPaths(e){const t=this.resolveLocalServiceBaseDir();if(t){const r=o.join(this.platform==="darwin"?o.join(this.homeDir,"Library","Logs","grix-connector"):o.join(t,"logs"),y(this.normalizeRootDir(e),this.platform));return await _(r,{recursive:!0}),{stdoutPath:o.join(r,"daemon.out.log"),stderrPath:o.join(r,"daemon.err.log")}}return await _(o.join(e,"service"),{recursive:!0}),{stdoutPath:X(e),stderrPath:Q(e)}}buildServiceEnvironmentPath(){const e=[o.dirname(this.nodePath),"/opt/homebrew/bin","/usr/local/bin","/usr/bin","/bin","/usr/sbin","/sbin"],t=String(D.env.PATH??"").split(o.delimiter).filter(i=>i&&o.isAbsolute(i)).filter(i=>!i.includes("/node_modules/.bin")).filter(i=>!i.includes("/node-gyp-bin")),r=[],s=new Set;for(const i of[...e,...t]){if(s.has(i))continue;if(s.add(i),[...r,i].join(o.delimiter).length>2048)break;r.push(i)}return r.join(o.delimiter)}toStartParams(e){return{serviceID:e.service_id,definitionPath:e.definition_path,uid:this.uid,homeDir:this.homeDir,runCommand:m}}toStopParams(e){return{serviceID:e.service_id,uid:this.uid,homeDir:this.homeDir,runCommand:m}}toRestartParams(e){return{serviceID:e.service_id,definitionPath:e.definition_path,uid:this.uid,homeDir:this.homeDir,runCommand:m}}toUninstallParams(e){return{serviceID:e.service_id,definitionPath:e.definition_path,uid:this.uid,homeDir:this.homeDir,runCommand:m}}async loadDescriptor(e){const t=this.createStore(e),r=await t.load();if(!r)throw new Error("\u540E\u53F0\u670D\u52A1\u8FD8\u6CA1\u6709\u5B89\u88C5\uFF0C\u8BF7\u5148\u6267\u884C grix-connector start\uFF08\u4F1A\u81EA\u52A8\u5B89\u88C5\u5E76\u542F\u52A8\u670D\u52A1\uFF09\u3002");return{store:t,descriptor:r}}isDescriptorCurrent(e,t){if(t.node_path!==this.nodePath||t.cli_path!==this.resolveServiceCliPath(e)||!t.cli_hash)return!1;const r=this.computeServiceHash(t.cli_path),s=this.computeServiceHash(this.cliPath);return r!==null&&s!==null&&r===t.cli_hash&&s===t.cli_hash}computeFileHash(e){try{const t=h.readFileSync(e);return F("sha256").update(t).digest("hex")}catch{return null}}computeServiceHash(e){return this.resolveLocalServiceBaseDir()?this.computeLauncherHash(e):this.computeFileHash(e)}computeLauncherHash(e){try{const t=this.resolveSourcePackageDir(),r=E(t)??"",s=o.basename(e)===z,i=s?o.dirname(e):null,a=s?e:G(t);if(!a)return null;const d=J(a);if(d<0)return null;let c=!0;if(i){const n=R(i,"current");c=!!n&&h.existsSync(C(i,n))&&A(n,r)>=0}return c?F("sha256").update(`launcher:${d}`).update(`source:${r}`).digest("hex"):null}catch{return null}}computeRuntimeDirHash(e){try{const t=o.dirname(e),r=F("sha256"),s=a=>{const d=h.readdirSync(a,{withFileTypes:!0}).sort((c,n)=>c.name.localeCompare(n.name));for(const c of d){const n=o.join(a,c.name);r.update(o.relative(t,n)),c.isDirectory()?s(n):c.isFile()&&r.update(h.readFileSync(n))}};s(t);const i=o.join(o.dirname(t),"package.json");return h.existsSync(i)&&(r.update("package.json"),r.update(h.readFileSync(i))),r.digest("hex")}catch{return null}}async refreshDescriptor(e,t){const r=this.normalizeRootDir(e);await _(o.join(r,"service"),{recursive:!0}),this.prepareServiceRuntime(r);const s=this.resolveServiceCliPath(r),i={...t,platform:this.platform,service_id:t.service_id||y(r,this.platform),node_path:this.nodePath,cli_path:s,cli_hash:this.computeServiceHash(s)??"",config_dir:t.config_dir},a=await this.resolveServiceLogPaths(r),d=await this.adapter.install({...this.toAdapterPayload(i,r),stdoutPath:a.stdoutPath,stderrPath:a.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:m}),c={...i,installed_at:t.installed_at,definition_path:d?.definitionPath||i.definition_path,updated_at:Date.now()};return await this.createStore(r).save(c),c}async resolveActiveDescriptor(e){const{descriptor:t}=await this.loadDescriptor(e);return this.isDescriptorCurrent(e,t)?t:this.refreshDescriptor(e,t)}async discoverConflictingServices(e){const t=this.normalizeRootDir(e),r=y(t,this.platform);return(await this.adapter.discoverServices({homeDir:this.homeDir,runCommand:m})).filter(i=>i.serviceID!==r)}async cleanupConflictingServices(e){const t=await this.discoverConflictingServices(e);if(t.length>0){const s=t.map(i=>i.serviceID).join(", ");console.log(`\u53D1\u73B0 ${t.length} \u4E2A\u6B8B\u7559\u670D\u52A1\uFF0C\u6B63\u5728\u68C0\u67E5: ${s}`)}let r=0;for(const s of t){if(await this.adapter.isServiceLoaded({serviceID:s.serviceID,uid:this.uid,homeDir:this.homeDir,runCommand:m})){console.log(`\u8DF3\u8FC7\u6B63\u5728\u8FD0\u884C\u7684\u670D\u52A1: ${s.serviceID}`);continue}try{await this.adapter.stop({serviceID:s.serviceID,uid:this.uid,homeDir:this.homeDir,runCommand:m})}catch{}try{await this.adapter.uninstall({serviceID:s.serviceID,definitionPath:s.definitionPath,uid:this.uid,homeDir:this.homeDir,runCommand:m}),r++}catch{}}return r}captureDaemonBaseline(e){return{status:g(e.daemonStatusFile),lock:P(e.daemonLockFile)}}isDaemonReady(e,t,r){const s=g(e.daemonStatusFile),i=P(e.daemonLockFile);if(!s.running||s.state!=="running"||s.pid<=0||!i||i.pid!==s.pid||!H(i.pid)||typeof i.root_dir!="string"||o.resolve(i.root_dir)!==o.resolve(e.rootDir)||typeof i.instance_id!="string"||i.instance_id.length===0||s.instance_id!==i.instance_id)return!1;const a=s.pid!==t.status.pid||s.state!==t.status.state||s.updated_at!==t.status.updated_at||s.instance_id!==t.status.instance_id,d=!t.lock||i.pid!==t.lock.pid||i.acquired_at!==t.lock.acquired_at||i.instance_id!==t.lock.instance_id,c=a&&d,n=t.status.state==="starting"&&t.status.instance_id===i.instance_id&&t.lock?.pid===i.pid&&t.lock.instance_id===i.instance_id&&s.instance_id===i.instance_id&&s.updated_at!==t.status.updated_at,l=t.lock?.pid===i.pid&&t.lock.instance_id===i.instance_id&&t.lock.acquired_at===i.acquired_at&&(t.status.pid!==i.pid||t.status.instance_id!==i.instance_id||t.status.state!=="running")&&s.pid===i.pid&&s.instance_id===i.instance_id&&s.state==="running"&&s.updated_at!==t.status.updated_at;return!k(i)&&!c&&!n&&!l?!1:r?c||n||l:!0}canForceTerminateDaemon(e,t){const r=P(e.daemonLockFile);return t>0&&r!==null&&r.pid===t&&typeof r.root_dir=="string"&&o.resolve(r.root_dir)===o.resolve(e.rootDir)&&k(r)}defaultDaemonStartTimeoutMs(){return this.platform==="darwin"?5e4:2e4}async waitForDaemonStarted(e,t={}){const r=t.timeoutMs??this.defaultDaemonStartTimeoutMs(),s=t.baseline??this.captureDaemonBaseline(e),i=t.requireNewDaemon??!1,a=t.stdoutLogFile??e.stdoutLogFile,d=t.stderrLogFile??e.stderrLogFile,c=t.stdoutLogCursor??v(a),n=t.stderrLogCursor??v(d),l=Date.now();for(;Date.now()-l<r;){if(this.isDaemonReady(e,s,i))return;const f=g(e.daemonStatusFile);if(f.state==="failed"&&(s.status.state!=="failed"||f.pid!==s.status.pid||f.updated_at!==s.status.updated_at||f.instance_id!==s.status.instance_id)){const x=P(e.daemonLockFile),$=["daemon start failed",`status=${JSON.stringify(f)}`,`lock=${x?JSON.stringify(x):"missing"}`,w("stderr",d,n),w("stdout",a,c)].filter(Boolean);throw new Error($.join(`
2
2
  `))}await B(100)}const p=P(e.daemonLockFile),u=g(e.daemonStatusFile),j=[`daemon start timeout (${r}ms)`,`status=${JSON.stringify(u)}`,`lock=${p?JSON.stringify(p):"missing"}`,w("stderr",d,n),w("stdout",a,c)].filter(Boolean);throw new Error(j.join(`
3
3
  `))}async install(e){const t=this.normalizeRootDir(e.rootDir);await this.cleanupConflictingServices(t),await _(o.join(t,"service"),{recursive:!0}),this.prepareServiceRuntime(t);const r=S(t),s=this.captureDaemonBaseline(r),i=this.buildDescriptor(t,e.configDir),a=await this.resolveServiceLogPaths(t),d=v(a.stdoutPath),c=v(a.stderrPath),n=await this.adapter.isServiceLoaded({serviceID:i.service_id,uid:this.uid,homeDir:this.homeDir,runCommand:m}),l=await this.adapter.install({...this.toAdapterPayload(i,t),stdoutPath:a.stdoutPath,stderrPath:a.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:m}),p={...i,definition_path:l?.definitionPath??"",installed_at:Date.now(),updated_at:Date.now()};return await this.createStore(t).save(p),n?await this.adapter.restart(this.toRestartParams(p)):await this.adapter.start(this.toStartParams(p)),await this.waitForDaemonStarted(r,{baseline:s,requireNewDaemon:!0,stdoutLogFile:a.stdoutPath,stderrLogFile:a.stderrPath,stdoutLogCursor:d,stderrLogCursor:c}),this.status({rootDir:t})}async start(e){const t=this.normalizeRootDir(e.rootDir);await this.cleanupConflictingServices(t);const r=S(t),s=this.captureDaemonBaseline(r),i=s.status,a=v(r.stdoutLogFile),d=v(r.stderrLogFile),n=await this.createStore(t).load();if(i.running&&n&&this.isDescriptorCurrent(t,n)){if(i.state==="starting"){const u=s.lock;if(u!==null&&u.pid===i.pid&&typeof u.root_dir=="string"&&o.resolve(u.root_dir)===o.resolve(r.rootDir)&&typeof u.instance_id=="string"&&u.instance_id.length>0&&i.instance_id===u.instance_id&&k(u)){const f=await this.resolveServiceLogPaths(t);return await this.waitForDaemonStarted(r,{baseline:s,stdoutLogFile:f.stdoutPath,stderrLogFile:f.stderrPath,stdoutLogCursor:a,stderrLogCursor:d}),this.status({rootDir:t})}}if(this.isDaemonReady(r,s,!1))return this.status({rootDir:t})}const l=n?this.isDescriptorCurrent(t,n)&&i.running?n:await this.refreshDescriptor(t,n):await this.resolveActiveDescriptor(t),p=await this.resolveServiceLogPaths(t);try{i.running?await this.adapter.restart(this.toRestartParams(l)):await this.adapter.start(this.toStartParams(l))}catch(u){if(u instanceof Error&&u.message.includes("plist missing"))await this.adapter.install({...this.toAdapterPayload(l,t),stdoutPath:p.stdoutPath,stderrPath:p.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:m}),await this.adapter.start(this.toStartParams(l));else throw u}return await this.waitForDaemonStarted(r,{baseline:s,requireNewDaemon:!0,stdoutLogFile:p.stdoutPath,stderrLogFile:p.stderrPath,stdoutLogCursor:a,stderrLogCursor:d}),this.status({rootDir:t})}async stop(e){const t=this.normalizeRootDir(e.rootDir),{descriptor:r}=await this.loadDescriptor(t),s=S(t),i=g(s.daemonStatusFile);return await this.adapter.stop(this.toStopParams(r)),i.running&&i.pid&&(await this.waitForExit(i.pid,{timeoutMs:5e3})||this.canForceTerminateDaemon(s,i.pid)&&(await this.terminateTree(i.pid,{platform:this.platform,runCommandImpl:m}),await this.waitForExit(i.pid,{timeoutMs:5e3}))),this.status({rootDir:t})}async restart(e){const t=this.normalizeRootDir(e.rootDir);await this.cleanupConflictingServices(t);const r=await this.resolveActiveDescriptor(t),s=S(t),i=this.captureDaemonBaseline(s),a=await this.resolveServiceLogPaths(t),d=v(a.stdoutPath),c=v(a.stderrPath);try{await this.adapter.restart(this.toRestartParams(r))}catch(n){if(n instanceof Error&&n.message.includes("plist missing"))await this.adapter.install({...this.toAdapterPayload(r,t),stdoutPath:a.stdoutPath,stderrPath:a.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:m}),await this.adapter.start(this.toStartParams(r));else throw n}return await this.waitForDaemonStarted(s,{baseline:i,requireNewDaemon:!0,stdoutLogFile:a.stdoutPath,stderrLogFile:a.stderrPath,stdoutLogCursor:d,stderrLogCursor:c}),this.status({rootDir:t})}async uninstall(e){const t=this.normalizeRootDir(e.rootDir),r=this.createStore(t),s=await r.load();return s&&(await this.adapter.uninstall(this.toUninstallParams(s)),await r.clear()),this.status({rootDir:t})}async status(e){const t=this.normalizeRootDir(e.rootDir),s=await this.createStore(t).load(),i=S(t),a=g(i.daemonStatusFile);return s?{installed:!0,install_state:this.isDescriptorCurrent(t,s)?"current":"stale",service_kind:this.adapter.kind,service_id:s.service_id,definition_path:s.definition_path,root_dir:t,daemon_state:a.state,pid:a.pid,connection_state:a.connection_state,updated_at:a.updated_at,reason:a.reason}:{installed:!1,install_state:"missing",service_kind:this.adapter.kind,root_dir:t,daemon_state:a.state,pid:a.pid,reason:a.reason}}}export{Dt as ServiceManager};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grix-connector",
3
- "version": "4.6.4",
3
+ "version": "4.6.6",
4
4
  "description": "Connect local AI coding agents (Claude, Codex, Gemini, Qwen, DeepSeek, Cursor, OpenCode, Pi, OpenHuman, Reasonix) to the Grix scheduling platform. Also serves as an OpenClaw plugin for Grix channel transport.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1246,7 +1246,11 @@ export async function monitorWindowsUpgrade(rootDir, injected = {}) {
1246
1246
  }
1247
1247
 
1248
1248
  /**
1249
- * 找出跑指定 wrapper 脚本的 wscript 进程。wmic 在 Win11 上已被移除,回落 CIM。
1249
+ * 找出命令行含 wrapper 文件名的 wscript 进程。wmic 在 Win11 上已被移除,回落 CIM。
1250
+ *
1251
+ * 返回值区分两种"空":`[]` 表示查询工具正常工作且确实没有匹配进程;
1252
+ * `null` 表示 wmic 与 CIM 都失败、根本查不了。调用方(adopt-wrapper)靠这个
1253
+ * 区别决定敢不敢拉起新 wrapper——查不了就不能当作"没有旧 wrapper"。
1250
1254
  */
1251
1255
  function findWrapperPids(wrapperFileName) {
1252
1256
  const parse = (text) => String(text || '')
@@ -1268,7 +1272,7 @@ function findWrapperPids(wrapperFileName) {
1268
1272
  { encoding: 'utf8', timeout: 20_000, windowsHide: true },
1269
1273
  ));
1270
1274
  } catch {
1271
- return [];
1275
+ return null;
1272
1276
  }
1273
1277
  }
1274
1278
 
@@ -1282,15 +1286,35 @@ function findWrapperPids(wrapperFileName) {
1282
1286
  * 1. 先只杀 wscript 进程本身(不带 /T)。Windows 不级联杀子进程,daemon 因此
1283
1287
  * 毫发无伤,但旧 wrapper 的 5 秒重启循环就此停下,不会在交接窗口里再拉起
1284
1288
  * 一个 daemon 和我们抢。
1285
- * 2. 等 daemon 自己优雅退出。
1286
- * 3. 拉起刷新后的 wrapper。
1289
+ * 2. 复查确认旧 wrapper 全部死透。
1290
+ * 3. 等 daemon 自己优雅退出。
1291
+ * 4. 拉起刷新后的 wrapper。
1287
1292
  *
1288
- * 第 3 步放在 finally:中间任何一步失败都必须把 wrapper 拉回来,否则我们刚亲手
1289
- * 拆掉了这台机器的自启动监管。
1293
+ * 第 4 步的前置条件是第 2 步确认通过:查不到进程列表、或有旧 wrapper 杀不掉时,
1294
+ * 宁可这一轮不拉新 wrapper(daemon 还在跑,业务无感;下次登录 schtasks 拉起正确的
1295
+ * wrapper 自愈,handoff 记账也保证还有重试)。历史版本这里是"finally 无条件拉起",
1296
+ * 旧的没死透 + 新的必然起 = 双 supervisor 并存,正是线上双 wrapper 互杀/空转循环
1297
+ * 的制造窗口。
1290
1298
  */
1291
- async function adoptWindowsWrapper(rootDir, options) {
1299
+ export async function adoptWindowsWrapper(rootDir, options, injected = {}) {
1292
1300
  const paths = pathsFor(rootDir);
1293
1301
  const { wrapperPath, serviceId, daemonPid } = options;
1302
+ const deps = {
1303
+ findWrapperPids,
1304
+ isPidAlive,
1305
+ sleep,
1306
+ killPid: (pid) => execFileSync('taskkill', ['/PID', String(pid), '/F'], { timeout: 10_000, windowsHide: true }),
1307
+ startWrapper: (path_) => {
1308
+ spawn('wscript.exe', ['//B', '//NoLogo', path_], {
1309
+ detached: true,
1310
+ stdio: 'ignore',
1311
+ windowsHide: true,
1312
+ }).unref();
1313
+ },
1314
+ daemonExitTimeoutMs: ADOPT_WRAPPER_DAEMON_EXIT_TIMEOUT_MS,
1315
+ killVerifyTimeoutMs: 5_000,
1316
+ ...injected,
1317
+ };
1294
1318
  if (!wrapperPath || !serviceId) {
1295
1319
  appendLog(paths.log, 'adopt-wrapper: missing --wrapper or --service-id; nothing to do');
1296
1320
  return;
@@ -1300,37 +1324,60 @@ async function adoptWindowsWrapper(rootDir, options) {
1300
1324
  appendLog(paths.log, 'adopt-wrapper: upgrade transaction in flight; skipping wrapper adoption');
1301
1325
  return;
1302
1326
  }
1327
+ // 拉起新 wrapper 的前置条件:旧 wrapper 已【确认】不在了。此前这里是
1328
+ // "尽力杀、finally 无条件拉起"——杀失败或查不到进程时照样拉新的,旧的没死透
1329
+ // 就变成两个 supervisor 并存,正是线上双 wrapper 互杀/空转循环的制造窗口。
1330
+ // 现在杀不掉、查不了都宁可不拉:daemon 仍在跑,业务不受影响,只是暂时少一层
1331
+ // 自启动监管,下次登录 schtasks 会拉起正确的 wrapper 自愈;handoff 记账
1332
+ // (WRAPPER_HANDOFF_MAX_ATTEMPTS/间隔)也保证之后还有重试机会。
1303
1333
  let wrapperRestarted = false;
1334
+ let staleWrappersConfirmedGone = false;
1304
1335
  try {
1305
1336
  const wrapperFileName = `${serviceId}-wrapper.vbs`;
1306
- for (const pid of findWrapperPids(wrapperFileName)) {
1307
- if (pid === process.pid) continue;
1337
+ const found = deps.findWrapperPids(wrapperFileName);
1338
+ if (found === null) {
1339
+ appendLog(paths.log, 'adopt-wrapper: cannot enumerate wrapper processes (wmic and CIM both failed); NOT starting a wrapper that might duplicate a live one');
1340
+ return;
1341
+ }
1342
+ const stalePids = found.filter((pid) => pid !== process.pid);
1343
+ for (const pid of stalePids) {
1308
1344
  // 只杀 wscript 自己:带 /T 会连同它启动的 daemon 一起杀,交接就不再优雅。
1309
- try { execFileSync('taskkill', ['/PID', String(pid), '/F'], { timeout: 10_000, windowsHide: true }); } catch { /* already gone */ }
1345
+ try { deps.killPid(pid); } catch { /* verified below */ }
1310
1346
  appendLog(paths.log, `adopt-wrapper: stopped stale wrapper pid=${pid}`);
1311
1347
  }
1348
+ // taskkill /F 通常立即生效,留短暂余量复查(被杀进程的句柄残留会让
1349
+ // OpenProcess 短暂误报存活);有幸存者就绝不拉第二份。
1350
+ const killDeadline = Date.now() + deps.killVerifyTimeoutMs;
1351
+ let survivors = stalePids.filter((pid) => deps.isPidAlive(pid));
1352
+ while (survivors.length > 0 && Date.now() < killDeadline) {
1353
+ await deps.sleep(250);
1354
+ survivors = survivors.filter((pid) => deps.isPidAlive(pid));
1355
+ }
1356
+ if (survivors.length > 0) {
1357
+ appendLog(paths.log, `adopt-wrapper: stale wrapper pid(s) ${survivors.join(', ')} survived taskkill; NOT starting a second wrapper`);
1358
+ return;
1359
+ }
1360
+ staleWrappersConfirmedGone = true;
1312
1361
 
1313
1362
  if (daemonPid > 0) {
1314
- const deadline = Date.now() + ADOPT_WRAPPER_DAEMON_EXIT_TIMEOUT_MS;
1315
- while (Date.now() < deadline && isPidAlive(daemonPid)) await sleep(500);
1316
- if (isPidAlive(daemonPid)) {
1317
- // 不再等下去:没有 wrapper 的机器等于失去自启动监管,宁可让新 wrapper
1318
- // 拉起的 daemon 通过单例锁把这个赖着不走的旧进程换掉。
1363
+ const deadline = Date.now() + deps.daemonExitTimeoutMs;
1364
+ while (Date.now() < deadline && deps.isPidAlive(daemonPid)) await deps.sleep(500);
1365
+ if (deps.isPidAlive(daemonPid)) {
1366
+ // 不再等下去:旧 wrapper 已确认清掉,新 wrapper 拉起的 daemon 会在抢锁时
1367
+ // 对这个还在服务的旧进程让位空转,直到它退出后自然接上——收敛且无害。
1319
1368
  appendLog(paths.log, `adopt-wrapper: daemon pid=${daemonPid} still alive after timeout; restarting wrapper anyway`);
1320
1369
  } else {
1321
1370
  appendLog(paths.log, `adopt-wrapper: daemon pid=${daemonPid} exited`);
1322
1371
  }
1323
1372
  }
1324
1373
  } finally {
1325
- try {
1326
- spawn('wscript.exe', ['//B', '//NoLogo', wrapperPath], {
1327
- detached: true,
1328
- stdio: 'ignore',
1329
- windowsHide: true,
1330
- }).unref();
1331
- wrapperRestarted = true;
1332
- } catch (error) {
1333
- appendLog(paths.log, `adopt-wrapper: failed to start refreshed wrapper: ${error instanceof Error ? error.message : String(error)}`);
1374
+ if (staleWrappersConfirmedGone) {
1375
+ try {
1376
+ deps.startWrapper(wrapperPath);
1377
+ wrapperRestarted = true;
1378
+ } catch (error) {
1379
+ appendLog(paths.log, `adopt-wrapper: failed to start refreshed wrapper: ${error instanceof Error ? error.message : String(error)}`);
1380
+ }
1334
1381
  }
1335
1382
  }
1336
1383
  if (wrapperRestarted) appendLog(paths.log, `adopt-wrapper: started refreshed wrapper ${wrapperPath}`);