diff --git a/packages/cli/src/cmds/preview/task.ts b/packages/cli/src/cmds/preview/task.ts index 2042e8d8b36..4cf9393ba33 100644 --- a/packages/cli/src/cmds/preview/task.ts +++ b/packages/cli/src/cmds/preview/task.ts @@ -3,7 +3,7 @@ "use strict"; -import { ChildProcess, spawn } from "child_process"; +import cp from "child_process"; import { err, FxError, LogLevel, ok, Result } from "@microsoft/teamsfx-api"; import treeKill from "tree-kill"; import { ServiceLogWriter } from "./serviceLogWriter"; @@ -33,7 +33,7 @@ export class Task { private options?: TaskOptions; private resolved = false; - private task: ChildProcess | undefined; + private task: cp.ChildProcess | undefined; constructor( taskTitle: string, @@ -61,26 +61,25 @@ export class Task { ) => Promise ): Promise> { await startCallback(this.taskTitle, this.background); - this.task = spawn(this.command, this.args, this.options); + this.task = cp.spawn(this.command, this.args, this.options); const stdout: string[] = []; const stderr: string[] = []; return new Promise((resolve) => { this.task?.stdout?.on("data", (data) => { - // TODO: log stdout.push(data.toString()); }); this.task?.stderr?.on("data", (data) => { - // TODO: log stderr.push(data.toString()); }); - this.task?.on("exit", async () => { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + this.task?.on("exit", async (code) => { const result: TaskResult = { command: this.command, options: this.options, - success: this.task?.exitCode === 0, + success: code === 0, stdout: stdout, stderr: stderr, - exitCode: this.task?.exitCode === undefined ? null : this.task?.exitCode, + exitCode: code, }; const error = await stopCallback(this.taskTitle, this.background, result); if (error) { @@ -93,7 +92,7 @@ export class Task { } /** - * wait until stdout of the task matches the pattern or the task ends + * wait until stdout/stderr of the task matches the pattern or the task ends */ public async waitFor( pattern: RegExp, @@ -116,18 +115,19 @@ export class Task { `${this.command} ${this.args ? this.args?.join(" ") : ""}\n` ); await startCallback(this.taskTitle, this.background, serviceLogWriter); - this.task = spawn(this.command, this.args, this.options); + this.task = cp.spawn(this.command, this.args, this.options); const stdout: string[] = []; const stderr: string[] = []; return new Promise((resolve) => { if (timeout !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-misused-promises setTimeout(async () => { if (!this.resolved) { this.resolved = true; const result: TaskResult = { command: this.command, options: this.options, - success: true, + success: false, stdout: stdout, stderr: stderr, exitCode: null, @@ -142,6 +142,7 @@ export class Task { }, timeout); } + // eslint-disable-next-line @typescript-eslint/no-misused-promises this.task?.stdout?.on("data", async (data) => { const dataStr = data.toString(); await serviceLogWriter?.write(this.taskTitle, dataStr); @@ -170,6 +171,7 @@ export class Task { } } }); + // eslint-disable-next-line @typescript-eslint/no-misused-promises this.task?.stderr?.on("data", async (data) => { const dataStr = data.toString(); await serviceLogWriter?.write(this.taskTitle, dataStr); @@ -177,9 +179,30 @@ export class Task { logProvider.necessaryLog(LogLevel.Info, dataStr.trim(), true); } stderr.push(dataStr); + if (!this.resolved) { + const match = pattern.test(dataStr); + if (match) { + this.resolved = true; + const result: TaskResult = { + command: this.command, + options: this.options, + success: false, + stdout: stdout, + stderr: stderr, + exitCode: null, + }; + const error = await stopCallback(this.taskTitle, this.background, result); + if (error) { + resolve(err(error)); + } else { + resolve(ok(result)); + } + } + } }); - this.task?.on("exit", async () => { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + this.task?.on("exit", async (code) => { if (!this.resolved) { this.resolved = true; const result: TaskResult = { @@ -188,7 +211,7 @@ export class Task { success: false, stdout: stdout, stderr: stderr, - exitCode: this.task?.exitCode === undefined ? null : this.task?.exitCode, + exitCode: code, }; const error = await stopCallback(this.taskTitle, this.background, result); if (error) { diff --git a/packages/cli/tests/unit/cmds/preview/task.tests.ts b/packages/cli/tests/unit/cmds/preview/task.tests.ts new file mode 100644 index 00000000000..dcc50a6bce5 --- /dev/null +++ b/packages/cli/tests/unit/cmds/preview/task.tests.ts @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as sinon from "sinon"; +import { EventEmitter } from "events"; +import cp from "child_process"; +import { Readable } from "stream"; +import { Task } from "../../../../src/cmds/preview/task"; +import { expect } from "../../utils"; + +function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +describe("Task", () => { + describe("wait", () => { + let sandbox: sinon.SinonSandbox; + + afterEach(() => { + sandbox.restore(); + }); + + it("happy path", async () => { + sandbox = sinon.createSandbox(); + const startCallback = sinon.stub().resolves(); + const stopCallback = sinon.stub().resolves(null); + const spawnEvent = new EventEmitter(); + spawnEvent.stdout = new EventEmitter(); + spawnEvent.stderr = new EventEmitter(); + sandbox.stub(cp, "spawn").callsFake(() => { + return spawnEvent; + }); + + const task = new Task("taskTitle", false, "command"); + const promise = task.wait(startCallback, stopCallback); + + await delay(10); + + spawnEvent.stdout.emit("data", "stdout1"); + spawnEvent.stdout.emit("data", "stdout2"); + spawnEvent.stderr.emit("data", "stderr1"); + spawnEvent.stderr.emit("data", "stderr2"); + spawnEvent.emit("exit", 0); + + const resultRes = await promise; + + expect(resultRes.isOk()).to.be.true; + const result = (resultRes as any).value; + expect(result.success).to.be.true; + expect(result.stdout).to.deep.equals(["stdout1", "stdout2"]); + expect(result.stderr).to.deep.equals(["stderr1", "stderr2"]); + expect(result.exitCode).to.equals(0); + }); + }); + + describe("waitFor", () => { + let sandbox: sinon.SinonSandbox; + + afterEach(() => { + sandbox.restore(); + }); + + it("happy path: match stdout", async () => { + sandbox = sinon.createSandbox(); + const startCallback = sinon.stub().resolves(); + const stopCallback = sinon.stub().resolves(null); + const spawnEvent = new EventEmitter(); + spawnEvent.stdout = new EventEmitter(); + spawnEvent.stderr = new EventEmitter(); + sandbox.stub(cp, "spawn").callsFake(() => { + return spawnEvent; + }); + + const task = new Task("taskTitle", true, "command"); + const promise = task.waitFor(new RegExp("started", "i"), startCallback, stopCallback); + + await delay(5); + + spawnEvent.stdout.emit("data", "stdout1"); + spawnEvent.stdout.emit("data", "stdout2"); + spawnEvent.stdout.emit("data", "xxx started xxx"); + spawnEvent.stderr.emit("data", "stderr1"); + spawnEvent.stderr.emit("data", "stderr2"); + + const resultRes = await promise; + + expect(resultRes.isOk()).to.be.true; + const result = (resultRes as any).value; + expect(result.success).to.be.true; + expect(result.stdout).to.deep.equals(["stdout1", "stdout2", "xxx started xxx"]); + expect(result.stderr).to.deep.equals(["stderr1", "stderr2"]); + expect(result.exitCode).to.equals(null); + }); + + it("happy path: match stderr", async () => { + sandbox = sinon.createSandbox(); + const startCallback = sinon.stub().resolves(); + const stopCallback = sinon.stub().resolves(null); + const spawnEvent = new EventEmitter(); + spawnEvent.stdout = new EventEmitter(); + spawnEvent.stderr = new EventEmitter(); + sandbox.stub(cp, "spawn").callsFake(() => { + return spawnEvent; + }); + + const task = new Task("taskTitle", true, "command"); + const promise = task.waitFor(new RegExp("started", "i"), startCallback, stopCallback); + + await delay(5); + + spawnEvent.stdout.emit("data", "stdout1"); + spawnEvent.stdout.emit("data", "stdout2"); + spawnEvent.stderr.emit("data", "stderr1"); + spawnEvent.stderr.emit("data", "stderr2"); + spawnEvent.stderr.emit("data", "xxx started xxx"); + + const resultRes = await promise; + + expect(resultRes.isOk()).to.be.true; + const result = (resultRes as any).value; + expect(result.success).to.be.false; + expect(result.stdout).to.deep.equals(["stdout1", "stdout2"]); + expect(result.stderr).to.deep.equals(["stderr1", "stderr2", "xxx started xxx"]); + expect(result.exitCode).to.equals(null); + }); + + it("timeout", async () => { + sandbox = sinon.createSandbox(); + const startCallback = sinon.stub().resolves(); + const stopCallback = sinon.stub().resolves(null); + const spawnEvent = new EventEmitter(); + spawnEvent.stdout = new EventEmitter(); + spawnEvent.stderr = new EventEmitter(); + sandbox.stub(cp, "spawn").callsFake(() => { + return spawnEvent; + }); + + const task = new Task("taskTitle", true, "command"); + const promise = task.waitFor(new RegExp("started", "i"), startCallback, stopCallback, 30); + + await delay(5); + + spawnEvent.stdout.emit("data", "stdout1"); + spawnEvent.stdout.emit("data", "stdout2"); + spawnEvent.stderr.emit("data", "stderr1"); + spawnEvent.stderr.emit("data", "stderr2"); + + const resultRes = await promise; + + expect(resultRes.isOk()).to.be.true; + const result = (resultRes as any).value; + expect(result.success).to.be.false; + expect(result.stdout).to.deep.equals(["stdout1", "stdout2"]); + expect(result.stderr).to.deep.equals(["stderr1", "stderr2"]); + expect(result.exitCode).to.equals(null); + }); + }); +});