diff --git a/dist/index.js b/dist/index.js index 1518b8a..75e73cb 100644 --- a/dist/index.js +++ b/dist/index.js @@ -31409,20 +31409,42 @@ var DockerComposeService = class { composeFiles: optionsInputs.composeFiles, commandArgs: services }); - return await new Promise((resolve2, reject) => { + return new Promise((resolve2) => { + let settled = false; const childProcess = spawn2("docker", commandArgs, { cwd: optionsInputs.cwd }); - childProcess.on("error", reject); + childProcess.on("error", (error2) => { + if (settled) { + return; + } + settled = true; + resolve2({ + error: `Unable to collect docker compose logs: ${error2.message}`, + output: "" + }); + }); + if (!childProcess.stdout || !childProcess.stderr) { + settled = true; + resolve2({ + error: "Unable to collect docker compose logs: stdout/stderr unavailable", + output: "" + }); + return; + } childProcess.stdout.on("data", (chunk) => { optionsInputs.serviceLogger(chunk.toString()); }); childProcess.stderr.on("data", (chunk) => { optionsInputs.serviceLogger(chunk.toString()); }); - childProcess.on("close", (exitCode) => { + childProcess.on("close", (exitCode, signal) => { + if (settled) { + return; + } + settled = true; resolve2({ - error: exitCode && exitCode !== 0 ? `Docker Compose logs command failed with exit code ${exitCode}` : "", + error: signal ? `Docker Compose logs command failed with signal ${signal}` : exitCode !== null && exitCode !== 0 ? `Docker Compose logs command failed with exit code ${exitCode}` : "", output: "" }); }); @@ -31446,6 +31468,9 @@ var DockerComposeService = class { } }; } + /** + * Builds docker CLI arguments in the order expected by `docker compose`. + */ getDockerComposeCommandArgs(command, { dockerFlags, composeFlags, diff --git a/dist/post.js b/dist/post.js index b7e9341..a07d167 100644 --- a/dist/post.js +++ b/dist/post.js @@ -27146,20 +27146,42 @@ var DockerComposeService = class { composeFiles: optionsInputs.composeFiles, commandArgs: services }); - return await new Promise((resolve, reject) => { + return new Promise((resolve) => { + let settled = false; const childProcess = spawn("docker", commandArgs, { cwd: optionsInputs.cwd }); - childProcess.on("error", reject); + childProcess.on("error", (error2) => { + if (settled) { + return; + } + settled = true; + resolve({ + error: `Unable to collect docker compose logs: ${error2.message}`, + output: "" + }); + }); + if (!childProcess.stdout || !childProcess.stderr) { + settled = true; + resolve({ + error: "Unable to collect docker compose logs: stdout/stderr unavailable", + output: "" + }); + return; + } childProcess.stdout.on("data", (chunk) => { optionsInputs.serviceLogger(chunk.toString()); }); childProcess.stderr.on("data", (chunk) => { optionsInputs.serviceLogger(chunk.toString()); }); - childProcess.on("close", (exitCode) => { + childProcess.on("close", (exitCode, signal) => { + if (settled) { + return; + } + settled = true; resolve({ - error: exitCode && exitCode !== 0 ? `Docker Compose logs command failed with exit code ${exitCode}` : "", + error: signal ? `Docker Compose logs command failed with signal ${signal}` : exitCode !== null && exitCode !== 0 ? `Docker Compose logs command failed with exit code ${exitCode}` : "", output: "" }); }); @@ -27183,6 +27205,9 @@ var DockerComposeService = class { } }; } + /** + * Builds docker CLI arguments in the order expected by `docker compose`. + */ getDockerComposeCommandArgs(command, { dockerFlags, composeFlags, diff --git a/src/services/docker-compose.service.test.ts b/src/services/docker-compose.service.test.ts index ecc82d5..f76b163 100644 --- a/src/services/docker-compose.service.test.ts +++ b/src/services/docker-compose.service.test.ts @@ -440,5 +440,85 @@ describe("DockerComposeService", () => { output: "", }); }); + + it("should return a non-fatal error message when logs command is terminated by a signal", async () => { + const logsInputs = { + dockerFlags: [] as string[], + composeFiles: ["docker-compose.yml"] as string[], + services: [] as string[], + composeFlags: [] as string[], + cwd: "/current/working/dir", + serviceLogger: vi.fn(), + }; + + const childProcess = new EventEmitter() as EventEmitter & { + stdout: EventEmitter; + stderr: EventEmitter; + }; + childProcess.stdout = new EventEmitter(); + childProcess.stderr = new EventEmitter(); + spawnMock.mockReturnValue(childProcess); + + const logsPromise = service.logs(logsInputs); + + childProcess.emit("close", null, "SIGTERM"); + + await expect(logsPromise).resolves.toEqual({ + error: "Docker Compose logs command failed with signal SIGTERM", + output: "", + }); + }); + + it("should return a non-fatal error message when spawning logs fails", async () => { + const logsInputs = { + dockerFlags: [] as string[], + composeFiles: ["docker-compose.yml"] as string[], + services: [] as string[], + composeFlags: [] as string[], + cwd: "/current/working/dir", + serviceLogger: vi.fn(), + }; + + const childProcess = new EventEmitter() as EventEmitter & { + stdout: EventEmitter; + stderr: EventEmitter; + }; + childProcess.stdout = new EventEmitter(); + childProcess.stderr = new EventEmitter(); + spawnMock.mockReturnValue(childProcess); + + const logsPromise = service.logs(logsInputs); + + childProcess.emit("error", new Error("spawn ENOENT")); + + await expect(logsPromise).resolves.toEqual({ + error: "Unable to collect docker compose logs: spawn ENOENT", + output: "", + }); + }); + + it("should return a non-fatal error message when output streams are unavailable", async () => { + const logsInputs = { + dockerFlags: [] as string[], + composeFiles: ["docker-compose.yml"] as string[], + services: [] as string[], + composeFlags: [] as string[], + cwd: "/current/working/dir", + serviceLogger: vi.fn(), + }; + + const childProcess = new EventEmitter() as EventEmitter & { + stdout: EventEmitter | null; + stderr: EventEmitter | null; + }; + childProcess.stdout = null; + childProcess.stderr = null; + spawnMock.mockReturnValue(childProcess); + + await expect(service.logs(logsInputs)).resolves.toEqual({ + error: "Unable to collect docker compose logs: stdout/stderr unavailable", + output: "", + }); + }); }); }); diff --git a/src/services/docker-compose.service.ts b/src/services/docker-compose.service.ts index d5686e5..aec003f 100644 --- a/src/services/docker-compose.service.ts +++ b/src/services/docker-compose.service.ts @@ -66,24 +66,49 @@ export class DockerComposeService { commandArgs: services, }); - return await new Promise((resolve, reject) => { + return new Promise((resolve) => { + let settled = false; const childProcess = spawn("docker", commandArgs, { cwd: optionsInputs.cwd, }); - childProcess.on("error", reject); - childProcess.stdout.on("data", (chunk: Buffer) => { + childProcess.on("error", (error) => { + if (settled) { + return; + } + settled = true; + resolve({ + error: `Unable to collect docker compose logs: ${error.message}`, + output: "", + }); + }); + if (!childProcess.stdout || !childProcess.stderr) { + settled = true; + resolve({ + error: "Unable to collect docker compose logs: stdout/stderr unavailable", + output: "", + }); + return; + } + + childProcess.stdout.on("data", (chunk: Buffer | string) => { optionsInputs.serviceLogger(chunk.toString()); }); - childProcess.stderr.on("data", (chunk: Buffer) => { + childProcess.stderr.on("data", (chunk: Buffer | string) => { optionsInputs.serviceLogger(chunk.toString()); }); - childProcess.on("close", (exitCode) => { + childProcess.on("close", (exitCode, signal) => { + if (settled) { + return; + } + settled = true; resolve({ error: - exitCode && exitCode !== 0 - ? `Docker Compose logs command failed with exit code ${exitCode}` - : "", + signal + ? `Docker Compose logs command failed with signal ${signal}` + : exitCode !== null && exitCode !== 0 + ? `Docker Compose logs command failed with exit code ${exitCode}` + : "", output: "", }); }); @@ -109,8 +134,11 @@ export class DockerComposeService { }; } + /** + * Builds docker CLI arguments in the order expected by `docker compose`. + */ private getDockerComposeCommandArgs( - command: string, + command: "logs", { dockerFlags, composeFlags,