(ip: string, guestPort: number)
| 102 | |
| 103 | /** SSH local-forward host:localPort → guest:guestPort; resolves once it binds. */ |
| 104 | export const guestTunnel = async (ip: string, guestPort: number): Promise<Forward> => { |
| 105 | const localPort = await freePort(); |
| 106 | const child = spawn( |
| 107 | SSHPASS, |
| 108 | [ |
| 109 | "-p", |
| 110 | GUEST_PASS, |
| 111 | "ssh", |
| 112 | ...SSH_OPTS, |
| 113 | "-N", |
| 114 | "-L", |
| 115 | `${localPort}:127.0.0.1:${guestPort}`, |
| 116 | `${GUEST_USER}@${ip}`, |
| 117 | ], |
| 118 | { stdio: "ignore" }, |
| 119 | ); |
| 120 | for (let i = 0; i < 40; i++) { |
| 121 | const ok = await new Promise<boolean>((resolve) => { |
| 122 | const sock = net.connect({ host: "127.0.0.1", port: localPort }, () => { |
| 123 | sock.destroy(); |
| 124 | resolve(true); |
| 125 | }); |
| 126 | sock.on("error", () => resolve(false)); |
| 127 | sock.setTimeout(1000, () => { |
| 128 | sock.destroy(); |
| 129 | resolve(false); |
| 130 | }); |
| 131 | }); |
| 132 | if (ok) break; |
| 133 | await sleep(500); |
| 134 | } |
| 135 | return { localPort, close: () => child.kill() }; |
| 136 | }; |
| 137 | |
| 138 | const guestFileSize = (ip: string, remote: string): Promise<number> => |
| 139 | guestSsh(ip, `stat -f%z ${remote} 2>/dev/null || stat -c%s ${remote} 2>/dev/null || echo 0`) |
no test coverage detected