@rip-lang/server 0.5.6 → 0.6.0
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 +212 -214
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],
|
|
@@ -523,24 +492,26 @@ class Manager
|
|
|
523
492
|
tracked
|
|
524
493
|
|
|
525
494
|
monitor: (w) ->
|
|
526
|
-
|
|
495
|
+
# Watch for process exit in background (don't block)
|
|
496
|
+
w.process.exited.then =>
|
|
497
|
+
return if @shuttingDown
|
|
498
|
+
return if @retiringIds.has(w.id)
|
|
527
499
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
500
|
+
# Notify server to remove dead worker's socket entry (fire-and-forget)
|
|
501
|
+
try
|
|
502
|
+
ctl = getControlSocketPath(@flags.socketPrefix)
|
|
503
|
+
body = JSON.stringify({ op: 'quit', workerId: w.id })
|
|
504
|
+
fetch!('http://localhost/worker', { method: 'POST', body, headers: { 'content-type': 'application/json' }, unix: ctl }).catch(-> null)
|
|
505
|
+
catch
|
|
506
|
+
null
|
|
535
507
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
@workers[idx] = @spawnWorker!() if idx >= 0
|
|
508
|
+
w.restartCount++
|
|
509
|
+
w.backoffMs = Math.min(w.backoffMs * 2, MAX_BACKOFF_MS)
|
|
510
|
+
return if w.restartCount > MAX_RESTART_COUNT
|
|
511
|
+
setTimeout =>
|
|
512
|
+
idx = @workers.findIndex((x) -> x.id is w.id)
|
|
513
|
+
@workers[idx] = @spawnWorker(@currentVersion) if idx >= 0
|
|
514
|
+
, w.backoffMs
|
|
544
515
|
|
|
545
516
|
waitWorkerReady: (socketPath, timeoutMs = 5000) ->
|
|
546
517
|
start = Date.now()
|
|
@@ -565,8 +536,21 @@ class Manager
|
|
|
565
536
|
@workers.push(replacement)
|
|
566
537
|
pairs.push({ old: oldWorker, replacement })
|
|
567
538
|
|
|
568
|
-
|
|
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
|
|
569
552
|
|
|
553
|
+
# All ready - retire old workers
|
|
570
554
|
for { old } in pairs
|
|
571
555
|
@retiringIds.add(old.id)
|
|
572
556
|
try old.process.kill() catch then null
|
|
@@ -611,54 +595,39 @@ class Server
|
|
|
611
595
|
|
|
612
596
|
start: ->
|
|
613
597
|
httpOnly = @flags.httpsPort is null
|
|
598
|
+
fetchFn = @fetch.bind(@)
|
|
614
599
|
|
|
615
|
-
|
|
600
|
+
# Helper to start server, incrementing port if needed
|
|
601
|
+
startOnPort = (p, opts = {}) =>
|
|
616
602
|
port = p
|
|
617
603
|
while true
|
|
618
604
|
try
|
|
619
|
-
return Bun.serve({ port, idleTimeout: 8, fetch: fetchFn })
|
|
605
|
+
return Bun.serve(Object.assign({ port, idleTimeout: 8, fetch: fetchFn }, opts))
|
|
620
606
|
catch e
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
continue
|
|
624
|
-
throw e
|
|
607
|
+
throw e unless e?.code is 'EADDRINUSE'
|
|
608
|
+
port++
|
|
625
609
|
|
|
626
610
|
if httpOnly
|
|
627
611
|
if @flags.httpPort is 0
|
|
628
612
|
try
|
|
629
|
-
@server = Bun.serve({ port: 80, idleTimeout: 8, fetch:
|
|
613
|
+
@server = Bun.serve({ port: 80, idleTimeout: 8, fetch: fetchFn })
|
|
630
614
|
catch e
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
else
|
|
634
|
-
throw e
|
|
615
|
+
throw e unless e?.code in ['EADDRINUSE', 'EACCES']
|
|
616
|
+
@server = startOnPort(5700)
|
|
635
617
|
else
|
|
636
|
-
@server = startOnPort(@flags.httpPort
|
|
618
|
+
@server = startOnPort(@flags.httpPort)
|
|
637
619
|
@flags.httpPort = @server.port
|
|
638
620
|
else
|
|
639
621
|
tls = @loadTlsMaterial!
|
|
640
622
|
|
|
641
|
-
startOnTlsPort = (p) =>
|
|
642
|
-
port = p
|
|
643
|
-
while true
|
|
644
|
-
try
|
|
645
|
-
return Bun.serve({ port, idleTimeout: 8, tls, fetch: @fetch.bind(@) })
|
|
646
|
-
catch e
|
|
647
|
-
if e?.code is 'EADDRINUSE'
|
|
648
|
-
port++
|
|
649
|
-
continue
|
|
650
|
-
throw e
|
|
651
|
-
|
|
652
623
|
if not @flags.httpsPort or @flags.httpsPort is 0
|
|
653
624
|
try
|
|
654
|
-
@httpsServer = Bun.serve({ port: 443, idleTimeout: 8, tls, fetch:
|
|
625
|
+
@httpsServer = Bun.serve({ port: 443, idleTimeout: 8, tls, fetch: fetchFn })
|
|
655
626
|
catch e
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
else
|
|
659
|
-
throw e
|
|
627
|
+
throw e unless e?.code in ['EADDRINUSE', 'EACCES']
|
|
628
|
+
@httpsServer = startOnPort(5700, { tls })
|
|
660
629
|
else
|
|
661
|
-
@httpsServer =
|
|
630
|
+
@httpsServer = startOnPort(@flags.httpsPort, { tls })
|
|
662
631
|
|
|
663
632
|
httpsPort = @httpsServer.port
|
|
664
633
|
@flags.httpsPort = httpsPort
|
|
@@ -755,8 +724,26 @@ class Server
|
|
|
755
724
|
@newestVersion is null or worker.version is null or worker.version >= @newestVersion
|
|
756
725
|
|
|
757
726
|
releaseWorker: (worker) ->
|
|
727
|
+
return unless worker
|
|
728
|
+
return unless @sockets.some((s) -> s.socket is worker.socket) # Validate still in pool
|
|
758
729
|
worker.inflight = 0
|
|
759
|
-
|
|
730
|
+
if @isCurrentVersion(worker)
|
|
731
|
+
@availableWorkers.push(worker)
|
|
732
|
+
# Force array mutation to be visible (workaround for timing issue)
|
|
733
|
+
@availableWorkers.length
|
|
734
|
+
|
|
735
|
+
logAccess: (req, res, totalSeconds, workerSeconds) ->
|
|
736
|
+
if @flags.jsonLogging
|
|
737
|
+
logAccessJson(@flags.appName, req, res, totalSeconds, workerSeconds)
|
|
738
|
+
else if @flags.accessLog
|
|
739
|
+
logAccessHuman(@flags.appName, req, res, totalSeconds, workerSeconds)
|
|
740
|
+
|
|
741
|
+
buildResponse: (res, req, start, workerSeconds) ->
|
|
742
|
+
headers = stripInternalHeaders(res.headers)
|
|
743
|
+
headers.delete('date')
|
|
744
|
+
@maybeAddSecurityHeaders(headers)
|
|
745
|
+
@logAccess(req, res, (performance.now() - start) / 1000, workerSeconds)
|
|
746
|
+
new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
760
747
|
|
|
761
748
|
forwardToWorker: (req, socket) ->
|
|
762
749
|
start = performance.now()
|
|
@@ -770,7 +757,9 @@ class Server
|
|
|
770
757
|
res = @forwardOnce!(req, socket.socket)
|
|
771
758
|
workerSeconds = (performance.now() - t0) / 1000
|
|
772
759
|
|
|
773
|
-
|
|
760
|
+
# Only retry bodyless requests (body stream can't be reused)
|
|
761
|
+
canRetry = req.method in ['GET', 'HEAD', 'OPTIONS', 'DELETE']
|
|
762
|
+
if canRetry and res.status is 503 and res.headers.get('Rip-Worker-Busy') is '1'
|
|
774
763
|
retry = @getNextAvailableSocket()
|
|
775
764
|
if retry and retry isnt socket
|
|
776
765
|
@releaseWorker(socket)
|
|
@@ -779,15 +768,10 @@ class Server
|
|
|
779
768
|
t1 = performance.now()
|
|
780
769
|
res = @forwardOnce!(req, retry.socket)
|
|
781
770
|
workerSeconds = (performance.now() - t1) / 1000
|
|
782
|
-
headers = stripInternalHeaders(res.headers)
|
|
783
|
-
headers.delete('date')
|
|
784
|
-
if @flags.jsonLogging
|
|
785
|
-
logAccessJson(@flags.appName, req, res, (performance.now() - start) / 1000, workerSeconds)
|
|
786
|
-
else if @flags.accessLog
|
|
787
|
-
logAccessHuman(@flags.appName, req, res, (performance.now() - start) / 1000, workerSeconds)
|
|
788
771
|
@releaseWorker(retry)
|
|
789
|
-
return
|
|
790
|
-
catch
|
|
772
|
+
return @buildResponse(res, req, start, workerSeconds)
|
|
773
|
+
catch err
|
|
774
|
+
console.error "[server] forwardToWorker error:", err.message or err if process.env.RIP_DEBUG
|
|
791
775
|
@sockets = @sockets.filter((x) -> x.socket isnt socket.socket)
|
|
792
776
|
@availableWorkers = @availableWorkers.filter((x) -> x.socket isnt socket.socket)
|
|
793
777
|
released = true
|
|
@@ -796,32 +780,28 @@ class Server
|
|
|
796
780
|
@releaseWorker(socket) unless released
|
|
797
781
|
|
|
798
782
|
return new Response('Service unavailable', { status: 503, headers: { 'Retry-After': '1' } }) unless res
|
|
799
|
-
|
|
800
|
-
headers = stripInternalHeaders(res.headers)
|
|
801
|
-
headers.delete('date')
|
|
802
|
-
@maybeAddSecurityHeaders(headers)
|
|
803
|
-
|
|
804
|
-
if @flags.jsonLogging
|
|
805
|
-
logAccessJson(@flags.appName, req, res, (performance.now() - start) / 1000, workerSeconds)
|
|
806
|
-
else if @flags.accessLog
|
|
807
|
-
logAccessHuman(@flags.appName, req, res, (performance.now() - start) / 1000, workerSeconds)
|
|
808
|
-
|
|
809
|
-
new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
783
|
+
@buildResponse(res, req, start, workerSeconds)
|
|
810
784
|
|
|
811
785
|
forwardOnce: (req, socketPath) ->
|
|
812
786
|
inUrl = new URL(req.url)
|
|
813
787
|
forwardUrl = "http://localhost#{inUrl.pathname}#{inUrl.search}"
|
|
814
788
|
controller = new AbortController()
|
|
815
|
-
|
|
789
|
+
connectTimer = setTimeout (-> controller.abort()), @flags.connectTimeoutMs
|
|
790
|
+
readTimeoutMs = @flags.readTimeoutMs
|
|
816
791
|
|
|
817
792
|
try
|
|
818
|
-
|
|
819
|
-
|
|
793
|
+
# Don't await fetch - we need to race the promise against timeout
|
|
794
|
+
fetchPromise = fetch(forwardUrl, { method: req.method, headers: req.headers, body: req.body, unix: socketPath, signal: controller.signal })
|
|
820
795
|
readGuard = new Promise (_, rej) ->
|
|
821
|
-
setTimeout (-> rej(new
|
|
822
|
-
Promise.race!([
|
|
823
|
-
|
|
824
|
-
|
|
796
|
+
setTimeout (-> rej(new Error('Upstream timeout'))), readTimeoutMs
|
|
797
|
+
res = Promise.race!([fetchPromise, readGuard])
|
|
798
|
+
clearTimeout(connectTimer)
|
|
799
|
+
res
|
|
800
|
+
catch err
|
|
801
|
+
clearTimeout(connectTimer)
|
|
802
|
+
if err.message is 'Upstream timeout'
|
|
803
|
+
return new Response('Gateway timeout', { status: 504 })
|
|
804
|
+
throw err
|
|
825
805
|
|
|
826
806
|
drainQueue: ->
|
|
827
807
|
while @inflightTotal < Math.max(1, @sockets.length) and @availableWorkers.length > 0
|
|
@@ -897,18 +877,18 @@ class Server
|
|
|
897
877
|
console.error 'Failed to read TLS cert/key from provided paths. Use http or fix paths.'
|
|
898
878
|
process.exit(2)
|
|
899
879
|
|
|
900
|
-
#
|
|
880
|
+
# Ensure certs directory exists
|
|
881
|
+
certsDir = join(homedir(), '.rip', 'certs')
|
|
882
|
+
try mkdirSync(certsDir, { recursive: true }) catch then null
|
|
883
|
+
|
|
884
|
+
# Try mkcert first (trusted local CA)
|
|
901
885
|
if @flags.autoTls
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
certPath = join(dir, 'localhost.pem')
|
|
905
|
-
keyPath = join(dir, 'localhost-key.pem')
|
|
886
|
+
certPath = join(certsDir, 'localhost.pem')
|
|
887
|
+
keyPath = join(certsDir, 'localhost-key.pem')
|
|
906
888
|
unless existsSync(certPath) and existsSync(keyPath)
|
|
907
889
|
try
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
p = Bun.spawn(['mkcert', '-key-file', keyPath, '-cert-file', certPath, 'localhost', '127.0.0.1', '::1'])
|
|
911
|
-
p.exited
|
|
890
|
+
Bun.spawnSync(['mkcert', '-install'])
|
|
891
|
+
Bun.spawnSync(['mkcert', '-key-file', keyPath, '-cert-file', certPath, 'localhost', '127.0.0.1', '::1'])
|
|
912
892
|
catch
|
|
913
893
|
null # fall through to self-signed
|
|
914
894
|
if existsSync(certPath) and existsSync(keyPath)
|
|
@@ -918,14 +898,11 @@ class Server
|
|
|
918
898
|
return { cert, key }
|
|
919
899
|
|
|
920
900
|
# Self-signed via openssl
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
certPath = join(dir, 'selfsigned-localhost.pem')
|
|
924
|
-
keyPath = join(dir, 'selfsigned-localhost-key.pem')
|
|
901
|
+
certPath = join(certsDir, 'selfsigned-localhost.pem')
|
|
902
|
+
keyPath = join(certsDir, 'selfsigned-localhost-key.pem')
|
|
925
903
|
unless existsSync(certPath) and existsSync(keyPath)
|
|
926
904
|
try
|
|
927
|
-
|
|
928
|
-
p.exited
|
|
905
|
+
Bun.spawnSync(['openssl', 'req', '-x509', '-nodes', '-newkey', 'rsa:2048', '-keyout', keyPath, '-out', certPath, '-subj', '/CN=localhost', '-days', '3650'])
|
|
929
906
|
catch
|
|
930
907
|
console.error 'TLS required but could not provision a certificate (mkcert/openssl missing). Use http or provide --cert/--key.'
|
|
931
908
|
process.exit(2)
|
|
@@ -950,12 +927,12 @@ class Server
|
|
|
950
927
|
|
|
951
928
|
getLanIP: ->
|
|
952
929
|
try
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
return
|
|
930
|
+
nets = networkInterfaces()
|
|
931
|
+
for name, addrs of nets
|
|
932
|
+
for addr in addrs
|
|
933
|
+
continue if addr.internal or addr.family isnt 'IPv4'
|
|
934
|
+
continue if addr.address.startsWith('169.254.') # Link-local
|
|
935
|
+
return addr.address
|
|
959
936
|
catch
|
|
960
937
|
null
|
|
961
938
|
null
|
|
@@ -971,13 +948,14 @@ class Server
|
|
|
971
948
|
|
|
972
949
|
port = @flags.httpsPort or @flags.httpPort or 80
|
|
973
950
|
protocol = if @flags.httpsPort then 'https' else 'http'
|
|
951
|
+
serviceType = if @flags.httpsPort then '_https._tcp' else '_http._tcp'
|
|
974
952
|
serviceName = host.replace('.local', '')
|
|
975
953
|
|
|
976
954
|
try
|
|
977
955
|
proc = Bun.spawn [
|
|
978
956
|
'dns-sd', '-P'
|
|
979
957
|
serviceName
|
|
980
|
-
|
|
958
|
+
serviceType
|
|
981
959
|
'local'
|
|
982
960
|
String(port)
|
|
983
961
|
host
|
|
@@ -1002,46 +980,62 @@ class Server
|
|
|
1002
980
|
# ==============================================================================
|
|
1003
981
|
|
|
1004
982
|
main = ->
|
|
1005
|
-
#
|
|
1006
|
-
if '
|
|
983
|
+
# Version flag
|
|
984
|
+
if '--version' in process.argv or '-v' in process.argv
|
|
1007
985
|
try
|
|
1008
|
-
|
|
1009
|
-
|
|
986
|
+
pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'))
|
|
987
|
+
console.log "rip-server v#{pkg.version}"
|
|
1010
988
|
catch
|
|
1011
|
-
|
|
1012
|
-
console.log 'rip-server: stop requested'
|
|
989
|
+
console.log 'rip-server (version unknown)'
|
|
1013
990
|
return
|
|
1014
991
|
|
|
1015
|
-
#
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
undefined
|
|
992
|
+
# Helper functions for subcommands
|
|
993
|
+
getKV = (prefix) ->
|
|
994
|
+
for tok in process.argv
|
|
995
|
+
return tok.slice(prefix.length) if tok.startsWith(prefix)
|
|
996
|
+
undefined
|
|
1021
997
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
998
|
+
findAppPathToken = ->
|
|
999
|
+
for i in [2...process.argv.length]
|
|
1000
|
+
tok = process.argv[i]
|
|
1001
|
+
pathPart = if tok.includes('@') then tok.split('@')[0] else tok
|
|
1002
|
+
looksLikePath = pathPart.includes('/') or pathPart.startsWith('.') or pathPart.endsWith('.rip') or pathPart.endsWith('.ts')
|
|
1003
|
+
try
|
|
1004
|
+
return pathPart if looksLikePath and existsSync(if isAbsolute(pathPart) then pathPart else resolve(process.cwd(), pathPart))
|
|
1005
|
+
catch
|
|
1006
|
+
null
|
|
1007
|
+
undefined
|
|
1032
1008
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1009
|
+
computeSocketPrefix = ->
|
|
1010
|
+
override = getKV('--socket-prefix=')
|
|
1011
|
+
return override if override
|
|
1012
|
+
appTok = findAppPathToken()
|
|
1013
|
+
if appTok
|
|
1014
|
+
try
|
|
1015
|
+
{ appName } = resolveAppEntry(appTok)
|
|
1016
|
+
return "rip_#{appName}"
|
|
1017
|
+
catch
|
|
1018
|
+
null
|
|
1019
|
+
'rip_server'
|
|
1044
1020
|
|
|
1021
|
+
# Subcommand: stop
|
|
1022
|
+
if 'stop' in process.argv
|
|
1023
|
+
prefix = computeSocketPrefix()
|
|
1024
|
+
pidFile = getPidFilePath(prefix)
|
|
1025
|
+
try
|
|
1026
|
+
if existsSync(pidFile)
|
|
1027
|
+
pid = parseInt(readFileSync(pidFile, 'utf8').trim())
|
|
1028
|
+
process.kill(pid, 'SIGTERM')
|
|
1029
|
+
console.log "rip-server: sent SIGTERM to process #{pid}"
|
|
1030
|
+
else
|
|
1031
|
+
console.log "rip-server: no PID file found at #{pidFile}, trying pkill..."
|
|
1032
|
+
Bun.spawnSync(['pkill', '-f', __filename])
|
|
1033
|
+
catch e
|
|
1034
|
+
console.error "rip-server: stop failed: #{e.message}"
|
|
1035
|
+
return
|
|
1036
|
+
|
|
1037
|
+
# Subcommand: list
|
|
1038
|
+
if 'list' in process.argv
|
|
1045
1039
|
controlUnix = getControlSocketPath(computeSocketPrefix())
|
|
1046
1040
|
try
|
|
1047
1041
|
res = fetch!('http://localhost/registry', { unix: controlUnix, method: 'GET' })
|
|
@@ -1056,11 +1050,15 @@ main = ->
|
|
|
1056
1050
|
|
|
1057
1051
|
# Normal startup
|
|
1058
1052
|
flags = parseFlags(process.argv)
|
|
1053
|
+
pidFile = getPidFilePath(flags.socketPrefix)
|
|
1054
|
+
writeFileSync(pidFile, String(process.pid))
|
|
1055
|
+
|
|
1059
1056
|
svr = new Server(flags)
|
|
1060
1057
|
mgr = new Manager(flags)
|
|
1061
1058
|
|
|
1062
1059
|
cleanup = ->
|
|
1063
1060
|
console.log 'rip-server: shutting down...'
|
|
1061
|
+
try unlinkSync(pidFile) catch then null
|
|
1064
1062
|
svr.stop()
|
|
1065
1063
|
mgr.stop!
|
|
1066
1064
|
process.exit(0)
|