aiterm-mcp 0.37.1 → 0.37.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/CHANGELOG.md +17 -1
- package/README.ja.md +5 -3
- package/README.md +5 -3
- package/dist/codex-relay-arguments.js +36 -0
- package/dist/codex-relay-launcher.js +24 -61
- package/dist/codex-relay-setup.js +44 -0
- package/dist/codex-relay-stdio.js +81 -0
- package/dist/codex-stdio-relay.js +25 -92
- package/dist/setup-codex-relay.js +20 -53
- package/dist/windows-codex-connection.js +9 -17
- package/dist/windows-codex-launcher.js +62 -0
- package/dist/windows-codex-native.js +118 -0
- package/dist/windows-codex-relay.js +70 -128
- package/dist/windows-codex-setup.js +26 -107
- package/docs/DESIGN.md +14 -8
- package/docs/RELEASE.md +5 -1
- package/package.json +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { windowsNativeProcessSource } from "./windows-codex-native.js";
|
|
2
|
+
// POSIXのexecに対応するWindows adapter。公式CLIを起動元の直接の子、中継を公式CLIの子として起動する。
|
|
3
|
+
export function windowsLauncherSource(node, relay, binary, root) {
|
|
4
|
+
const literal = (value) => JSON.stringify(value);
|
|
5
|
+
return `using System;
|
|
6
|
+
using System.Diagnostics;
|
|
7
|
+
using System.Text;
|
|
8
|
+
using System.IO;
|
|
9
|
+
using System.ComponentModel;
|
|
10
|
+
using System.Runtime.InteropServices;
|
|
11
|
+
using System.Runtime.Serialization;
|
|
12
|
+
using System.Runtime.Serialization.Json;
|
|
13
|
+
${windowsNativeProcessSource}
|
|
14
|
+
[DataContract] public sealed class LaunchPlan {
|
|
15
|
+
[DataMember] public string directory;
|
|
16
|
+
[DataMember] public string endpoint;
|
|
17
|
+
[DataMember] public string relay;
|
|
18
|
+
[DataMember] public string[] arguments;
|
|
19
|
+
}
|
|
20
|
+
public static class AitermLauncher {
|
|
21
|
+
static LaunchPlan Prepare(string[] args) {
|
|
22
|
+
var arguments=new StringBuilder();
|
|
23
|
+
foreach(var value in new string[] { ${[relay, "--prepare", root].map(literal).join(", ")} }) arguments.Append(AitermNativeProcess.Quote(value)).Append(' ');
|
|
24
|
+
foreach(var value in args) arguments.Append(AitermNativeProcess.Quote(value)).Append(' ');
|
|
25
|
+
var start=new ProcessStartInfo(${literal(node)},arguments.ToString());
|
|
26
|
+
start.UseShellExecute=false; start.CreateNoWindow=true;
|
|
27
|
+
start.RedirectStandardInput=true; start.RedirectStandardOutput=true; start.RedirectStandardError=true;
|
|
28
|
+
start.StandardOutputEncoding=new UTF8Encoding(false);
|
|
29
|
+
using(var child=Process.Start(start)) {
|
|
30
|
+
child.StandardInput.Close();
|
|
31
|
+
var output=child.StandardOutput.ReadToEndAsync();
|
|
32
|
+
var error=child.StandardError.BaseStream.CopyToAsync(Console.OpenStandardError());
|
|
33
|
+
child.WaitForExit(); System.Threading.Tasks.Task.WaitAll(output,error);
|
|
34
|
+
if(child.ExitCode!=0) throw new InvalidOperationException("中継の起動準備に失敗しました");
|
|
35
|
+
using(var stream=new MemoryStream(Encoding.UTF8.GetBytes(output.Result)))
|
|
36
|
+
return (LaunchPlan)new DataContractJsonSerializer(typeof(LaunchPlan)).ReadObject(stream);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
public static int Main(string[] args) {
|
|
40
|
+
LaunchPlan plan=null;
|
|
41
|
+
try {
|
|
42
|
+
Console.OutputEncoding=new UTF8Encoding(false);
|
|
43
|
+
plan=Prepare(args);
|
|
44
|
+
using(var lifetime=new AitermNativeProcess.Lifetime())
|
|
45
|
+
using(var server=AitermNativeProcess.Start(AitermNativeProcess.ParentId(),${literal(binary)},plan.arguments,plan.directory!=null,lifetime)) {
|
|
46
|
+
if(plan.directory==null) return server.Wait();
|
|
47
|
+
// 中継は公式CLIのjobを継承する。launcherは終了監視だけを担い、JSON-RPCを通さない。
|
|
48
|
+
using(var bridge=AitermNativeProcess.Start(server.Pid,${literal(node)},new string[] {
|
|
49
|
+
plan.relay,"--serve",${literal(binary)},plan.directory,plan.endpoint,server.Pid.ToString()
|
|
50
|
+
},false,null)) {
|
|
51
|
+
int code=bridge.Wait();
|
|
52
|
+
lifetime.Dispose();
|
|
53
|
+
server.Wait();
|
|
54
|
+
return code;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch(Exception error) { Console.Error.WriteLine("aiterm-mcp: Windows起動失敗: "+error.Message); return 1; }
|
|
58
|
+
finally { if(plan!=null && plan.directory!=null && Directory.Exists(plan.directory)) Directory.Delete(plan.directory,true); }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Windowsの標準process作成APIで、公式CLIの直接の親と必要なstdioだけを引き継ぐ。
|
|
2
|
+
// launcherと公式CLIのPIDは異なる。Macのexecと同じPIDだとは扱わない。
|
|
3
|
+
export const windowsNativeProcessSource = String.raw `
|
|
4
|
+
public static class AitermNativeProcess {
|
|
5
|
+
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] struct StartupInfo {
|
|
6
|
+
public int cb; public string reserved,desktop,title;
|
|
7
|
+
public int x,y,xSize,ySize,xChars,yChars,fill,flags; public short show,reservedSize;
|
|
8
|
+
public IntPtr reservedBytes,input,output,error;
|
|
9
|
+
}
|
|
10
|
+
[StructLayout(LayoutKind.Sequential)] struct StartupInfoEx { public StartupInfo startup; public IntPtr attributes; }
|
|
11
|
+
[StructLayout(LayoutKind.Sequential)] struct ProcessInfo { public IntPtr process,thread; public int pid,tid; }
|
|
12
|
+
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] struct ProcessEntry {
|
|
13
|
+
public uint size,usage,pid; public UIntPtr heap; public uint module,threads,parent; public int priority; public uint flags;
|
|
14
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)] public string executable;
|
|
15
|
+
}
|
|
16
|
+
[StructLayout(LayoutKind.Sequential)] struct JobLimits {
|
|
17
|
+
public long processTime,jobTime; public uint flags; public UIntPtr minWorking,maxWorking;
|
|
18
|
+
public uint activeProcesses; public UIntPtr affinity; public uint priority,scheduling;
|
|
19
|
+
}
|
|
20
|
+
[StructLayout(LayoutKind.Sequential)] struct JobInfo {
|
|
21
|
+
public JobLimits limits; public ulong reads,writes,others,readBytes,writeBytes,otherBytes;
|
|
22
|
+
public UIntPtr processMemory,jobMemory,peakProcessMemory,peakJobMemory;
|
|
23
|
+
}
|
|
24
|
+
[DllImport("kernel32.dll",SetLastError=true)] static extern IntPtr CreateToolhelp32Snapshot(uint flags,uint pid);
|
|
25
|
+
[DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern bool Process32FirstW(IntPtr snapshot,ref ProcessEntry entry);
|
|
26
|
+
[DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern bool Process32NextW(IntPtr snapshot,ref ProcessEntry entry);
|
|
27
|
+
[DllImport("kernel32.dll",SetLastError=true)] static extern IntPtr OpenProcess(uint access,bool inherit,int pid);
|
|
28
|
+
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess();
|
|
29
|
+
[DllImport("kernel32.dll")] static extern IntPtr GetStdHandle(int kind);
|
|
30
|
+
[DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern IntPtr CreateFileW(string path,uint access,uint share,IntPtr security,uint creation,uint flags,IntPtr template);
|
|
31
|
+
[DllImport("kernel32.dll",SetLastError=true)] static extern bool DuplicateHandle(IntPtr source,IntPtr handle,IntPtr target,out IntPtr duplicate,uint access,bool inherit,uint options);
|
|
32
|
+
[DllImport("kernel32.dll",SetLastError=true)] static extern bool InitializeProcThreadAttributeList(IntPtr list,int count,int flags,ref IntPtr size);
|
|
33
|
+
[DllImport("kernel32.dll",SetLastError=true)] static extern bool UpdateProcThreadAttribute(IntPtr list,uint flags,IntPtr attribute,IntPtr value,IntPtr size,IntPtr previous,IntPtr returned);
|
|
34
|
+
[DllImport("kernel32.dll")] static extern void DeleteProcThreadAttributeList(IntPtr list);
|
|
35
|
+
[DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern bool CreateProcessW(string app,StringBuilder command,IntPtr processSecurity,IntPtr threadSecurity,bool inherit,uint flags,IntPtr environment,string cwd,ref StartupInfoEx startup,out ProcessInfo info);
|
|
36
|
+
[DllImport("kernel32.dll",SetLastError=true)] static extern IntPtr CreateJobObjectW(IntPtr security,IntPtr name);
|
|
37
|
+
[DllImport("kernel32.dll",SetLastError=true)] static extern bool SetInformationJobObject(IntPtr job,int kind,ref JobInfo info,uint size);
|
|
38
|
+
[DllImport("kernel32.dll",SetLastError=true)] static extern bool AssignProcessToJobObject(IntPtr job,IntPtr process);
|
|
39
|
+
[DllImport("kernel32.dll",SetLastError=true)] static extern uint ResumeThread(IntPtr thread);
|
|
40
|
+
[DllImport("kernel32.dll")] static extern bool TerminateProcess(IntPtr process,uint code);
|
|
41
|
+
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
|
|
42
|
+
[DllImport("kernel32.dll")] static extern uint WaitForSingleObject(IntPtr handle,uint time);
|
|
43
|
+
[DllImport("kernel32.dll",SetLastError=true)] static extern bool GetExitCodeProcess(IntPtr process,out uint exitCode);
|
|
44
|
+
static void Check(bool ok) { if (!ok) throw new Win32Exception(Marshal.GetLastWin32Error()); }
|
|
45
|
+
public static int ParentId() {
|
|
46
|
+
IntPtr snapshot=CreateToolhelp32Snapshot(2,0); Check(snapshot!=new IntPtr(-1));
|
|
47
|
+
try {
|
|
48
|
+
var entry=new ProcessEntry(); entry.size=(uint)Marshal.SizeOf(entry);
|
|
49
|
+
Check(Process32FirstW(snapshot,ref entry)); int current=Process.GetCurrentProcess().Id;
|
|
50
|
+
do { if(entry.pid==current) return (int)entry.parent; } while(Process32NextW(snapshot,ref entry));
|
|
51
|
+
throw new InvalidOperationException("launcherの親processが見つかりません");
|
|
52
|
+
} finally { CloseHandle(snapshot); }
|
|
53
|
+
}
|
|
54
|
+
public static string Quote(string text) {
|
|
55
|
+
if(text.Length>0 && text.IndexOfAny(new char[] {' ','\t','\n','\v','"'})<0) return text;
|
|
56
|
+
var result=new StringBuilder().Append('"'); int slashes=0;
|
|
57
|
+
foreach(char c in text) {
|
|
58
|
+
if(c=='\\') { slashes++; continue; }
|
|
59
|
+
if(c=='"') { result.Append('\\',slashes*2+1); result.Append(c); }
|
|
60
|
+
else { result.Append('\\',slashes); result.Append(c); }
|
|
61
|
+
slashes=0;
|
|
62
|
+
}
|
|
63
|
+
result.Append('\\',slashes*2); return result.Append('"').ToString();
|
|
64
|
+
}
|
|
65
|
+
public sealed class Child : IDisposable {
|
|
66
|
+
internal IntPtr handle; public readonly int Pid;
|
|
67
|
+
internal Child(IntPtr process,int pid) { handle=process; Pid=pid; }
|
|
68
|
+
public int Wait() { WaitForSingleObject(handle,0xffffffff); uint code; Check(GetExitCodeProcess(handle,out code)); return (int)code; }
|
|
69
|
+
public void Dispose() { if(handle!=IntPtr.Zero) { CloseHandle(handle); handle=IntPtr.Zero; } }
|
|
70
|
+
}
|
|
71
|
+
public sealed class Lifetime : IDisposable {
|
|
72
|
+
internal IntPtr handle;
|
|
73
|
+
public Lifetime() {
|
|
74
|
+
handle=CreateJobObjectW(IntPtr.Zero,IntPtr.Zero); Check(handle!=IntPtr.Zero);
|
|
75
|
+
var info=new JobInfo(); info.limits.flags=0x2000;
|
|
76
|
+
try { Check(SetInformationJobObject(handle,9,ref info,(uint)Marshal.SizeOf(info))); }
|
|
77
|
+
catch { Dispose(); throw; }
|
|
78
|
+
}
|
|
79
|
+
public void Dispose() { if(handle!=IntPtr.Zero) { CloseHandle(handle); handle=IntPtr.Zero; } }
|
|
80
|
+
}
|
|
81
|
+
public static Child Start(int parentId,string binary,string[] args,bool quiet,Lifetime lifetime) {
|
|
82
|
+
IntPtr parent=IntPtr.Zero,list=IntPtr.Zero,parentValue=IntPtr.Zero,handleList=IntPtr.Zero,nul=IntPtr.Zero;
|
|
83
|
+
var remote=new IntPtr[3]; var info=new ProcessInfo(); bool initialized=false;
|
|
84
|
+
try {
|
|
85
|
+
parent=OpenProcess(0x80|0x40,false,parentId); Check(parent!=IntPtr.Zero);
|
|
86
|
+
var current=GetCurrentProcess();
|
|
87
|
+
if(quiet) { nul=CreateFileW("NUL",0xc0000000,3,IntPtr.Zero,3,0,IntPtr.Zero); Check(nul!=new IntPtr(-1)); }
|
|
88
|
+
// 指定した親へ必要な3 handleだけを一時複製し、child作成直後に親から閉じる。
|
|
89
|
+
for(int i=0;i<3;i++) Check(DuplicateHandle(current,quiet && i<2 ? nul : GetStdHandle(-10-i),parent,out remote[i],0,true,2));
|
|
90
|
+
IntPtr size=IntPtr.Zero; InitializeProcThreadAttributeList(IntPtr.Zero,2,0,ref size);
|
|
91
|
+
list=Marshal.AllocHGlobal(size); Check(InitializeProcThreadAttributeList(list,2,0,ref size)); initialized=true;
|
|
92
|
+
parentValue=Marshal.AllocHGlobal(IntPtr.Size); Marshal.WriteIntPtr(parentValue,parent);
|
|
93
|
+
Check(UpdateProcThreadAttribute(list,0,(IntPtr)0x20000,parentValue,(IntPtr)IntPtr.Size,IntPtr.Zero,IntPtr.Zero));
|
|
94
|
+
handleList=Marshal.AllocHGlobal(3*IntPtr.Size);
|
|
95
|
+
for(int i=0;i<3;i++) Marshal.WriteIntPtr(handleList,i*IntPtr.Size,remote[i]);
|
|
96
|
+
Check(UpdateProcThreadAttribute(list,0,(IntPtr)0x20002,handleList,(IntPtr)(3*IntPtr.Size),IntPtr.Zero,IntPtr.Zero));
|
|
97
|
+
var startup=new StartupInfoEx(); startup.startup.cb=Marshal.SizeOf(startup); startup.attributes=list;
|
|
98
|
+
startup.startup.flags=0x100; startup.startup.input=remote[0]; startup.startup.output=remote[1]; startup.startup.error=remote[2];
|
|
99
|
+
var command=new StringBuilder(Quote(binary)); foreach(var arg in args) command.Append(' ').Append(Quote(arg));
|
|
100
|
+
Check(CreateProcessW(binary,command,IntPtr.Zero,IntPtr.Zero,true,0x80000|0x8000000|4,IntPtr.Zero,Environment.CurrentDirectory,ref startup,out info));
|
|
101
|
+
if(lifetime!=null) Check(AssignProcessToJobObject(lifetime.handle,info.process));
|
|
102
|
+
for(int i=0;i<3;i++) { IntPtr local; Check(DuplicateHandle(parent,remote[i],current,out local,0,false,3)); CloseHandle(local); remote[i]=IntPtr.Zero; }
|
|
103
|
+
Check(ResumeThread(info.thread)!=0xffffffff);
|
|
104
|
+
var child=new Child(info.process,info.pid); info.process=IntPtr.Zero; return child;
|
|
105
|
+
} catch {
|
|
106
|
+
if(info.process!=IntPtr.Zero) TerminateProcess(info.process,1);
|
|
107
|
+
throw;
|
|
108
|
+
} finally {
|
|
109
|
+
if(nul!=IntPtr.Zero && nul!=new IntPtr(-1)) CloseHandle(nul);
|
|
110
|
+
if(info.thread!=IntPtr.Zero) CloseHandle(info.thread); if(info.process!=IntPtr.Zero) CloseHandle(info.process);
|
|
111
|
+
foreach(var handle in remote) if(handle!=IntPtr.Zero) { IntPtr local; if(DuplicateHandle(parent,handle,GetCurrentProcess(),out local,0,false,3)) CloseHandle(local); }
|
|
112
|
+
if(initialized) DeleteProcThreadAttributeList(list);
|
|
113
|
+
if(list!=IntPtr.Zero) Marshal.FreeHGlobal(list); if(parentValue!=IntPtr.Zero) Marshal.FreeHGlobal(parentValue); if(handleList!=IntPtr.Zero) Marshal.FreeHGlobal(handleList);
|
|
114
|
+
if(parent!=IntPtr.Zero) CloseHandle(parent);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
`;
|
|
@@ -1,154 +1,96 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Windowsの起動processと認証付きloopback接続を所有する。JSON-RPC本文は変更しない。
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
// Windowsの認証付きloopback接続と接続情報だけを所有する。起動・stdio中継の契約は共通。
|
|
4
2
|
import { createServer } from "node:net";
|
|
5
|
-
import { createInterface } from "node:readline";
|
|
6
3
|
import { once } from "node:events";
|
|
7
4
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
8
5
|
import { join } from "node:path";
|
|
9
6
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { writeFileSync, rmSync } from "node:fs";
|
|
11
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
7
|
+
import { writeFileSync, rmSync, realpathSync } from "node:fs";
|
|
12
8
|
import WebSocket from "ws";
|
|
13
9
|
import { ensurePrivateDirectory, makeFilePrivate } from "./windows-codex-state.js";
|
|
14
10
|
import { readWindowsProcesses } from "./windows-codex-connection.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (/^(?:--config=|--enable=|--disable=|-c.)/.test(arg)) {
|
|
25
|
-
result.push(arg);
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
if (!server) {
|
|
29
|
-
if (arg !== "app-server")
|
|
30
|
-
return null;
|
|
31
|
-
server = true;
|
|
32
|
-
result.push(arg);
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
if (["proxy", "daemon", "start", "stop", "status", "generate-ts", "generate-json-schema", "--help", "-h", "--version", "-V"].includes(arg))
|
|
36
|
-
return null;
|
|
37
|
-
if (arg === "--stdio" || arg === "--listen=stdio://")
|
|
38
|
-
continue;
|
|
39
|
-
if (arg === "--listen") {
|
|
40
|
-
if (args[++i] !== "stdio://")
|
|
41
|
-
throw new Error("stdio以外の接続指定は変更できません");
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (arg.startsWith("--listen=") || arg.startsWith("--ws-"))
|
|
45
|
-
throw new Error("既存の接続・認証指定は変更できません");
|
|
46
|
-
result.push(arg);
|
|
47
|
-
}
|
|
48
|
-
return server ? result : null;
|
|
49
|
-
}
|
|
50
|
-
export async function runWindowsCodexRelay(binary, root, args) {
|
|
51
|
-
const serverArgs = windowsServerArguments(args);
|
|
52
|
-
if (!serverArgs) {
|
|
53
|
-
const child = spawn(binary, args, { stdio: "inherit", windowsHide: true });
|
|
54
|
-
return (await once(child, "close"))[0] ?? 1;
|
|
55
|
-
}
|
|
11
|
+
import { codexServerArguments } from "./codex-relay-arguments.js";
|
|
12
|
+
import { relayCodexStdio } from "./codex-relay-stdio.js";
|
|
13
|
+
export { codexServerArguments as windowsServerArguments } from "./codex-relay-arguments.js";
|
|
14
|
+
export async function prepareWindowsRelay(root, args) {
|
|
15
|
+
const serverArgs = codexServerArguments(args);
|
|
16
|
+
if (!serverArgs)
|
|
17
|
+
return { directory: null, endpoint: null, arguments: args };
|
|
18
|
+
// MSIXの仮想AppData名はnative子から見えないため、起動済みの自身の実体を渡す。
|
|
19
|
+
const relay = realpathSync.native(fileURLToPath(import.meta.url));
|
|
56
20
|
const directory = join(root, randomUUID());
|
|
57
21
|
ensurePrivateDirectory(directory);
|
|
58
|
-
const file = join(directory, "connection.json");
|
|
59
|
-
const tokenFile = join(directory, "token");
|
|
60
|
-
const token = randomBytes(32).toString("hex");
|
|
61
|
-
writeFileSync(tokenFile, token, { flag: "wx" });
|
|
62
|
-
makeFilePrivate(tokenFile);
|
|
63
|
-
// OSが割り当てた空きportを使い、取得競合は公式serverの起動失敗として明示する。
|
|
64
|
-
const reservation = createServer();
|
|
65
|
-
reservation.listen(0, "127.0.0.1");
|
|
66
|
-
await once(reservation, "listening");
|
|
67
|
-
const port = reservation.address().port;
|
|
68
|
-
await new Promise((resolve, reject) => reservation.close(error => error ? reject(error) : resolve()));
|
|
69
|
-
const endpoint = `ws://127.0.0.1:${port}`;
|
|
70
|
-
const child = spawn(binary, [...serverArgs, "--listen", endpoint, "--ws-auth", "capability-token", "--ws-token-file", tokenFile], {
|
|
71
|
-
stdio: ["ignore", "ignore", "pipe"], windowsHide: true,
|
|
72
|
-
});
|
|
73
|
-
const exited = new Promise(resolve => child.once("close", () => resolve()));
|
|
74
|
-
let failed;
|
|
75
|
-
child.on("error", error => { failed = error; });
|
|
76
|
-
// WebSocketの定型起動案内だけを除き、公式CLIの診断はDesktopへそのまま返す。
|
|
77
|
-
const diagnostics = createInterface({ input: child.stderr });
|
|
78
|
-
diagnostics.on("line", line => {
|
|
79
|
-
if (!/^(?:\s*|codex app-server \(WebSockets\)| {2}(?:listening on:|readyz:|healthz:|note:).*)$/.test(line))
|
|
80
|
-
process.stderr.write(line + "\n");
|
|
81
|
-
});
|
|
82
|
-
let socket;
|
|
83
|
-
const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
84
|
-
const lines = input[Symbol.asyncIterator]();
|
|
85
|
-
let ending = false;
|
|
86
|
-
const stop = () => { ending = true; child.kill(); socket?.terminate(); input.close(); process.stdin.destroy(); };
|
|
87
|
-
input.once("close", stop);
|
|
88
|
-
process.stdout.once("error", stop);
|
|
89
22
|
try {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
23
|
+
const tokenFile = join(directory, "token");
|
|
24
|
+
writeFileSync(tokenFile, randomBytes(32).toString("hex"), { flag: "wx" });
|
|
25
|
+
makeFilePrivate(tokenFile);
|
|
26
|
+
// port取得後の競合は公式serverの起動失敗として明示する。
|
|
27
|
+
const reservation = createServer();
|
|
28
|
+
reservation.listen(0, "127.0.0.1");
|
|
29
|
+
await once(reservation, "listening");
|
|
30
|
+
const port = reservation.address().port;
|
|
31
|
+
await new Promise((resolve, reject) => reservation.close(error => error ? reject(error) : resolve()));
|
|
32
|
+
const endpoint = "ws://127.0.0.1:" + port;
|
|
33
|
+
return { directory, endpoint, relay, arguments: [...serverArgs, "--listen", endpoint, "--ws-auth", "capability-token", "--ws-token-file", tokenFile] };
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
rmSync(directory, { recursive: true, force: true });
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function serveWindowsRelay(binary, directory, endpoint, serverPid) {
|
|
41
|
+
if (process.ppid !== serverPid)
|
|
42
|
+
throw new Error("公式Codexの子として中継を起動してください");
|
|
43
|
+
const { readFileSync } = await import("node:fs");
|
|
44
|
+
const token = readFileSync(join(directory, "token"), "utf8");
|
|
45
|
+
try {
|
|
46
|
+
await relayCodexStdio({
|
|
47
|
+
alive: () => { try {
|
|
48
|
+
process.kill(serverPid, 0);
|
|
49
|
+
return true;
|
|
98
50
|
}
|
|
99
51
|
catch (error) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
52
|
+
if (error.code === "ESRCH")
|
|
53
|
+
return false;
|
|
54
|
+
throw error;
|
|
55
|
+
} },
|
|
56
|
+
socket: () => new WebSocket(endpoint, { headers: { Authorization: "Bearer " + token }, handshakeTimeout: 1_000, perMessageDeflate: false }),
|
|
57
|
+
stop: async () => {
|
|
58
|
+
// launcherが保持するprocess handleにより、このPIDは終了確認まで再利用されない。
|
|
59
|
+
try {
|
|
60
|
+
process.kill(serverPid);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.code !== "ESRCH")
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
connected: () => {
|
|
68
|
+
const server = readWindowsProcesses().find(row => row.pid === serverPid);
|
|
69
|
+
if (!server || server.executable.toLowerCase() !== binary.toLowerCase())
|
|
70
|
+
throw new Error("公式Codexの起動を確認できません");
|
|
71
|
+
const record = { schema: "aiterm.windows-relay.v1", serverPid, serverStarted: server.started, binary, endpoint };
|
|
72
|
+
const file = join(directory, "connection.json");
|
|
73
|
+
writeFileSync(file, JSON.stringify(record), { flag: "wx" });
|
|
74
|
+
makeFilePrivate(file);
|
|
75
|
+
},
|
|
122
76
|
});
|
|
123
|
-
process.stdout.on("drain", () => connected.resume());
|
|
124
|
-
connected.on("error", () => { failed = new Error("公式Codexとの接続に失敗しました"); stop(); });
|
|
125
|
-
connected.on("close", () => { if (!ending) {
|
|
126
|
-
failed = new Error("公式Codexとの接続が終了しました");
|
|
127
|
-
stop();
|
|
128
|
-
} });
|
|
129
|
-
// initializeが接続準備前に届いても失わないよう、iteratorを接続前から保持する。
|
|
130
|
-
for await (const line of lines)
|
|
131
|
-
await new Promise((resolve, reject) => connected.send(line, error => error ? reject(error) : resolve()));
|
|
132
|
-
if (failed)
|
|
133
|
-
throw failed;
|
|
134
|
-
return 0;
|
|
135
77
|
}
|
|
136
78
|
finally {
|
|
137
|
-
stop();
|
|
138
|
-
await exited;
|
|
139
|
-
diagnostics.close();
|
|
140
79
|
rmSync(directory, { recursive: true, force: true });
|
|
141
80
|
}
|
|
142
81
|
}
|
|
143
82
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
144
|
-
const [
|
|
83
|
+
const [mode, ...args] = process.argv.slice(2);
|
|
145
84
|
try {
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
85
|
+
if (mode === "--prepare" && args[0])
|
|
86
|
+
process.stdout.write(JSON.stringify(await prepareWindowsRelay(args[0], args.slice(1))));
|
|
87
|
+
else if (mode === "--serve" && args.length === 4 && /^[1-9][0-9]*$/.test(args[3]))
|
|
88
|
+
await serveWindowsRelay(args[0], args[1], args[2], Number(args[3]));
|
|
89
|
+
else
|
|
90
|
+
throw new Error("Windows中継の起動指定が不正です");
|
|
149
91
|
}
|
|
150
92
|
catch (error) {
|
|
151
|
-
process.stderr.write(
|
|
93
|
+
process.stderr.write("aiterm-mcp: " + (error instanceof Error ? error.message : "Windows中継が失敗しました") + "\n");
|
|
152
94
|
process.exitCode = 1;
|
|
153
95
|
}
|
|
154
96
|
}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
// gpt-connectorのWindows実装をMIT条件で移植。実行時の外部製品依存は持たない。
|
|
2
2
|
// Windows Desktop用の実行file、ユーザー環境変数、親processの接続確認を所有する。
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync, renameSync, readdirSync, rmSync } from "node:fs";
|
|
4
|
-
import { join
|
|
4
|
+
import { join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { randomUUID, createHash } from "node:crypto";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
|
-
import { relayConfigDirectory
|
|
9
|
+
import { relayConfigDirectory } from "./codex-relay-config.js";
|
|
10
10
|
import { SetupError } from "./setup-platform.js";
|
|
11
11
|
import { verifyRelayLauncher } from "./setup-codex-relay.js";
|
|
12
12
|
import { withCodexRelay } from "./codex-relay-client.js";
|
|
13
13
|
import { windowsPowerShellSync, quotePowerShell } from "./windows-codex-state.js";
|
|
14
14
|
import { ensurePrivateDirectory, makeFilePrivate } from "./windows-codex-state.js";
|
|
15
|
-
import {
|
|
15
|
+
import { windowsDesktopRelay } from "./windows-codex-connection.js";
|
|
16
|
+
import { configureRelay } from "./codex-relay-setup.js";
|
|
17
|
+
import { windowsLauncherSource } from "./windows-codex-launcher.js";
|
|
16
18
|
export function findWindowsCodexCache(resources, cache) {
|
|
17
19
|
// Desktopが展開した4実行fileを配布元と照合する。展開・更新はDesktop自身が所有する。
|
|
18
20
|
const names = ["codex.exe", "codex-code-mode-host.exe", "codex-windows-sandbox-setup.exe", "codex-command-runner.exe"];
|
|
@@ -60,48 +62,7 @@ $result = [UIntPtr]::Zero
|
|
|
60
62
|
[void][AitermEnvironment]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [UIntPtr]::Zero, 'Environment', 2, 5000, [ref]$result)
|
|
61
63
|
`, 10_000);
|
|
62
64
|
}
|
|
63
|
-
export
|
|
64
|
-
return `using System;
|
|
65
|
-
using System.Diagnostics;
|
|
66
|
-
using System.Text;
|
|
67
|
-
public static class AitermLauncher {
|
|
68
|
-
static async System.Threading.Tasks.Task Input(Process child) {
|
|
69
|
-
var input = Console.OpenStandardInput(); var output = child.StandardInput.BaseStream;
|
|
70
|
-
var buffer = new byte[8192]; int count;
|
|
71
|
-
while ((count = await input.ReadAsync(buffer, 0, buffer.Length)) > 0) {
|
|
72
|
-
await output.WriteAsync(buffer, 0, count); await output.FlushAsync();
|
|
73
|
-
}
|
|
74
|
-
child.StandardInput.Close();
|
|
75
|
-
}
|
|
76
|
-
static string Quote(string text) {
|
|
77
|
-
var result = new StringBuilder().Append('"'); int slashes = 0;
|
|
78
|
-
foreach (char c in text) {
|
|
79
|
-
if (c == '\\\\') { slashes++; continue; }
|
|
80
|
-
if (c == '"') { result.Append('\\\\', slashes * 2 + 1); result.Append(c); }
|
|
81
|
-
else { result.Append('\\\\', slashes); result.Append(c); }
|
|
82
|
-
slashes = 0;
|
|
83
|
-
}
|
|
84
|
-
result.Append('\\\\', slashes * 2); return result.Append('"').ToString();
|
|
85
|
-
}
|
|
86
|
-
public static int Main(string[] args) {
|
|
87
|
-
try {
|
|
88
|
-
var arguments = new StringBuilder();
|
|
89
|
-
foreach (var value in new string[] { ${[relay, binary, root].map(value => JSON.stringify(value)).join(", ")} }) arguments.Append(Quote(value)).Append(' ');
|
|
90
|
-
foreach (var value in args) arguments.Append(Quote(value)).Append(' ');
|
|
91
|
-
var start = new ProcessStartInfo(${JSON.stringify(node)}, arguments.ToString());
|
|
92
|
-
start.UseShellExecute = false; start.CreateNoWindow = true;
|
|
93
|
-
start.RedirectStandardInput = true; start.RedirectStandardOutput = true; start.RedirectStandardError = true;
|
|
94
|
-
using (var child = Process.Start(start)) {
|
|
95
|
-
Input(child).ContinueWith(task => { if (task.IsFaulted && !child.HasExited) child.Kill(); });
|
|
96
|
-
var output = child.StandardOutput.BaseStream.CopyToAsync(Console.OpenStandardOutput());
|
|
97
|
-
var error = child.StandardError.BaseStream.CopyToAsync(Console.OpenStandardError());
|
|
98
|
-
child.WaitForExit(); System.Threading.Tasks.Task.WaitAll(output, error); return child.ExitCode;
|
|
99
|
-
}
|
|
100
|
-
} catch { Console.Error.WriteLine("aiterm-mcp: Windows中継を起動できません"); return 1; }
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
`;
|
|
104
|
-
}
|
|
65
|
+
export { windowsLauncherSource } from "./windows-codex-launcher.js";
|
|
105
66
|
export function buildWindowsLauncher(directory, source) {
|
|
106
67
|
const digest = createHash("sha256").update(source).digest("hex").slice(0, 16);
|
|
107
68
|
const launcher = join(directory, `codex-${digest}.exe`);
|
|
@@ -113,80 +74,38 @@ export function buildWindowsLauncher(directory, source) {
|
|
|
113
74
|
throw new SetupError("codex_launcher_compiler_unavailable", "Windows標準.NET Frameworkのコンパイラが見つかりません。");
|
|
114
75
|
writeFileSync(sourceFile, source);
|
|
115
76
|
makeFilePrivate(sourceFile);
|
|
116
|
-
const result = spawnSync(compiler, ["/nologo", "/target:exe", `/out:${launcher}`, sourceFile], { encoding: "utf8", windowsHide: true, timeout: 20_000 });
|
|
77
|
+
const result = spawnSync(compiler, ["/nologo", "/target:exe", "/reference:System.Runtime.Serialization.dll", `/out:${launcher}`, sourceFile], { encoding: "utf8", windowsHide: true, timeout: 20_000 });
|
|
117
78
|
if (result.status !== 0)
|
|
118
79
|
throw new SetupError("codex_launcher_build_failed", "Windows用のCodex起動fileを作成できません。");
|
|
119
80
|
makeFilePrivate(launcher);
|
|
120
81
|
return launcher;
|
|
121
82
|
}
|
|
122
83
|
async function live(config) {
|
|
123
|
-
const file = windowsDesktopRelay(config.launcher);
|
|
84
|
+
const file = windowsDesktopRelay(config.launcher, undefined, config.socket_root);
|
|
124
85
|
if (!file)
|
|
125
86
|
return false;
|
|
126
87
|
await withCodexRelay(file, request => request("thread/loaded/list", { limit: 1 }));
|
|
127
88
|
return true;
|
|
128
89
|
}
|
|
90
|
+
export function windowsCodexRuntime(overrides = {}) {
|
|
91
|
+
const directory = overrides.directory ?? relayConfigDirectory();
|
|
92
|
+
const runtime = {
|
|
93
|
+
directory, socket_root: join(directory, "sessions"), node: process.execPath,
|
|
94
|
+
relay: fileURLToPath(new URL("./windows-codex-relay.js", import.meta.url)),
|
|
95
|
+
findBinary: findWindowsCodexBinary, getGui, setGui, verify: verifyRelayLauncher, live,
|
|
96
|
+
// User環境変数はsetGuiの一操作で永続化される。
|
|
97
|
+
persist: () => { }, unpersist: () => { }, prepare: ensurePrivateDirectory, save,
|
|
98
|
+
build: async (binary) => {
|
|
99
|
+
ensurePrivateDirectory(runtime.socket_root);
|
|
100
|
+
const launcher = buildWindowsLauncher(runtime.directory, windowsLauncherSource(runtime.node, runtime.relay, binary, runtime.socket_root));
|
|
101
|
+
await runtime.verify(launcher);
|
|
102
|
+
return launcher;
|
|
103
|
+
}, ...overrides,
|
|
104
|
+
};
|
|
105
|
+
return runtime;
|
|
106
|
+
}
|
|
129
107
|
export async function configureWindowsCodexSteer(action, overrides = {}) {
|
|
130
|
-
|
|
131
|
-
relay: fileURLToPath(new URL("./windows-codex-relay.js", import.meta.url)), findBinary: findWindowsCodexBinary, getGui, setGui,
|
|
132
|
-
verify: verifyRelayLauncher, live,
|
|
133
|
-
shared: async (launcher) => {
|
|
134
|
-
const file = windowsDesktopRelay(launcher);
|
|
135
|
-
if (!file)
|
|
136
|
-
return null;
|
|
137
|
-
await withCodexRelay(file, request => request("thread/loaded/list", { limit: 1 }));
|
|
138
|
-
return { binary: readWindowsRelay(file).binary, root: dirname(dirname(file)) };
|
|
139
|
-
}, ...overrides };
|
|
140
|
-
const previous = readRelayConfig(runtime.directory);
|
|
141
|
-
const current = runtime.getGui("CODEX_CLI_PATH");
|
|
142
|
-
if (action === "status") {
|
|
143
|
-
if (!previous?.enabled)
|
|
144
|
-
return { status: "disabled" };
|
|
145
|
-
if (current !== previous.launcher)
|
|
146
|
-
return { status: "failed", reason_code: "codex_steer_configuration_changed" };
|
|
147
|
-
return await runtime.live(previous) ? { status: "ready" } : { status: "restart_required", reason_code: "codex_restart_required" };
|
|
148
|
-
}
|
|
149
|
-
if (action === "disable") {
|
|
150
|
-
if (!previous?.enabled)
|
|
151
|
-
return { status: "disabled" };
|
|
152
|
-
if (current !== previous.launcher)
|
|
153
|
-
throw new SetupError("codex_steer_configuration_changed", "変更されたCodex起動設定は上書きしません。");
|
|
154
|
-
if (previous.launcher !== previous.previous_cli_path)
|
|
155
|
-
runtime.setGui("CODEX_CLI_PATH", previous.previous_cli_path);
|
|
156
|
-
if (runtime.getGui("CODEX_CLI_PATH") !== previous.previous_cli_path)
|
|
157
|
-
throw new SetupError("codex_steer_readback_failed", "元の起動設定を確認できません。");
|
|
158
|
-
save(runtime.directory, { ...previous, enabled: false });
|
|
159
|
-
return { status: "restart_required", reason_code: "codex_restart_required" };
|
|
160
|
-
}
|
|
161
|
-
if (previous?.enabled && previous.launcher !== previous.previous_cli_path && ![previous.launcher, previous.previous_cli_path].includes(current)) {
|
|
162
|
-
throw new SetupError("codex_steer_configuration_changed", "変更されたCodex起動設定は上書きしません。");
|
|
163
|
-
}
|
|
164
|
-
if (current && (current !== previous?.launcher || previous.launcher === previous.previous_cli_path)) {
|
|
165
|
-
const shared = await runtime.shared(current);
|
|
166
|
-
if (!shared)
|
|
167
|
-
throw new SetupError("codex_steer_configuration_conflict", "別のCodex起動設定との互換接続を確認できないため変更していません。");
|
|
168
|
-
ensurePrivateDirectory(runtime.directory);
|
|
169
|
-
save(runtime.directory, { schema: "aiterm.codex-relay.v1", enabled: true, binary: shared.binary,
|
|
170
|
-
node: runtime.node, launcher: current, socket_root: shared.root, previous_cli_path: current });
|
|
171
|
-
return { status: "ready" };
|
|
172
|
-
}
|
|
173
|
-
for (const key of ["CODEX_APP_SERVER_WS_URL", "CODEX_APP_SERVER_USE_LOCAL_DAEMON", "CODEX_APP_SERVER_FORCE_CLI"]) {
|
|
174
|
-
if (runtime.getGui(key))
|
|
175
|
-
throw new SetupError("codex_steer_configuration_conflict", `${key}が設定されているため変更していません。`);
|
|
176
|
-
}
|
|
177
|
-
ensurePrivateDirectory(runtime.directory);
|
|
178
|
-
const binary = runtime.findBinary();
|
|
179
|
-
const root = join(runtime.directory, "sessions");
|
|
180
|
-
ensurePrivateDirectory(root);
|
|
181
|
-
const launcher = buildWindowsLauncher(runtime.directory, windowsLauncherSource(runtime.node, runtime.relay, binary, root));
|
|
182
|
-
await runtime.verify(launcher);
|
|
183
|
-
const config = { schema: "aiterm.codex-relay.v1", enabled: true, binary, node: runtime.node, launcher,
|
|
184
|
-
socket_root: root, previous_cli_path: previous?.enabled && previous.launcher !== previous.previous_cli_path ? previous.previous_cli_path : current };
|
|
185
|
-
save(runtime.directory, config);
|
|
186
|
-
runtime.setGui("CODEX_CLI_PATH", launcher);
|
|
187
|
-
if (runtime.getGui("CODEX_CLI_PATH") !== launcher)
|
|
188
|
-
throw new SetupError("codex_steer_readback_failed", "WindowsのCodex起動設定を確認できません。");
|
|
189
|
-
return await runtime.live(config) ? { status: "ready" } : { status: "restart_required", reason_code: "codex_restart_required" };
|
|
108
|
+
return configureRelay(action, windowsCodexRuntime(overrides));
|
|
190
109
|
}
|
|
191
110
|
function save(directory, config) {
|
|
192
111
|
const file = join(directory, `${randomUUID()}.tmp`);
|
package/docs/DESIGN.md
CHANGED
|
@@ -68,7 +68,7 @@ Cursorのsubmitはadapterがextended keyboard protocolのEnterへ変換し、呼
|
|
|
68
68
|
宛先はCodexのMCP handshakeと各要求の`_meta.threadId`から取得する。modelが指定したIDや環境変数で代用しない。
|
|
69
69
|
単品導入の`src/codex-parent-receiver.ts`は同じ`CODEX_HOME`の公式app-serverへstdioで接続し、
|
|
70
70
|
`thread/read`と`thread/queue/list`で宛先を確認してから`thread/queue/add`へ本文をJSONで渡す。
|
|
71
|
-
Steerを選択したmacOS Desktopでは、MCP processの祖先にある公式App Server
|
|
71
|
+
Steerを選択したmacOS・Windows Desktopでは、MCP processの祖先にある公式App Serverの本人専用接続へ接続する。
|
|
72
72
|
`thread/loaded/list`と`thread/read`で同じ親を確認し、公式`turn/start`へ一度だけ送る。
|
|
73
73
|
公式の`start_or_steer_turn`が実行中なら同じturnへ追加入力し、終了後なら同じtaskの次turnを開始する。
|
|
74
74
|
状態を読んでから送信方法を選ぶraceや再送は作らない。model・権限のoverrideも渡さない。
|
|
@@ -84,17 +84,23 @@ macOSのユーザーLaunchAgentはログイン時に同じ起動設定を適用
|
|
|
84
84
|
保存した元の`CODEX_CLI_PATH`へ戻す。他から変更された設定は上書きしない。
|
|
85
85
|
`status=ready`は公式binary・Desktopの直接子・同じsocketのRPC応答を確認した場合だけ返す。
|
|
86
86
|
初回の設定保存後は`restart_required`であり、Desktopの完全終了・再起動を要する。
|
|
87
|
-
|
|
88
|
-
Windows標準の.NET Frameworkでlauncher
|
|
87
|
+
引数の解釈、JSONL中継、設定の有効化・競合・復元は両OSで共通の処理を使う。
|
|
88
|
+
Windows標準の.NET Frameworkでlauncherを作り、`CreateProcessW`の親process属性と継承handle一覧で、
|
|
89
|
+
公式CLIをDesktopの直接の子、中継Nodeを公式CLIの直接の子として起動する。
|
|
90
|
+
launcherは終了監視だけを担い、JSON-RPCを通さない。Windowsには同一PIDでのexecがないためPIDは変わるが、
|
|
91
|
+
公式binary、起動元との直接の親子関係、通常の環境とstdioは維持する。
|
|
92
|
+
MSIXの仮想AppDataはnative子へ同じ論理パスを渡しても見えない場合があるため、準備processが
|
|
93
|
+
自身の`realpathSync.native`を起動計画へ返し、後続の中継Nodeはその実体を起動する。
|
|
94
|
+
解決は起動時に行い、設定時の保存先で固定しない。
|
|
89
95
|
公式Desktopが展開した4実行ファイルを配布元のSHA-256と照合し、AitermがCodexをコピー・再配布しない。
|
|
90
96
|
公式CLIの認証付きloopback WebSocketを使い、本人専用ACLのtokenと接続記録をAitermのsessions下へ置く。
|
|
91
97
|
Windowsのprocess情報はprocess-runtimeが所有し、接続はPID・開始時刻・実行ファイル・token引数で照合する。
|
|
92
98
|
MCP processの祖先から親を選び、同じloaded taskへ送る。開始時刻は小数桁の表記差を正規化して比較する。
|
|
93
|
-
stdio EOFで自分が起動した公式serverを停止し、接続記録を削除する。
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
stdio EOFで自分が起動した公式serverを停止し、接続記録を削除する。native launcherはJob Objectで
|
|
100
|
+
所有するprocessだけを終了し、公式serverの終了確認後に戻る。
|
|
101
|
+
両OSとも有効化前に元の起動設定を保存し、Aitermのlauncherを設定する。解除は元の値へ戻す。
|
|
102
|
+
旧Windows版で既存launcherを共有した設定は読取り可能だが、次のenableでAitermのlauncherへ移行する。
|
|
103
|
+
起動・配送・設定・診断は他製品のコード、設定、コマンドへ依存しない。
|
|
98
104
|
LinuxのSteer選択はunsupportedとする。通常の単品導入・queue配送は全対応OSで維持する。
|
|
99
105
|
|
|
100
106
|
単品導入のqueue配送は親の実行中turnを中断せず、親がidleになった後に処理される。
|