| 1 | import * as NodeFSP from "node:fs/promises"; |
| 2 | import * as NodeNet from "node:net"; |
| 3 | import * as NodePath from "node:path"; |
| 4 | import * as NodeTimersPromises from "node:timers/promises"; |
| 5 | |
| 6 | const defaultTcpHosts = ["127.0.0.1", "localhost", "::1"]; |
| 7 | |
| 8 | async function fileExists(filePath) { |
| 9 | try { |
| 10 | await NodeFSP.access(filePath); |
| 11 | return true; |
| 12 | } catch { |
| 13 | return false; |
| 14 | } |
| 15 | } |
| 16 | |
| 17 | function tcpPortIsReady({ host, port, connectTimeoutMs = 500 }) { |
| 18 | return new Promise((resolveReady) => { |
| 19 | const socket = NodeNet.createConnection({ host, port }); |
| 20 | let settled = false; |
| 21 | |
| 22 | const finish = (ready) => { |
| 23 | if (settled) { |
| 24 | return; |
| 25 | } |
| 26 | |
| 27 | settled = true; |
| 28 | socket.removeAllListeners(); |
| 29 | socket.destroy(); |
| 30 | resolveReady(ready); |
| 31 | }; |
| 32 | |
| 33 | socket.once("connect", () => { |
| 34 | finish(true); |
| 35 | }); |
| 36 | socket.once("timeout", () => { |
| 37 | finish(false); |
| 38 | }); |
| 39 | socket.once("error", () => { |
| 40 | finish(false); |
| 41 | }); |
| 42 | socket.setTimeout(connectTimeoutMs); |
| 43 | }); |
| 44 | } |
| 45 | |
| 46 | async function resolvePendingResources({ baseDir, files, tcpPort, tcpHosts, connectTimeoutMs }) { |
| 47 | const pendingFiles = []; |
| 48 | |
| 49 | for (const relativeFilePath of files) { |
| 50 | const ready = await fileExists(NodePath.resolve(baseDir, relativeFilePath)); |
| 51 | if (!ready) { |
| 52 | pendingFiles.push(relativeFilePath); |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | let tcpReady = false; |
| 57 | for (const host of tcpHosts) { |
| 58 | tcpReady = await tcpPortIsReady({ |
| 59 | host, |
| 60 | port: tcpPort, |
| 61 | connectTimeoutMs, |
| 62 | }); |
| 63 | if (tcpReady) { |
| 64 | break; |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | return { |
| 69 | pendingFiles, |
| 70 | tcpReady, |
| 71 | }; |
| 72 | } |
| 73 | |
| 74 | export async function waitForResources({ |
| 75 | baseDir, |
| 76 | files = [], |
| 77 | intervalMs = 100, |
| 78 | timeoutMs = 120_000, |
| 79 | tcpHost, |
| 80 | tcpPort, |
| 81 | connectTimeoutMs = 500, |
| 82 | }) { |
| 83 | if (!Number.isInteger(tcpPort) || tcpPort <= 0) { |
| 84 | throw new TypeError("waitForResources requires a positive integer tcpPort"); |
| 85 | } |
| 86 | |
| 87 | const startedAt = Date.now(); |
| 88 | const tcpHosts = tcpHost ? [tcpHost] : defaultTcpHosts; |
| 89 | |
| 90 | while (true) { |
| 91 | const { pendingFiles, tcpReady } = await resolvePendingResources({ |
| 92 | baseDir, |
| 93 | files, |
| 94 | tcpPort, |
| 95 | tcpHosts, |
| 96 | connectTimeoutMs, |
| 97 | }); |
| 98 | |
| 99 | if (pendingFiles.length === 0 && tcpReady) { |
| 100 | return; |
| 101 | } |
| 102 | |
| 103 | if (Date.now() - startedAt >= timeoutMs) { |
| 104 | const pendingResources = []; |
| 105 | if (!tcpReady) { |
| 106 | pendingResources.push(tcpHost ? `tcp:${tcpHost}:${tcpPort}` : `tcp:${tcpPort}`); |
| 107 | } |
| 108 | for (const filePath of pendingFiles) { |
| 109 | pendingResources.push(`file:${filePath}`); |
| 110 | } |
| 111 | |
| 112 | throw new Error( |
| 113 | `Timed out waiting for desktop dev resources after ${timeoutMs}ms: ${pendingResources.join(", ")}`, |
| 114 | ); |
| 115 | } |
| 116 | |
| 117 | await NodeTimersPromises.setTimeout(intervalMs); |
| 118 | } |
| 119 | } |