grix-connector
Version:
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 lines (4 loc) • 9.73 kB
JavaScript
import{createHash as w}from"node:crypto";import D from"node:fs";import S from"node:os";import h from"node:path";import m from"node:process";import{mkdir as p}from"node:fs/promises";import{setTimeout as _}from"node:timers/promises";import{resolveRuntimePaths as u}from"../core/config/paths.js";import{inspectDaemonState as l}from"../runtime/service-state.js";import{isProcessRunning as y,killOrphanedDaemonProcesses as F,runCommand as n,terminateProcessTree as L,waitForProcessExit as v}from"./process-control.js";import{readDaemonLock as g}from"../runtime/daemon-lock.js";import{ServiceInstallStore as z}from"./service-install-store.js";import{getPlatformServiceAdapter as R}from"./platform-adapter.js";import{buildServiceID as P,resolveServiceInstallRecordPath as b,resolveServiceStderrPath as I,resolveServiceStdoutPath as $}from"./service-paths.js";class G{adapter;platform;homeDir;uid;nodePath;cliPath;constructor(t){this.platform=t.platform??m.platform,this.homeDir=t.homeDir??S.homedir(),this.uid=t.uid??m.getuid?.()??0,this.nodePath=t.nodePath??m.execPath,this.cliPath=t.cliPath,this.adapter=R(this.platform)}normalizeRootDir(t){return h.resolve(String(t??"").trim())}buildDescriptor(t,i,r=""){const e=this.normalizeRootDir(t);return{schema_version:1,platform:this.platform,service_id:P(e,this.platform),node_path:this.nodePath,cli_path:this.cliPath,cli_hash:this.computeFileHash(this.cliPath)??"",definition_path:r,config_dir:i??"",installed_at:0,updated_at:0}}createStore(t){return new z(b(this.normalizeRootDir(t)))}toAdapterPayload(t){return{serviceID:t.service_id,nodePath:t.node_path,cliPath:t.cli_path,configDir:t.config_dir||void 0,environmentPath:this.buildServiceEnvironmentPath()}}async resolveServiceLogPaths(t,i){if(this.platform==="darwin"){const r=h.join(this.homeDir,"Library","Logs","grix-connector");return await p(r,{recursive:!0}),{stdoutPath:h.join(r,`${i}.out.log`),stderrPath:h.join(r,`${i}.err.log`)}}return await p(h.join(t,"service"),{recursive:!0}),{stdoutPath:$(t),stderrPath:I(t)}}buildServiceEnvironmentPath(){const t=[h.dirname(this.nodePath),"/opt/homebrew/bin","/usr/local/bin","/usr/bin","/bin","/usr/sbin","/sbin"],i=String(m.env.PATH??"").split(h.delimiter).filter(s=>s&&h.isAbsolute(s)).filter(s=>!s.includes("/node_modules/.bin")).filter(s=>!s.includes("/node-gyp-bin")),r=[],e=new Set;for(const s of[...t,...i]){if(e.has(s))continue;if(e.add(s),[...r,s].join(h.delimiter).length>2048)break;r.push(s)}return r.join(h.delimiter)}toStartParams(t){return{serviceID:t.service_id,definitionPath:t.definition_path,uid:this.uid,homeDir:this.homeDir,runCommand:n}}toStopParams(t){return{serviceID:t.service_id,uid:this.uid,homeDir:this.homeDir,runCommand:n}}toRestartParams(t){return{serviceID:t.service_id,definitionPath:t.definition_path,uid:this.uid,homeDir:this.homeDir,runCommand:n}}toUninstallParams(t){return{serviceID:t.service_id,definitionPath:t.definition_path,uid:this.uid,homeDir:this.homeDir,runCommand:n}}async loadDescriptor(t){const i=this.createStore(t),r=await i.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:i,descriptor:r}}isDescriptorCurrent(t){if(t.node_path!==this.nodePath||t.cli_path!==this.cliPath||!t.cli_hash)return!1;const i=this.computeFileHash(t.cli_path);return i!==null&&i===t.cli_hash}computeFileHash(t){try{const i=D.readFileSync(t);return w("sha256").update(i).digest("hex")}catch{return null}}async refreshDescriptor(t,i){const r=this.normalizeRootDir(t);await p(h.join(r,"service"),{recursive:!0});const e={...i,platform:this.platform,service_id:i.service_id||P(r,this.platform),node_path:this.nodePath,cli_path:this.cliPath,cli_hash:this.computeFileHash(this.cliPath)??"",config_dir:i.config_dir},s=await this.resolveServiceLogPaths(r,e.service_id),a=await this.adapter.install({...this.toAdapterPayload(e),stdoutPath:s.stdoutPath,stderrPath:s.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:n}),o={...e,installed_at:i.installed_at,definition_path:a?.definitionPath||e.definition_path,updated_at:Date.now()};return await this.createStore(r).save(o),o}async resolveActiveDescriptor(t){const{descriptor:i}=await this.loadDescriptor(t);return this.isDescriptorCurrent(i)?i:this.refreshDescriptor(t,i)}async discoverConflictingServices(t){const i=this.normalizeRootDir(t),r=P(i,this.platform);return(await this.adapter.discoverServices({homeDir:this.homeDir,runCommand:n})).filter(s=>s.serviceID!==r)}async cleanupConflictingServices(t){const i=await this.discoverConflictingServices(t);if(i.length>0){const e=i.map(s=>s.serviceID).join(", ");console.log(`\u53D1\u73B0 ${i.length} \u4E2A\u6B8B\u7559\u670D\u52A1\uFF0C\u6B63\u5728\u68C0\u67E5: ${e}`)}let r=0;for(const e of i){if(await this.adapter.isServiceLoaded({serviceID:e.serviceID,uid:this.uid,homeDir:this.homeDir,runCommand:n})){console.log(`\u8DF3\u8FC7\u6B63\u5728\u8FD0\u884C\u7684\u670D\u52A1: ${e.serviceID}`);continue}try{await this.adapter.stop({serviceID:e.serviceID,uid:this.uid,homeDir:this.homeDir,runCommand:n})}catch{}try{await this.adapter.uninstall({serviceID:e.serviceID,definitionPath:e.definitionPath,uid:this.uid,homeDir:this.homeDir,runCommand:n}),r++}catch{}}return r}async waitForDaemonStarted(t,i={}){const{oldPid:r=0,timeoutMs:e=2e4}=i,s=Date.now();for(;Date.now()-s<e;){const c=g(t.daemonLockFile);if(c&&c.pid>0&&y(c.pid)&&c.pid!==r)return;const f=l(t.daemonStatusFile);if(f.running&&f.pid>0&&f.pid!==r)return;await _(100)}const a=g(t.daemonLockFile),o=l(t.daemonStatusFile),d=[`daemon start timeout (${e}ms)`,`status=${JSON.stringify(o)}`,`lock=${a?JSON.stringify(a):"missing"}`,this.tailLogForError("stderr",i.stderrLogFile??t.stderrLogFile),this.tailLogForError("stdout",i.stdoutLogFile??t.stdoutLogFile)].filter(Boolean);throw new Error(d.join(`
`))}tailLogForError(t,i,r=20){if(!D.existsSync(i))return`${t}=missing`;try{const e=D.readFileSync(i,"utf8").trim();if(!e)return`${t}=empty`;const s=e.split(/\r?\n/).slice(-r);return`${t}:
${s.join(`
`)}`}catch(e){return`${t}=unreadable: ${e instanceof Error?e.message:String(e)}`}}async install(t){const i=this.normalizeRootDir(t.rootDir);await this.cleanupConflictingServices(i),await p(h.join(i,"service"),{recursive:!0});const r=this.buildDescriptor(i,t.configDir),e=await this.resolveServiceLogPaths(i,r.service_id),s=await this.adapter.install({...this.toAdapterPayload(r),stdoutPath:e.stdoutPath,stderrPath:e.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:n}),a={...r,definition_path:s?.definitionPath??"",installed_at:Date.now(),updated_at:Date.now()};await this.createStore(i).save(a),await this.adapter.start(this.toStartParams(a));const o=u(i);return await this.waitForDaemonStarted(o,{stdoutLogFile:e.stdoutPath,stderrLogFile:e.stderrPath}),this.status({rootDir:i})}async start(t){const i=this.normalizeRootDir(t.rootDir);await this.cleanupConflictingServices(i);const r=u(i),e=l(r.daemonStatusFile),a=await this.createStore(i).load();if(e.running&&a&&this.isDescriptorCurrent(a))return this.status({rootDir:i});const o=a?this.isDescriptorCurrent(a)&&e.running?a:await this.refreshDescriptor(i,a):await this.resolveActiveDescriptor(i),d=await this.resolveServiceLogPaths(i,o.service_id);try{e.running?await this.adapter.restart(this.toRestartParams(o)):await this.adapter.start(this.toStartParams(o))}catch(c){if(c instanceof Error&&c.message.includes("plist missing"))await this.adapter.install({...this.toAdapterPayload(o),stdoutPath:d.stdoutPath,stderrPath:d.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:n}),await this.adapter.start(this.toStartParams(o));else throw c}return await this.waitForDaemonStarted(r,{oldPid:e.pid,stdoutLogFile:d.stdoutPath,stderrLogFile:d.stderrPath}),this.status({rootDir:i})}async stop(t){const{descriptor:i}=await this.loadDescriptor(t.rootDir),r=u(i.config_dir||this.normalizeRootDir(t.rootDir)),e=l(r.daemonStatusFile);await this.adapter.stop(this.toStopParams(i)),e.running&&e.pid&&(await v(e.pid,{timeoutMs:5e3})||(await L(e.pid,{platform:this.platform,runCommandImpl:n}),await v(e.pid,{timeoutMs:5e3})));const s=await F(i.cli_path);return s>0&&console.log(`\u6E05\u7406\u4E86 ${s} \u4E2A\u6B8B\u7559 daemon \u8FDB\u7A0B`),this.status({rootDir:t.rootDir})}async restart(t){const i=this.normalizeRootDir(t.rootDir);await this.cleanupConflictingServices(i);const r=await this.resolveActiveDescriptor(i),e=u(i),s=l(e.daemonStatusFile),a=await this.resolveServiceLogPaths(i,r.service_id);try{await this.adapter.restart(this.toRestartParams(r))}catch(o){if(o instanceof Error&&o.message.includes("plist missing"))await this.adapter.install({...this.toAdapterPayload(r),stdoutPath:a.stdoutPath,stderrPath:a.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:n}),await this.adapter.start(this.toStartParams(r));else throw o}return await this.waitForDaemonStarted(e,{oldPid:s.pid,stdoutLogFile:a.stdoutPath,stderrLogFile:a.stderrPath}),this.status({rootDir:i})}async uninstall(t){const i=this.normalizeRootDir(t.rootDir),r=this.createStore(i),e=await r.load();return e&&(await this.adapter.uninstall(this.toUninstallParams(e)),await r.clear()),this.status({rootDir:i})}async status(t){const i=this.normalizeRootDir(t.rootDir),e=await this.createStore(i).load(),s=u(i),a=l(s.daemonStatusFile);return e?{installed:!0,install_state:this.isDescriptorCurrent(e)?"current":"stale",service_kind:this.adapter.kind,service_id:e.service_id,definition_path:e.definition_path,root_dir:i,daemon_state:a.running?"running":a.state,pid:a.pid,connection_state:a.connection_state,updated_at:a.updated_at}:{installed:!1,install_state:"missing",service_kind:this.adapter.kind,root_dir:i,daemon_state:a.running?"running":a.state,pid:a.pid}}}export{G as ServiceManager};