Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39538
1
##############################################################################
2
#
3
# CoCalc: Collaborative Calculation in the Cloud
4
#
5
# Copyright (C) 2016, Sagemath Inc.
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#
20
###############################################################################
21
22
###
23
This is the CoCalc Global HUB. It runs as a daemon, sitting in the
24
middle of the action, connected to potentially thousands of clients,
25
many Sage sessions, and PostgreSQL database. There are
26
many HUBs running.
27
###
28
29
require('coffee-cache')
30
31
DEBUG = false
32
33
if not process.env.SMC_TEST
34
if process.env.SMC_DEBUG or process.env.DEVEL
35
DEBUG = true
36
37
# node.js -- builtin libraries
38
net = require('net')
39
assert = require('assert')
40
fs = require('fs')
41
path_module = require('path')
42
underscore = require('underscore')
43
{EventEmitter} = require('events')
44
mime = require('mime')
45
46
program = undefined # defined below -- can't import with nodev6 at module level when hub.coffee used as a module.
47
48
# CoCalc path configurations (shared with webpack)
49
misc_node = require('smc-util-node/misc_node')
50
SMC_ROOT = misc_node.SMC_ROOT
51
SALVUS_HOME = misc_node.SALVUS_HOME
52
OUTPUT_DIR = misc_node.OUTPUT_DIR
53
STATIC_PATH = path_module.join(SALVUS_HOME, OUTPUT_DIR)
54
WEBAPP_LIB = misc_node.WEBAPP_LIB
55
56
underscore = require('underscore')
57
58
# CoCalc libraries
59
misc = require('smc-util/misc')
60
{defaults, required} = misc
61
message = require('smc-util/message') # message protocol between front-end and back-end
62
client_lib = require('smc-util/client')
63
{Client} = require('./client')
64
sage = require('./sage') # sage server
65
auth = require('./auth')
66
base_url = require('./base-url')
67
68
local_hub_connection = require('./local_hub_connection')
69
hub_proxy = require('./proxy')
70
71
MetricsRecorder = require('./metrics-recorder')
72
73
# express http server -- serves some static/dynamic endpoints
74
hub_http_server = require('./hub_http_server')
75
76
# registers the hub with the database periodically
77
hub_register = require('./hub_register')
78
79
# How frequently to register with the database that this hub is up and running,
80
# and also report number of connected clients
81
REGISTER_INTERVAL_S = 45 # every 45 seconds
82
83
smc_version = {}
84
init_smc_version = () ->
85
smc_version = require('./hub-version')
86
# winston.debug("init smc_version: #{misc.to_json(smc_version.version)}")
87
smc_version.on 'change', (version) ->
88
winston.debug("smc_version changed -- sending updates to clients")
89
for id, c of clients
90
if c.smc_version < version.version
91
c.push_version_update()
92
93
to_json = misc.to_json
94
from_json = misc.from_json
95
96
# third-party libraries: add any new nodejs dependencies to the NODEJS_PACKAGES list in build.py
97
async = require("async")
98
99
Cookies = require('cookies') # https://github.com/jed/cookies
100
101
winston = require('winston') # logging -- https://github.com/flatiron/winston
102
103
# Set the log level
104
winston.remove(winston.transports.Console)
105
if not process.env.SMC_TEST
106
winston.add(winston.transports.Console, {level: 'debug', timestamp:true, colorize:true})
107
108
# module scope variables:
109
database = null
110
111
{init_support} = require('./support')
112
113
# the connected clients
114
clients = require('./clients').get_clients()
115
116
##############################
117
# File use tracking
118
##############################
119
120
normalize_path = (path) ->
121
# Rules:
122
# kdkd/tmp/.test.sagews.sage-chat --> kdkd/tmp/test.sagews, comment "chat"
123
# foo/bar/.2014-11-01-175408.ipynb.syncdoc --> foo/bar/2014-11-01-175408.ipynb
124
path = misc.trunc_middle(path, 2048) # prevent potential attacks/mistakes involving a large path breaking things...
125
ext = misc.filename_extension(path)
126
action = 'edit'
127
{head, tail} = misc.path_split(path)
128
if ext == "sage-chat"
129
action = 'chat' # editing sage-chat gets the extra important chat action (instead of just edit)
130
if tail?[0] == '.'
131
# hidden sage-chat associated to a regular file, so notify about the regular file
132
path = path.slice(0, path.length-'.sage-chat'.length)
133
{head, tail} = misc.path_split(path)
134
tail = tail.slice(1) # get rid of .
135
if head
136
path = head + '/' + tail
137
else
138
path = tail
139
else if ext.slice(0,7) == 'syncdoc' # for IPython, and possibly other things later
140
path = path.slice(0, path.length - ext.length - 1)
141
{head, tail} = misc.path_split(path)
142
tail = tail.slice(1) # get rid of .
143
if head
144
path = head + '/' + tail
145
else
146
path = tail
147
else if ext == "sage-history"
148
path = undefined
149
#else if ext == '.sagemathcloud.log' # ignore for now
150
# path = undefined
151
return {path:path, action:action}
152
153
path_activity_cache = {}
154
path_activity = (opts) ->
155
opts = defaults opts,
156
account_id : required
157
project_id : required
158
path : required
159
client : required
160
cb : undefined
161
162
{path, action} = normalize_path(opts.path)
163
winston.debug("path_activity(#{opts.account_id},#{opts.project_id},#{path}): #{action}")
164
if not path?
165
opts.cb?()
166
return
167
168
opts.client.touch
169
project_id : opts.project_id
170
path : path
171
action : action
172
force : action == 'chat'
173
cb : opts.cb
174
175
##############################
176
# Create the Primus realtime socket server
177
##############################
178
primus_server = undefined
179
init_primus_server = (http_server) ->
180
Primus = require('primus')
181
# change also requires changing head.html
182
opts =
183
pathname : path_module.join(BASE_URL, '/hub')
184
primus_server = new Primus(http_server, opts)
185
winston.debug("primus_server: listening on #{opts.pathname}")
186
187
primus_server.on "connection", (conn) ->
188
# Now handle the connection
189
winston.debug("primus_server: new connection from #{conn.address.ip} -- #{conn.id}")
190
primus_conn_sent_data = false
191
f = (data) ->
192
primus_conn_sent_data = true
193
id = data.toString()
194
winston.debug("primus_server: got id='#{id}'")
195
conn.removeListener('data',f)
196
C = clients[id]
197
#winston.debug("primus client ids=#{misc.to_json(misc.keys(clients))}")
198
if C?
199
if C.closed
200
winston.debug("primus_server: '#{id}' matches expired Client -- deleting")
201
delete clients[id]
202
C = undefined
203
else
204
winston.debug("primus_server: '#{id}' matches existing Client -- re-using")
205
206
# In case the connection hadn't been officially ended yet the changefeeds might
207
# have been left open sending messages that won't get through. So ensure the client
208
# must recreate them all before continuing.
209
C.query_cancel_all_changefeeds()
210
211
cookies = new Cookies(conn.request)
212
if C._remember_me_value == cookies.get(BASE_URL + 'remember_me')
213
old_id = C.conn.id
214
C.conn.removeAllListeners()
215
C.conn.end()
216
C.conn = conn
217
conn.id = id
218
conn.write(conn.id)
219
C.install_conn_handlers()
220
else
221
winston.debug("primus_server: '#{id}' matches but cookies do not match, so not re-using")
222
C = undefined
223
if not C?
224
winston.debug("primus_server: '#{id}' unknown, so making a new Client with id #{conn.id}")
225
conn.write(conn.id)
226
clients[conn.id] = new Client
227
conn : conn
228
logger : winston
229
database : database
230
compute_server : compute_server
231
host : program.host
232
port : program.port
233
234
conn.on("data",f)
235
236
# Given the client up to 15s to send info about itself. If get nothing, just
237
# end the connection.
238
no_data = ->
239
if conn? and not primus_conn_sent_data
240
winston.debug("primus_server: #{conn.id} sent no data after 15s, so closing")
241
conn.end()
242
setTimeout(no_data, 15000)
243
244
245
#######################################################
246
# Pushing a message to clients; querying for clients.
247
# This is (or will be) subtle, due to having
248
# multiple HUBs running on different computers.
249
#######################################################
250
251
# get_client_ids -- given query parameters, returns a list of id's,
252
# where the id is the connection id, which we assume is
253
# globally unique across all of space and time.
254
get_client_ids = (opts) ->
255
opts = defaults opts,
256
account_id : undefined # include connected clients logged in under this account
257
project_id : undefined # include connected clients that are a user of this project
258
exclude : undefined # array of id's to exclude from results
259
cb : required
260
result = [] # will have list of client id's in it
261
262
# include a given client id in result, if it isn't in the exclude array
263
include = (id) ->
264
if id not in result
265
if opts.exclude?
266
if id in opts.exclude
267
return
268
result.push(id)
269
270
account_ids = {} # account_id's to consider
271
272
if opts.account_id?
273
account_ids[opts.account_id] = true
274
275
async.series([
276
# If considering a given project, then get all the relevant account_id's.
277
(cb) ->
278
if opts.project_id?
279
database.get_account_ids_using_project
280
project_id : opts.project_id
281
cb : (err, result) ->
282
if err
283
cb(err); return
284
for r in result
285
account_ids[r] = true
286
cb()
287
else
288
cb()
289
# Now get the corresponding connected client id's.
290
(cb) ->
291
for id, client of clients
292
if account_ids[client.account_id]?
293
include(id)
294
cb()
295
], (err) ->
296
opts.cb(err, result)
297
)
298
299
300
# Send a message to a bunch of clients connected to this hub.
301
# This does not send anything to other hubs or clients at other hubs; the only
302
# way for a message to go to a client at another hub is via some local hub.
303
# This design means that we do not have to track which hubs which
304
# clients are connected to in a database or registry, which wold be a nightmare
305
# especially due to synchronization issues (some TODO comments might refer to such
306
# a central design, because that *was* the non-implemented design at some point).
307
push_to_clients = (opts) ->
308
opts = defaults opts,
309
mesg : required
310
where : undefined # see the get_client_ids function
311
to : undefined
312
cb : undefined
313
314
dest = []
315
316
async.series([
317
(cb) ->
318
if opts.where?
319
get_client_ids(misc.merge(opts.where, cb:(error, result) ->
320
if error
321
opts.cb?(true)
322
cb(true)
323
else
324
dest = dest.concat(result)
325
cb()
326
))
327
else
328
cb()
329
330
(cb) ->
331
# include all clients explicitly listed in "to"
332
if opts.to?
333
dest = dest.concat(opts.to)
334
335
for id in dest
336
client = clients[id]
337
if client?
338
winston.debug("pushing a message to client #{id}")
339
client.push_to_client(opts.mesg)
340
else
341
winston.debug("not pushing message to client #{id} since not actually connected")
342
opts.cb?(false)
343
cb()
344
345
346
])
347
348
349
350
reset_password = (email_address, cb) ->
351
async.series([
352
(cb) ->
353
connect_to_database
354
pool : 1
355
cb : cb
356
(cb) ->
357
database.reset_password
358
email_address : email_address
359
cb : cb
360
], (err) ->
361
if err
362
winston.debug("Error -- #{err}")
363
else
364
winston.debug("Password changed for #{email_address}")
365
cb?()
366
)
367
368
369
###
370
Connect to database
371
###
372
database = undefined
373
374
connect_to_database = (opts) ->
375
opts = defaults opts,
376
error : undefined # ignored
377
pool : program.db_pool
378
cb : required
379
dbg = (m) -> winston.debug("connect_to_database (PostgreSQL): #{m}")
380
if database? # already did this
381
dbg("already done")
382
opts.cb(); return
383
dbg("connecting...")
384
database = require('./postgres').db
385
host : program.database_nodes.split(',')[0] # postgres has only one master server
386
database : program.keyspace
387
concurrent_warn : program.db_concurrent_warn
388
database.connect(cb:opts.cb)
389
390
# client for compute servers
391
# The name "compute_server" below is CONFUSING; this is really a client for a
392
# remote server.
393
compute_server = undefined
394
init_compute_server = (cb) ->
395
winston.debug("init_compute_server: creating compute_server client")
396
f = (err, x) ->
397
if not err
398
winston.debug("compute server created")
399
else
400
winston.debug("FATAL ERROR creating compute server -- #{err}")
401
cb?(err)
402
return
403
compute_server = x
404
database.compute_server = compute_server
405
# This is used by the database when handling certain writes to make sure
406
# that the there is a connection to the corresponding project, so that
407
# the project can respond.
408
database.ensure_connection_to_project = (project_id) ->
409
local_hub_connection.connect_to_project(project_id, database, compute_server)
410
cb?()
411
412
if program.kucalc
413
f(undefined, require('./kucalc/compute-client').compute_client(database, winston))
414
else
415
require('./compute-client').compute_server
416
database : database
417
dev : program.dev
418
single : program.single
419
base_url : BASE_URL
420
cb : f
421
422
update_primus = (cb) ->
423
misc_node.execute_code
424
command : path_module.join(SMC_ROOT, WEBAPP_LIB, '/primus/update_primus')
425
cb : cb
426
427
428
429
# Delete expired data from the database.
430
delete_expired = (cb) ->
431
async.series([
432
(cb) ->
433
connect_to_database(cb:cb)
434
(cb) ->
435
database.delete_expired
436
count_only : false
437
cb : cb
438
], cb)
439
440
blob_maintenance = (cb) ->
441
async.series([
442
(cb) ->
443
connect_to_database(error:99999, pool:5, cb:cb)
444
(cb) ->
445
database.blob_maintenance(cb:cb)
446
], cb)
447
448
update_stats = (cb) ->
449
# This calculates and updates the statistics for the /stats endpoint.
450
# It's important that we call this periodically, because otherwise the /stats data is outdated.
451
async.series([
452
(cb) ->
453
connect_to_database(error:99999, pool:5, cb:cb)
454
(cb) ->
455
database.get_stats(cb:cb)
456
], cb)
457
458
stripe_sync = (dump_only, cb) ->
459
dbg = (m) -> winston.debug("stripe_sync: #{m}")
460
dbg()
461
async.series([
462
(cb) ->
463
dbg("connect to the database")
464
connect_to_database(error:99999, cb:cb)
465
(cb) ->
466
require('./stripe/sync').stripe_sync
467
database : database
468
dump_only : dump_only
469
logger : winston
470
cb : cb
471
], cb)
472
473
474
#############################################
475
# Start everything running
476
#############################################
477
BASE_URL = ''
478
metric_blocked = undefined
479
480
exports.start_server = start_server = (cb) ->
481
winston.debug("start_server")
482
483
winston.debug("dev = #{program.dev}")
484
485
BASE_URL = base_url.init(program.base_url)
486
winston.debug("base_url='#{BASE_URL}'")
487
488
fs.writeFileSync(path_module.join(SMC_ROOT, 'data', 'base_url'), BASE_URL)
489
490
# the order of init below is important
491
winston.debug("port = #{program.port}, proxy_port=#{program.proxy_port}")
492
winston.info("using database #{program.keyspace}")
493
hosts = program.database_nodes.split(',')
494
http_server = express_router = undefined
495
496
# Log anything that blocks the CPU for more than 10ms -- see https://github.com/tj/node-blocked
497
blocked = require('blocked')
498
blocked (ms) ->
499
if ms > 0
500
metric_blocked?.inc(ms)
501
# record that something blocked for over 10ms
502
winston.debug("BLOCKED for #{ms}ms")
503
504
init_smc_version()
505
506
async.series([
507
(cb) ->
508
if not program.port
509
cb(); return
510
winston.debug("Initializing Metrics Recorder")
511
MetricsRecorder.init(winston, (err, mr) ->
512
if err?
513
cb(err)
514
else
515
metric_blocked = MetricsRecorder.new_counter('blocked_ms_total', 'accumulates the "blocked" time in the hub [ms]')
516
cb()
517
)
518
(cb) ->
519
# this defines the global (to this file) database variable.
520
winston.debug("Connecting to the database.")
521
misc.retry_until_success
522
f : (cb) -> connect_to_database(cb:cb)
523
start_delay : 1000
524
max_delay : 10000
525
cb : () ->
526
winston.debug("connected to database.")
527
cb()
528
(cb) ->
529
if not program.port
530
cb(); return
531
if program.dev or program.update
532
winston.debug("updating the database schema...")
533
database.update_schema(cb:cb)
534
else
535
cb()
536
(cb) ->
537
if not program.port
538
cb(); return
539
require('./stripe/connect').init_stripe
540
database : database
541
logger : winston
542
cb : cb
543
(cb) ->
544
if not program.port
545
cb(); return
546
init_support(cb)
547
(cb) ->
548
init_compute_server(cb)
549
(cb) ->
550
if not program.port
551
cb(); return
552
# proxy server and http server; this working etc. *relies* on compute_server having been created
553
# However it can still serve many things without database. TODO: Eventually it could inform user
554
# that database isn't working.
555
x = hub_http_server.init_express_http_server
556
base_url : BASE_URL
557
dev : program.dev
558
compute_server : compute_server
559
database : database
560
{http_server, express_router} = x
561
winston.debug("starting express webserver listening on #{program.host}:#{program.port}")
562
http_server.listen(program.port, program.host, cb)
563
(cb) ->
564
if not program.port
565
cb(); return
566
async.parallel([
567
(cb) ->
568
# init authentication via passport (requires database)
569
auth.init_passport
570
router : express_router
571
database : database
572
base_url : BASE_URL
573
host : program.host
574
cb : cb
575
(cb) ->
576
if (program.dev or program.update) and not program.kucalc
577
update_primus(cb)
578
else
579
cb()
580
], cb)
581
], (err) =>
582
if err
583
winston.error("Error starting hub services! err=#{err}")
584
else
585
# Synchronous initialize of other functionality, now that the database, etc., are working.
586
winston.debug("base_url='#{BASE_URL}'")
587
588
if program.port
589
winston.debug("initializing primus websocket server")
590
init_primus_server(http_server)
591
592
if program.proxy_port
593
winston.debug("initializing the http proxy server on port #{program.proxy_port}")
594
hub_proxy.init_http_proxy_server
595
database : database
596
compute_server : compute_server
597
base_url : BASE_URL
598
port : program.proxy_port
599
host : program.host
600
601
if program.port
602
# Register periodically with the database.
603
hub_register.start
604
database : database
605
clients : clients
606
host : program.host
607
port : program.port
608
interval_s : REGISTER_INTERVAL_S
609
610
winston.info("Started hub. HTTP port #{program.port}; keyspace #{program.keyspace}")
611
cb?(err)
612
)
613
614
###
615
# Command line admin stuff -- should maybe be moved to another program?
616
###
617
add_user_to_project = (project_id, email_address, cb) ->
618
account_id = undefined
619
async.series([
620
# ensure database object is initialized
621
(cb) ->
622
connect_to_database(cb:cb)
623
# find account id corresponding to email address
624
(cb) ->
625
database.account_exists
626
email_address : email_address
627
cb : (err, _account_id) ->
628
account_id = _account_id
629
cb(err)
630
# add user to that project as a collaborator
631
(cb) ->
632
database.add_user_to_project
633
project_id : project_id
634
account_id : account_id
635
group : 'collaborator'
636
cb : cb
637
], cb)
638
639
640
#############################################
641
# Process command line arguments
642
#############################################
643
644
command_line = () ->
645
program = require('commander') # command line arguments -- https://github.com/visionmedia/commander.js/
646
daemon = require("start-stop-daemon") # don't import unless in a script; otherwise breaks in node v6+
647
default_db = process.env.PGHOST ? 'localhost'
648
649
program.usage('[start/stop/restart/status/nodaemon] [options]')
650
.option('--port <n>', 'port to listen on (default: 5000; 0 -- do not start)', ((n)->parseInt(n)), 5000)
651
.option('--proxy_port <n>', 'port that the proxy server listens on (default: 0 -- do not start)', ((n)->parseInt(n)), 0)
652
.option('--log_level [level]', "log level (default: debug) useful options include INFO, WARNING and DEBUG", String, "debug")
653
.option('--host [string]', 'host of interface to bind to (default: "127.0.0.1")', String, "127.0.0.1")
654
.option('--pidfile [string]', 'store pid in this file (default: "data/pids/hub.pid")', String, "data/pids/hub.pid")
655
.option('--logfile [string]', 'write log to this file (default: "data/logs/hub.log")', String, "data/logs/hub.log")
656
.option('--database_nodes <string,string,...>', "database address (default: '#{default_db}')", String, default_db)
657
.option('--keyspace [string]', 'Database name to use (default: "smc")', String, 'smc')
658
.option('--passwd [email_address]', 'Reset password of given user', String, '')
659
.option('--update', 'Update schema and primus on startup (always true for --dev; otherwise, false)')
660
.option('--stripe_sync', 'Sync stripe subscriptions to database for all users with stripe id', String, 'yes')
661
.option('--stripe_dump', 'Dump stripe subscriptions info to ~/stripe/', String, 'yes')
662
.option('--update_stats', 'Calculates the statistics for the /stats endpoint and stores them in the database', String, 'yes')
663
.option('--delete_expired', 'Delete expired data from the database', String, 'yes')
664
.option('--blob_maintenance', 'Do blob-related maintenance (dump to tarballs, offload to gcloud)', String, 'yes')
665
.option('--add_user_to_project [project_id,email_address]', 'Add user with given email address to project with given ID', String, '')
666
.option('--base_url [string]', 'Base url, so https://sitenamebase_url/', String, '') # '' or string that starts with /
667
.option('--local', 'If option is specified, then *all* projects run locally as the same user as the server and store state in .sagemathcloud-local instead of .sagemathcloud; also do not kill all processes on project restart -- for development use (default: false, since not given)', Boolean, false)
668
.option('--foreground', 'If specified, do not run as a deamon')
669
       .option('--kucalc', 'if given, assume running in the KuCalc kubernetes environment')
670
.option('--dev', 'if given, then run in VERY UNSAFE single-user local dev mode')
671
.option('--single', 'if given, then run in LESS SAFE single-machine mode')
672
.option('--db_pool <n>', 'number of db connections in pool (default: 1)', ((n)->parseInt(n)), 1)
673
.option('--db_concurrent_warn <n>', 'be very unhappy if number of concurrent db requests exceeds this (default: 300)', ((n)->parseInt(n)), 300)
674
.parse(process.argv)
675
676
# NOTE: the --local option above may be what is used later for single user installs, i.e., the version included with Sage.
677
678
if program._name.slice(0,3) == 'hub'
679
# run as a server/daemon (otherwise, is being imported as a library)
680
681
#if program.rawArgs[1] in ['start', 'restart']
682
process.addListener "uncaughtException", (err) ->
683
winston.debug("BUG ****************************************************************************")
684
winston.debug("Uncaught exception: " + err)
685
winston.debug(err.stack)
686
winston.debug("BUG ****************************************************************************")
687
database?.uncaught_exception(err)
688
689
if program.passwd
690
winston.debug("Resetting password")
691
reset_password(program.passwd, (err) -> process.exit())
692
else if program.stripe_sync
693
winston.debug("Stripe sync")
694
stripe_sync(false, (err) -> winston.debug("DONE", err); process.exit())
695
else if program.stripe_dump
696
winston.debug("Stripe dump")
697
stripe_sync(true, (err) -> winston.debug("DONE", err); process.exit())
698
else if program.delete_expired
699
delete_expired (err) ->
700
winston.debug("DONE", err)
701
process.exit()
702
else if program.blob_maintenance
703
blob_maintenance (err) ->
704
winston.debug("DONE", err)
705
process.exit()
706
else if program.update_stats
707
update_stats (err) ->
708
winston.debug("DONE", err)
709
process.exit()
710
else if program.add_user_to_project
711
console.log("Adding user to project")
712
v = program.add_user_to_project.split(',')
713
add_user_to_project v[0], v[1], (err) ->
714
if err
715
console.log("Failed to add user: #{err}")
716
else
717
console.log("User added to project.")
718
process.exit()
719
else
720
console.log("Running hub; pidfile=#{program.pidfile}, port=#{program.port}, proxy_port=#{program.proxy_port}")
721
# logFile = /dev/null to prevent huge duplicated output that is already in program.logfile
722
if program.foreground
723
start_server (err) ->
724
if err and program.dev
725
process.exit(1)
726
else
727
daemon({pidFile:program.pidfile, outFile:program.logfile, errFile:program.logfile, logFile:'/dev/null', max:30}, start_server)
728
729
730
if process.argv.length > 1
731
command_line()
732
733