Skip to content

Commit

Permalink
Merge pull request #85 from VividVisions/graceful-shutdown
Browse files Browse the repository at this point in the history
Makes shutdown process asynchronous and graceful
  • Loading branch information
zachleat authored Jul 30, 2024
2 parents fece5bf + 14927ec commit 5878059
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 41 deletions.
2 changes: 1 addition & 1 deletion cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Arguments:

close() {
if(this.server) {
this.server.close();
return this.server.close();
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions cmd.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ try {
domDiff: argv.domdiff,
});

process.on("SIGINT", () => {
cli.close();
process.exit();
process.on("SIGINT", async () => {
await cli.close();
process.exitCode = 0;
});
}
} catch (e) {
Expand Down
36 changes: 30 additions & 6 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -795,23 +795,47 @@ class EleventyDevServer {
}
}

close() {
// Helper for promisifying close methods with callbacks, like http.Server or ws.WebSocketServer.
_closeServer(server) {
return new Promise((resolve, reject) => {
server.close(err => {
if (err) {
reject(err);
}
resolve();
});
});
}

async close() {
// Prevent multiple invocations.
if (this?._isClosing) {
return;
}
this._isClosing = true;

// TODO would be awesome to set a delayed redirect when port changed to redirect to new _server_
this.sendUpdateNotification({
type: "eleventy.status",
status: "disconnected",
});

if(this.server) {
this.server.close();
}
if(this.updateServer) {
this.updateServer.close();
// Close all existing WS connections.
this.updateServer?.clients.forEach(socket => socket.close());
await this._closeServer(this.updateServer);
}

if(this._server?.listening) {
await this._closeServer(this.server);
}

if(this._watcher) {
this._watcher.close();
await this._watcher.close();
delete this._watcher;
}

delete this._isClosing;
}

sendError({ error }) {
Expand Down
36 changes: 19 additions & 17 deletions test/testServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function testNormalizeFilePath(filepath) {
return filepath.split("/").join(path.sep);
}

test("Url mappings for resource/index.html", t => {
test("Url mappings for resource/index.html", async (t) => {
let server = new EleventyDevServer("test-server", "./test/stubs/");

t.deepEqual(server.mapUrlToFilePath("/route1/"), {
Expand All @@ -27,11 +27,11 @@ test("Url mappings for resource/index.html", t => {
statusCode: 200,
filepath: testNormalizeFilePath("test/stubs/route1/index.html")
});

server.close();
await server.close();
});

test("Url mappings for resource.html", t => {
test("Url mappings for resource.html", async (t) => {
let server = new EleventyDevServer("test-server", "./test/stubs/");

t.deepEqual(server.mapUrlToFilePath("/route2/"), {
Expand All @@ -53,10 +53,10 @@ test("Url mappings for resource.html", t => {
filepath: testNormalizeFilePath("test/stubs/route2.html",)
});

server.close();
await server.close();
});

test("Url mappings for resource.html and resource/index.html", t => {
test("Url mappings for resource.html and resource/index.html", async (t) => {
let server = new EleventyDevServer("test-server", "./test/stubs/");

// Production mismatch warning: Netlify 301 redirects to /route3 here
Expand All @@ -80,29 +80,29 @@ test("Url mappings for resource.html and resource/index.html", t => {
filepath: testNormalizeFilePath("test/stubs/route3.html",)
});

server.close();
await server.close();
});

test("Url mappings for missing resource", t => {
test("Url mappings for missing resource", async (t) => {
let server = new EleventyDevServer("test-server", "./test/stubs/");

// 404s
t.deepEqual(server.mapUrlToFilePath("/does-not-exist/"), {
statusCode: 404
});

server.close();
await server.close();
});

test("Url mapping for a filename with a space in it", t => {
test("Url mapping for a filename with a space in it", async (t) => {
let server = new EleventyDevServer("test-server", "./test/stubs/");

t.deepEqual(server.mapUrlToFilePath("/route space.html"), {
statusCode: 200,
filepath: testNormalizeFilePath("test/stubs/route space.html",)
});

server.close();
await server.close();
});

test("matchPassthroughAlias", async (t) => {
Expand Down Expand Up @@ -131,6 +131,8 @@ test("matchPassthroughAlias", async (t) => {

// Map entry exists, file exists
t.is(server.matchPassthroughAlias("/elsewhere/index.css"), "./test/stubs/with-css/style.css");

await server.close();
});


Expand All @@ -155,7 +157,7 @@ test("pathPrefix matching", async (t) => {
url: '/pathprefix/',
});

server.close();
await server.close();
});

test("pathPrefix without leading slash", async (t) => {
Expand All @@ -179,7 +181,7 @@ test("pathPrefix without leading slash", async (t) => {
url: '/pathprefix/',
});

server.close();
await server.close();
});

test("pathPrefix without trailing slash", async (t) => {
Expand All @@ -203,7 +205,7 @@ test("pathPrefix without trailing slash", async (t) => {
url: '/pathprefix/',
});

server.close();
await server.close();
});

test("pathPrefix without leading or trailing slash", async (t) => {
Expand All @@ -227,7 +229,7 @@ test("pathPrefix without leading or trailing slash", async (t) => {
url: '/pathprefix/',
});

server.close();
await server.close();
});

test("indexFileName option: serve custom index when provided", async (t) => {
Expand All @@ -244,7 +246,7 @@ test("indexFileName option: serve custom index when provided", async (t) => {
filepath: testNormalizeFilePath("test/stubs/route1/custom-index.html"),
});

server.close();
await server.close();
});

test("indexFileName option: return 404 when custom index file doesn't exist", async (t) => {
Expand All @@ -254,5 +256,5 @@ test("indexFileName option: return 404 when custom index file doesn't exist", as
statusCode: 404,
});

server.close();
await server.close();
});
28 changes: 14 additions & 14 deletions test/testServerRequests.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,15 @@ async function fetchHeadersForRequest(t, server, path, extras) {
})
}

test("Standard request", async t => {
test("Standard request", async (t) => {
let server = new EleventyDevServer("test-server", "./test/stubs/", getOptions());
server.serve(8100);

let data = await makeRequestTo(t, server, "/sample");
t.true(data.includes("<script "));
t.true(data.startsWith("SAMPLE"));

server.close();
await server.close();
});

test("One sync middleware", async t => {
Expand All @@ -101,7 +101,7 @@ test("One sync middleware", async t => {
t.true(data.includes("<script "));
t.true(data.startsWith("SAMPLE"));

server.close();
await server.close();
});

test("Two sync middleware", async t => {
Expand All @@ -121,7 +121,7 @@ test("Two sync middleware", async t => {
t.true(data.includes("<script "));
t.true(data.startsWith("SAMPLE"));

server.close();
await server.close();
});

test("One async middleware", async t => {
Expand All @@ -138,7 +138,7 @@ test("One async middleware", async t => {
t.true(data.includes("<script "));
t.true(data.startsWith("SAMPLE"));

server.close();
await server.close();
});

test("Two async middleware", async t => {
Expand All @@ -158,7 +158,7 @@ test("Two async middleware", async t => {
t.true(data.includes("<script "));
t.true(data.startsWith("SAMPLE"));

server.close();
await server.close();
});

test("Async middleware that writes", async t => {
Expand Down Expand Up @@ -186,7 +186,7 @@ test("Async middleware that writes", async t => {
t.true(data.includes("<script "));
t.true(data.startsWith("Injected"));

server.close();
await server.close();
});

test("Second async middleware that writes", async t => {
Expand Down Expand Up @@ -223,7 +223,7 @@ test("Second async middleware that writes", async t => {
t.true(data.includes("<script "));
t.true(data.startsWith("Injected"));

server.close();
await server.close();
});


Expand All @@ -250,7 +250,7 @@ test("Second middleware that consumes first middleware response body, issue #29"
t.true(data.includes("<script "));
t.true(data.startsWith("First Second "));

server.close();
await server.close();
});

test("Two middlewares, end() in the first, skip the second", async t => {
Expand All @@ -276,7 +276,7 @@ test("Two middlewares, end() in the first, skip the second", async t => {
t.true(data.startsWith("First "));
t.true(!data.startsWith("First Second "));

server.close();
await server.close();
});

test("Fun unicode paths", async t => {
Expand All @@ -287,7 +287,7 @@ test("Fun unicode paths", async t => {
t.true(data.includes("<script "));
t.true(data.startsWith("This is a test"));

server.close();
await server.close();
});

test("Content-Type header via middleware", async t => {
Expand All @@ -307,7 +307,7 @@ test("Content-Type header via middleware", async t => {
let data = await fetchHeadersForRequest(t, server, encodeURI(`/index.php`));
t.true(data['content-type'] === 'text/html; charset=utf-8');

server.close();
await server.close();
});

test("Content-Range request", async (t) => {
Expand All @@ -325,7 +325,7 @@ test("Content-Range request", async (t) => {
t.true("content-range" in data);
t.true(data["content-range"].startsWith("bytes 0-48/"));

server.close();
await server.close();
});

test("Standard request does not include range headers", async (t) => {
Expand All @@ -340,5 +340,5 @@ test("Standard request does not include range headers", async (t) => {
t.false("accept-ranges" in data);
t.false("content-range" in data);

server.close();
await server.close();
});

0 comments on commit 5878059

Please sign in to comment.