@rip-lang/server 0.5.7 → 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 +198 -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,8 +536,21 @@ 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
|
|
@@ -613,54 +595,39 @@ class Server
|
|
|
613
595
|
|
|
614
596
|
start: ->
|
|
615
597
|
httpOnly = @flags.httpsPort is null
|
|
598
|
+
fetchFn = @fetch.bind(@)
|
|
616
599
|
|
|
617
|
-
|
|
600
|
+
# Helper to start server, incrementing port if needed
|
|
601
|
+
startOnPort = (p, opts = {}) =>
|
|
618
602
|
port = p
|
|
619
603
|
while true
|
|
620
604
|
try
|
|
621
|
-
return Bun.serve({ port, idleTimeout: 8, fetch: fetchFn })
|
|
605
|
+
return Bun.serve(Object.assign({ port, idleTimeout: 8, fetch: fetchFn }, opts))
|
|
622
606
|
catch e
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
continue
|
|
626
|
-
throw e
|
|
607
|
+
throw e unless e?.code is 'EADDRINUSE'
|
|
608
|
+
port++
|
|
627
609
|
|
|
628
610
|
if httpOnly
|
|
629
611
|
if @flags.httpPort is 0
|
|
630
612
|
try
|
|
631
|
-
@server = Bun.serve({ port: 80, idleTimeout: 8, fetch:
|
|
613
|
+
@server = Bun.serve({ port: 80, idleTimeout: 8, fetch: fetchFn })
|
|
632
614
|
catch e
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
else
|
|
636
|
-
throw e
|
|
615
|
+
throw e unless e?.code in ['EADDRINUSE', 'EACCES']
|
|
616
|
+
@server = startOnPort(5700)
|
|
637
617
|
else
|
|
638
|
-
@server = startOnPort(@flags.httpPort
|
|
618
|
+
@server = startOnPort(@flags.httpPort)
|
|
639
619
|
@flags.httpPort = @server.port
|
|
640
620
|
else
|
|
641
621
|
tls = @loadTlsMaterial!
|
|
642
622
|
|
|
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
623
|
if not @flags.httpsPort or @flags.httpsPort is 0
|
|
655
624
|
try
|
|
656
|
-
@httpsServer = Bun.serve({ port: 443, idleTimeout: 8, tls, fetch:
|
|
625
|
+
@httpsServer = Bun.serve({ port: 443, idleTimeout: 8, tls, fetch: fetchFn })
|
|
657
626
|
catch e
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
else
|
|
661
|
-
throw e
|
|
627
|
+
throw e unless e?.code in ['EADDRINUSE', 'EACCES']
|
|
628
|
+
@httpsServer = startOnPort(5700, { tls })
|
|
662
629
|
else
|
|
663
|
-
@httpsServer =
|
|
630
|
+
@httpsServer = startOnPort(@flags.httpsPort, { tls })
|
|
664
631
|
|
|
665
632
|
httpsPort = @httpsServer.port
|
|
666
633
|
@flags.httpsPort = httpsPort
|
|
@@ -757,8 +724,26 @@ class Server
|
|
|
757
724
|
@newestVersion is null or worker.version is null or worker.version >= @newestVersion
|
|
758
725
|
|
|
759
726
|
releaseWorker: (worker) ->
|
|
727
|
+
return unless worker
|
|
728
|
+
return unless @sockets.some((s) -> s.socket is worker.socket) # Validate still in pool
|
|
760
729
|
worker.inflight = 0
|
|
761
|
-
|
|
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 })
|
|
762
747
|
|
|
763
748
|
forwardToWorker: (req, socket) ->
|
|
764
749
|
start = performance.now()
|
|
@@ -772,7 +757,9 @@ class Server
|
|
|
772
757
|
res = @forwardOnce!(req, socket.socket)
|
|
773
758
|
workerSeconds = (performance.now() - t0) / 1000
|
|
774
759
|
|
|
775
|
-
|
|
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'
|
|
776
763
|
retry = @getNextAvailableSocket()
|
|
777
764
|
if retry and retry isnt socket
|
|
778
765
|
@releaseWorker(socket)
|
|
@@ -781,15 +768,10 @@ class Server
|
|
|
781
768
|
t1 = performance.now()
|
|
782
769
|
res = @forwardOnce!(req, retry.socket)
|
|
783
770
|
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
771
|
@releaseWorker(retry)
|
|
791
|
-
return
|
|
792
|
-
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
|
|
793
775
|
@sockets = @sockets.filter((x) -> x.socket isnt socket.socket)
|
|
794
776
|
@availableWorkers = @availableWorkers.filter((x) -> x.socket isnt socket.socket)
|
|
795
777
|
released = true
|
|
@@ -798,32 +780,28 @@ class Server
|
|
|
798
780
|
@releaseWorker(socket) unless released
|
|
799
781
|
|
|
800
782
|
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 })
|
|
783
|
+
@buildResponse(res, req, start, workerSeconds)
|
|
812
784
|
|
|
813
785
|
forwardOnce: (req, socketPath) ->
|
|
814
786
|
inUrl = new URL(req.url)
|
|
815
787
|
forwardUrl = "http://localhost#{inUrl.pathname}#{inUrl.search}"
|
|
816
788
|
controller = new AbortController()
|
|
817
|
-
|
|
789
|
+
connectTimer = setTimeout (-> controller.abort()), @flags.connectTimeoutMs
|
|
790
|
+
readTimeoutMs = @flags.readTimeoutMs
|
|
818
791
|
|
|
819
792
|
try
|
|
820
|
-
|
|
821
|
-
|
|
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 })
|
|
822
795
|
readGuard = new Promise (_, rej) ->
|
|
823
|
-
setTimeout (-> rej(new
|
|
824
|
-
Promise.race!([
|
|
825
|
-
|
|
826
|
-
|
|
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
|
|
827
805
|
|
|
828
806
|
drainQueue: ->
|
|
829
807
|
while @inflightTotal < Math.max(1, @sockets.length) and @availableWorkers.length > 0
|
|
@@ -899,18 +877,18 @@ class Server
|
|
|
899
877
|
console.error 'Failed to read TLS cert/key from provided paths. Use http or fix paths.'
|
|
900
878
|
process.exit(2)
|
|
901
879
|
|
|
902
|
-
#
|
|
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)
|
|
903
885
|
if @flags.autoTls
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
certPath = join(dir, 'localhost.pem')
|
|
907
|
-
keyPath = join(dir, 'localhost-key.pem')
|
|
886
|
+
certPath = join(certsDir, 'localhost.pem')
|
|
887
|
+
keyPath = join(certsDir, 'localhost-key.pem')
|
|
908
888
|
unless existsSync(certPath) and existsSync(keyPath)
|
|
909
889
|
try
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
p = Bun.spawn(['mkcert', '-key-file', keyPath, '-cert-file', certPath, 'localhost', '127.0.0.1', '::1'])
|
|
913
|
-
p.exited
|
|
890
|
+
Bun.spawnSync(['mkcert', '-install'])
|
|
891
|
+
Bun.spawnSync(['mkcert', '-key-file', keyPath, '-cert-file', certPath, 'localhost', '127.0.0.1', '::1'])
|
|
914
892
|
catch
|
|
915
893
|
null # fall through to self-signed
|
|
916
894
|
if existsSync(certPath) and existsSync(keyPath)
|
|
@@ -920,14 +898,11 @@ class Server
|
|
|
920
898
|
return { cert, key }
|
|
921
899
|
|
|
922
900
|
# Self-signed via openssl
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
certPath = join(dir, 'selfsigned-localhost.pem')
|
|
926
|
-
keyPath = join(dir, 'selfsigned-localhost-key.pem')
|
|
901
|
+
certPath = join(certsDir, 'selfsigned-localhost.pem')
|
|
902
|
+
keyPath = join(certsDir, 'selfsigned-localhost-key.pem')
|
|
927
903
|
unless existsSync(certPath) and existsSync(keyPath)
|
|
928
904
|
try
|
|
929
|
-
|
|
930
|
-
p.exited
|
|
905
|
+
Bun.spawnSync(['openssl', 'req', '-x509', '-nodes', '-newkey', 'rsa:2048', '-keyout', keyPath, '-out', certPath, '-subj', '/CN=localhost', '-days', '3650'])
|
|
931
906
|
catch
|
|
932
907
|
console.error 'TLS required but could not provision a certificate (mkcert/openssl missing). Use http or provide --cert/--key.'
|
|
933
908
|
process.exit(2)
|
|
@@ -952,12 +927,12 @@ class Server
|
|
|
952
927
|
|
|
953
928
|
getLanIP: ->
|
|
954
929
|
try
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
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
|
|
961
936
|
catch
|
|
962
937
|
null
|
|
963
938
|
null
|
|
@@ -973,13 +948,14 @@ class Server
|
|
|
973
948
|
|
|
974
949
|
port = @flags.httpsPort or @flags.httpPort or 80
|
|
975
950
|
protocol = if @flags.httpsPort then 'https' else 'http'
|
|
951
|
+
serviceType = if @flags.httpsPort then '_https._tcp' else '_http._tcp'
|
|
976
952
|
serviceName = host.replace('.local', '')
|
|
977
953
|
|
|
978
954
|
try
|
|
979
955
|
proc = Bun.spawn [
|
|
980
956
|
'dns-sd', '-P'
|
|
981
957
|
serviceName
|
|
982
|
-
|
|
958
|
+
serviceType
|
|
983
959
|
'local'
|
|
984
960
|
String(port)
|
|
985
961
|
host
|
|
@@ -1004,46 +980,62 @@ class Server
|
|
|
1004
980
|
# ==============================================================================
|
|
1005
981
|
|
|
1006
982
|
main = ->
|
|
1007
|
-
#
|
|
1008
|
-
if '
|
|
983
|
+
# Version flag
|
|
984
|
+
if '--version' in process.argv or '-v' in process.argv
|
|
1009
985
|
try
|
|
1010
|
-
|
|
1011
|
-
|
|
986
|
+
pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'))
|
|
987
|
+
console.log "rip-server v#{pkg.version}"
|
|
1012
988
|
catch
|
|
1013
|
-
|
|
1014
|
-
console.log 'rip-server: stop requested'
|
|
989
|
+
console.log 'rip-server (version unknown)'
|
|
1015
990
|
return
|
|
1016
991
|
|
|
1017
|
-
#
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
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
|
|
1023
997
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
|
1034
1008
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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'
|
|
1046
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
|
|
1047
1039
|
controlUnix = getControlSocketPath(computeSocketPrefix())
|
|
1048
1040
|
try
|
|
1049
1041
|
res = fetch!('http://localhost/registry', { unix: controlUnix, method: 'GET' })
|
|
@@ -1058,11 +1050,15 @@ main = ->
|
|
|
1058
1050
|
|
|
1059
1051
|
# Normal startup
|
|
1060
1052
|
flags = parseFlags(process.argv)
|
|
1053
|
+
pidFile = getPidFilePath(flags.socketPrefix)
|
|
1054
|
+
writeFileSync(pidFile, String(process.pid))
|
|
1055
|
+
|
|
1061
1056
|
svr = new Server(flags)
|
|
1062
1057
|
mgr = new Manager(flags)
|
|
1063
1058
|
|
|
1064
1059
|
cleanup = ->
|
|
1065
1060
|
console.log 'rip-server: shutting down...'
|
|
1061
|
+
try unlinkSync(pidFile) catch then null
|
|
1066
1062
|
svr.stop()
|
|
1067
1063
|
mgr.stop!
|
|
1068
1064
|
process.exit(0)
|