| 81 | } |
| 82 | |
| 83 | export async function start(opts: StartOptions): Promise<ProcessInfo> { |
| 84 | const existing = processes.get(opts.name); |
| 85 | if (existing && existing.exitCode === null) { |
| 86 | if (opts.replace) { |
| 87 | await stop(opts.name); |
| 88 | } else { |
| 89 | throw new DevServerError(`Process '${opts.name}' is already running (pid ${existing.pid}). Pass replace=true to restart it.`); |
| 90 | } |
| 91 | } |
| 92 | // Use shell: true so things like "npm run dev" and "php -S localhost:8000 -t public" work as-is. |
| 93 | const child = spawn(opts.command, { |
| 94 | cwd: opts.cwd ?? process.cwd(), |
| 95 | env: { ...process.env, ...(opts.env ?? {}) }, |
| 96 | shell: true, |
| 97 | detached: false, |
| 98 | stdio: ['pipe', 'pipe', 'pipe'], |
| 99 | }); |
| 100 | |
| 101 | if (!child.pid) { |
| 102 | throw new DevServerError(`Failed to spawn '${opts.command}'`); |
| 103 | } |
| 104 | |
| 105 | const proc: ManagedProcess = { |
| 106 | name: opts.name, |
| 107 | command: opts.command, |
| 108 | cwd: opts.cwd ?? process.cwd(), |
| 109 | startedAt: Date.now(), |
| 110 | pid: child.pid, |
| 111 | child, |
| 112 | stdout: '', |
| 113 | stderr: '', |
| 114 | combined: '', |
| 115 | exitCode: null, |
| 116 | exitSignal: null, |
| 117 | }; |
| 118 | child.stdout?.on('data', (data: Buffer) => { |
| 119 | const str = data.toString('utf-8'); |
| 120 | proc.stdout = appendCapped(proc.stdout, str); |
| 121 | proc.combined = appendCapped(proc.combined, str); |
| 122 | }); |
| 123 | child.stderr?.on('data', (data: Buffer) => { |
| 124 | const str = data.toString('utf-8'); |
| 125 | proc.stderr = appendCapped(proc.stderr, str); |
| 126 | proc.combined = appendCapped(proc.combined, str); |
| 127 | }); |
| 128 | child.on('exit', (code, signal) => { |
| 129 | proc.exitCode = code; |
| 130 | proc.exitSignal = signal; |
| 131 | logger.info(`Process '${opts.name}' exited`, { pid: proc.pid, code, signal }); |
| 132 | }); |
| 133 | child.on('error', (err) => { |
| 134 | logger.warn(`Process '${opts.name}' error`, { err: err.message }); |
| 135 | }); |
| 136 | processes.set(opts.name, proc); |
| 137 | logger.info(`Started process '${opts.name}'`, { pid: child.pid, command: opts.command }); |
| 138 | return infoFor(proc); |
| 139 | } |
| 140 | |