Skip to content

Instantly share code, notes, and snippets.

@lideming
Last active October 24, 2023 17:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lideming/dd7ec6911caa3b4f89d75e05bc1d2263 to your computer and use it in GitHub Desktop.
Save lideming/dd7ec6911caa3b4f89d75e05bc1d2263 to your computer and use it in GitHub Desktop.
TCP Socket activation process/docker by Deno

Install

Copy the script into somewhere (or PATH) and chmod +x them.

Deno is required.

Socket activating process

./socket_activation.ts -l 0.0.0.0:1234 -p 127.0.0.1:1235 --idle-timeout 300 ./my_service
  • Script listens at port 1234 of all interfaces, while my_service will listen at port 1235.
  • Runs ./my_service program/script on connect.
  • Stops (SIGTERM) the process if no connections for 300 seconds. (--idle-timeout)

Docker start/stop (with helper script)

./socket_activation.ts -l 0.0.0.0:1234 -p 127.0.0.1:1235 --idle-timeout 300 ./start_docker.sh my_container

The start_docker.sh script will docker start the container. Also docker stop it on SIGTERM.

Options

See the first few lines of the socket_activation.ts source code.

#!/usr/bin/env -S deno run --allow-net --allow-run
const helpText = `Usage: -l <listen_addr>:<port> -t <target_addr>:<port> <program> <program_arg>...
Optional:
--wait-start <sec> wait after start before connect to target (default: 10)
--idle-timeout <sec> terminate program after idle (default: disabled(0))
--idle-sig <sig> signal to send for termination (default: SIGTERM)
--kill-timeout <sec> kill (SIGKILL) program after trying termination (default: disabled(0))
--wait-before-start <sec> check connection can be read before starting process (default: disabled(0))
`;
import { parse } from "https://deno.land/std@0.177.0/flags/mod.ts";
const cmdOpts = parse(Deno.args, {
stopEarly: true,
});
if (cmdOpts.help || !cmdOpts.l || !cmdOpts.t) {
console.error(helpText);
Deno.exit(1);
}
const listenAddr = splitAddress(cmdOpts.l);
const targetAddr = splitAddress(cmdOpts.t);
const cmd = cmdOpts._.map(String);
const sec = 1000;
const waitStart = (cmdOpts["wait-start"] || 10) * sec;
const waitBeforeStart = (cmdOpts["wait-before-start"] || 0) * sec;
const idleTimeout = (cmdOpts["idle-timeout"] || 0) * sec;
const killTimeout = (cmdOpts["kill-timeout"] || 0) * sec;
const idleSig = cmdOpts["idle-sig"] || "SIGTERM";
socketActivation(listenAddr, targetAddr, cmd);
function socketActivation(
listenAddr: Deno.TcpListenOptions,
targetAddr: Deno.ConnectOptions,
cmd: string[],
) {
const listener = Deno.listen(listenAddr);
log("[connection] listening", listenAddr);
log("[process] run on activation:", cmd);
listenLoop();
async function listenLoop() {
while (true) {
const conn = await listener.accept();
log("[connection] accepted from", conn.remoteAddr);
onConnection(conn);
}
}
async function onConnection(client: Deno.Conn) {
let target;
let addedConnection = false;
try {
let firstRead: Uint8Array | null = null;
if (connections === 0 && waitBeforeStart) {
log("[connection] checking first connection", client.remoteAddr);
const buf = new Uint8Array(4096);
await Promise.race([
client.read(buf).then((hasRead) => {
if (hasRead) firstRead = buf.subarray(0, hasRead);
}),
new Promise((r) => setTimeout(r, waitBeforeStart)),
]);
if (!firstRead) {
log(
"[connection] not starting process: cannot read",
client.remoteAddr,
);
return;
}
}
addedConnection = true;
addConnection();
const waitFor = startTime + waitStart - Date.now();
if (waitFor > 0) {
await new Promise((r) => setTimeout(r, waitFor));
}
target = await Deno.connect(targetAddr);
if (firstRead) {
await writeAll(target, firstRead, (firstRead as Uint8Array).length);
}
await Promise.all([connCopy(client, target), connCopy(target, client)]);
} catch (error) {
// console.error("connection error", error);
} finally {
log("[connection] closed", client.remoteAddr);
client.close();
target?.close();
if (addedConnection) removeConnection();
}
}
async function connCopy(from: Deno.Conn, to: Deno.Conn) {
const buf = new Uint8Array(64 * 1024);
while (true) {
const haveRead = await from.read(buf);
if (!haveRead) break;
await writeAll(to, buf, haveRead);
}
await to.closeWrite();
}
let connections = 0;
let proc: Deno.Process | null = null;
let startTime = 0;
let termTimer = 0;
let killTimer = 0;
function addConnection() {
connections++;
log("[connection]", connections, "connections");
clearTimeout(termTimer);
if (!proc) {
log("[process] starting");
startTime = Date.now();
proc = Deno.run({ cmd });
proc.status().then((status) => {
proc = null;
clearTimeout(termTimer);
termTimer = 0;
clearTimeout(killTimer);
killTimer = 0;
log("[process] exited, code", status.code);
});
}
}
function removeConnection() {
connections--;
log("[connection]", connections, "connections");
if (idleTimeout && connections === 0 && proc) {
clearTimeout(termTimer);
termTimer = setTimeout(() => {
log("[process] idle timeout, sending " + idleSig);
proc?.kill(idleSig);
if (killTimeout) {
killTimer = setTimeout(() => {
log("[process] kill timeout, sending SIGKILL");
proc?.kill("SIGKILL");
}, killTimeout);
}
}, idleTimeout);
}
}
}
function log(...args: any[]) {
console.info(...args, "\r");
}
function splitAddress(str: string) {
const colon = str.lastIndexOf(":");
return { hostname: str.slice(0, colon), port: +str.slice(colon + 1) };
}
async function writeAll(conn: Deno.Conn, buf: Uint8Array, len: number) {
let haveWritten = 0;
while (haveWritten < len) {
haveWritten += await conn.write(buf.subarray(haveWritten, len));
}
}
#!/bin/bash
container="$1"
running=1
trap on_sig 15
on_sig() {
echo stopping container...
sudo docker stop "$container"
wait
exit 0
}
sudo docker start "$container"
sudo docker logs -f "$container" &
while [[ $running == 1 ]] ; do
sleep 1
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment