Skip to content

Commit

Permalink
feat: Handle SIGINT and cleanup offline files unless specified to skip (
Browse files Browse the repository at this point in the history
#233)

Cleans up offline files when user interrupts offline process. Allows user to skip cleanup with CLI option.
  • Loading branch information
tbarlow12 authored Aug 8, 2019
1 parent 2157db1 commit 3618646
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 4 deletions.
14 changes: 13 additions & 1 deletion src/plugins/offline/azureOfflinePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ export class AzureOfflinePlugin extends AzureBasePlugin {
usage: "Start Azure Function app - assumes offline build has already occurred",
lifecycleEvents: [
"start"
]
],
options: {
nocleanup: {
usage: "Do not clean up offline files after finishing process",
shortcut: "n",
}
}
},
build: {
usage: "Build necessary files for running Azure Function App offline",
Expand All @@ -44,6 +50,12 @@ export class AzureOfflinePlugin extends AzureBasePlugin {
"cleanup"
]
}
},
options: {
nocleanup: {
usage: "Do not clean up offline files after finishing process",
shortcut: "n",
}
}
}
}
Expand Down
115 changes: 113 additions & 2 deletions src/services/offlineService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { OfflineService } from "./offlineService";
describe("Offline Service", () => {
let mySpawn;

function createService(sls?: Serverless): OfflineService {
function createService(sls?: Serverless, options?: any): OfflineService {
return new OfflineService(
sls || MockFactory.createTestServerless(),
MockFactory.createTestServerlessOptions(),
MockFactory.createTestServerlessOptions(options),
)
}

Expand All @@ -27,6 +27,7 @@ describe("Offline Service", () => {

afterEach(() => {
mockFs.restore();
jest.resetAllMocks();
});

it("builds required files for offline execution", async () => {
Expand Down Expand Up @@ -145,4 +146,114 @@ describe("Offline Service", () => {
expect(call.command).toEqual("func.cmd");
expect(call.args).toEqual(["host", "start"]);
});

it("cleans up after offline call as default behavior", async () => {
mockFs({
hello: {
"function.json": "contents"
},
goodbye: {
"function.json": "contents"
},
"local.settings.json": "contents",
});

Object.defineProperty(process, "platform", {
value: "win32",
writable: true,
});

const sls = MockFactory.createTestServerless();
const processOnSpy = jest.spyOn(process, "on");
const unlinkSpy = jest.spyOn(fs, "unlinkSync");
const rmdirSpy = jest.spyOn(fs, "rmdirSync")

const service = createService(sls);

await service.start();

const calls = mySpawn.calls;
expect(calls).toHaveLength(1);
const call = calls[0];
expect(call.command).toEqual("func.cmd");
expect(call.args).toEqual(["host", "start"]);

const processOnCalls = processOnSpy.mock.calls;
expect(processOnCalls).toHaveLength(1);
expect(processOnCalls[0][0]).toEqual("SIGINT");
expect(processOnCalls[0][1]).toBeInstanceOf(Function);

// SIGINT handler function
const sigintCallback = processOnCalls[0][1] as any;

process.exit = jest.fn() as any;
await sigintCallback();

/* Offline Cleanup assertions*/

const unlinkCalls = unlinkSpy.mock.calls;

expect(unlinkCalls).toHaveLength(3);
expect(unlinkCalls[0][0]).toBe(`hello${path.sep}function.json`);
expect(unlinkCalls[1][0]).toBe(`goodbye${path.sep}function.json`);
expect(unlinkCalls[2][0]).toBe("local.settings.json");

const rmdirCalls = rmdirSpy.mock.calls;

expect(rmdirCalls[0][0]).toBe("hello");
expect(rmdirCalls[1][0]).toBe("goodbye");

unlinkSpy.mockRestore();
rmdirSpy.mockRestore();

expect(process.exit).toBeCalledTimes(1);
});

it("does not clean up after offline call if specified in options", async () => {

Object.defineProperty(process, "platform", {
value: "win32",
writable: true,
});

const processOnSpy = jest.spyOn(process, "on");
const unlinkSpy = jest.spyOn(fs, "unlinkSync");
const rmdirSpy = jest.spyOn(fs, "rmdirSync");

const sls = MockFactory.createTestServerless();


const service = createService(sls, {
"nocleanup": ""
});

await service.start();

const calls = mySpawn.calls;
expect(calls).toHaveLength(1);
const call = calls[0];
expect(call.command).toEqual("func.cmd");
expect(call.args).toEqual(["host", "start"]);

const processOnCalls = processOnSpy.mock.calls;
expect(processOnCalls).toHaveLength(1);
expect(processOnCalls[0][0]).toEqual("SIGINT");
expect(processOnCalls[0][1]).toBeInstanceOf(Function);

// SIGINT handler function
const sigintCallback = processOnCalls[0][1] as any;

process.exit = jest.fn() as any;
await sigintCallback();

/* Offline Cleanup assertions*/

expect(unlinkSpy).not.toBeCalled();
expect(rmdirSpy).not.toBeCalled();

unlinkSpy.mockRestore();
rmdirSpy.mockRestore();

expect(process.exit).toBeCalledTimes(1);
});
});
17 changes: 16 additions & 1 deletion src/services/offlineService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,25 @@ export class OfflineService extends BaseService {
...this.serverless.service.provider["environment"],
}
this.log(`Spawning process '${command} ${spawnArgs.join(" ")}'`);
return new Promise((resolve, reject) => {
return new Promise(async (resolve, reject) => {
const spawnOptions: SpawnOptions = { env, stdio: "inherit" };
const childProcess = spawn(command, spawnArgs, spawnOptions);

process.on("SIGINT", async () => {
try {
if (this.getOption("nocleanup")) {
this.log("Skipping offline file cleanup...");
} else {
await this.cleanup();
}
} catch {
// Swallowing `scandir` error that gets thrown after
// trying to remove the same directory twice
} finally {
process.exit();
}
});

childProcess.on("exit", (code) => {
if (code === 0) {
resolve();
Expand Down

0 comments on commit 3618646

Please sign in to comment.