|
#!/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)); |
|
} |
|
} |