Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39535
1
###
2
3
CoCalc: Collaborative web-based SageMath, Jupyter, LaTeX and Terminals.
4
Copyright 2015, SageMath, Inc., GPL v3.
5
6
local_hub -- a node.js program that runs as a regular user, and
7
coordinates and maintains the connections between
8
the global hubs and *all* projects running as
9
this particular user.
10
11
The local_hub is a bit like the "screen" program for Unix, except
12
that it simultaneously manages numerous sessions, since simultaneously
13
doing a lot of IO-based things is what Node.JS is good at.
14
###
15
16
17
require('coffee-cache').setCacheDir("#{process.env.HOME}/.coffee")
18
19
20
BUG_COUNTER = 0
21
22
process.addListener "uncaughtException", (err) ->
23
winston.debug("BUG ****************************************************************************")
24
winston.debug("Uncaught exception: " + err)
25
winston.debug(err.stack)
26
winston.debug("BUG ****************************************************************************")
27
if console? and console.trace?
28
console.trace()
29
BUG_COUNTER += 1
30
31
exports.get_bugs_total = ->
32
return BUG_COUNTER
33
34
path = require('path')
35
async = require('async')
36
fs = require('fs')
37
os = require('os')
38
net = require('net')
39
uuid = require('uuid')
40
winston = require('winston')
41
request = require('request')
42
program = require('commander') # command line arguments -- https://github.com/visionmedia/commander.js/
43
44
# Set the log level
45
winston.remove(winston.transports.Console)
46
winston.add(winston.transports.Console, {level: 'debug', timestamp:true, colorize:true})
47
48
require('coffee-script/register')
49
50
message = require('smc-util/message')
51
misc = require('smc-util/misc')
52
smc_version = require('smc-util/smc-version')
53
misc_node = require('smc-util-node/misc_node')
54
55
{to_json, from_json, defaults, required} = require('smc-util/misc')
56
57
# Functionality special to the KuCalc environment.
58
kucalc = require('./kucalc')
59
60
# The raw http server
61
raw_server = require('./raw_server')
62
63
# Printing a file to pdf
64
print_to_pdf = require('./print_to_pdf')
65
66
# Generation of the secret token used to auth tcp connections
67
secret_token = require('./secret_token')
68
69
# Console sessions
70
console_session_manager = require('./console_session_manager')
71
console_sessions = new console_session_manager.ConsoleSessions()
72
73
# Ports for the various servers
74
port_manager = require('./port_manager')
75
76
# Reading and writing files to/from project and sending over socket
77
read_write_files = require('./read_write_files')
78
79
# Jupyter server
80
jupyter_manager = require('./jupyter_manager')
81
82
# Executing shell code
83
{exec_shell_code} = require('./exec_shell_code')
84
85
# Saving blobs to a hub
86
blobs = require('./blobs')
87
88
# Client for connecting back to a hub
89
{Client} = require('./client')
90
91
# WARNING -- the sage_server.py program can't get these definitions from
92
# here, since it is not written in node; if this path changes, it has
93
# to be change there as well (it will use the SMC environ
94
# variable though).
95
96
if process.env.SMC_LOCAL_HUB_HOME?
97
process.env.HOME = process.env.SMC_LOCAL_HUB_HOME
98
99
if not process.env.SMC?
100
process.env.SMC = path.join(process.env.HOME, '.smc')
101
102
SMC = process.env.SMC
103
104
process.chdir(process.env.HOME)
105
106
DATA = path.join(SMC, 'local_hub')
107
108
# See https://github.com/sagemathinc/cocalc/issues/174 -- some stupid (?)
109
# code sometimes assumes this exists, and it's not so hard to just ensure
110
# it does, rather than fixing any such code.
111
SAGE = path.join(process.env.HOME, '.sage')
112
113
for directory in [SMC, DATA, SAGE]
114
if not fs.existsSync(directory)
115
fs.mkdirSync(directory)
116
117
118
CONFPATH = exports.CONFPATH = misc_node.abspath(DATA)
119
120
common = require('./common')
121
json = common.json
122
123
INFO = undefined
124
hub_client = undefined
125
init_info_json = (cb) ->
126
winston.debug("Writing 'info.json'")
127
filename = "#{SMC}/info.json"
128
if kucalc.IN_KUCALC and process.env.COCALC_PROJECT_ID? and process.env.COCALC_USERNAME?
129
project_id = process.env.COCALC_PROJECT_ID
130
username = process.env.COCALC_USERNAME
131
else
132
v = process.env.HOME.split('/')
133
project_id = v[v.length-1]
134
username = project_id.replace(/-/g,'')
135
if process.env.SMC_HOST?
136
host = process.env.SMC_HOST
137
else if os.hostname() == 'sagemathcloud'
138
# special case for the VirtualBox VM
139
host = 'localhost'
140
else
141
# what we want for the Google Compute engine deployment
142
# earlier, there was eth0, but newer Ubuntu's on GCP have ens4
143
nics = require('os').networkInterfaces()
144
mynic = nics.eth0 ? nics.ens4
145
host = mynic?[0].address
146
base_url = process.env.SMC_BASE_URL ? ''
147
port = 22
148
INFO =
149
project_id : project_id
150
location : {host:host, username:username, port:port, path:'.'}
151
base_url : base_url
152
exports.client = hub_client = new Client(INFO.project_id)
153
fs.writeFile(filename, misc.to_json(INFO), cb)
154
155
# Connecting to existing session or making a new one.
156
connect_to_session = (socket, mesg) ->
157
winston.debug("connect_to_session -- type='#{mesg.type}'")
158
switch mesg.type
159
when 'console'
160
console_sessions.connect(socket, mesg)
161
else
162
err = message.error(id:mesg.id, error:"Unsupported session type '#{mesg.type}'")
163
socket.write_mesg('json', err)
164
165
# Kill an existing session.
166
terminate_session = (socket, mesg) ->
167
cb = (err) ->
168
if err
169
mesg = message.error(id:mesg.id, error:err)
170
socket.write_mesg('json', mesg)
171
172
sid = mesg.session_uuid
173
if console_sessions.session_exists(sid)
174
console_sessions.terminate_session(sid, cb)
175
else
176
cb()
177
178
# Handle a message from the client (=hub)
179
handle_mesg = (socket, mesg, handler) ->
180
#dbg = (m) -> winston.debug("handle_mesg: #{m}")
181
#dbg("mesg=#{json(mesg)}")
182
183
if hub_client.handle_mesg(mesg, socket)
184
return
185
186
switch mesg.event
187
when 'connect_to_session', 'start_session'
188
# These sessions completely take over this connection, so we stop listening
189
# for further control messages on this connection.
190
socket.removeListener('mesg', handler)
191
connect_to_session(socket, mesg)
192
when 'jupyter_port'
193
# start jupyter server if necessary and send back a message with the port it is serving on
194
jupyter_manager.jupyter_port(socket, mesg)
195
when 'project_exec'
196
exec_shell_code(socket, mesg)
197
when 'read_file_from_project'
198
read_write_files.read_file_from_project(socket, mesg)
199
when 'write_file_to_project'
200
read_write_files.write_file_to_project(socket, mesg)
201
when 'print_to_pdf'
202
print_to_pdf.print_to_pdf(socket, mesg)
203
when 'send_signal'
204
misc_node.process_kill(mesg.pid, mesg.signal)
205
if mesg.id?
206
# send back confirmation that a signal was sent
207
socket.write_mesg('json', message.signal_sent(id:mesg.id))
208
when 'terminate_session'
209
terminate_session(socket, mesg)
210
when 'save_blob'
211
blobs.handle_save_blob_message(mesg)
212
when 'error'
213
winston.debug("ERROR from hub: #{mesg.error}")
214
when 'hello'
215
# No action -- this is used by the hub to send an initial control message that has no effect, so that
216
# we know this socket will be used for control messages.
217
winston.debug("hello from hub -- sending back our version = #{smc_version.version}")
218
socket.write_mesg('json', message.version(version:smc_version.version))
219
else
220
if mesg.id?
221
# only respond with error if there is an id -- otherwise response has no meaning.
222
err = message.error(id:mesg.id, error:"Local hub failed to handle mesg of type '#{mesg.event}'")
223
socket.write_mesg('json', err)
224
else
225
winston.debug("Dropping unknown mesg type '#{mesg.event}'")
226
227
228
###
229
Use exports.client object below to work with the local_hub
230
interactively for debugging purposes when developing SMC in an SMC project.
231
232
1. Cd to the directory of the project, e.g.,
233
/projects/45f4aab5-7698-4ac8-9f63-9fd307401ad7/smc/src/data/projects/f821cc2a-a6a2-4c3d-89a7-bcc6de780ebb
234
2. Setup the environment:
235
export HOME=`pwd`; export SMC=$HOME/.smc/; export SMC_PROXY_HOST=0.0.0.0
236
3. Start coffees interpreter running
237
coffee
238
4. Start the local_hub server:
239
{client} = require('smc-project/local_hub')
240
5. Restart the hub, then get a directory listing of the project from the hub.
241
242
You have to restart the hub, since otherwise the hub will restart the
243
project, which will cause it to make another local_hub server, separate
244
from the one you just started running.
245
###
246
247
start_tcp_server = (secret_token, port, cb) ->
248
# port: either numeric or 'undefined'
249
if not secret_token?
250
cb("secret token must be defined")
251
return
252
253
winston.info("starting tcp server: project <--> hub...")
254
server = net.createServer (socket) ->
255
winston.debug("received new connection")
256
socket.on 'error', (err) ->
257
winston.debug("socket error - #{err}")
258
259
misc_node.unlock_socket socket, secret_token, (err) ->
260
if err
261
winston.debug(err)
262
else
263
socket.id = uuid.v4()
264
misc_node.enable_mesg(socket)
265
266
handler = (type, mesg) ->
267
if mesg.event not in ['connect_to_session', 'start_session']
268
# This is a control connection, so we can use it to call the hub later.
269
hub_client.active_socket(socket)
270
if type == "json" # other types are handled elsewhere in event handling code.
271
#winston.debug("received control mesg -- #{json(mesg)}")
272
handle_mesg(socket, mesg, handler)
273
274
socket.on('mesg', handler)
275
276
port_file = misc_node.abspath("#{DATA}/local_hub.port")
277
# https://nodejs.org/api/net.html#net_server_listen_port_hostname_backlog_callback ?
278
server.listen port, '0.0.0.0', (err) ->
279
if err
280
winston.info("tcp_server failed to start -- #{err}")
281
cb(err)
282
else
283
winston.info("tcp_server listening 0.0.0.0:#{server.address().port}")
284
fs.writeFile(port_file, server.address().port, cb)
285
286
# Start listening for connections on the socket.
287
start_server = (tcp_port, raw_port, cb) ->
288
the_secret_token = undefined
289
if program.console_port
290
console_sessions.set_port(program.console_port)
291
async.series([
292
(cb) ->
293
init_info_json(cb)
294
(cb) ->
295
# This is also written by forever; however, by writing it directly it's also possible
296
# to run the local_hub server in a console, which is useful for debugging and development.
297
fs.writeFile(misc_node.abspath("#{DATA}/local_hub.pid"), "#{process.pid}", cb)
298
(cb) ->
299
secret_token.init_secret_token (err, token) ->
300
if err
301
cb(err)
302
else
303
the_secret_token = token
304
console_sessions.set_secret_token(token)
305
cb()
306
(cb) ->
307
start_tcp_server(the_secret_token, tcp_port, cb)
308
(cb) ->
309
raw_server.start_raw_server
310
project_id : INFO.project_id
311
base_url : INFO.base_url
312
host : process.env.SMC_PROXY_HOST ? INFO.location.host ? 'localhost'
313
data_path : DATA
314
home : process.env.HOME
315
port : raw_port
316
logger : winston
317
cb : cb
318
], (err) ->
319
if err
320
winston.debug("ERROR starting server -- #{err}")
321
else
322
winston.debug("Successfully started servers.")
323
cb(err)
324
)
325
326
program.usage('[?] [options]')
327
.option('--tcp_port <n>', 'TCP server port to listen on (default: 0 = os assigned)', ((n)->parseInt(n)), 0)
328
.option('--raw_port <n>', 'RAW server port to listen on (default: 0 = os assigned)', ((n)->parseInt(n)), 0)
329
.option('--console_port <n>', 'port to find console server on (optional; uses port file if not given); if this is set we assume some other system is managing the console server and do not try to start it -- just assume it is listening on this port always', ((n)->parseInt(n)), 0)
330
.option('--kucalc', "Running in the kucalc environment")
331
.option('--test_firewall', 'Abort and exit w/ code 99 if internal GCE information is accessible')
332
.parse(process.argv)
333
334
if program.kucalc
335
kucalc.IN_KUCALC = true
336
if program.test_firewall
337
kucalc.init_gce_firewall_test(winston)
338
339
start_server program.tcp_port, program.raw_port, (err) ->
340
if err
341
process.exit(1)
342
343