openvibers/openvibe.tools Commits
Tools apps: systemd holds each app's listener too (audio, docs, food, img, maps, text, yt), so a restart queues the gateway's requests instead of refusing them; the process listens on fd 3 only when handed it (#79)
Changes
nnew file mode 100644@@@ -0,0 +1,9 @@++# Bind the service to the socket unit that owns its listener. A drop-in, so the main unit stays usable on a box that++# has not been switched over to socket activation.++[Unit]++Requires=openvibe-tools-audio.socket++After=openvibe-tools-audio.socket++++[Service]++# Node's libuv expects a non-blocking descriptor. (A [Service] option: in [Socket] systemd ignores it.)++NonBlocking=truennew file mode 100644@@@ -0,0 +1,17 @@++# OpenVibe.Tools audio app — listening socket, owned by systemd rather than by the process (as the gateway's).++#++# The gateway (openvibe-tools.service) proxies this app's pages and API to 127.0.0.1:4014. Without this, each restart++# closed the listener for the whole boot and the gateway got ECONNREFUSED; with it, requests that arrive mid-restart++# wait in the kernel's accept queue and are served as soon as the new process calls accept().++[Unit]++Description=OpenVibe.Tools audio HTTP socket++# No PartOf=openvibe-tools-audio.service: the socket must outlive every restart.++++[Socket]++# Loopback only: the gateway is the only client (the host firewall is off, so 0.0.0.0 would be public).++ListenStream=127.0.0.1:4014++NoDelay=true++Backlog=511++++[Install]++WantedBy=sockets.target@@@ -315,7 +315,10 @@ app.get('*', (req, res) => {});// ── Start ────────────────────────────────────────────────────--const server = app.listen(config.port, config.host, () => {++// systemd hands this process the listener when deploy/systemd/openvibe-tools-audio.socket is installed: a restart then++// queues the gateway's requests in the kernel instead of refusing them. Otherwise it listens on the port itself.++const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;++const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {retention.startCleanup();console.log(`\n╔═══════════════════════════════════════╗`);console.log(`║ 🎵 Audio.OpenVibe — Audio Processing Hub ║`);nnew file mode 100644@@@ -0,0 +1,9 @@++# Bind the service to the socket unit that owns its listener. A drop-in, so the main unit stays usable on a box that++# has not been switched over to socket activation.++[Unit]++Requires=openvibe-tools-docs.socket++After=openvibe-tools-docs.socket++++[Service]++# Node's libuv expects a non-blocking descriptor. (A [Service] option: in [Socket] systemd ignores it.)++NonBlocking=truennew file mode 100644@@@ -0,0 +1,17 @@++# OpenVibe.Tools docs app — listening socket, owned by systemd rather than by the process (as the gateway's).++#++# The gateway (openvibe-tools.service) proxies this app's pages and API to 127.0.0.1:4016. Without this, each restart++# closed the listener for the whole boot and the gateway got ECONNREFUSED; with it, requests that arrive mid-restart++# wait in the kernel's accept queue and are served as soon as the new process calls accept().++[Unit]++Description=OpenVibe.Tools docs HTTP socket++# No PartOf=openvibe-tools-docs.service: the socket must outlive every restart.++++[Socket]++# Loopback only: the gateway is the only client (the host firewall is off, so 0.0.0.0 would be public).++ListenStream=127.0.0.1:4016++NoDelay=true++Backlog=511++++[Install]++WantedBy=sockets.target@@@ -311,7 +311,10 @@ app.get('*', (req, res) => {});// ── Start ────────────────────────────────────────────────────--const server = app.listen(config.port, config.host, () => {++// systemd hands this process the listener when deploy/systemd/openvibe-tools-docs.socket is installed: a restart then++// queues the gateway's requests in the kernel instead of refusing them. Otherwise it listens on the port itself.++const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;++const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {retention.startCleanup();console.log(`\n╔═══════════════════════════════════════╗`);console.log(`║ 📄 Docs.OpenVibe — Document Tools Hub ║`);nnew file mode 100644@@@ -0,0 +1,9 @@++# Bind the service to the socket unit that owns its listener. A drop-in, so the main unit stays usable on a box that++# has not been switched over to socket activation.++[Unit]++Requires=openvibe-tools-food.socket++After=openvibe-tools-food.socket++++[Service]++# Node's libuv expects a non-blocking descriptor. (A [Service] option: in [Socket] systemd ignores it.)++NonBlocking=truennew file mode 100644@@@ -0,0 +1,17 @@++# OpenVibe.Tools food app — listening socket, owned by systemd rather than by the process (as the gateway's).++#++# The gateway (openvibe-tools.service) proxies this app's pages and API to 127.0.0.1:4011. Without this, each restart++# closed the listener for the whole boot and the gateway got ECONNREFUSED; with it, requests that arrive mid-restart++# wait in the kernel's accept queue and are served as soon as the new process calls accept().++[Unit]++Description=OpenVibe.Tools food HTTP socket++# No PartOf=openvibe-tools-food.service: the socket must outlive every restart.++++[Socket]++# Loopback only: the gateway is the only client (the host firewall is off, so 0.0.0.0 would be public).++ListenStream=127.0.0.1:4011++NoDelay=true++Backlog=511++++[Install]++WantedBy=sockets.target@@@ -171,7 +171,10 @@ app.get('/api/internal/analytics/bots', internalAccess, async (req, res) => {// What used to be the page's own paths go to OpenVibe.Food as well (the API answers JSON 404s).app.get('*', (req, res) => (req.path.startsWith('/api/') ? res.status(404).json({ error: 'Not found' }) : toFood(req, res)));--const server = app.listen(PORT, '127.0.0.1', () => {++// systemd hands this process the listener when deploy/systemd/openvibe-tools-food.socket is installed: a restart then++// queues the gateway's requests in the kernel instead of refusing them. Otherwise it listens on the port itself.++const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;++const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [PORT, '127.0.0.1']), () => {console.log(`[Food.OpenVibe] 🍽️ food.openvibe.tools listening on 127.0.0.1:${PORT}`);});@@@ -31,4 +31,22 @@ const index = fs.readFileSync(path.join(__dirname, '..', 'server', 'index.js'),assert.match(index, /process\.env\.LISTEN_FDS === '1' && Number\(process\.env\.LISTEN_PID\) === process\.pid/, 'fd 3 only when handed to this process');assert.ok(index.includes('const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {'.trim()), 'otherwise the configured port');++// The apps behind the gateway: the same rules, each on its own port.++const APPS = { audio: 4014, docs: 4016, food: 4011, img: 4012, maps: 4010, text: 4015, yt: 4013 };++for (const [app, port] of Object.entries(APPS)) {++ const dir = path.join(__dirname, '..', '..', app, 'deploy', 'systemd');++ const rd = (...p) => fs.readFileSync(path.join(dir, ...p), 'utf8');++ const sock = rd(`openvibe-tools-${app}.socket`);++ assert.deepStrictEqual([...section(sock, 'Socket').matchAll(/^ListenStream=(.+)$/gm)].map((m) => m[1].trim()), [`127.0.0.1:${port}`], `${app}: loopback, its port`);++ assert.ok(!/^NonBlocking=/m.test(section(sock, 'Socket')) && !/^PartOf=/m.test(section(sock, 'Unit')), `${app}: no NonBlocking/PartOf in the socket`);++ const drop = rd(`openvibe-tools-${app}.service.d`, 'socket.conf');++ assert.match(section(drop, 'Unit'), new RegExp(`^Requires=openvibe-tools-${app}\\.socket$`, 'm'), app);++ assert.match(section(drop, 'Unit'), new RegExp(`^After=openvibe-tools-${app}\\.socket$`, 'm'), app);++ assert.match(section(drop, 'Service'), /^NonBlocking=true$/m, app);++ assert.match(rd(`openvibe-tools-${app}.service`), new RegExp(`^Environment=PORT=${port}$`, 'm'), `${app}: the unit's PORT is the socket's`);++ const src = fs.readFileSync(path.join(__dirname, '..', '..', app, 'server', 'index.js'), 'utf8');++ assert.match(src, /process\.env\.LISTEN_FDS === '1' && Number\(process\.env\.LISTEN_PID\) === process\.pid/, `${app}: fd 3 only when handed to this process`);++ assert.match(src, /app\.listen\(\.\.\.\(socketActivated \? \[\{ fd: 3 \}\] : \[/, `${app}: otherwise its port`);++}++console.log('systemd units: all checks passed');nnew file mode 100644@@@ -0,0 +1,9 @@++# Bind the service to the socket unit that owns its listener. A drop-in, so the main unit stays usable on a box that++# has not been switched over to socket activation.++[Unit]++Requires=openvibe-tools-img.socket++After=openvibe-tools-img.socket++++[Service]++# Node's libuv expects a non-blocking descriptor. (A [Service] option: in [Socket] systemd ignores it.)++NonBlocking=truennew file mode 100644@@@ -0,0 +1,17 @@++# OpenVibe.Tools img app — listening socket, owned by systemd rather than by the process (as the gateway's).++#++# The gateway (openvibe-tools.service) proxies this app's pages and API to 127.0.0.1:4012. Without this, each restart++# closed the listener for the whole boot and the gateway got ECONNREFUSED; with it, requests that arrive mid-restart++# wait in the kernel's accept queue and are served as soon as the new process calls accept().++[Unit]++Description=OpenVibe.Tools img HTTP socket++# No PartOf=openvibe-tools-img.service: the socket must outlive every restart.++++[Socket]++# Loopback only: the gateway is the only client (the host firewall is off, so 0.0.0.0 would be public).++ListenStream=127.0.0.1:4012++NoDelay=true++Backlog=511++++[Install]++WantedBy=sockets.target@@@ -278,7 +278,10 @@ app.get('*', (req, res) => {});// ── Start ────────────────────────────────────────────────────--const server = app.listen(config.port, config.host, () => {++// systemd hands this process the listener when deploy/systemd/openvibe-tools-img.socket is installed: a restart then++// queues the gateway's requests in the kernel instead of refusing them. Otherwise it listens on the port itself.++const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;++const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {retention.startCleanup();console.log(`\n╔═══════════════════════════════════════╗`);console.log(`║ 🖼️ Img.OpenVibe — Image Conversion Hub ║`);nnew file mode 100644@@@ -0,0 +1,9 @@++# Bind the service to the socket unit that owns its listener. A drop-in, so the main unit stays usable on a box that++# has not been switched over to socket activation.++[Unit]++Requires=openvibe-tools-maps.socket++After=openvibe-tools-maps.socket++++[Service]++# Node's libuv expects a non-blocking descriptor. (A [Service] option: in [Socket] systemd ignores it.)++NonBlocking=truennew file mode 100644@@@ -0,0 +1,17 @@++# OpenVibe.Tools maps app — listening socket, owned by systemd rather than by the process (as the gateway's).++#++# The gateway (openvibe-tools.service) proxies this app's pages and API to 127.0.0.1:4010. Without this, each restart++# closed the listener for the whole boot and the gateway got ECONNREFUSED; with it, requests that arrive mid-restart++# wait in the kernel's accept queue and are served as soon as the new process calls accept().++[Unit]++Description=OpenVibe.Tools maps HTTP socket++# No PartOf=openvibe-tools-maps.service: the socket must outlive every restart.++++[Socket]++# Loopback only: the gateway is the only client (the host firewall is off, so 0.0.0.0 would be public).++ListenStream=127.0.0.1:4010++NoDelay=true++Backlog=511++++[Install]++WantedBy=sockets.target@@@ -494,7 +494,10 @@ app.get('*', (req, res) => {});// ── Start ──────────────────────────────────────────────────--const server = app.listen(config.port, config.host, () => {++// systemd hands this process the listener when deploy/systemd/openvibe-tools-maps.socket is installed: a restart then++// queues the gateway's requests in the kernel instead of refusing them. Otherwise it listens on the port itself.++const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;++const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {console.log(`[Maps.OpenVibe] 🗺️ maps.openvibe.tools listening on ${config.host}:${config.port}`);});nnew file mode 100644@@@ -0,0 +1,9 @@++# Bind the service to the socket unit that owns its listener. A drop-in, so the main unit stays usable on a box that++# has not been switched over to socket activation.++[Unit]++Requires=openvibe-tools-text.socket++After=openvibe-tools-text.socket++++[Service]++# Node's libuv expects a non-blocking descriptor. (A [Service] option: in [Socket] systemd ignores it.)++NonBlocking=truennew file mode 100644@@@ -0,0 +1,17 @@++# OpenVibe.Tools text app — listening socket, owned by systemd rather than by the process (as the gateway's).++#++# The gateway (openvibe-tools.service) proxies this app's pages and API to 127.0.0.1:4015. Without this, each restart++# closed the listener for the whole boot and the gateway got ECONNREFUSED; with it, requests that arrive mid-restart++# wait in the kernel's accept queue and are served as soon as the new process calls accept().++[Unit]++Description=OpenVibe.Tools text HTTP socket++# No PartOf=openvibe-tools-text.service: the socket must outlive every restart.++++[Socket]++# Loopback only: the gateway is the only client (the host firewall is off, so 0.0.0.0 would be public).++ListenStream=127.0.0.1:4015++NoDelay=true++Backlog=511++++[Install]++WantedBy=sockets.target@@@ -168,7 +168,10 @@ app.get('*', (req, res) => {});// ── Start ────────────────────────────────────────────────────--const server = app.listen(config.port, config.host, () => {++// systemd hands this process the listener when deploy/systemd/openvibe-tools-text.socket is installed: a restart then++// queues the gateway's requests in the kernel instead of refusing them. Otherwise it listens on the port itself.++const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;++const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {const toolCount = new Set(Object.values(HOSTNAME_MAP)).size;const domainCount = Object.keys(HOSTNAME_MAP).length;console.log(`\n╔═══════════════════════════════════════╗`);nnew file mode 100644@@@ -0,0 +1,9 @@++# Bind the service to the socket unit that owns its listener. A drop-in, so the main unit stays usable on a box that++# has not been switched over to socket activation.++[Unit]++Requires=openvibe-tools-yt.socket++After=openvibe-tools-yt.socket++++[Service]++# Node's libuv expects a non-blocking descriptor. (A [Service] option: in [Socket] systemd ignores it.)++NonBlocking=truennew file mode 100644@@@ -0,0 +1,17 @@++# OpenVibe.Tools yt app — listening socket, owned by systemd rather than by the process (as the gateway's).++#++# The gateway (openvibe-tools.service) proxies this app's pages and API to 127.0.0.1:4013. Without this, each restart++# closed the listener for the whole boot and the gateway got ECONNREFUSED; with it, requests that arrive mid-restart++# wait in the kernel's accept queue and are served as soon as the new process calls accept().++[Unit]++Description=OpenVibe.Tools yt HTTP socket++# No PartOf=openvibe-tools-yt.service: the socket must outlive every restart.++++[Socket]++# Loopback only: the gateway is the only client (the host firewall is off, so 0.0.0.0 would be public).++ListenStream=127.0.0.1:4013++NoDelay=true++Backlog=511++++[Install]++WantedBy=sockets.target@@@ -317,7 +317,10 @@ app.use((req, res) => {});// ── Start ────────────────────────────────────────────────────--const server = app.listen(config.port, config.host, () => {++// systemd hands this process the listener when deploy/systemd/openvibe-tools-yt.socket is installed: a restart then++// queues the gateway's requests in the kernel instead of refusing them. Otherwise it listens on the port itself.++const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;++const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {downloader.startCleanup();downloader.startUpstreamProbe();console.log(`\n╔═══════════════════════════════════════╗`);
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+# Bind the service to the socket unit that owns its listener. A drop-in, so the main unit stays usable on a box that
+# has not been switched over to socket activation.
+[Unit]
+Requires=openvibe-tools-audio.socket
+After=openvibe-tools-audio.socket
+
+[Service]
+# Node's libuv expects a non-blocking descriptor. (A [Service] option: in [Socket] systemd ignores it.)
+NonBlocking=true
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+# OpenVibe.Tools audio app — listening socket, owned by systemd rather than by the process (as the gateway's).
+#
+# The gateway (openvibe-tools.service) proxies this app's pages and API to 127.0.0.1:4014. Without this, each restart
+# closed the listener for the whole boot and the gateway got ECONNREFUSED; with it, requests that arrive mid-restart
+# wait in the kernel's accept queue and are served as soon as the new process calls accept().
+[Unit]
+Description=OpenVibe.Tools audio HTTP socket
+# No PartOf=openvibe-tools-audio.service: the socket must outlive every restart.
+
+[Socket]
+# Loopback only: the gateway is the only client (the host firewall is off, so 0.0.0.0 would be public).
+ListenStream=127.0.0.1:4014
+NoDelay=true
+Backlog=511
+
+[Install]
+WantedBy=sockets.target
@@ -315,7 +315,10 @@ app.get('*', (req, res) => {
@@ -315,7 +315,10 @@ app.get('*', (req, res) => {
});
});
// ── Start ────────────────────────────────────────────────────
// ── Start ────────────────────────────────────────────────────
-const server = app.listen(config.port, config.host, () => {
+// systemd hands this process the listener when deploy/systemd/openvibe-tools-audio.socket is installed: a restart then
+// queues the gateway's requests in the kernel instead of refusing them. Otherwise it listens on the port itself.
+const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;
+const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {
retention.startCleanup();
retention.startCleanup();
console.log(`\n╔═══════════════════════════════════════╗`);
console.log(`\n╔═══════════════════════════════════════╗`);
console.log(`║ 🎵 Audio.OpenVibe — Audio Processing Hub ║`);
console.log(`║ 🎵 Audio.OpenVibe — Audio Processing Hub ║`);
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+# Bind the service to the socket unit that owns its listener. A drop-in, so the main unit stays usable on a box that
+# has not been switched over to socket activation.
+[Unit]
+Requires=openvibe-tools-docs.socket
+After=openvibe-tools-docs.socket
+
+[Service]
+# Node's libuv expects a non-blocking descriptor. (A [Service] option: in [Socket] systemd ignores it.)
+NonBlocking=true
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+# OpenVibe.Tools docs app — listening socket, owned by systemd rather than by the process (as the gateway's).
+#
+# The gateway (openvibe-tools.service) proxies this app's pages and API to 127.0.0.1:4016. Without this, each restart
+# closed the listener for the whole boot and the gateway got ECONNREFUSED; with it, requests that arrive mid-restart
+# wait in the kernel's accept queue and are served as soon as the new process calls accept().
+[Unit]
+Description=OpenVibe.Tools docs HTTP socket
+# No PartOf=openvibe-tools-docs.service: the socket must outlive every restart.
+
+[Socket]
+# Loopback only: the gateway is the only client (the host firewall is off, so 0.0.0.0 would be public).
+ListenStream=127.0.0.1:4016
+NoDelay=true
+Backlog=511
+
+[Install]
+WantedBy=sockets.target
@@ -311,7 +311,10 @@ app.get('*', (req, res) => {
@@ -311,7 +311,10 @@ app.get('*', (req, res) => {
});
});
// ── Start ────────────────────────────────────────────────────
// ── Start ────────────────────────────────────────────────────
-const server = app.listen(config.port, config.host, () => {
+// systemd hands this process the listener when deploy/systemd/openvibe-tools-docs.socket is installed: a restart then
+// queues the gateway's requests in the kernel instead of refusing them. Otherwise it listens on the port itself.
+const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;
+const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {
retention.startCleanup();
retention.startCleanup();
console.log(`\n╔═══════════════════════════════════════╗`);
console.log(`\n╔═══════════════════════════════════════╗`);
console.log(`║ 📄 Docs.OpenVibe — Document Tools Hub ║`);
console.log(`║ 📄 Docs.OpenVibe — Document Tools Hub ║`);
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+# Bind the service to the socket unit that owns its listener. A drop-in, so the main unit stays usable on a box that
+# has not been switched over to socket activation.
+[Unit]
+Requires=openvibe-tools-food.socket
+After=openvibe-tools-food.socket
+
+[Service]
+# Node's libuv expects a non-blocking descriptor. (A [Service] option: in [Socket] systemd ignores it.)
+NonBlocking=true
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+# OpenVibe.Tools food app — listening socket, owned by systemd rather than by the process (as the gateway's).
+#
+# The gateway (openvibe-tools.service) proxies this app's pages and API to 127.0.0.1:4011. Without this, each restart
+# closed the listener for the whole boot and the gateway got ECONNREFUSED; with it, requests that arrive mid-restart
+# wait in the kernel's accept queue and are served as soon as the new process calls accept().
+[Unit]
+Description=OpenVibe.Tools food HTTP socket
+# No PartOf=openvibe-tools-food.service: the socket must outlive every restart.
+
+[Socket]
+# Loopback only: the gateway is the only client (the host firewall is off, so 0.0.0.0 would be public).
+ListenStream=127.0.0.1:4011
+NoDelay=true
+Backlog=511
+
+[Install]
+WantedBy=sockets.target
@@ -171,7 +171,10 @@ app.get('/api/internal/analytics/bots', internalAccess, async (req, res) => {
@@ -171,7 +171,10 @@ app.get('/api/internal/analytics/bots', internalAccess, async (req, res) => {
// What used to be the page's own paths go to OpenVibe.Food as well (the API answers JSON 404s).
// What used to be the page's own paths go to OpenVibe.Food as well (the API answers JSON 404s).
app.get('*', (req, res) => (req.path.startsWith('/api/') ? res.status(404).json({ error: 'Not found' }) : toFood(req, res)));
app.get('*', (req, res) => (req.path.startsWith('/api/') ? res.status(404).json({ error: 'Not found' }) : toFood(req, res)));
-const server = app.listen(PORT, '127.0.0.1', () => {
+// systemd hands this process the listener when deploy/systemd/openvibe-tools-food.socket is installed: a restart then
+// queues the gateway's requests in the kernel instead of refusing them. Otherwise it listens on the port itself.
+const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;
+const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [PORT, '127.0.0.1']), () => {
console.log(`[Food.OpenVibe] 🍽️ food.openvibe.tools listening on 127.0.0.1:${PORT}`);
console.log(`[Food.OpenVibe] 🍽️ food.openvibe.tools listening on 127.0.0.1:${PORT}`);
});
});
@@ -31,4 +31,22 @@ const index = fs.readFileSync(path.join(__dirname, '..', 'server', 'index.js'),
@@ -31,4 +31,22 @@ const index = fs.readFileSync(path.join(__dirname, '..', 'server', 'index.js'),
assert.match(index, /process\.env\.LISTEN_FDS === '1' && Number\(process\.env\.LISTEN_PID\) === process\.pid/, 'fd 3 only when handed to this process');
assert.match(index, /process\.env\.LISTEN_FDS === '1' && Number\(process\.env\.LISTEN_PID\) === process\.pid/, 'fd 3 only when handed to this process');
assert.ok(index.includes('const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {'.trim()), 'otherwise the configured port');
assert.ok(index.includes('const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {'.trim()), 'otherwise the configured port');
+// The apps behind the gateway: the same rules, each on its own port.
+const APPS = { audio: 4014, docs: 4016, food: 4011, img: 4012, maps: 4010, text: 4015, yt: 4013 };
+for (const [app, port] of Object.entries(APPS)) {
+ const dir = path.join(__dirname, '..', '..', app, 'deploy', 'systemd');
+ const rd = (...p) => fs.readFileSync(path.join(dir, ...p), 'utf8');
+ const sock = rd(`openvibe-tools-${app}.socket`);
+ assert.deepStrictEqual([...section(sock, 'Socket').matchAll(/^ListenStream=(.+)$/gm)].map((m) => m[1].trim()), [`127.0.0.1:${port}`], `${app}: loopback, its port`);
+ assert.ok(!/^NonBlocking=/m.test(section(sock, 'Socket')) && !/^PartOf=/m.test(section(sock, 'Unit')), `${app}: no NonBlocking/PartOf in the socket`);
+ const drop = rd(`openvibe-tools-${app}.service.d`, 'socket.conf');
+ assert.match(section(drop, 'Unit'), new RegExp(`^Requires=openvibe-tools-${app}\\.socket$`, 'm'), app);
+ assert.match(section(drop, 'Unit'), new RegExp(`^After=openvibe-tools-${app}\\.socket$`, 'm'), app);
+ assert.match(section(drop, 'Service'), /^NonBlocking=true$/m, app);
+ assert.match(rd(`openvibe-tools-${app}.service`), new RegExp(`^Environment=PORT=${port}$`, 'm'), `${app}: the unit's PORT is the socket's`);
+ const src = fs.readFileSync(path.join(__dirname, '..', '..', app, 'server', 'index.js'), 'utf8');
+ assert.match(src, /process\.env\.LISTEN_FDS === '1' && Number\(process\.env\.LISTEN_PID\) === process\.pid/, `${app}: fd 3 only when handed to this process`);
+ assert.match(src, /app\.listen\(\.\.\.\(socketActivated \? \[\{ fd: 3 \}\] : \[/, `${app}: otherwise its port`);
+}
+
console.log('systemd units: all checks passed');
console.log('systemd units: all checks passed');
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+# Bind the service to the socket unit that owns its listener. A drop-in, so the main unit stays usable on a box that
+# has not been switched over to socket activation.
+[Unit]
+Requires=openvibe-tools-img.socket
+After=openvibe-tools-img.socket
+
+[Service]
+# Node's libuv expects a non-blocking descriptor. (A [Service] option: in [Socket] systemd ignores it.)
+NonBlocking=true
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+# OpenVibe.Tools img app — listening socket, owned by systemd rather than by the process (as the gateway's).
+#
+# The gateway (openvibe-tools.service) proxies this app's pages and API to 127.0.0.1:4012. Without this, each restart
+# closed the listener for the whole boot and the gateway got ECONNREFUSED; with it, requests that arrive mid-restart
+# wait in the kernel's accept queue and are served as soon as the new process calls accept().
+[Unit]
+Description=OpenVibe.Tools img HTTP socket
+# No PartOf=openvibe-tools-img.service: the socket must outlive every restart.
+
+[Socket]
+# Loopback only: the gateway is the only client (the host firewall is off, so 0.0.0.0 would be public).
+ListenStream=127.0.0.1:4012
+NoDelay=true
+Backlog=511
+
+[Install]
+WantedBy=sockets.target
@@ -278,7 +278,10 @@ app.get('*', (req, res) => {
@@ -278,7 +278,10 @@ app.get('*', (req, res) => {
});
});
// ── Start ────────────────────────────────────────────────────
// ── Start ────────────────────────────────────────────────────
-const server = app.listen(config.port, config.host, () => {
+// systemd hands this process the listener when deploy/systemd/openvibe-tools-img.socket is installed: a restart then
+// queues the gateway's requests in the kernel instead of refusing them. Otherwise it listens on the port itself.
+const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;
+const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {
retention.startCleanup();
retention.startCleanup();
console.log(`\n╔═══════════════════════════════════════╗`);
console.log(`\n╔═══════════════════════════════════════╗`);
console.log(`║ 🖼️ Img.OpenVibe — Image Conversion Hub ║`);
console.log(`║ 🖼️ Img.OpenVibe — Image Conversion Hub ║`);
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+# Bind the service to the socket unit that owns its listener. A drop-in, so the main unit stays usable on a box that
+# has not been switched over to socket activation.
+[Unit]
+Requires=openvibe-tools-maps.socket
+After=openvibe-tools-maps.socket
+
+[Service]
+# Node's libuv expects a non-blocking descriptor. (A [Service] option: in [Socket] systemd ignores it.)
+NonBlocking=true
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+# OpenVibe.Tools maps app — listening socket, owned by systemd rather than by the process (as the gateway's).
+#
+# The gateway (openvibe-tools.service) proxies this app's pages and API to 127.0.0.1:4010. Without this, each restart
+# closed the listener for the whole boot and the gateway got ECONNREFUSED; with it, requests that arrive mid-restart
+# wait in the kernel's accept queue and are served as soon as the new process calls accept().
+[Unit]
+Description=OpenVibe.Tools maps HTTP socket
+# No PartOf=openvibe-tools-maps.service: the socket must outlive every restart.
+
+[Socket]
+# Loopback only: the gateway is the only client (the host firewall is off, so 0.0.0.0 would be public).
+ListenStream=127.0.0.1:4010
+NoDelay=true
+Backlog=511
+
+[Install]
+WantedBy=sockets.target
@@ -494,7 +494,10 @@ app.get('*', (req, res) => {
@@ -494,7 +494,10 @@ app.get('*', (req, res) => {
});
});
// ── Start ──────────────────────────────────────────────────
// ── Start ──────────────────────────────────────────────────
-const server = app.listen(config.port, config.host, () => {
+// systemd hands this process the listener when deploy/systemd/openvibe-tools-maps.socket is installed: a restart then
+// queues the gateway's requests in the kernel instead of refusing them. Otherwise it listens on the port itself.
+const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;
+const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {
console.log(`[Maps.OpenVibe] 🗺️ maps.openvibe.tools listening on ${config.host}:${config.port}`);
console.log(`[Maps.OpenVibe] 🗺️ maps.openvibe.tools listening on ${config.host}:${config.port}`);
});
});
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+# Bind the service to the socket unit that owns its listener. A drop-in, so the main unit stays usable on a box that
+# has not been switched over to socket activation.
+[Unit]
+Requires=openvibe-tools-text.socket
+After=openvibe-tools-text.socket
+
+[Service]
+# Node's libuv expects a non-blocking descriptor. (A [Service] option: in [Socket] systemd ignores it.)
+NonBlocking=true
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+# OpenVibe.Tools text app — listening socket, owned by systemd rather than by the process (as the gateway's).
+#
+# The gateway (openvibe-tools.service) proxies this app's pages and API to 127.0.0.1:4015. Without this, each restart
+# closed the listener for the whole boot and the gateway got ECONNREFUSED; with it, requests that arrive mid-restart
+# wait in the kernel's accept queue and are served as soon as the new process calls accept().
+[Unit]
+Description=OpenVibe.Tools text HTTP socket
+# No PartOf=openvibe-tools-text.service: the socket must outlive every restart.
+
+[Socket]
+# Loopback only: the gateway is the only client (the host firewall is off, so 0.0.0.0 would be public).
+ListenStream=127.0.0.1:4015
+NoDelay=true
+Backlog=511
+
+[Install]
+WantedBy=sockets.target
@@ -168,7 +168,10 @@ app.get('*', (req, res) => {
@@ -168,7 +168,10 @@ app.get('*', (req, res) => {
});
});
// ── Start ────────────────────────────────────────────────────
// ── Start ────────────────────────────────────────────────────
-const server = app.listen(config.port, config.host, () => {
+// systemd hands this process the listener when deploy/systemd/openvibe-tools-text.socket is installed: a restart then
+// queues the gateway's requests in the kernel instead of refusing them. Otherwise it listens on the port itself.
+const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;
+const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {
const toolCount = new Set(Object.values(HOSTNAME_MAP)).size;
const toolCount = new Set(Object.values(HOSTNAME_MAP)).size;
const domainCount = Object.keys(HOSTNAME_MAP).length;
const domainCount = Object.keys(HOSTNAME_MAP).length;
console.log(`\n╔═══════════════════════════════════════╗`);
console.log(`\n╔═══════════════════════════════════════╗`);
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+# Bind the service to the socket unit that owns its listener. A drop-in, so the main unit stays usable on a box that
+# has not been switched over to socket activation.
+[Unit]
+Requires=openvibe-tools-yt.socket
+After=openvibe-tools-yt.socket
+
+[Service]
+# Node's libuv expects a non-blocking descriptor. (A [Service] option: in [Socket] systemd ignores it.)
+NonBlocking=true
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+# OpenVibe.Tools yt app — listening socket, owned by systemd rather than by the process (as the gateway's).
+#
+# The gateway (openvibe-tools.service) proxies this app's pages and API to 127.0.0.1:4013. Without this, each restart
+# closed the listener for the whole boot and the gateway got ECONNREFUSED; with it, requests that arrive mid-restart
+# wait in the kernel's accept queue and are served as soon as the new process calls accept().
+[Unit]
+Description=OpenVibe.Tools yt HTTP socket
+# No PartOf=openvibe-tools-yt.service: the socket must outlive every restart.
+
+[Socket]
+# Loopback only: the gateway is the only client (the host firewall is off, so 0.0.0.0 would be public).
+ListenStream=127.0.0.1:4013
+NoDelay=true
+Backlog=511
+
+[Install]
+WantedBy=sockets.target
@@ -317,7 +317,10 @@ app.use((req, res) => {
@@ -317,7 +317,10 @@ app.use((req, res) => {
});
});
// ── Start ────────────────────────────────────────────────────
// ── Start ────────────────────────────────────────────────────
-const server = app.listen(config.port, config.host, () => {
+// systemd hands this process the listener when deploy/systemd/openvibe-tools-yt.socket is installed: a restart then
+// queues the gateway's requests in the kernel instead of refusing them. Otherwise it listens on the port itself.
+const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;
+const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {
downloader.startCleanup();
downloader.startCleanup();
downloader.startUpstreamProbe();
downloader.startUpstreamProbe();
console.log(`\n╔═══════════════════════════════════════╗`);
console.log(`\n╔═══════════════════════════════════════╗`);