Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
| Download
Views: 39598
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
####################################################################
24
#
25
# misc JS functionality that only makes sense on the node side (not on
26
# the client)
27
#
28
####################################################################
29
30
assert = require('assert')
31
fs = require('fs')
32
net = require('net')
33
winston = require('winston')
34
async = require('async')
35
path = require('path')
36
37
misc = require('smc-util/misc')
38
{walltime, defaults, required, to_json} = misc
39
message = require('smc-util/message')
40
41
exports.SALVUS_HOME = exports.SMC_ROOT = SMC_ROOT = process.env.SMC_ROOT
42
43
exports.WEBAPP_LIB = 'webapp-lib' # was 'static' in the old days, contains js libraries
44
45
###
46
Asynchronous JSON functionality: these are slower but block the main thread *less*.
47
48
- to_json_async - convert object to JSON string without blocking.
49
This uses https://github.com/ckknight/async-json
50
51
- from_json_async - convert JSON string to object/etc., without blocking,
52
though 2x times as slow as JSON.parse. This uses https://github.com/bjouhier/i-json
53
54
TESTS:
55
56
m=require('misc_node');s=JSON.stringify({x:new Buffer(10000000).toString('hex')}); d=new Date(); m.from_json_async(string: s, chunk_size:10000, cb: (e, r) -> console.log(e, new Date() - d)); new Date() - d
57
###
58
59
###
60
exports.to_json_async = (opts) ->
61
opts = defaults opts,
62
obj : required # Javascript object to convert to a JSON string
63
cb : required # cb(err, JSON string)
64
65
ijson = require('i-json')
66
exports.from_json_async = (opts) ->
67
opts = defaults opts,
68
string : required # string in JSON format
69
chunk_size : 50000 # size of chunks to parse -- affects how long this blocks the main thread
70
cb : required
71
p = ijson.createParser()
72
s = opts.string
73
f = (i, cb) ->
74
#t = misc.mswalltime()
75
p.update(s.slice(i*opts.chunk_size, (i+1)*opts.chunk_size))
76
#console.log("update: #{misc.mswalltime(t)}")
77
setTimeout(cb, 0)
78
async.mapSeries [0...s.length/opts.chunk_size], f, (err) ->
79
opts.cb(err, p.result())
80
###
81
82
######################################################################
83
# Our TCP messaging system. We send a message by first
84
# sending the length, then the bytes of the actual message. The code
85
# in this section is used by:
86
# * hub -- to communicate with sage_server and console_server
87
######################################################################
88
89
# Extend the socket object so that listens to all data coming in on this socket
90
# and fires a 'mesg' event, along with the JSON object or blob in the message
91
# So, one listens with:
92
# socket.on('mesg', (type, value) -> ...)
93
# where type is one if 'json' or 'blob'.
94
#
95
# Calling this function also adds a function .write_mesg to the socket, so that
96
# socket.write_mesg(type, data)
97
# will send the message of the given type on the socket. When type='json',
98
# data is just a JSON-able object. When type='blob', data={uuid:..., blob:...};
99
# since every blob is tagged with a uuid.
100
101
102
exports.enable_mesg = enable_mesg = (socket, desc) ->
103
socket.setMaxListeners(500) # we use a lot of listeners for listening for messages
104
socket._buf = null
105
socket._buf_target_length = -1
106
socket._listen_for_mesg = (data) ->
107
socket._buf = if socket._buf == null then data else Buffer.concat([socket._buf, data])
108
loop
109
if socket._buf_target_length == -1
110
# starting to read a new message
111
if socket._buf.length >= 4
112
socket._buf_target_length = socket._buf.readUInt32BE(0) + 4
113
else
114
return # have to wait for more data to find out message length
115
if socket._buf_target_length <= socket._buf.length
116
# read a new message from our buffer
117
type = socket._buf.slice(4, 5).toString()
118
mesg = socket._buf.slice(5, socket._buf_target_length)
119
switch type
120
when 'j' # JSON
121
s = mesg.toString()
122
try
123
# Do not use "obj = JSON.parse(s)"
124
obj = misc.from_json_socket(s) # this properly parses Date objects
125
catch e
126
winston.debug("Error parsing JSON message='#{misc.trunc(s,512)}' on socket #{desc}")
127
# TODO -- this throw can seriously mess up the server; handle this
128
# in a better way in production. This could happen if there is
129
# corruption of the connection.
130
#throw(e)
131
return
132
socket.emit('mesg', 'json', obj)
133
when 'b' # BLOB (tagged by a uuid)
134
socket.emit('mesg', 'blob', {uuid:mesg.slice(0,36).toString(), blob:mesg.slice(36)})
135
else
136
throw("unknown message type '#{type}'")
137
socket._buf = socket._buf.slice(socket._buf_target_length)
138
socket._buf_target_length = -1
139
if socket._buf.length == 0
140
return
141
else # nothing to do but wait for more data
142
return
143
144
socket.on('data', socket._listen_for_mesg)
145
146
socket.write_mesg = (type, data, cb) -> # cb(err)
147
if not data?
148
# uncomment this to get a traceback to see what might be causing this...
149
#throw Error("write_mesg(type='#{type}': data must be defined")
150
cb?("write_mesg(type='#{type}': data must be defined")
151
return
152
send = (s) ->
153
buf = new Buffer(4)
154
# This line was 4 hours of work. It is absolutely
155
# *critical* to change the (possibly a string) s into a
156
# buffer before computing its length and sending it!!
157
# Otherwise unicode characters will cause trouble.
158
if typeof(s) == "string"
159
s = Buffer(s)
160
buf.writeInt32BE(s.length, 0)
161
if not socket.writable
162
cb?("socket not writable")
163
return
164
else
165
socket.write(buf)
166
167
if not socket.writable
168
cb?("socket not writable")
169
return
170
else
171
socket.write(s, cb)
172
173
switch type
174
when 'json'
175
send('j' + misc.to_json_socket(data))
176
when 'blob'
177
assert(data.uuid?, "data object *must* have a uuid attribute")
178
assert(data.blob?, "data object *must* have a blob attribute")
179
send(Buffer.concat([new Buffer('b'), new Buffer(data.uuid), new Buffer(data.blob)]))
180
else
181
cb?("unknown message type '#{type}'")
182
183
# Wait until we receive exactly *one* message of the given type
184
# with the given id, then call the callback with that message.
185
# (If the type is 'blob', with the given uuid.)
186
socket.recv_mesg = (opts) ->
187
opts = defaults opts,
188
type : required
189
id : required # or uuid
190
cb : required # called with cb(mesg)
191
timeout : undefined
192
193
f = (type, mesg) ->
194
if type == opts.type and ((type == 'json' and mesg.id == opts.id) or (type=='blob' and mesg.uuid=opts.id))
195
socket.removeListener('mesg', f)
196
opts.cb(mesg)
197
socket.on 'mesg', f
198
199
if opts.timeout?
200
timeout = () ->
201
if socket? and f in socket.listeners('mesg')
202
socket.removeListener('mesg', f)
203
opts.cb(message.error(error:"Timed out after #{opts.timeout} seconds."))
204
setTimeout(timeout, opts.timeout*1000)
205
206
207
# Stop watching data stream for messages and delete the write_mesg function.
208
exports.disable_mesg = (socket) ->
209
if socket._listen_for_mesg?
210
socket.removeListener('data', socket._listen_for_mesg)
211
delete socket._listen_for_mesg
212
213
214
# Wait to receive token over the socket; when it is received, call
215
# cb(false), then send back "y". If any mistake is made (or the
216
# socket times out after 10 seconds), send back "n" and close the
217
# connection.
218
exports.unlock_socket = (socket, token, cb) -> # cb(err)
219
timeout = setTimeout((() -> socket.destroy(); cb("Unlock socket -- timed out waiting for secret token")), 10000)
220
221
user_token = ''
222
listener = (data) ->
223
user_token += data.toString()
224
if user_token == token
225
socket.removeListener('data', listener)
226
# got it!
227
socket.write('y')
228
clearTimeout(timeout)
229
cb(false)
230
else if user_token.length > token.length or token.slice(0, user_token.length) != user_token
231
socket.removeListener('data', listener)
232
socket.write('n')
233
socket.write("Invalid secret token.")
234
socket.destroy()
235
clearTimeout(timeout)
236
cb("Invalid secret token.")
237
socket.on('data', listener)
238
239
# Connect to a locked socket on host, unlock it, and do
240
# cb(err, unlocked_socket).
241
# WARNING: Use only on an encrypted VPN, since this is not
242
# an *encryption* protocol.
243
exports.connect_to_locked_socket = (opts) ->
244
{port, host, token, timeout, cb} = defaults opts,
245
host : 'localhost'
246
port : required
247
token : required
248
timeout : 5
249
cb : required
250
251
if not (port > 0 and port < 65536)
252
cb("connect_to_locked_socket -- RangeError: port should be > 0 and < 65536: #{port}")
253
return
254
255
winston.debug("misc_node: connecting to a locked socket on port #{port}...")
256
timer = undefined
257
258
timed_out = () ->
259
m = "misc_node: timed out trying to connect to locked socket on port #{port}"
260
winston.debug(m)
261
cb?(m)
262
cb = undefined # NOTE: here and everywhere below we set cb to undefined after calling it, and only call it if defined, since the event and timer callback stuff is very hard to do right here without calling cb more than once (which is VERY bad to do).
263
socket?.end()
264
timer = undefined
265
266
timer = setTimeout(timed_out, timeout*1000)
267
268
socket = net.connect {host:host, port:port}, () =>
269
listener = (data) ->
270
winston.debug("misc_node: got back response: #{data}")
271
socket.removeListener('data', listener)
272
if data.toString() == 'y'
273
if timer?
274
clearTimeout(timer)
275
cb?(undefined, socket)
276
cb = undefined
277
else
278
socket.destroy()
279
if timer?
280
clearTimeout(timer)
281
cb?("Permission denied (invalid secret token) when connecting to the local hub.")
282
cb = undefined
283
socket.on 'data', listener
284
winston.debug("misc_node: connected, now sending secret token")
285
socket.write(token)
286
287
# This is called in case there is an error trying to make the connection, e.g., "connection refused".
288
socket.on "error", (err) =>
289
if timer?
290
clearTimeout(timer)
291
cb?(err)
292
cb = undefined
293
294
295
# Connect two sockets together.
296
# If max_burst is optionally given, then parts of a big burst of data
297
# from s2 will be replaced by '[...]'.
298
exports.plug = (s1, s2, max_burst) -> # s1 = hub; s2 = console server
299
last_tm = misc.mswalltime()
300
last_data = ''
301
amount = 0
302
# Connect the sockets together.
303
s1_data = (data) ->
304
if not s2.writable
305
s1.removeListener('data', s1_data)
306
else
307
s2.write(data)
308
s2_data = (data) ->
309
if not s1.writable
310
s2.removeListener('data', s2_data)
311
else
312
if max_burst?
313
tm = misc.mswalltime()
314
if tm - last_tm >= 20
315
if amount < 0 # was truncating
316
try
317
x = last_data.slice(Math.max(0, last_data.length - Math.floor(max_burst/4)))
318
catch e
319
# I don't know why the above sometimes causes an exception, but it *does* in
320
# Buffer.slice, which is a serious problem. Best to ignore that data.
321
x = ''
322
data = "]" + x + data
323
#console.log("max_burst: reset")
324
amount = 0
325
last_tm = tm
326
#console.log("max_burst: amount=#{amount}")
327
if amount >= max_burst
328
last_data = data
329
data = data.slice(0,Math.floor(max_burst/4)) + "[..."
330
amount = -1 # so do only once every 20ms.
331
setTimeout((()=>s2_data('')), 25) # write nothing in 25ms just to make sure ...] appears.
332
else if amount < 0
333
last_data += data
334
setTimeout((()=>s2_data('')), 25) # write nothing in 25ms just to make sure ...] appears.
335
else
336
amount += data.length
337
# Never push more than max_burst characters at once to hub, since that could overwhelm
338
s1.write(data)
339
s1.on('data', s1_data)
340
s2.on('data', s2_data)
341
342
###
343
sha1 hash functionality
344
###
345
346
crypto = require('crypto')
347
# compute sha1 hash of data in hex
348
exports.sha1 = (data) ->
349
if typeof(data) == 'string'
350
# CRITICAL: Code below assumes data is a Buffer; it will seem to work on a string, but give
351
# the wrong result where wrong means that it doesn't agree with the frontend version defined
352
# in misc.
353
data = new Buffer(data)
354
sha1sum = crypto.createHash('sha1')
355
sha1sum.update(data)
356
return sha1sum.digest('hex')
357
358
# Compute a uuid v4 from the Sha-1 hash of data.
359
exports.uuidsha1 = (data) ->
360
s = exports.sha1(data)
361
i = -1
362
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) ->
363
i += 1
364
switch c
365
when 'x'
366
return s[i]
367
when 'y'
368
# take 8 + low order 3 bits of hex number.
369
return ((parseInt('0x'+s[i],16)&0x3)|0x8).toString(16)
370
)
371
372
373
374
###################################################################
375
# Execute code
376
###################################################################
377
#
378
temp = require('temp')
379
async = require('async')
380
fs = require('fs')
381
child_process = require 'child_process'
382
383
exports.execute_code = execute_code = (opts) ->
384
opts = defaults opts,
385
command : required
386
args : []
387
path : undefined # defaults to home directory; where code is executed from
388
timeout : 10 # timeout in *seconds*
389
ulimit_timeout : true # if set, use ulimit to ensure a cpu timeout -- don't use when launching a daemon!
390
err_on_exit: true # if true, then a nonzero exit code will result in cb(error_message)
391
max_output : undefined # bound on size of stdout and stderr; further output ignored
392
bash : false # if true, ignore args and evaluate command as a bash command
393
home : undefined
394
uid : undefined
395
gid : undefined
396
env : undefined # if given, added to exec environment
397
verbose : true
398
cb : undefined
399
400
start_time = walltime()
401
if opts.verbose
402
winston.debug("execute_code: \"#{opts.command} #{opts.args.join(' ')}\"")
403
404
s = opts.command.split(/\s+/g) # split on whitespace
405
if opts.args.length == 0 and s.length > 1
406
opts.bash = true
407
408
if not opts.home?
409
opts.home = process.env.HOME
410
411
if not opts.path?
412
opts.path = opts.home
413
else if opts.path[0] != '/'
414
opts.path = opts.home + '/' + opts.path
415
416
stdout = ''
417
stderr = ''
418
exit_code = undefined
419
420
env = misc.copy(process.env)
421
422
if opts.env?
423
for k, v of opts.env
424
env[k] = v
425
426
if opts.uid?
427
env.HOME = opts.home
428
429
ran_code = false
430
info = undefined
431
432
async.series([
433
(c) ->
434
if not opts.bash
435
c()
436
return
437
if opts.timeout and opts.ulimit_timeout
438
# This ensures that everything involved with this
439
# command really does die no matter what; it's
440
# better than killing from outside, since it gets
441
# all subprocesses since they inherit the limits.
442
cmd = "ulimit -t #{opts.timeout}\n#{opts.command}"
443
else
444
cmd = opts.command
445
446
if opts.verbose
447
winston.debug("execute_code: writing temporary file that contains bash program.")
448
temp.open '', (err, _info) ->
449
if err
450
c(err)
451
else
452
info = _info
453
opts.command = 'bash'
454
opts.args = [info.path]
455
fs.writeFile(info.fd, cmd, c)
456
(c) ->
457
if info?
458
fs.close(info.fd, c)
459
else
460
c()
461
(c) ->
462
if info?
463
fs.chmod(info.path, 0o700, c)
464
else
465
c()
466
467
(c) ->
468
if opts.verbose
469
winston.debug("Spawning the command #{opts.command} with given args #{opts.args} and timeout of #{opts.timeout}s...")
470
o = {cwd:opts.path}
471
if env?
472
o.env = env
473
if opts.uid
474
o.uid = opts.uid
475
if opts.gid
476
o.gid = opts.gid
477
478
try
479
r = child_process.spawn(opts.command, opts.args, o)
480
if not r.stdout? or not r.stderr?
481
# The docs/examples at https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
482
# suggest that r.stdout and r.stderr are always defined. However, this is
483
# definitely NOT the case in edge cases, as we have observed.
484
c("error creating child process -- couldn't spawn child process")
485
return
486
catch e
487
# Yes, spawn can cause this error if there is no memory, and there's no event! -- Error: spawn ENOMEM
488
c("error #{misc.to_json(e)}")
489
return
490
491
ran_code = true
492
493
if opts.verbose
494
winston.debug("Listen for stdout, stderr and exit events.")
495
stdout = ''
496
r.stdout.on 'data', (data) ->
497
data = data.toString()
498
if opts.max_output?
499
if stdout.length < opts.max_output
500
stdout += data.slice(0,opts.max_output - stdout.length)
501
else
502
stdout += data
503
504
r.stderr.on 'data', (data) ->
505
data = data.toString()
506
if opts.max_output?
507
if stderr.length < opts.max_output
508
stderr += data.slice(0,opts.max_output - stderr.length)
509
else
510
stderr += data
511
512
stderr_is_done = stdout_is_done = false
513
514
r.stderr.on 'end', () ->
515
stderr_is_done = true
516
finish()
517
518
r.stdout.on 'end', () ->
519
stdout_is_done = true
520
finish()
521
522
r.on 'exit', (code) ->
523
exit_code = code
524
finish()
525
526
# This can happen, e.g., "Error: spawn ENOMEM" if there is no memory. Without this handler,
527
# an unhandled exception gets raised, which is nasty.
528
# From docs: "Note that the exit-event may or may not fire after an error has occured. "
529
r.on 'error', (err) ->
530
if not exit_code?
531
exit_code = 1
532
stderr += to_json(err)
533
finish()
534
535
callback_done = false
536
finish = () ->
537
if stdout_is_done and stderr_is_done and exit_code?
538
if opts.err_on_exit and exit_code != 0
539
if not callback_done
540
callback_done = true
541
c("command '#{opts.command}' (args=#{opts.args.join(' ')}) exited with nonzero code #{exit_code} -- stderr='#{stderr}'")
542
else
543
if opts.max_output?
544
if stdout.length >= opts.max_output
545
stdout += " (truncated at #{opts.max_output} characters)"
546
if stderr.length >= opts.max_output
547
stderr += " (truncated at #{opts.max_output} characters)"
548
if not callback_done
549
callback_done = true
550
c()
551
552
if opts.timeout
553
f = () ->
554
if r.exitCode == null
555
if opts.verbose
556
winston.debug("execute_code: subprocess did not exit after #{opts.timeout} seconds, so killing with SIGKILL")
557
try
558
r.kill("SIGKILL") # this does not kill the process group :-(
559
catch e
560
# Exceptions can happen, which left uncaught messes up calling code bigtime.
561
if opts.verbose
562
winston.debug("execute_code: r.kill raised an exception.")
563
if not callback_done
564
callback_done = true
565
c("killed command '#{opts.command} #{opts.args.join(' ')}'")
566
setTimeout(f, opts.timeout*1000)
567
568
], (err) ->
569
if not exit_code?
570
exit_code = 1 # don't have one due to SIGKILL
571
572
# TODO: This is dangerous, e.g., it could print out a secret_token to a log file.
573
# winston.debug("(time: #{walltime() - start_time}): Done running '#{opts.command} #{opts.args.join(' ')}'; resulted in stdout='#{misc.trunc(stdout,512)}', stderr='#{misc.trunc(stderr,512)}', exit_code=#{exit_code}, err=#{err}")
574
# Do not litter:
575
if info?.path?
576
try
577
fs.unlink(info.path)
578
catch e
579
winston.debug("failed to unlink #{info.path}")
580
581
582
if opts.verbose
583
winston.debug("finished exec of #{opts.command} (took #{walltime(start_time)}s)")
584
winston.debug("stdout='#{misc.trunc(stdout,512)}', stderr='#{misc.trunc(stderr,512)}', exit_code=#{exit_code}")
585
if not opts.err_on_exit and ran_code
586
# as long as we made it to running some code, we consider this a success (that is what err_on_exit means).
587
opts.cb?(false, {stdout:stdout, stderr:stderr, exit_code:exit_code})
588
else
589
opts.cb?(err, {stdout:stdout, stderr:stderr, exit_code:exit_code})
590
)
591
592
593
#
594
# Applications of execute_code
595
596
exports.disk_usage = (path, cb) -> # cb(err, usage in K (1024 bytes) of path)
597
exports.execute_code
598
command : "du"
599
args : ['-s', path]
600
cb : (err, output) ->
601
if err
602
cb(err)
603
else
604
cb(false, parseInt(output.stdout.split(' ')[0]))
605
606
607
#
608
# project_id --> username mapping
609
610
# The username associated to a given project id is just the string of
611
# the uuid, but with -'s replaced by _'s so we obtain a valid unix
612
# account name, and shortened to fit Linux and sanity requirements.
613
exports.username = (project_id) ->
614
if '..' in project_id or project_id.length != 36
615
# a sanity check -- this should never ever be allowed to happen, ever.
616
throw Error("invalid project id #{project_id}")
617
# Return a for-sure safe username
618
return project_id.slice(0,8).replace(/[^a-z0-9]/g,'')
619
620
# project_id --> LINUX uid mapping
621
exports.uid = (project_id) ->
622
# (comment copied from smc_compute.py)
623
# We take the sha-512 of the uuid just to make it harder to force a collision. Thus even if a
624
# user could somehow generate an account id of their choosing, this wouldn't help them get the
625
# same uid as another user.
626
# 2^31-1=max uid which works with FUSE and node (and Linux, which goes up to 2^32-2).
627
sha512sum = crypto.createHash('sha512')
628
n = parseInt(sha512sum.update(project_id).digest('hex').slice(0,8), 16) # up to 2^32
629
n //= 2 # floor division
630
return if n>65537 then n else n+65537 # 65534 used by linux for user sync, etc.
631
632
address_to_local_port = {}
633
local_port_to_child_process = {}
634
635
exports.keep_portforward_alive = (port) ->
636
r = local_port_to_child_process[port]
637
if r?
638
r.activity = true
639
640
exports.unforward_port = (opts) ->
641
opts = defaults opts,
642
port : required
643
cb : required
644
winston.debug("Unforwarding port #{opts.port}")
645
r = local_port_to_child_process[local_port]
646
if r?
647
r.kill("SIGKILL")
648
649
exports.unforward_all_ports = () ->
650
for port, r of local_port_to_child_process
651
r.kill("SIGKILL")
652
653
free_port = exports.free_port = (cb) -> # cb(err, available port as assigned by the operating system)
654
server = require("net").createServer()
655
port = 0
656
server.on "listening", () ->
657
port = server.address().port
658
server.close()
659
server.on "close", ->
660
f = () ->
661
cb(null, port)
662
# give the OS a chance to really make the port available again.
663
setTimeout(f, 500)
664
server.listen(0)
665
666
exports.forward_remote_port_to_localhost = (opts) ->
667
opts = defaults opts,
668
username : required
669
host : required
670
ssh_port : 22
671
remote_port : required
672
activity_time : 2000 # kill connection if the HUB doesn't
673
# actively *receive* something on this
674
# port for this many seconds.
675
keep_alive_time:2000 # network activity every this many
676
# seconds.; lower to more quickly detect
677
# a broken connection; raise to reduce resources
678
cb : required # cb(err, local_port)
679
680
opts.ssh_port = parseInt(opts.ssh_port)
681
if not (opts.ssh_port >= 1 and opts.ssh_port <= 66000)
682
opts.cb("Invalid ssh_port option")
683
return
684
685
opts.remote_port = parseInt(opts.remote_port)
686
if not (opts.remote_port >= 1 and opts.remote_port <= 66000)
687
opts.cb("Invalid remote_port option")
688
return
689
690
winston.debug("Forward a remote port #{opts.remote_port} on #{opts.host} to localhost.")
691
692
remote_address = "#{opts.username}@#{opts.host}:#{opts.remote_port} -p#{opts.ssh_port}"
693
694
###
695
local_port = address_to_local_port[remote_address]
696
if local_port?
697
# We already have a valid forward
698
opts.cb(false, local_port)
699
return
700
###
701
702
# We have to make a new port forward
703
free_port (err, local_port) ->
704
if err
705
opts.cb(err)
706
return
707
winston.debug("forward_remote_port_to_local_host: local port #{local_port} available")
708
command = "ssh"
709
args = ['-o', 'StrictHostKeyChecking=no', "-p", opts.ssh_port,
710
'-L', "#{local_port}:localhost:#{opts.remote_port}",
711
"#{opts.username}@#{opts.host}",
712
"TERM=vt100 /usr/bin/watch -t -n #{opts.keep_alive_time} date"]
713
r = child_process.spawn(command, args)
714
cb_happened = false
715
new_output = false
716
r.stdout.on 'data', (data) ->
717
718
# Got a local_port -- let's use it.
719
address_to_local_port[remote_address] = local_port
720
local_port_to_child_process[local_port] = r
721
722
new_output = true
723
# as soon as something is output, it's working (I hope).
724
if not cb_happened
725
opts.cb(false, local_port)
726
cb_happened = true
727
728
stderr = ''
729
r.stderr.on 'data', (data) ->
730
stderr += data.toString()
731
732
kill_if_no_new_output = () ->
733
if not new_output
734
winston.debug("Killing ssh port forward #{remote_address} --> localhost:#{local_port} due to it not working")
735
r.kill("SIGKILL")
736
new_output = false
737
738
# check every few seconds
739
kill_no_output_timer = setInterval(kill_if_no_new_output, 1000*opts.keep_alive_time)
740
741
kill_if_no_new_activity = () ->
742
if not r.activity?
743
winston.debug("Killing ssh port forward #{remote_address} --> localhost:#{local_port} due to not receiving any data for at least #{opts.activity_time} seconds.")
744
r.kill("SIGKILL")
745
else
746
# delete it -- the only way connection won't be killed is if this gets set again by an active call to keep_portforward_alive above.
747
delete r.activity
748
749
kill_no_activity_timer = setInterval(kill_if_no_new_activity, 1000*opts.activity_time)
750
751
r.on 'exit', (code) ->
752
if not cb_happened
753
opts.cb("Problem setting up ssh port forward -- #{stderr}")
754
delete address_to_local_port[remote_address]
755
clearInterval(kill_no_output_timer)
756
clearInterval(kill_no_activity_timer)
757
758
759
exports.process_kill = (pid, signal) ->
760
switch signal
761
when 2
762
signal = 'SIGINT'
763
when 3
764
signal = 'SIGQUIT'
765
when 9
766
signal = 'SIGKILL'
767
else
768
winston.debug("BUG -- process_kill: only signals 2 (SIGINT), 3 (SIGQUIT), and 9 (SIGKILL) are supported")
769
return
770
try
771
process.kill(pid, signal)
772
catch e
773
# it's normal to get an exception when sending a signal... to a process that doesn't exist.
774
775
776
# Any non-absolute path is assumed to be relative to the user's home directory.
777
# This function converts such a path to an absolute path.
778
exports.abspath = abspath = (path) ->
779
if path.length == 0
780
return process.env.HOME
781
if path[0] == '/'
782
return path # already an absolute path
783
p = process.env.HOME + '/' + path
784
p = p.replace(/\/\.\//g,'/') # get rid of /./, which is the same as /...
785
return p
786
787
788
789
# Other path related functions...
790
791
# Make sure that that the directory containing the file indicated by
792
# the path exists and has restrictive permissions.
793
ensure_containing_directory_exists = (path, cb) -> # cb(err)
794
path = abspath(path)
795
dir = misc.path_split(path).head # containing path
796
797
fs.exists dir, (exists) ->
798
if exists
799
cb?()
800
else
801
async.series([
802
(cb) ->
803
if dir != ''
804
# recursively make sure the entire chain of directories exists.
805
ensure_containing_directory_exists(dir, cb)
806
else
807
cb()
808
(cb) ->
809
fs.mkdir(dir, 0o700, cb)
810
], (err) ->
811
if err?.code == 'EEXIST'
812
cb?()
813
else
814
cb?(err)
815
)
816
817
exports.ensure_containing_directory_exists = ensure_containing_directory_exists
818
819
820
# Determine if path (file or directory) is writable -- this works even if permissions are right but
821
# filesystem is read only, e.g., ~/.zfs/snapshot/...
822
# It's an error if the path doesn't exist.
823
exports.is_file_readonly = (opts) ->
824
opts = defaults opts,
825
path : required
826
cb : required # cb(err, true if read only (false otherwise))
827
828
if process.platform == 'darwin'
829
# TODO: there is no -writable option to find on OS X, which breaks this; for now skip check
830
opts.cb(undefined, false)
831
return
832
833
readonly = undefined
834
# determine if file is writable
835
execute_code
836
command : 'find'
837
args : [opts.path, '-maxdepth', '0', '-writable']
838
err_on_exit : false
839
cb : (err, output) =>
840
if err
841
opts.cb(err)
842
else if output.stderr or output.exit_code
843
opts.cb("no such path '#{opts.path}'")
844
else
845
readonly = output.stdout.length == 0
846
opts.cb(undefined, readonly)
847
848
# like in sage, a quick way to save/load JSON-able objects to disk; blocking and not compressed.
849
exports.saveSync = (obj, filename) ->
850
fs.writeFileSync(filename, JSON.stringify(obj))
851
852
exports.loadSync = (filename) ->
853
JSON.parse(fs.readFileSync(filename).toString())
854
855
856
# WA state sales tax rates, as of July 11 2017.
857
# Generated via scripts/sales_tax.py
858
859
WA_sales_tax = {98001:0.099000, 98002:0.086000, 98003:0.100000, 98004:0.100000, 98005:0.100000, 98006:0.086000, 98007:0.100000, 98008:0.100000, 98009:0.100000, 98010:0.086000, 98011:0.100000, 98012:0.077000, 98013:0.086000, 98014:0.086000, 98015:0.100000, 98019:0.077000, 98020:0.100000, 98021:0.077000, 98022:0.079000, 98023:0.100000, 98024:0.086000, 98025:0.086000, 98026:0.100000, 98027:0.086000, 98028:0.100000, 98029:0.086000, 98030:0.100000, 98031:0.100000, 98032:0.100000, 98033:0.100000, 98034:0.100000, 98035:0.100000, 98036:0.103000, 98037:0.103000, 98038:0.086000, 98039:0.100000, 98040:0.100000, 98041:0.100000, 98042:0.086000, 98043:0.100000, 98045:0.086000, 98046:0.104000, 98047:0.093000, 98050:0.086000, 98051:0.086000, 98052:0.086000, 98053:0.086000, 98054:0.086000, 98055:0.100000, 98056:0.100000, 98057:0.100000, 98058:0.086000, 98059:0.086000, 98061:0.090000, 98062:0.100000, 98063:0.100000, 98064:0.100000, 98065:0.086000, 98068:0.080000, 98070:0.086000, 98071:0.100000, 98072:0.077000, 98073:0.100000, 98074:0.086000, 98075:0.086000, 98077:0.077000, 98082:0.104000, 98083:0.100000, 98087:0.103000, 98089:0.100000, 98092:0.086000, 98093:0.100000, 98101:0.101000, 98102:0.101000, 98103:0.101000, 98104:0.101000, 98105:0.101000, 98106:0.100000, 98107:0.101000, 98108:0.100000, 98109:0.101000, 98110:0.090000, 98111:0.101000, 98112:0.101000, 98113:0.101000, 98114:0.101000, 98115:0.101000, 98116:0.101000, 98117:0.101000, 98118:0.101000, 98119:0.101000, 98121:0.101000, 98122:0.101000, 98124:0.101000, 98125:0.100000, 98126:0.100000, 98127:0.101000, 98129:0.101000, 98131:0.100000, 98132:0.100000, 98133:0.100000, 98134:0.101000, 98136:0.101000, 98138:0.100000, 98139:0.101000, 98141:0.101000, 98144:0.100000, 98145:0.101000, 98146:0.100000, 98148:0.100000, 98151:0.100000, 98154:0.101000, 98155:0.100000, 98158:0.100000, 98160:0.100000, 98161:0.101000, 98164:0.101000, 98165:0.101000, 98166:0.100000, 98168:0.100000, 98170:0.100000, 98171:0.100000, 98174:0.101000, 98175:0.101000, 98177:0.100000, 98178:0.100000, 98181:0.101000, 98184:0.101000, 98185:0.101000, 98188:0.100000, 98189:0.100000, 98190:0.100000, 98191:0.101000, 98194:0.101000, 98195:0.101000, 98198:0.100000, 98199:0.101000, 98201:0.089000, 98203:0.077000, 98204:0.097000, 98205:0.097000, 98206:0.097000, 98207:0.097000, 98208:0.077000, 98213:0.097000, 98220:0.085000, 98221:0.081000, 98222:0.081000, 98223:0.077000, 98224:0.086000, 98225:0.079000, 98226:0.079000, 98227:0.087000, 98228:0.087000, 98229:0.079000, 98230:0.079000, 98231:0.085000, 98232:0.085000, 98233:0.081000, 98235:0.085000, 98236:0.087000, 98237:0.079000, 98238:0.081000, 98239:0.087000, 98240:0.085000, 98241:0.081000, 98243:0.081000, 98244:0.079000, 98245:0.081000, 98247:0.085000, 98248:0.079000, 98249:0.087000, 98250:0.081000, 98251:0.086000, 98252:0.077000, 98253:0.087000, 98255:0.085000, 98256:0.089000, 98257:0.081000, 98258:0.077000, 98259:0.091000, 98260:0.087000, 98261:0.081000, 98262:0.079000, 98263:0.085000, 98264:0.085000, 98266:0.085000, 98267:0.085000, 98270:0.077000, 98271:0.077000, 98272:0.077000, 98273:0.081000, 98274:0.081000, 98275:0.097000, 98276:0.085000, 98277:0.087000, 98278:0.087000, 98279:0.081000, 98280:0.081000, 98281:0.079000, 98282:0.077000, 98283:0.079000, 98284:0.077000, 98286:0.081000, 98287:0.089000, 98288:0.086000, 98290:0.077000, 98291:0.091000, 98292:0.077000, 98293:0.089000, 98294:0.077000, 98295:0.085000, 98296:0.077000, 98297:0.081000, 98303:0.079000, 98304:0.078000, 98305:0.084000, 98310:0.085000, 98311:0.090000, 98312:0.085000, 98314:0.090000, 98315:0.090000, 98320:0.085000, 98321:0.079000, 98322:0.090000, 98323:0.079000, 98324:0.084000, 98325:0.090000, 98326:0.084000, 98327:0.079000, 98328:0.079000, 98329:0.079000, 98330:0.078000, 98331:0.084000, 98332:0.079000, 98333:0.079000, 98335:0.079000, 98336:0.078000, 98337:0.090000, 98338:0.079000, 98339:0.090000, 98340:0.090000, 98342:0.090000, 98343:0.084000, 98344:0.079000, 98345:0.090000, 98346:0.090000, 98348:0.079000, 98349:0.079000, 98350:0.084000, 98351:0.079000, 98352:0.093000, 98353:0.090000, 98354:0.099000, 98355:0.078000, 98356:0.078000, 98357:0.084000, 98358:0.090000, 98359:0.079000, 98360:0.079000, 98361:0.078000, 98362:0.084000, 98363:0.084000, 98364:0.090000, 98365:0.090000, 98366:0.090000, 98367:0.090000, 98368:0.090000, 98370:0.090000, 98371:0.099000, 98372:0.093000, 98373:0.093000, 98374:0.093000, 98375:0.093000, 98376:0.090000, 98377:0.078000, 98378:0.090000, 98380:0.085000, 98381:0.084000, 98382:0.084000, 98383:0.090000, 98384:0.090000, 98385:0.079000, 98386:0.090000, 98387:0.079000, 98388:0.079000, 98390:0.093000, 98391:0.079000, 98392:0.090000, 98393:0.090000, 98394:0.079000, 98395:0.079000, 98396:0.079000, 98397:0.079000, 98398:0.079000, 98401:0.101000, 98402:0.099000, 98403:0.101000, 98404:0.099000, 98405:0.093000, 98406:0.093000, 98407:0.099000, 98408:0.099000, 98409:0.099000, 98411:0.101000, 98412:0.101000, 98413:0.101000, 98415:0.101000, 98416:0.101000, 98417:0.101000, 98418:0.101000, 98419:0.101000, 98421:0.099000, 98422:0.099000, 98424:0.099000, 98430:0.093000, 98431:0.093000, 98433:0.093000, 98438:0.093000, 98439:0.093000, 98442:0.093000, 98443:0.099000, 98444:0.093000, 98445:0.093000, 98446:0.079000, 98447:0.099000, 98448:0.099000, 98450:0.099000, 98455:0.099000, 98460:0.099000, 98464:0.099000, 98465:0.099000, 98466:0.099000, 98467:0.099000, 98471:0.101000, 98477:0.101000, 98481:0.101000, 98490:0.101000, 98492:0.101000, 98493:0.099000, 98496:0.099000, 98497:0.099000, 98498:0.093000, 98499:0.093000, 98501:0.079000, 98502:0.079000, 98503:0.087000, 98504:0.088000, 98505:0.087000, 98506:0.079000, 98507:0.088000, 98508:0.088000, 98509:0.089000, 98511:0.089000, 98512:0.079000, 98513:0.079000, 98516:0.079000, 98520:0.088000, 98522:0.078000, 98524:0.085000, 98526:0.088000, 98527:0.080000, 98528:0.079000, 98530:0.079000, 98531:0.078000, 98532:0.078000, 98533:0.078000, 98535:0.088000, 98536:0.088000, 98537:0.080000, 98538:0.078000, 98539:0.078000, 98540:0.079000, 98541:0.085000, 98542:0.078000, 98544:0.078000, 98546:0.085000, 98547:0.080000, 98548:0.085000, 98550:0.088000, 98552:0.088000, 98554:0.080000, 98555:0.085000, 98556:0.079000, 98557:0.085000, 98558:0.079000, 98559:0.088000, 98560:0.085000, 98561:0.080000, 98562:0.088000, 98563:0.085000, 98564:0.078000, 98565:0.078000, 98566:0.088000, 98568:0.078000, 98569:0.088000, 98570:0.078000, 98571:0.088000, 98572:0.078000, 98575:0.088000, 98576:0.079000, 98577:0.080000, 98579:0.078000, 98580:0.079000, 98581:0.078000, 98582:0.078000, 98583:0.088000, 98584:0.085000, 98585:0.078000, 98586:0.080000, 98587:0.088000, 98588:0.085000, 98589:0.079000, 98590:0.080000, 98591:0.078000, 98592:0.085000, 98593:0.078000, 98595:0.088000, 98596:0.078000, 98597:0.079000, 98599:0.088000, 98601:0.077000, 98602:0.070000, 98603:0.078000, 98604:0.077000, 98605:0.070000, 98606:0.077000, 98607:0.077000, 98609:0.078000, 98610:0.077000, 98611:0.078000, 98612:0.076000, 98613:0.070000, 98614:0.080000, 98616:0.078000, 98617:0.070000, 98619:0.070000, 98620:0.070000, 98621:0.076000, 98622:0.077000, 98623:0.070000, 98624:0.080000, 98625:0.078000, 98626:0.078000, 98628:0.070000, 98629:0.077000, 98631:0.080000, 98632:0.076000, 98635:0.070000, 98637:0.080000, 98638:0.076000, 98639:0.077000, 98640:0.080000, 98641:0.080000, 98642:0.077000, 98643:0.076000, 98644:0.080000, 98645:0.078000, 98647:0.076000, 98648:0.077000, 98649:0.078000, 98650:0.070000, 98651:0.077000, 98660:0.077000, 98661:0.084000, 98662:0.077000, 98663:0.084000, 98664:0.084000, 98665:0.084000, 98666:0.084000, 98667:0.084000, 98668:0.084000, 98670:0.070000, 98671:0.077000, 98672:0.070000, 98673:0.070000, 98674:0.077000, 98675:0.077000, 98682:0.077000, 98683:0.084000, 98684:0.084000, 98685:0.077000, 98686:0.077000, 98687:0.084000, 98801:0.082000, 98802:0.082000, 98807:0.084000, 98811:0.082000, 98812:0.078000, 98813:0.077000, 98814:0.081000, 98815:0.082000, 98816:0.081000, 98817:0.082000, 98819:0.081000, 98821:0.082000, 98822:0.082000, 98823:0.078000, 98824:0.079000, 98826:0.082000, 98827:0.081000, 98828:0.082000, 98829:0.081000, 98830:0.077000, 98831:0.082000, 98832:0.079000, 98833:0.081000, 98834:0.081000, 98836:0.082000, 98837:0.079000, 98840:0.077000, 98841:0.077000, 98843:0.078000, 98844:0.081000, 98845:0.078000, 98846:0.081000, 98847:0.082000, 98848:0.078000, 98849:0.081000, 98850:0.078000, 98851:0.079000, 98852:0.082000, 98853:0.079000, 98855:0.081000, 98856:0.081000, 98857:0.077000, 98858:0.078000, 98859:0.081000, 98860:0.079000, 98862:0.081000, 98901:0.079000, 98902:0.081000, 98903:0.079000, 98904:0.079000, 98907:0.082000, 98908:0.079000, 98909:0.081000, 98920:0.079000, 98921:0.079000, 98922:0.080000, 98923:0.079000, 98925:0.080000, 98926:0.080000, 98929:0.080000, 98930:0.079000, 98932:0.079000, 98933:0.079000, 98934:0.080000, 98935:0.070000, 98936:0.079000, 98937:0.078000, 98938:0.079000, 98939:0.079000, 98940:0.080000, 98941:0.080000, 98942:0.079000, 98943:0.080000, 98944:0.079000, 98946:0.080000, 98947:0.079000, 98948:0.079000, 98950:0.080000, 98951:0.079000, 98952:0.079000, 98953:0.079000, 99001:0.088000, 99003:0.081000, 99004:0.081000, 99005:0.081000, 99006:0.076000, 99008:0.080000, 99009:0.076000, 99011:0.088000, 99012:0.081000, 99013:0.076000, 99014:0.088000, 99016:0.081000, 99017:0.078000, 99018:0.081000, 99019:0.081000, 99020:0.081000, 99021:0.081000, 99022:0.081000, 99023:0.081000, 99025:0.081000, 99026:0.076000, 99027:0.081000, 99029:0.080000, 99030:0.081000, 99031:0.081000, 99032:0.077000, 99033:0.078000, 99034:0.076000, 99036:0.081000, 99037:0.081000, 99039:0.081000, 99040:0.076000, 99101:0.076000, 99102:0.078000, 99103:0.079000, 99104:0.078000, 99105:0.077000, 99107:0.077000, 99109:0.076000, 99110:0.076000, 99111:0.078000, 99113:0.078000, 99114:0.076000, 99115:0.078000, 99116:0.077000, 99117:0.080000, 99118:0.077000, 99119:0.076000, 99121:0.077000, 99122:0.076000, 99123:0.079000, 99124:0.077000, 99125:0.077000, 99126:0.076000, 99128:0.078000, 99129:0.076000, 99130:0.078000, 99131:0.076000, 99133:0.078000, 99134:0.080000, 99135:0.079000, 99136:0.078000, 99137:0.076000, 99138:0.077000, 99139:0.076000, 99140:0.077000, 99141:0.076000, 99143:0.078000, 99144:0.080000, 99146:0.077000, 99147:0.080000, 99148:0.076000, 99149:0.078000, 99150:0.077000, 99151:0.076000, 99152:0.076000, 99153:0.076000, 99154:0.080000, 99155:0.077000, 99156:0.076000, 99157:0.076000, 99158:0.078000, 99159:0.077000, 99160:0.077000, 99161:0.078000, 99163:0.078000, 99164:0.078000, 99165:0.078000, 99166:0.077000, 99167:0.076000, 99169:0.077000, 99170:0.078000, 99171:0.078000, 99173:0.076000, 99174:0.078000, 99176:0.078000, 99179:0.078000, 99180:0.076000, 99181:0.076000, 99185:0.080000, 99201:0.088000, 99202:0.088000, 99203:0.081000, 99204:0.088000, 99205:0.081000, 99206:0.081000, 99207:0.088000, 99208:0.081000, 99209:0.088000, 99210:0.088000, 99211:0.088000, 99212:0.081000, 99213:0.088000, 99214:0.088000, 99215:0.088000, 99216:0.081000, 99217:0.081000, 99218:0.081000, 99219:0.088000, 99220:0.088000, 99223:0.081000, 99224:0.081000, 99228:0.088000, 99251:0.088000, 99252:0.088000, 99256:0.088000, 99258:0.088000, 99260:0.088000, 99299:0.088000, 99301:0.080000, 99302:0.086000, 99320:0.080000, 99321:0.079000, 99322:0.070000, 99323:0.081000, 99324:0.087000, 99326:0.077000, 99328:0.081000, 99329:0.081000, 99330:0.080000, 99333:0.078000, 99335:0.080000, 99336:0.080000, 99337:0.080000, 99338:0.080000, 99341:0.077000, 99343:0.080000, 99344:0.077000, 99345:0.080000, 99346:0.080000, 99347:0.079000, 99348:0.081000, 99349:0.079000, 99350:0.070000, 99352:0.080000, 99353:0.080000, 99354:0.086000, 99356:0.070000, 99357:0.079000, 99359:0.081000, 99360:0.081000, 99361:0.081000, 99362:0.081000, 99363:0.081000, 99371:0.077000, 99401:0.077000, 99402:0.077000, 99403:0.079000}
860
861
exports.sales_tax = (zip) -> return WA_sales_tax[zip] ? 0
862
863
864
# Sanitizing HTML: loading the jquery file, caching it, and then exposing it in the API
865
_jQuery_cached = null
866
867
run_jQuery = (cb) ->
868
if _jQuery_cached != null
869
cb(_jQuery_cached)
870
else
871
jquery_file = fs.readFileSync("../#{exports.WEBAPP_LIB}/jquery/jquery.min.js", "utf-8")
872
require("jsdom").env
873
html: "<html></html>",
874
src: [jquery_file],
875
done: (err, window) ->
876
_jQuery_cached = window.$
877
cb(_jQuery_cached)
878
879
# http://api.jquery.com/jQuery.parseHTML/ (expanded behavior in version 3+)
880
exports.sanitize_html = (html, cb, keepScripts = true, keepUnsafeAttributes = true) ->
881
{sanitize_html_attributes} = require('smc-util/misc')
882
run_jQuery ($) ->
883
sani = $($.parseHTML('<div>' + html + '</div>', null, keepScripts))
884
if not keepUnsafeAttributes
885
sani.find('*').each ->
886
sanitize_html_attributes($, this)
887
cb(sani.html())
888
889
exports.sanitize_html_safe = (html, cb) -> exports.sanitize_html(html, cb, false, false)
890
891
# common configuration for webpack and hub
892
# inside the project, there is no SALVUS_HOME set, and the code below can't work anyways?
893
if exports.SALVUS_HOME?
894
# this is the directory where webpack builds everything
895
exports.OUTPUT_DIR = "static"
896
base_url_fn = path.resolve(exports.SALVUS_HOME, 'data/base_url')
897
exports.BASE_URL = if fs.existsSync(base_url_fn) then fs.readFileSync(base_url_fn).toString().trim() + "/" else '/'
898
899
# mathjax location and version: we read it from its package.json
900
# webpack symlinks with the version in the path (MATHJAX_ROOT)
901
# this is used by the webapp (via webpack.config and the hub)
902
# the purpose is, that both of them have to know where the final directory of the mathjax root is
903
904
exports.MATHJAX_LIB = 'smc-webapp/node_modules/mathjax'
905
mathjax_package_json = path.resolve(exports.SALVUS_HOME, "#{exports.MATHJAX_LIB}", 'package.json')
906
# if the line below causes an exception, there is no mathjax or at the wrong location (should be in MATHJAX_LIB)
907
# without that information here, the jupyter notebook can't work
908
# hsy: disabling this for the hub, until we have a better solution.
909
# fallback: mathjax for jupyter is served from the unversioned /mathjax/ path
910
if fs.existsSync(mathjax_package_json)
911
exports.MATHJAX_VERSION = JSON.parse(fs.readFileSync(mathjax_package_json, 'utf8')).version
912
else
913
exports.MATHJAX_VERSION = null
914
# that's where webpack writes the symlink to
915
exports.MATHJAX_NOVERS = path.join(exports.OUTPUT_DIR, "mathjax")
916
if exports.MATHJAX_VERSION?
917
exports.MATHJAX_SUBDIR = "mathjax-#{exports.MATHJAX_VERSION}"
918
exports.MATHJAX_ROOT = path.join(exports.OUTPUT_DIR, exports.MATHJAX_SUBDIR)
919
else
920
exports.MATHJAX_SUBDIR = "mathjax"
921
exports.MATHJAX_ROOT = exports.MATHJAX_NOVERS
922
# this is where the webapp and the jupyter notebook should get mathjax from
923
exports.MATHJAX_URL = path.join(exports.BASE_URL, exports.MATHJAX_ROOT, 'MathJax.js')
924
925
# google analytics (or later on some other analytics provider) needs a token as a parameter
926
# if defined in data/config/google_analytics, tell webpack about it.
927
ga_snippet_fn = path.join(exports.SALVUS_HOME, 'data', 'config', 'google_analytics')
928
# file could contain the token or nothing (empty string, see k8s/smc-webapp-static)
929
ga = null
930
if fs.existsSync(ga_snippet_fn)
931
token = fs.readFileSync(ga_snippet_fn).toString().trim()
932
if token.length > 0
933
ga = token
934
exports.GOOGLE_ANALYTICS = ga
935
936
937