()
| 260 | // ========== Server Startup ========== |
| 261 | |
| 262 | function startServer() { |
| 263 | if (!fs.existsSync(CONTENT_DIR)) fs.mkdirSync(CONTENT_DIR, { recursive: true }); |
| 264 | if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true }); |
| 265 | |
| 266 | // Track known files to distinguish new screens from updates. |
| 267 | // macOS fs.watch reports 'rename' for both new files and overwrites, |
| 268 | // so we can't rely on eventType alone. |
| 269 | const knownFiles = new Set( |
| 270 | fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.html')) |
| 271 | ); |
| 272 | |
| 273 | const server = http.createServer(handleRequest); |
| 274 | server.on('upgrade', handleUpgrade); |
| 275 | |
| 276 | const watcher = fs.watch(CONTENT_DIR, (eventType, filename) => { |
| 277 | if (!filename || !filename.endsWith('.html')) return; |
| 278 | |
| 279 | if (debounceTimers.has(filename)) clearTimeout(debounceTimers.get(filename)); |
| 280 | debounceTimers.set(filename, setTimeout(() => { |
| 281 | debounceTimers.delete(filename); |
| 282 | const filePath = path.join(CONTENT_DIR, filename); |
| 283 | |
| 284 | if (!fs.existsSync(filePath)) return; // file was deleted |
| 285 | touchActivity(); |
| 286 | |
| 287 | if (!knownFiles.has(filename)) { |
| 288 | knownFiles.add(filename); |
| 289 | const eventsFile = path.join(STATE_DIR, 'events'); |
| 290 | if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile); |
| 291 | console.log(JSON.stringify({ type: 'screen-added', file: filePath })); |
| 292 | } else { |
| 293 | console.log(JSON.stringify({ type: 'screen-updated', file: filePath })); |
| 294 | } |
| 295 | |
| 296 | broadcast({ type: 'reload' }); |
| 297 | }, 100)); |
| 298 | }); |
| 299 | watcher.on('error', (err) => console.error('fs.watch error:', err.message)); |
| 300 | |
| 301 | function shutdown(reason) { |
| 302 | console.log(JSON.stringify({ type: 'server-stopped', reason })); |
| 303 | const infoFile = path.join(STATE_DIR, 'server-info'); |
| 304 | if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile); |
| 305 | fs.writeFileSync( |
| 306 | path.join(STATE_DIR, 'server-stopped'), |
| 307 | JSON.stringify({ reason, timestamp: Date.now() }) + '\n' |
| 308 | ); |
| 309 | watcher.close(); |
| 310 | clearInterval(lifecycleCheck); |
| 311 | server.close(() => process.exit(0)); |
| 312 | } |
| 313 | |
| 314 | function ownerAlive() { |
| 315 | if (!ownerPid) return true; |
| 316 | try { process.kill(ownerPid, 0); return true; } catch (e) { return e.code === 'EPERM'; } |
| 317 | } |
| 318 | |
| 319 | // Check every 60s: exit if owner process died or idle for 30 minutes |
no test coverage detected