| … | … | @@ -115,6 +115,30 @@ const ATPROTO_OPS = { |
| 115 | 115 | * ⚠ Binds 127.0.0.1 explicitly. bare-http1's `listen(port)` defaults to all interfaces on some |
| 116 | 116 | * platforms, and a favela node answering on a phone's Wi-Fi address is not what anyone asked for. |
| 117 | 117 | */ |
| 118 | /** |
| 119 | * Bind `port` if we can, otherwise any free one. |
| 120 | * |
| 121 | * ⛔ A STABLE PORT, BECAUSE THE WEBVIEW HOLDS THE URL. Every restart used to bind a fresh ephemeral |
| 122 | * port, so the page the user was looking at pointed at a dead server the moment the node came back — |
| 123 | * which is why "restart to apply" looked like it did nothing and the banner appeared frozen. The node |
| 124 | * HAD restarted and was serving the new bundle; nobody could see it. |
| 125 | * |
| 126 | * ⚠ Falls back rather than failing. Another app can hold the remembered port, and refusing to boot over |
| 127 | * a port number would be a far worse failure than a changed URL. |
| 128 | */ |
| 129 | export function listenPreferring (server, port, host = '127.0.0.1') { |
| 130 | return new Promise((resolve, reject) => { |
| 131 | const onError = (err) => { |
| 132 | if (!port) return reject(err) |
| 133 | server.removeListener('error', onError) |
| 134 | server.once('error', reject) |
| 135 | server.listen(0, host, resolve) |
| 136 | } |
| 137 | server.once('error', onError) |
| 138 | server.listen(port, host, () => { server.removeListener('error', onError); resolve() }) |
| 139 | }) |
| 140 | } |
| 141 | |
| 118 | 142 | export async function serveSurface (node, { port = 0, sessions = new Sessions(), appUpdate = null } = {}) { |
| 119 | 143 | const client = inProcessClient(node) |
| 120 | 144 | const server = http.createServer((req, res) => { |
| … | … | @@ -126,10 +150,14 @@ export async function serveSurface (node, { port = 0, sessions = new Sessions(), |
| 126 | 150 | res.end(String((err && err.message) || err)) |
| 127 | 151 | }) |
| 128 | 152 | }) |
| 129 | | await new Promise((resolve, reject) => { |
| 130 | | server.once('error', reject) |
| 131 | | server.listen(port, '127.0.0.1', resolve) |
| 132 | | }) |
| 153 | // ⛔ A STABLE PORT, BECAUSE THE WEBVIEW HOLDS THE URL. Every restart used to bind a fresh ephemeral |
| 154 | // port, so the page the user was looking at pointed at a dead server the moment the node came back — |
| 155 | // which is why "restart to apply" looked like it did nothing and the banner appeared frozen. The node |
| 156 | // HAD restarted and was serving the new bundle; nobody could see it. |
| 157 | // |
| 158 | // ⚠ Falls back to ephemeral if the remembered port is taken. Another app can hold it, and refusing to |
| 159 | // boot over a port number would be a far worse failure than a changed URL. |
| 160 | await listenPreferring(server, port) |
| 133 | 161 | const bound = server.address().port |
| 134 | 162 | return { server, sessions, client, port: bound, url: 'http://127.0.0.1:' + bound } |
| 135 | 163 | } |
| … | … | @@ -141,6 +169,8 @@ export async function serveSurface (node, { port = 0, sessions = new Sessions(), |
| 141 | 169 | // is why attesting one would hand a WebView the power to mint devices. The app authenticates; the |
| 142 | 170 | // daemon writes. |
| 143 | 171 | const BROWSER_KEY = 'browser.key' |
| 172 | // Where the last bound surface port is remembered, so a restart serves the same URL the WebView holds. |
| 173 | const PORT_FILE = 'surface-port' |
| 144 | 174 | |
| 145 | 175 | /** |
| 146 | 176 | * Load or mint this app's anproto keypair. |
| … | … | @@ -246,7 +276,11 @@ export async function boot (storage) { |
| 246 | 276 | const appUpdate = () => { |
| 247 | 277 | try { return JSON.parse(fs.readFileSync(join(storage, 'app-update.json'), 'utf8')) } catch { return null } |
| 248 | 278 | } |
| 249 | | surface = await serveSurface(node, { port: 0, appUpdate }) |
| 279 | // Reuse the port this app bound last time so the WebView's URL survives a restart. See serveSurface. |
| 280 | let wantedPort = 0 |
| 281 | try { wantedPort = Number(String(fs.readFileSync(join(storage, PORT_FILE), 'utf8')).trim()) || 0 } catch {} |
| 282 | surface = await serveSurface(node, { port: wantedPort, appUpdate }) |
| 283 | try { fs.writeFileSync(join(storage, PORT_FILE), String(surface.port)) } catch { /* best effort */ } |
| 250 | 284 | state.ready = true |
| 251 | 285 | state.feed = node.key.toString('hex') |
| 252 | 286 | state.url = surface.url |