🌳
pt0/nextF/deployF/startDevServerAI.mts
3import { execSync } from 'node:child_process'
6import { fileURLToPath } from 'node:url'
14const killPort = (port: number | undefined) => {
15 try { execSync(`lsof -ti:${port} -sTCP:LISTEN | xargs kill -9 2>/dev/null`, { stdio: 'ignore' }) } catch {}
18const checkPortFree = async (port: number | undefined) => {
19 try { await fetch(`http://127.0.0.1:${port}`, { signal: AbortSignal.timeout(300) }); return false }
20 catch { return true }
23const getPortOwner = (port: number | undefined) => {
24 try { return execSync(`lsof -i:${port} 2>/dev/null || true`).toString().trim() }
25 catch { return '' }
27export const startDevServer = async ({envH = {}, isQuiet = true, captureOutput = false, onStop, testInstance = true}: {envH?: Record<string, string>, isQuiet?: boolean, captureOutput?: boolean, onStop?: () => void, testInstance?: boolean} = {}) => {
28 const {appPath, importMetaUrl, svcPortNo, testSvcPortNo} = getAppCfg()
29 assertDefined(importMetaUrl)
30 const port = testInstance ? (testSvcPortNo ?? svcPortNo) : svcPortNo
31 if (testInstance) envH = {...envH, NEXT_SVC_PORT_NO: String(port)}
32 const appDir = `${ptDir}/${appPath}`
33 const syncScript = fileURLToPath(importMetaUrl)
35 killPort(port)
36 const portFreed = await pollUntil(async () => {
37 if (await checkPortFree(port)) return true
38 killPort(port)
39 return false
40 }, {timeoutMs: 10000})
41 if (!portFreed) {
42 const owner = getPortOwner(port)
43 throw new Error(`Port ${port} in use by another process (couldn't kill). Stop it manually:\n${owner}`)
44 }
46 const abortController = new AbortController()
47 const envStr = Object.entries(envH).map(([k, v]) => `${k}=${v}`).join(' ')
48 const cmd = `cd ${appDir} && ${envStr ? envStr + ' ' : ''}PT_BG_TASK=1 node ${syncScript} devserver`
50 // Always capture output for debugging; also used by tests when captureOutput=true
51 const capturedOutput = { stdout: '', stderr: '' }
52 const onDataFnc = (data: string) => {
53 capturedOutput.stdout += data
54 return undefined
55 }
57 const serverPromise = liveSpawn({ cmd, signal: abortController.signal, isQuiet, noOutCmd: true, onDataFnc })
59 const exitHandler = () => {
60 if (!abortController.signal.aborted) {
61 try { execSync(`lsof -ti:${port} -sTCP:LISTEN | xargs kill -9 2>/dev/null`, { stdio: 'ignore' }) } catch {} // catch:userapproved
62 }
63 }
64 process.on('exit', exitHandler)
66 const devHost = testInstance ? `127.0.0.1:${port}` : ((getPubEnv().gpSiteHost as string | undefined)?.includes('.localhost:') ? getPubEnv().gpSiteHost as string : `127.0.0.1:${port}`)
67 const ready = await pollUntil(async () => {
68 try {
69 const r = await fetch(`http://${devHost}`, {signal: AbortSignal.timeout(2000)})
70 return r.ok || r.status === 404
71 } catch { return false }
72 }, {timeoutMs: 90000, intervalMs: 1000})
74 if (!ready) {
75 abortController.abort()
76 await sleep(300) // let subprocess flush output
77 const output = capturedOutput.stdout.trim()
78 if (output) {
79 console.log('\n--- devserver failed to start ---')
80 console.log(output.slice(-2000))
81 console.log('---')
82 }
83 const logPath = process.env.PTNODE_LOG_PATH
84 if (logPath) console.log(`fullLog: ${shortPtPath(logPath)}`)
85 throPtErr('devServerNotReady', {port, appDir})
86 }
88 const stop = async () => {
89 process.removeListener('exit', exitHandler)
90 onStop?.()
91 abortController.abort()
92 await serverPromise.catch(() => {})
93 await sleep(2000)
94 }
96 if (captureOutput) {
97 return { stop, getOutput: () => capturedOutput.stdout + capturedOutput.stderr }
98 }
99 return stop