mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 06:32:59 +00:00
mcp: handle spawn errors and a stopped daemon
This commit is contained in:
1 parent
d921b066f2
commit
9516414097
3 files changed
+39
-7
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import {spawn} from "child_process"
|
||||
import {ChildProcess, spawn} from "child_process"
|
||||
import {closeSync} from "fs"
|
||||
import {join} from "path"
|
||||
import {Readable} from "stream"
|
||||
@@ -32,7 +32,7 @@ export class DaemonClient {
|
||||
async ensureDaemon(version: string, spawnIt: SpawnDaemon = root => spawnDaemon(root)): Promise<StatusReply> {
|
||||
let status = await this.status()
|
||||
if (status !== undefined && compareVersions(status.version, version) < 0) {
|
||||
await this.request("POST", "/stop")
|
||||
await this.stopDaemon()
|
||||
status = undefined
|
||||
}
|
||||
if (status === undefined) {
|
||||
@@ -43,6 +43,14 @@ export class DaemonClient {
|
||||
return status
|
||||
}
|
||||
|
||||
private async stopDaemon(): Promise<void> {
|
||||
try {
|
||||
await this.request("POST", "/stop")
|
||||
} catch (error) {
|
||||
if (!isNotRunning(error)) throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async status(): Promise<StatusReply | undefined> {
|
||||
try {
|
||||
return await this.request<StatusReply>("GET", "/status")
|
||||
@@ -68,7 +76,7 @@ export async function spawnDaemon(root: string, [command, args]: [string, string
|
||||
})
|
||||
const channel = child.stdio[readyFd] as Readable
|
||||
try {
|
||||
const line = await firstLine(channel)
|
||||
const line = await firstLine(channel, child)
|
||||
if (line === "ready" || line === "running") return
|
||||
throw new ExitError(line.startsWith(errorPrefix) ? line.slice(errorPrefix.length) : `The SimpleX daemon exited; see ${logFile(root)}`, 1)
|
||||
} finally {
|
||||
@@ -80,13 +88,14 @@ export async function spawnDaemon(root: string, [command, args]: [string, string
|
||||
}
|
||||
}
|
||||
|
||||
function firstLine(stream: Readable): Promise<string> {
|
||||
function firstLine(stream: Readable, child: ChildProcess): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let text = ""
|
||||
const finish = (settle: () => void): void => {
|
||||
clearTimeout(timer)
|
||||
stream.off("data", onData)
|
||||
stream.off("end", onEnd)
|
||||
child.off("error", onSpawnError)
|
||||
settle()
|
||||
}
|
||||
const onData = (chunk: string): void => {
|
||||
@@ -95,11 +104,13 @@ function firstLine(stream: Readable): Promise<string> {
|
||||
if (i >= 0) finish(() => resolve(text.slice(0, i)))
|
||||
}
|
||||
const onEnd = (): void => finish(() => resolve(text.trim()))
|
||||
const onSpawnError = (error: Error): void => finish(() => reject(new ExitError(error.message, 1)))
|
||||
const timer = setTimeout(() => finish(() => reject(new ExitError("The SimpleX daemon did not start in 5 minutes.", 1))), daemonStartTimeout)
|
||||
stream.setEncoding("utf8")
|
||||
stream.on("data", onData)
|
||||
stream.on("end", onEnd)
|
||||
stream.on("error", onEnd)
|
||||
child.once("error", onSpawnError)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {createHash, randomBytes} from "crypto"
|
||||
import {chmodSync, existsSync, fchmodSync, mkdirSync, linkSync, openSync, readFileSync, readdirSync, statSync, truncateSync, unlinkSync, writeFileSync} from "fs"
|
||||
import {chmodSync, existsSync, fchmodSync, mkdirSync, linkSync, openSync, readFileSync, readdirSync, rmSync, statSync, truncateSync, unlinkSync, writeFileSync} from "fs"
|
||||
import {homedir, tmpdir} from "os"
|
||||
import {join, resolve} from "path"
|
||||
import {ExitError} from "./log"
|
||||
@@ -99,8 +99,8 @@ export function serverToken(dir: string): string {
|
||||
const file = join(dir, "token")
|
||||
const token = randomBytes(32).toString("base64url")
|
||||
const temp = join(dir, `token.${process.pid}.${randomBytes(6).toString("hex")}.tmp`)
|
||||
writePrivateFile(temp, `${token}\n`)
|
||||
try {
|
||||
writePrivateFile(temp, `${token}\n`)
|
||||
linkSync(temp, file)
|
||||
return token
|
||||
} catch (error) {
|
||||
@@ -108,7 +108,7 @@ export function serverToken(dir: string): string {
|
||||
if (process.platform !== "win32") chmodSync(file, 0o600)
|
||||
return readFileSync(file, "utf8").trim()
|
||||
} finally {
|
||||
unlinkSync(temp)
|
||||
rmSync(temp, {force: true})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,11 @@ describe("spawning the daemon", () => {
|
||||
expect(readFileSync(logFile(root), "utf8")).toContain("boom")
|
||||
})
|
||||
|
||||
it("reports a command that cannot start", async () => {
|
||||
const missing = join(root, "no-such-command")
|
||||
await expect(spawnDaemon(root, [missing, []])).rejects.toEqual(new ExitError(`spawn ${missing} ENOENT`, 1))
|
||||
})
|
||||
|
||||
it("gives the daemon the data root", async () => {
|
||||
await spawnDaemon(root, script(`require("fs").writeFileSync(process.env.SIMPLEX_MCP_HOME + "/seen", process.env.SIMPLEX_MCP_HOME); require("fs").writeSync(3, "ready\\n")`))
|
||||
expect(readFileSync(join(root, "seen"), "utf8")).toBe(root)
|
||||
@@ -75,6 +80,22 @@ describe("ensureDaemon", () => {
|
||||
await closeServer(server)
|
||||
})
|
||||
|
||||
it("tolerates an older daemon that another client already stopped", async () => {
|
||||
const client = DaemonClient.forRoot(root)
|
||||
let server = await listen(socketPath(root), serverToken(root), {
|
||||
"GET /status": async (_b, res) => {
|
||||
sendJson(res, 200, status("0.1.9"))
|
||||
setImmediate(() => closeServer(server))
|
||||
},
|
||||
})
|
||||
const spawned = jest.fn(async () => {
|
||||
server = await listen(socketPath(root), serverToken(root), {"GET /status": async (_b, res) => sendJson(res, 200, status("0.2.0"))})
|
||||
})
|
||||
expect(await client.ensureDaemon("0.2.0", spawned)).toMatchObject({version: "0.2.0"})
|
||||
expect(spawned).toHaveBeenCalledTimes(1)
|
||||
await closeServer(server)
|
||||
})
|
||||
|
||||
it("compares versions numerically", () => {
|
||||
expect(compareVersions("0.2.0", "0.10.0")).toBeLessThan(0)
|
||||
expect(compareVersions("1.0.0", "0.9.9")).toBeGreaterThan(0)
|
||||
|
||||
Reference in new issue
Block a user