@rip-lang/server 0.5.7 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -30
- package/package.json +1 -1
- package/server.rip +207 -202
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
8
8
|
|
|
9
|
-
`@rip-lang/server` is a production-grade application server written entirely in Rip. It provides multi-worker process management, hot
|
|
9
|
+
`@rip-lang/server` is a production-grade application server written entirely in Rip. It provides multi-worker process management, hot reloading, automatic HTTPS, and mDNS service discovery — all in a single ~1,100 line file.
|
|
10
10
|
|
|
11
|
-
- **`server.rip`** (~1,
|
|
11
|
+
- **`server.rip`** (~1,100 lines) — Complete server: CLI, workers, load balancing, TLS, mDNS
|
|
12
12
|
|
|
13
13
|
**Core Philosophy**: Application servers should be simple, fast, and reliable. No complex configuration files. No dependency on external process managers. Just run your app.
|
|
14
14
|
|
|
@@ -54,7 +54,7 @@ rip-server ./app.rip@myapp
|
|
|
54
54
|
Create `app.rip`:
|
|
55
55
|
|
|
56
56
|
```coffee
|
|
57
|
-
import { get,
|
|
57
|
+
import { get, read, start } from '@rip-lang/api'
|
|
58
58
|
|
|
59
59
|
get '/', ->
|
|
60
60
|
'Hello from Rip Server!'
|
|
@@ -66,8 +66,7 @@ get '/users/:id', ->
|
|
|
66
66
|
id = read 'id', 'id!'
|
|
67
67
|
{ user: { id, name: "User #{id}" } }
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
export default startHandler()
|
|
69
|
+
start()
|
|
71
70
|
```
|
|
72
71
|
|
|
73
72
|
Run it:
|
|
@@ -104,24 +103,28 @@ rip-server [flags] <app-path>[@alias1,alias2,...]
|
|
|
104
103
|
|
|
105
104
|
| Flag | Description | Default |
|
|
106
105
|
|------|-------------|---------|
|
|
106
|
+
| `-v`, `--version` | Show version and exit | — |
|
|
107
107
|
| `http` | HTTP-only mode (no HTTPS) | HTTPS enabled |
|
|
108
108
|
| `https` | HTTPS mode (explicit) | Auto |
|
|
109
109
|
| `http:<port>` | Set HTTP port | 80 or 5700 |
|
|
110
110
|
| `https:<port>` | Set HTTPS port | 443 or 5700 |
|
|
111
111
|
| `w:<n>` | Worker count (`auto`, `half`, `2x`, `3x`, or number) | `half` of cores |
|
|
112
|
-
| `r:<policy>` | Restart policy (e.g., `
|
|
112
|
+
| `r:<policy>` | Restart policy (e.g., `5000,3600s`) | `10000,3600s` |
|
|
113
113
|
| `--cert=<path>` | TLS certificate path | Auto-generated |
|
|
114
114
|
| `--key=<path>` | TLS private key path | Auto-generated |
|
|
115
115
|
| `--auto-tls` | Use mkcert for TLS | Fallback to self-signed |
|
|
116
116
|
| `--hsts` | Enable HSTS headers | Disabled |
|
|
117
117
|
| `--no-redirect-http` | Don't redirect HTTP to HTTPS | Redirects enabled |
|
|
118
|
-
| `--
|
|
118
|
+
| `--static` | Disable hot reload (production) | Hot reload enabled |
|
|
119
119
|
| `--json-logging` | Output JSON access logs | Human-readable |
|
|
120
120
|
| `--no-access-log` | Disable access logging | Enabled |
|
|
121
121
|
|
|
122
122
|
### Subcommands
|
|
123
123
|
|
|
124
124
|
```bash
|
|
125
|
+
# Show version
|
|
126
|
+
rip-server --version
|
|
127
|
+
|
|
125
128
|
# Stop running server
|
|
126
129
|
rip-server stop
|
|
127
130
|
|
|
@@ -147,7 +150,7 @@ rip-server http:3000 app.rip
|
|
|
147
150
|
# With mDNS aliases (accessible as myapp.local and api.local)
|
|
148
151
|
rip-server app.rip@myapp,api
|
|
149
152
|
|
|
150
|
-
# Restart after 5000 requests or 1 hour
|
|
153
|
+
# Restart workers after 5000 requests or 1 hour
|
|
151
154
|
rip-server r:5000,3600s app.rip
|
|
152
155
|
```
|
|
153
156
|
|
|
@@ -193,18 +196,14 @@ When `RIP_WORKER_MODE=1` is set, the same `server.rip` file runs as a worker ins
|
|
|
193
196
|
|
|
194
197
|
### Hot Reloading
|
|
195
198
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
- **`process` mode** (default): File changes trigger rolling restart of all workers
|
|
199
|
-
- **`module` mode**: Each worker reloads the app module on file change (faster, but uses more memory)
|
|
199
|
+
By default, file changes trigger a rolling restart of all workers. Use `--static` in production to disable.
|
|
200
200
|
|
|
201
201
|
### Worker Lifecycle
|
|
202
202
|
|
|
203
|
-
Workers are recycled
|
|
203
|
+
Workers are automatically recycled to prevent memory leaks and ensure reliability:
|
|
204
204
|
|
|
205
|
-
- **maxRequests**: Restart after N requests (default: 10,000)
|
|
206
|
-
- **maxSeconds**: Restart after N seconds (default: 3,600)
|
|
207
|
-
- **maxReloads**: Maximum hot reloads before restart (default: 10)
|
|
205
|
+
- **maxRequests**: Restart worker after N requests (default: 10,000)
|
|
206
|
+
- **maxSeconds**: Restart worker after N seconds (default: 3,600)
|
|
208
207
|
|
|
209
208
|
## Built-in Endpoints
|
|
210
209
|
|
|
@@ -248,18 +247,20 @@ Requires `dns-sd` (available on macOS by default).
|
|
|
248
247
|
|
|
249
248
|
## App Requirements
|
|
250
249
|
|
|
251
|
-
Your app must
|
|
250
|
+
Your app must provide a fetch handler. Three patterns are supported:
|
|
252
251
|
|
|
253
|
-
### Pattern 1:
|
|
252
|
+
### Pattern 1: Use `@rip-lang/api` with `start()` (Recommended)
|
|
254
253
|
|
|
255
254
|
```coffee
|
|
256
|
-
import { get,
|
|
255
|
+
import { get, start } from '@rip-lang/api'
|
|
257
256
|
|
|
258
257
|
get '/', -> 'Hello!'
|
|
259
258
|
|
|
260
|
-
|
|
259
|
+
start()
|
|
261
260
|
```
|
|
262
261
|
|
|
262
|
+
The `start()` function automatically detects when running under `rip-server` and registers the handler.
|
|
263
|
+
|
|
263
264
|
### Pattern 2: Export fetch function directly
|
|
264
265
|
|
|
265
266
|
```coffee
|
|
@@ -282,10 +283,9 @@ export default
|
|
|
282
283
|
| `RIP_DEBUG` | Enable debug logging |
|
|
283
284
|
| `RIP_MAX_REQUESTS` | Default max requests per worker |
|
|
284
285
|
| `RIP_MAX_SECONDS` | Default max seconds per worker |
|
|
285
|
-
| `RIP_MAX_RELOADS` | Default max reloads per worker |
|
|
286
286
|
| `RIP_MAX_QUEUE` | Maximum request queue size |
|
|
287
287
|
| `RIP_QUEUE_TIMEOUT_MS` | Queue timeout in milliseconds |
|
|
288
|
-
| `
|
|
288
|
+
| `RIP_STATIC` | Set to `1` to disable hot reload |
|
|
289
289
|
|
|
290
290
|
## Dashboard
|
|
291
291
|
|
|
@@ -304,7 +304,7 @@ The server includes a built-in dashboard accessible at `http://rip.local/` (when
|
|
|
304
304
|
| Feature | rip-server | PM2 | Nginx |
|
|
305
305
|
|---------|------------|-----|-------|
|
|
306
306
|
| Pure Rip | ✅ | ❌ | ❌ |
|
|
307
|
-
| Single File | ✅ (~1,
|
|
307
|
+
| Single File | ✅ (~1,100 lines) | ❌ | ❌ |
|
|
308
308
|
| Hot Reload | ✅ | ✅ | ❌ |
|
|
309
309
|
| Multi-Worker | ✅ | ✅ | ✅ |
|
|
310
310
|
| Auto HTTPS | ✅ | ❌ | ❌ |
|
|
@@ -312,18 +312,16 @@ The server includes a built-in dashboard accessible at `http://rip.local/` (when
|
|
|
312
312
|
| Zero Config | ✅ | ❌ | ❌ |
|
|
313
313
|
| Built-in LB | ✅ | ❌ | ✅ |
|
|
314
314
|
|
|
315
|
-
##
|
|
315
|
+
## Roadmap
|
|
316
316
|
|
|
317
317
|
> *Planned improvements for future releases:*
|
|
318
318
|
|
|
319
|
-
- [ ]
|
|
320
|
-
- [ ]
|
|
321
|
-
- [ ]
|
|
322
|
-
- [ ] Scaling guidelines
|
|
323
|
-
- [ ] More troubleshooting scenarios
|
|
319
|
+
- [ ] Request ID tracing for debugging
|
|
320
|
+
- [ ] Metrics endpoint (Prometheus format)
|
|
321
|
+
- [ ] Multi-file hot reload (watch entire directory)
|
|
324
322
|
- [ ] Static file serving
|
|
325
323
|
- [ ] Rate limiting
|
|
326
|
-
- [ ]
|
|
324
|
+
- [ ] Performance benchmarks
|
|
327
325
|
|
|
328
326
|
## License
|
|
329
327
|
|
package/package.json
CHANGED
package/server.rip
CHANGED
|
@@ -12,14 +12,22 @@
|
|
|
12
12
|
# bun server.rip list # List registered hosts
|
|
13
13
|
# ==============================================================================
|
|
14
14
|
|
|
15
|
-
import { existsSync, statSync, readFileSync, unlinkSync, mkdirSync } from 'node:fs'
|
|
15
|
+
import { existsSync, statSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs'
|
|
16
16
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
|
|
17
|
-
import { homedir, cpus } from 'node:os'
|
|
17
|
+
import { homedir, cpus, networkInterfaces } from 'node:os'
|
|
18
18
|
import { X509Certificate } from 'node:crypto'
|
|
19
19
|
|
|
20
20
|
# Match capture holder for Rip's =~
|
|
21
21
|
_ = null
|
|
22
22
|
|
|
23
|
+
# ==============================================================================
|
|
24
|
+
# Constants
|
|
25
|
+
# ==============================================================================
|
|
26
|
+
|
|
27
|
+
MAX_BACKOFF_MS = 30000 # Max delay between worker restart attempts
|
|
28
|
+
MAX_RESTART_COUNT = 10 # Max consecutive worker crashes before giving up
|
|
29
|
+
SHUTDOWN_TIMEOUT_MS = 30000 # Max time to wait for in-flight requests on shutdown
|
|
30
|
+
|
|
23
31
|
# ==============================================================================
|
|
24
32
|
# Utilities
|
|
25
33
|
# ==============================================================================
|
|
@@ -28,6 +36,7 @@ nowMs = -> Date.now()
|
|
|
28
36
|
|
|
29
37
|
getWorkerSocketPath = (prefix, id) -> "/tmp/#{prefix}.#{id}.sock"
|
|
30
38
|
getControlSocketPath = (prefix) -> "/tmp/#{prefix}.ctl.sock"
|
|
39
|
+
getPidFilePath = (prefix) -> "/tmp/#{prefix}.pid"
|
|
31
40
|
|
|
32
41
|
coerceInt = (value, fallback) ->
|
|
33
42
|
return fallback unless value? and value isnt ''
|
|
@@ -129,24 +138,20 @@ parseWorkersToken = (token, fallback) ->
|
|
|
129
138
|
n = parseInt(token)
|
|
130
139
|
if Number.isFinite(n) and n > 0 then n else fallback
|
|
131
140
|
|
|
132
|
-
parseRestartPolicy = (token, defReqs, defSecs
|
|
133
|
-
return { maxRequests: defReqs, maxSeconds: defSecs
|
|
141
|
+
parseRestartPolicy = (token, defReqs, defSecs) ->
|
|
142
|
+
return { maxRequests: defReqs, maxSeconds: defSecs } unless token
|
|
134
143
|
maxRequests = defReqs
|
|
135
144
|
maxSeconds = defSecs
|
|
136
|
-
maxReloads = defReloads
|
|
137
145
|
|
|
138
146
|
for part in token.split(',').map((s) -> s.trim()).filter(Boolean)
|
|
139
147
|
if part.endsWith('s')
|
|
140
148
|
secs = parseInt(part.slice(0, -1))
|
|
141
149
|
maxSeconds = secs if Number.isFinite(secs) and secs >= 0
|
|
142
|
-
else if part.endsWith('r')
|
|
143
|
-
rls = parseInt(part.slice(0, -1))
|
|
144
|
-
maxReloads = rls if Number.isFinite(rls) and rls >= 0
|
|
145
150
|
else
|
|
146
151
|
n = parseInt(part)
|
|
147
152
|
maxRequests = n if Number.isFinite(n) and n > 0
|
|
148
153
|
|
|
149
|
-
{ maxRequests, maxSeconds
|
|
154
|
+
{ maxRequests, maxSeconds }
|
|
150
155
|
|
|
151
156
|
resolveAppEntry = (appPathInput) ->
|
|
152
157
|
abs = if isAbsolute(appPathInput) then appPathInput else resolve(process.cwd(), appPathInput)
|
|
@@ -253,12 +258,10 @@ parseFlags = (argv) ->
|
|
|
253
258
|
policy = parseRestartPolicy(
|
|
254
259
|
getKV('r:'),
|
|
255
260
|
coerceInt(process.env.RIP_MAX_REQUESTS, 10000),
|
|
256
|
-
coerceInt(process.env.RIP_MAX_SECONDS, 3600)
|
|
257
|
-
coerceInt(process.env.RIP_MAX_RELOADS, 10)
|
|
261
|
+
coerceInt(process.env.RIP_MAX_SECONDS, 3600)
|
|
258
262
|
)
|
|
259
263
|
|
|
260
|
-
|
|
261
|
-
reload = if reloadFlag in ['none', 'process', 'module'] then reloadFlag else 'process'
|
|
264
|
+
reload = not (has('--static') or process.env.RIP_STATIC is '1')
|
|
262
265
|
|
|
263
266
|
httpsPort = do ->
|
|
264
267
|
kv = getKV('--https-port=')
|
|
@@ -274,7 +277,6 @@ parseFlags = (argv) ->
|
|
|
274
277
|
workers
|
|
275
278
|
maxRequestsPerWorker: policy.maxRequests
|
|
276
279
|
maxSecondsPerWorker: policy.maxSeconds
|
|
277
|
-
maxReloadsPerWorker: policy.maxReloads
|
|
278
280
|
httpPort
|
|
279
281
|
httpsPort
|
|
280
282
|
certPath: getKV('--cert=')
|
|
@@ -284,7 +286,7 @@ parseFlags = (argv) ->
|
|
|
284
286
|
redirectHttp: not has('--no-redirect-http')
|
|
285
287
|
reload
|
|
286
288
|
socketPrefix
|
|
287
|
-
maxQueue: coerceInt(getKV('--max-queue='), coerceInt(process.env.RIP_MAX_QUEUE,
|
|
289
|
+
maxQueue: coerceInt(getKV('--max-queue='), coerceInt(process.env.RIP_MAX_QUEUE, 512))
|
|
288
290
|
queueTimeoutMs: coerceInt(getKV('--queue-timeout-ms='), coerceInt(process.env.RIP_QUEUE_TIMEOUT_MS, 2000))
|
|
289
291
|
connectTimeoutMs: coerceInt(getKV('--connect-timeout-ms='), coerceInt(process.env.RIP_CONNECT_TIMEOUT_MS, 200))
|
|
290
292
|
readTimeoutMs: coerceInt(getKV('--read-timeout-ms='), coerceInt(process.env.RIP_READ_TIMEOUT_MS, 5000))
|
|
@@ -299,68 +301,29 @@ parseFlags = (argv) ->
|
|
|
299
301
|
runWorker = ->
|
|
300
302
|
workerId = parseInt(process.env.WORKER_ID or '0')
|
|
301
303
|
maxRequests = parseInt(process.env.MAX_REQUESTS or '10000')
|
|
302
|
-
maxReloads = parseInt(process.env.MAX_RELOADS or '10')
|
|
303
304
|
maxSeconds = parseInt(process.env.MAX_SECONDS or '0')
|
|
304
305
|
appEntry = process.env.APP_ENTRY
|
|
305
306
|
socketPath = process.env.SOCKET_PATH
|
|
306
|
-
hotReloadMode = process.env.RIP_RELOAD or 'none'
|
|
307
307
|
socketPrefix = process.env.SOCKET_PREFIX
|
|
308
308
|
version = parseInt(process.env.RIP_VERSION or '1')
|
|
309
309
|
|
|
310
|
-
appReady = false
|
|
311
|
-
inflight = false
|
|
312
|
-
handled = 0
|
|
313
310
|
startedAtMs = Date.now()
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
checkForChanges = ->
|
|
321
|
-
return false unless hotReloadMode is 'module'
|
|
322
|
-
now = Date.now()
|
|
323
|
-
return false if now - lastCheckTime < CHECK_INTERVAL_MS
|
|
324
|
-
lastCheckTime = now
|
|
325
|
-
try
|
|
326
|
-
stats = statSync(appEntry)
|
|
327
|
-
currentMtime = stats.mtime.getTime()
|
|
328
|
-
if lastMtime is 0
|
|
329
|
-
lastMtime = currentMtime
|
|
330
|
-
return false
|
|
331
|
-
if currentMtime > lastMtime
|
|
332
|
-
lastMtime = currentMtime
|
|
333
|
-
return true
|
|
334
|
-
false
|
|
335
|
-
catch
|
|
336
|
-
false
|
|
311
|
+
# Use object to avoid Rip closure scoping issues with mutable variables
|
|
312
|
+
workerState =
|
|
313
|
+
appReady: false
|
|
314
|
+
inflight: false
|
|
315
|
+
handled: 0
|
|
316
|
+
handler: null
|
|
337
317
|
|
|
338
318
|
getHandler = ->
|
|
339
|
-
|
|
340
|
-
if hasChanged
|
|
341
|
-
hotReloadCount++
|
|
342
|
-
console.log "[worker #{workerId}] File changed, reloading... (#{hotReloadCount}/#{maxReloads})"
|
|
343
|
-
cachedHandler = null
|
|
344
|
-
|
|
345
|
-
if hotReloadCount >= maxReloads
|
|
346
|
-
console.log "[worker #{workerId}] Reached maxReloads (#{maxReloads}), graceful exit"
|
|
347
|
-
setTimeout (-> process.exit(0)), 100
|
|
348
|
-
return -> new Response('Worker cycling', { status: 503 })
|
|
349
|
-
|
|
350
|
-
return cachedHandler if cachedHandler and not hasChanged
|
|
319
|
+
return workerState.handler if workerState.handler
|
|
351
320
|
|
|
352
321
|
try
|
|
353
|
-
|
|
354
|
-
api = null
|
|
355
|
-
try
|
|
356
|
-
api = await import('@rip-lang/api')
|
|
357
|
-
catch
|
|
358
|
-
null
|
|
322
|
+
mod = await import(appEntry)
|
|
359
323
|
|
|
360
|
-
|
|
324
|
+
# Ensure module has fully executed by yielding to microtask queue
|
|
325
|
+
await Promise.resolve()
|
|
361
326
|
|
|
362
|
-
bustQuery = if hotReloadMode is 'module' then "?bust=#{Date.now()}" else ''
|
|
363
|
-
mod = await import(appEntry + bustQuery)
|
|
364
327
|
fresh = mod.default or mod
|
|
365
328
|
|
|
366
329
|
if typeof fresh is 'function'
|
|
@@ -370,12 +333,18 @@ runWorker = ->
|
|
|
370
333
|
else
|
|
371
334
|
h = globalThis.__ripHandler # Handler set by start() in rip-server mode
|
|
372
335
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
336
|
+
unless h
|
|
337
|
+
try
|
|
338
|
+
api = await import('@rip-lang/api')
|
|
339
|
+
h = api?.startHandler?()
|
|
340
|
+
catch
|
|
341
|
+
null
|
|
342
|
+
|
|
343
|
+
workerState.handler = h if h
|
|
344
|
+
workerState.handler or (-> new Response('not ready', { status: 503 }))
|
|
376
345
|
catch e
|
|
377
|
-
console.error "[worker #{workerId}] import failed:", e
|
|
378
|
-
|
|
346
|
+
console.error "[worker #{workerId}] import failed:", e
|
|
347
|
+
workerState.handler or (-> new Response('not ready', { status: 503 }))
|
|
379
348
|
|
|
380
349
|
selfJoin = ->
|
|
381
350
|
try
|
|
@@ -398,7 +367,7 @@ runWorker = ->
|
|
|
398
367
|
# Preload handler
|
|
399
368
|
try
|
|
400
369
|
initial = getHandler!
|
|
401
|
-
appReady = typeof initial is 'function'
|
|
370
|
+
workerState.appReady = typeof initial is 'function'
|
|
402
371
|
catch
|
|
403
372
|
null
|
|
404
373
|
|
|
@@ -407,35 +376,38 @@ runWorker = ->
|
|
|
407
376
|
maxRequestBodySize: 100 * 1024 * 1024
|
|
408
377
|
fetch: (req) ->
|
|
409
378
|
url = new URL(req.url)
|
|
410
|
-
return new Response(if appReady then 'ok' else 'not-ready') if url.pathname is '/ready'
|
|
379
|
+
return new Response(if workerState.appReady then 'ok' else 'not-ready') if url.pathname is '/ready'
|
|
411
380
|
|
|
412
|
-
if inflight
|
|
381
|
+
if workerState.inflight
|
|
413
382
|
return new Response 'busy',
|
|
414
383
|
status: 503
|
|
415
384
|
headers: { 'Rip-Worker-Busy': '1', 'Retry-After': '0', 'Rip-Worker-Id': String(workerId) }
|
|
416
385
|
|
|
417
386
|
handlerFn = getHandler!
|
|
418
|
-
appReady = typeof handlerFn is 'function'
|
|
419
|
-
inflight = true
|
|
387
|
+
workerState.appReady = typeof handlerFn is 'function'
|
|
388
|
+
workerState.inflight = true
|
|
420
389
|
|
|
421
390
|
try
|
|
422
391
|
return new Response('not ready', { status: 503 }) unless typeof handlerFn is 'function'
|
|
423
392
|
res = handlerFn!(req)
|
|
424
393
|
res = res!(req) if typeof res is 'function'
|
|
425
394
|
if res instanceof Response then res else new Response(String(res))
|
|
426
|
-
catch
|
|
395
|
+
catch err
|
|
396
|
+
console.error "[worker #{workerId}] ERROR:", err
|
|
427
397
|
new Response('error', { status: 500 })
|
|
428
398
|
finally
|
|
429
|
-
inflight = false
|
|
430
|
-
handled++
|
|
431
|
-
exceededReqs = handled >= maxRequests
|
|
399
|
+
workerState.inflight = false
|
|
400
|
+
workerState.handled++
|
|
401
|
+
exceededReqs = workerState.handled >= maxRequests
|
|
432
402
|
exceededTime = maxSeconds > 0 and (Date.now() - startedAtMs) / 1000 >= maxSeconds
|
|
433
403
|
setTimeout (-> process.exit(0)), 10 if exceededReqs or exceededTime
|
|
434
404
|
|
|
435
405
|
selfJoin!
|
|
436
406
|
|
|
437
407
|
shutdown = ->
|
|
438
|
-
|
|
408
|
+
# Wait for in-flight request to complete (with timeout)
|
|
409
|
+
start = Date.now()
|
|
410
|
+
while workerState.inflight and Date.now() - start < SHUTDOWN_TIMEOUT_MS
|
|
439
411
|
await new Promise (r) -> setTimeout(r, 10)
|
|
440
412
|
try server.stop() catch then null
|
|
441
413
|
selfQuit!
|
|
@@ -470,7 +442,7 @@ class Manager
|
|
|
470
442
|
w = @spawnWorker!(@currentVersion)
|
|
471
443
|
@workers.push(w)
|
|
472
444
|
|
|
473
|
-
if @flags.reload
|
|
445
|
+
if @flags.reload
|
|
474
446
|
@currentMtime = @getEntryMtime()
|
|
475
447
|
interval = setInterval =>
|
|
476
448
|
return clearInterval(interval) if @shuttingDown
|
|
@@ -489,14 +461,13 @@ class Manager
|
|
|
489
461
|
stop: ->
|
|
490
462
|
for w in @workers
|
|
491
463
|
try w.process.kill() catch then null
|
|
492
|
-
try w.
|
|
493
|
-
try Bun.spawn(['rm', '-f', w.socketPath]).exited catch then null
|
|
464
|
+
try unlinkSync(w.socketPath) catch then null
|
|
494
465
|
@workers = []
|
|
495
466
|
|
|
496
467
|
spawnWorker: (version) ->
|
|
497
468
|
workerId = ++@nextWorkerId
|
|
498
469
|
socketPath = getWorkerSocketPath(@flags.socketPrefix, workerId)
|
|
499
|
-
try
|
|
470
|
+
try unlinkSync(socketPath) catch then null
|
|
500
471
|
|
|
501
472
|
workerEnv = Object.assign {}, process.env,
|
|
502
473
|
RIP_WORKER_MODE: '1'
|
|
@@ -505,10 +476,8 @@ class Manager
|
|
|
505
476
|
SOCKET_PREFIX: @flags.socketPrefix
|
|
506
477
|
APP_ENTRY: @flags.appEntry
|
|
507
478
|
MAX_REQUESTS: String(@flags.maxRequestsPerWorker)
|
|
508
|
-
MAX_RELOADS: String(@flags.maxReloadsPerWorker)
|
|
509
479
|
MAX_SECONDS: String(@flags.maxSecondsPerWorker)
|
|
510
480
|
RIP_LOG_JSON: if @flags.jsonLogging then '1' else '0'
|
|
511
|
-
RIP_RELOAD: @flags.reload
|
|
512
481
|
RIP_VERSION: String(version or @currentVersion)
|
|
513
482
|
|
|
514
483
|
proc = Bun.spawn ['rip', __filename],
|
|
@@ -528,17 +497,17 @@ class Manager
|
|
|
528
497
|
return if @shuttingDown
|
|
529
498
|
return if @retiringIds.has(w.id)
|
|
530
499
|
|
|
531
|
-
# Notify server to remove dead worker's socket entry
|
|
500
|
+
# Notify server to remove dead worker's socket entry (fire-and-forget)
|
|
532
501
|
try
|
|
533
502
|
ctl = getControlSocketPath(@flags.socketPrefix)
|
|
534
503
|
body = JSON.stringify({ op: 'quit', workerId: w.id })
|
|
535
|
-
fetch('http://localhost/worker', { method: 'POST', body, headers: { 'content-type': 'application/json' }, unix: ctl })
|
|
504
|
+
fetch!('http://localhost/worker', { method: 'POST', body, headers: { 'content-type': 'application/json' }, unix: ctl }).catch(-> null)
|
|
536
505
|
catch
|
|
537
506
|
null
|
|
538
507
|
|
|
539
508
|
w.restartCount++
|
|
540
|
-
w.backoffMs = Math.min(w.backoffMs * 2,
|
|
541
|
-
return if w.restartCount >
|
|
509
|
+
w.backoffMs = Math.min(w.backoffMs * 2, MAX_BACKOFF_MS)
|
|
510
|
+
return if w.restartCount > MAX_RESTART_COUNT
|
|
542
511
|
setTimeout =>
|
|
543
512
|
idx = @workers.findIndex((x) -> x.id is w.id)
|
|
544
513
|
@workers[idx] = @spawnWorker(@currentVersion) if idx >= 0
|
|
@@ -567,14 +536,36 @@ class Manager
|
|
|
567
536
|
@workers.push(replacement)
|
|
568
537
|
pairs.push({ old: oldWorker, replacement })
|
|
569
538
|
|
|
570
|
-
|
|
539
|
+
# Wait for all replacements and check readiness
|
|
540
|
+
readyResults = Promise.all!(pairs.map((p) => @waitWorkerReady(p.replacement.socketPath, 3000)))
|
|
541
|
+
|
|
542
|
+
# Check if all replacements are ready
|
|
543
|
+
allReady = readyResults.every((ready) -> ready)
|
|
544
|
+
unless allReady
|
|
545
|
+
console.error "[manager] Rolling restart aborted: not all new workers ready"
|
|
546
|
+
# Kill failed replacements and keep old workers
|
|
547
|
+
for pair, i in pairs
|
|
548
|
+
unless readyResults[i]
|
|
549
|
+
try pair.replacement.process.kill() catch then null
|
|
550
|
+
@workers = @workers.filter((w) -> w.id isnt pair.replacement.id)
|
|
551
|
+
return
|
|
571
552
|
|
|
553
|
+
# All ready - retire old workers
|
|
572
554
|
for { old } in pairs
|
|
573
555
|
@retiringIds.add(old.id)
|
|
574
556
|
try old.process.kill() catch then null
|
|
575
557
|
|
|
576
558
|
Promise.all!(pairs.map(({ old }) -> old.process.exited.catch(-> null)))
|
|
577
559
|
|
|
560
|
+
# Notify server to remove old workers' socket entries
|
|
561
|
+
ctl = getControlSocketPath(@flags.socketPrefix)
|
|
562
|
+
for { old } in pairs
|
|
563
|
+
try
|
|
564
|
+
body = JSON.stringify({ op: 'quit', workerId: old.id })
|
|
565
|
+
fetch!('http://localhost/worker', { method: 'POST', body, headers: { 'content-type': 'application/json' }, unix: ctl }).catch(-> null)
|
|
566
|
+
catch
|
|
567
|
+
null
|
|
568
|
+
|
|
578
569
|
retiring = new Set(pairs.map((p) -> p.old.id))
|
|
579
570
|
@workers = @workers.filter((w) -> not retiring.has(w.id))
|
|
580
571
|
@retiringIds.delete(id) for id from retiring
|
|
@@ -613,54 +604,39 @@ class Server
|
|
|
613
604
|
|
|
614
605
|
start: ->
|
|
615
606
|
httpOnly = @flags.httpsPort is null
|
|
607
|
+
fetchFn = @fetch.bind(@)
|
|
616
608
|
|
|
617
|
-
|
|
609
|
+
# Helper to start server, incrementing port if needed
|
|
610
|
+
startOnPort = (p, opts = {}) =>
|
|
618
611
|
port = p
|
|
619
612
|
while true
|
|
620
613
|
try
|
|
621
|
-
return Bun.serve({ port, idleTimeout: 8, fetch: fetchFn })
|
|
614
|
+
return Bun.serve(Object.assign({ port, idleTimeout: 8, fetch: fetchFn }, opts))
|
|
622
615
|
catch e
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
continue
|
|
626
|
-
throw e
|
|
616
|
+
throw e unless e?.code is 'EADDRINUSE'
|
|
617
|
+
port++
|
|
627
618
|
|
|
628
619
|
if httpOnly
|
|
629
620
|
if @flags.httpPort is 0
|
|
630
621
|
try
|
|
631
|
-
@server = Bun.serve({ port: 80, idleTimeout: 8, fetch:
|
|
622
|
+
@server = Bun.serve({ port: 80, idleTimeout: 8, fetch: fetchFn })
|
|
632
623
|
catch e
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
else
|
|
636
|
-
throw e
|
|
624
|
+
throw e unless e?.code in ['EADDRINUSE', 'EACCES']
|
|
625
|
+
@server = startOnPort(5700)
|
|
637
626
|
else
|
|
638
|
-
@server = startOnPort(@flags.httpPort
|
|
627
|
+
@server = startOnPort(@flags.httpPort)
|
|
639
628
|
@flags.httpPort = @server.port
|
|
640
629
|
else
|
|
641
630
|
tls = @loadTlsMaterial!
|
|
642
631
|
|
|
643
|
-
startOnTlsPort = (p) =>
|
|
644
|
-
port = p
|
|
645
|
-
while true
|
|
646
|
-
try
|
|
647
|
-
return Bun.serve({ port, idleTimeout: 8, tls, fetch: @fetch.bind(@) })
|
|
648
|
-
catch e
|
|
649
|
-
if e?.code is 'EADDRINUSE'
|
|
650
|
-
port++
|
|
651
|
-
continue
|
|
652
|
-
throw e
|
|
653
|
-
|
|
654
632
|
if not @flags.httpsPort or @flags.httpsPort is 0
|
|
655
633
|
try
|
|
656
|
-
@httpsServer = Bun.serve({ port: 443, idleTimeout: 8, tls, fetch:
|
|
634
|
+
@httpsServer = Bun.serve({ port: 443, idleTimeout: 8, tls, fetch: fetchFn })
|
|
657
635
|
catch e
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
else
|
|
661
|
-
throw e
|
|
636
|
+
throw e unless e?.code in ['EADDRINUSE', 'EACCES']
|
|
637
|
+
@httpsServer = startOnPort(5700, { tls })
|
|
662
638
|
else
|
|
663
|
-
@httpsServer =
|
|
639
|
+
@httpsServer = startOnPort(@flags.httpsPort, { tls })
|
|
664
640
|
|
|
665
641
|
httpsPort = @httpsServer.port
|
|
666
642
|
@flags.httpsPort = httpsPort
|
|
@@ -757,8 +733,26 @@ class Server
|
|
|
757
733
|
@newestVersion is null or worker.version is null or worker.version >= @newestVersion
|
|
758
734
|
|
|
759
735
|
releaseWorker: (worker) ->
|
|
736
|
+
return unless worker
|
|
737
|
+
return unless @sockets.some((s) -> s.socket is worker.socket) # Validate still in pool
|
|
760
738
|
worker.inflight = 0
|
|
761
|
-
|
|
739
|
+
if @isCurrentVersion(worker)
|
|
740
|
+
@availableWorkers.push(worker)
|
|
741
|
+
# Force array mutation to be visible (workaround for timing issue)
|
|
742
|
+
@availableWorkers.length
|
|
743
|
+
|
|
744
|
+
logAccess: (req, res, totalSeconds, workerSeconds) ->
|
|
745
|
+
if @flags.jsonLogging
|
|
746
|
+
logAccessJson(@flags.appName, req, res, totalSeconds, workerSeconds)
|
|
747
|
+
else if @flags.accessLog
|
|
748
|
+
logAccessHuman(@flags.appName, req, res, totalSeconds, workerSeconds)
|
|
749
|
+
|
|
750
|
+
buildResponse: (res, req, start, workerSeconds) ->
|
|
751
|
+
headers = stripInternalHeaders(res.headers)
|
|
752
|
+
headers.delete('date')
|
|
753
|
+
@maybeAddSecurityHeaders(headers)
|
|
754
|
+
@logAccess(req, res, (performance.now() - start) / 1000, workerSeconds)
|
|
755
|
+
new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
762
756
|
|
|
763
757
|
forwardToWorker: (req, socket) ->
|
|
764
758
|
start = performance.now()
|
|
@@ -772,7 +766,9 @@ class Server
|
|
|
772
766
|
res = @forwardOnce!(req, socket.socket)
|
|
773
767
|
workerSeconds = (performance.now() - t0) / 1000
|
|
774
768
|
|
|
775
|
-
|
|
769
|
+
# Only retry bodyless requests (body stream can't be reused)
|
|
770
|
+
canRetry = req.method in ['GET', 'HEAD', 'OPTIONS', 'DELETE']
|
|
771
|
+
if canRetry and res.status is 503 and res.headers.get('Rip-Worker-Busy') is '1'
|
|
776
772
|
retry = @getNextAvailableSocket()
|
|
777
773
|
if retry and retry isnt socket
|
|
778
774
|
@releaseWorker(socket)
|
|
@@ -781,15 +777,10 @@ class Server
|
|
|
781
777
|
t1 = performance.now()
|
|
782
778
|
res = @forwardOnce!(req, retry.socket)
|
|
783
779
|
workerSeconds = (performance.now() - t1) / 1000
|
|
784
|
-
headers = stripInternalHeaders(res.headers)
|
|
785
|
-
headers.delete('date')
|
|
786
|
-
if @flags.jsonLogging
|
|
787
|
-
logAccessJson(@flags.appName, req, res, (performance.now() - start) / 1000, workerSeconds)
|
|
788
|
-
else if @flags.accessLog
|
|
789
|
-
logAccessHuman(@flags.appName, req, res, (performance.now() - start) / 1000, workerSeconds)
|
|
790
780
|
@releaseWorker(retry)
|
|
791
|
-
return
|
|
792
|
-
catch
|
|
781
|
+
return @buildResponse(res, req, start, workerSeconds)
|
|
782
|
+
catch err
|
|
783
|
+
console.error "[server] forwardToWorker error:", err.message or err if process.env.RIP_DEBUG
|
|
793
784
|
@sockets = @sockets.filter((x) -> x.socket isnt socket.socket)
|
|
794
785
|
@availableWorkers = @availableWorkers.filter((x) -> x.socket isnt socket.socket)
|
|
795
786
|
released = true
|
|
@@ -798,32 +789,28 @@ class Server
|
|
|
798
789
|
@releaseWorker(socket) unless released
|
|
799
790
|
|
|
800
791
|
return new Response('Service unavailable', { status: 503, headers: { 'Retry-After': '1' } }) unless res
|
|
801
|
-
|
|
802
|
-
headers = stripInternalHeaders(res.headers)
|
|
803
|
-
headers.delete('date')
|
|
804
|
-
@maybeAddSecurityHeaders(headers)
|
|
805
|
-
|
|
806
|
-
if @flags.jsonLogging
|
|
807
|
-
logAccessJson(@flags.appName, req, res, (performance.now() - start) / 1000, workerSeconds)
|
|
808
|
-
else if @flags.accessLog
|
|
809
|
-
logAccessHuman(@flags.appName, req, res, (performance.now() - start) / 1000, workerSeconds)
|
|
810
|
-
|
|
811
|
-
new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
792
|
+
@buildResponse(res, req, start, workerSeconds)
|
|
812
793
|
|
|
813
794
|
forwardOnce: (req, socketPath) ->
|
|
814
795
|
inUrl = new URL(req.url)
|
|
815
796
|
forwardUrl = "http://localhost#{inUrl.pathname}#{inUrl.search}"
|
|
816
797
|
controller = new AbortController()
|
|
817
|
-
|
|
798
|
+
connectTimer = setTimeout (-> controller.abort()), @flags.connectTimeoutMs
|
|
799
|
+
readTimeoutMs = @flags.readTimeoutMs
|
|
818
800
|
|
|
819
801
|
try
|
|
820
|
-
|
|
821
|
-
|
|
802
|
+
# Don't await fetch - we need to race the promise against timeout
|
|
803
|
+
fetchPromise = fetch(forwardUrl, { method: req.method, headers: req.headers, body: req.body, unix: socketPath, signal: controller.signal })
|
|
822
804
|
readGuard = new Promise (_, rej) ->
|
|
823
|
-
setTimeout (-> rej(new
|
|
824
|
-
Promise.race!([
|
|
825
|
-
|
|
826
|
-
|
|
805
|
+
setTimeout (-> rej(new Error('Upstream timeout'))), readTimeoutMs
|
|
806
|
+
res = Promise.race!([fetchPromise, readGuard])
|
|
807
|
+
clearTimeout(connectTimer)
|
|
808
|
+
res
|
|
809
|
+
catch err
|
|
810
|
+
clearTimeout(connectTimer)
|
|
811
|
+
if err.message is 'Upstream timeout'
|
|
812
|
+
return new Response('Gateway timeout', { status: 504 })
|
|
813
|
+
throw err
|
|
827
814
|
|
|
828
815
|
drainQueue: ->
|
|
829
816
|
while @inflightTotal < Math.max(1, @sockets.length) and @availableWorkers.length > 0
|
|
@@ -899,18 +886,18 @@ class Server
|
|
|
899
886
|
console.error 'Failed to read TLS cert/key from provided paths. Use http or fix paths.'
|
|
900
887
|
process.exit(2)
|
|
901
888
|
|
|
902
|
-
#
|
|
889
|
+
# Ensure certs directory exists
|
|
890
|
+
certsDir = join(homedir(), '.rip', 'certs')
|
|
891
|
+
try mkdirSync(certsDir, { recursive: true }) catch then null
|
|
892
|
+
|
|
893
|
+
# Try mkcert first (trusted local CA)
|
|
903
894
|
if @flags.autoTls
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
certPath = join(dir, 'localhost.pem')
|
|
907
|
-
keyPath = join(dir, 'localhost-key.pem')
|
|
895
|
+
certPath = join(certsDir, 'localhost.pem')
|
|
896
|
+
keyPath = join(certsDir, 'localhost-key.pem')
|
|
908
897
|
unless existsSync(certPath) and existsSync(keyPath)
|
|
909
898
|
try
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
p = Bun.spawn(['mkcert', '-key-file', keyPath, '-cert-file', certPath, 'localhost', '127.0.0.1', '::1'])
|
|
913
|
-
p.exited
|
|
899
|
+
Bun.spawnSync(['mkcert', '-install'])
|
|
900
|
+
Bun.spawnSync(['mkcert', '-key-file', keyPath, '-cert-file', certPath, 'localhost', '127.0.0.1', '::1'])
|
|
914
901
|
catch
|
|
915
902
|
null # fall through to self-signed
|
|
916
903
|
if existsSync(certPath) and existsSync(keyPath)
|
|
@@ -920,14 +907,11 @@ class Server
|
|
|
920
907
|
return { cert, key }
|
|
921
908
|
|
|
922
909
|
# Self-signed via openssl
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
certPath = join(dir, 'selfsigned-localhost.pem')
|
|
926
|
-
keyPath = join(dir, 'selfsigned-localhost-key.pem')
|
|
910
|
+
certPath = join(certsDir, 'selfsigned-localhost.pem')
|
|
911
|
+
keyPath = join(certsDir, 'selfsigned-localhost-key.pem')
|
|
927
912
|
unless existsSync(certPath) and existsSync(keyPath)
|
|
928
913
|
try
|
|
929
|
-
|
|
930
|
-
p.exited
|
|
914
|
+
Bun.spawnSync(['openssl', 'req', '-x509', '-nodes', '-newkey', 'rsa:2048', '-keyout', keyPath, '-out', certPath, '-subj', '/CN=localhost', '-days', '3650'])
|
|
931
915
|
catch
|
|
932
916
|
console.error 'TLS required but could not provision a certificate (mkcert/openssl missing). Use http or provide --cert/--key.'
|
|
933
917
|
process.exit(2)
|
|
@@ -952,12 +936,12 @@ class Server
|
|
|
952
936
|
|
|
953
937
|
getLanIP: ->
|
|
954
938
|
try
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
return
|
|
939
|
+
nets = networkInterfaces()
|
|
940
|
+
for name, addrs of nets
|
|
941
|
+
for addr in addrs
|
|
942
|
+
continue if addr.internal or addr.family isnt 'IPv4'
|
|
943
|
+
continue if addr.address.startsWith('169.254.') # Link-local
|
|
944
|
+
return addr.address
|
|
961
945
|
catch
|
|
962
946
|
null
|
|
963
947
|
null
|
|
@@ -973,13 +957,14 @@ class Server
|
|
|
973
957
|
|
|
974
958
|
port = @flags.httpsPort or @flags.httpPort or 80
|
|
975
959
|
protocol = if @flags.httpsPort then 'https' else 'http'
|
|
960
|
+
serviceType = if @flags.httpsPort then '_https._tcp' else '_http._tcp'
|
|
976
961
|
serviceName = host.replace('.local', '')
|
|
977
962
|
|
|
978
963
|
try
|
|
979
964
|
proc = Bun.spawn [
|
|
980
965
|
'dns-sd', '-P'
|
|
981
966
|
serviceName
|
|
982
|
-
|
|
967
|
+
serviceType
|
|
983
968
|
'local'
|
|
984
969
|
String(port)
|
|
985
970
|
host
|
|
@@ -1004,46 +989,62 @@ class Server
|
|
|
1004
989
|
# ==============================================================================
|
|
1005
990
|
|
|
1006
991
|
main = ->
|
|
1007
|
-
#
|
|
1008
|
-
if '
|
|
992
|
+
# Version flag
|
|
993
|
+
if '--version' in process.argv or '-v' in process.argv
|
|
1009
994
|
try
|
|
1010
|
-
|
|
1011
|
-
|
|
995
|
+
pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'))
|
|
996
|
+
console.log "rip-server v#{pkg.version}"
|
|
1012
997
|
catch
|
|
1013
|
-
|
|
1014
|
-
console.log 'rip-server: stop requested'
|
|
998
|
+
console.log 'rip-server (version unknown)'
|
|
1015
999
|
return
|
|
1016
1000
|
|
|
1017
|
-
#
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
undefined
|
|
1001
|
+
# Helper functions for subcommands
|
|
1002
|
+
getKV = (prefix) ->
|
|
1003
|
+
for tok in process.argv
|
|
1004
|
+
return tok.slice(prefix.length) if tok.startsWith(prefix)
|
|
1005
|
+
undefined
|
|
1023
1006
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1007
|
+
findAppPathToken = ->
|
|
1008
|
+
for i in [2...process.argv.length]
|
|
1009
|
+
tok = process.argv[i]
|
|
1010
|
+
pathPart = if tok.includes('@') then tok.split('@')[0] else tok
|
|
1011
|
+
looksLikePath = pathPart.includes('/') or pathPart.startsWith('.') or pathPart.endsWith('.rip') or pathPart.endsWith('.ts')
|
|
1012
|
+
try
|
|
1013
|
+
return pathPart if looksLikePath and existsSync(if isAbsolute(pathPart) then pathPart else resolve(process.cwd(), pathPart))
|
|
1014
|
+
catch
|
|
1015
|
+
null
|
|
1016
|
+
undefined
|
|
1034
1017
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1018
|
+
computeSocketPrefix = ->
|
|
1019
|
+
override = getKV('--socket-prefix=')
|
|
1020
|
+
return override if override
|
|
1021
|
+
appTok = findAppPathToken()
|
|
1022
|
+
if appTok
|
|
1023
|
+
try
|
|
1024
|
+
{ appName } = resolveAppEntry(appTok)
|
|
1025
|
+
return "rip_#{appName}"
|
|
1026
|
+
catch
|
|
1027
|
+
null
|
|
1028
|
+
'rip_server'
|
|
1046
1029
|
|
|
1030
|
+
# Subcommand: stop
|
|
1031
|
+
if 'stop' in process.argv
|
|
1032
|
+
prefix = computeSocketPrefix()
|
|
1033
|
+
pidFile = getPidFilePath(prefix)
|
|
1034
|
+
try
|
|
1035
|
+
if existsSync(pidFile)
|
|
1036
|
+
pid = parseInt(readFileSync(pidFile, 'utf8').trim())
|
|
1037
|
+
process.kill(pid, 'SIGTERM')
|
|
1038
|
+
console.log "rip-server: sent SIGTERM to process #{pid}"
|
|
1039
|
+
else
|
|
1040
|
+
console.log "rip-server: no PID file found at #{pidFile}, trying pkill..."
|
|
1041
|
+
Bun.spawnSync(['pkill', '-f', __filename])
|
|
1042
|
+
catch e
|
|
1043
|
+
console.error "rip-server: stop failed: #{e.message}"
|
|
1044
|
+
return
|
|
1045
|
+
|
|
1046
|
+
# Subcommand: list
|
|
1047
|
+
if 'list' in process.argv
|
|
1047
1048
|
controlUnix = getControlSocketPath(computeSocketPrefix())
|
|
1048
1049
|
try
|
|
1049
1050
|
res = fetch!('http://localhost/registry', { unix: controlUnix, method: 'GET' })
|
|
@@ -1058,11 +1059,15 @@ main = ->
|
|
|
1058
1059
|
|
|
1059
1060
|
# Normal startup
|
|
1060
1061
|
flags = parseFlags(process.argv)
|
|
1062
|
+
pidFile = getPidFilePath(flags.socketPrefix)
|
|
1063
|
+
writeFileSync(pidFile, String(process.pid))
|
|
1064
|
+
|
|
1061
1065
|
svr = new Server(flags)
|
|
1062
1066
|
mgr = new Manager(flags)
|
|
1063
1067
|
|
|
1064
1068
|
cleanup = ->
|
|
1065
1069
|
console.log 'rip-server: shutting down...'
|
|
1070
|
+
try unlinkSync(pidFile) catch then null
|
|
1066
1071
|
svr.stop()
|
|
1067
1072
|
mgr.stop!
|
|
1068
1073
|
process.exit(0)
|