Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39534
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
# console_server -- a node.js tty console server
26
#
27
# * the server, which runs as a command-line daemon (or can
28
# be used as a library)
29
#
30
# * the client, which e.g. gets imported by hub and used
31
# for communication between hub and the server daemon.
32
#
33
#################################################################
34
35
path = require('path')
36
async = require('async')
37
fs = require('fs')
38
net = require('net')
39
child_process = require('child_process')
40
winston = require('winston')
41
assert = require('assert')
42
program = require('commander')
43
44
message = require('smc-util/message')
45
misc_node = require('smc-util-node/misc_node')
46
{secret_token_filename} = require('./common')
47
48
port_manager = require('./port_manager')
49
misc = require('smc-util/misc')
50
{to_json, from_json, defaults, required} = misc
51
52
abspath = (path) ->
53
if path.length == 0
54
return process.env.HOME
55
if path[0] == '/'
56
return path # already an absolute path
57
return process.env.HOME + '/' + path
58
59
if not process.env.SMC?
60
process.env.SMC = path.join(process.env.HOME, '.smc')
61
62
DATA = path.join(process.env['SMC'], 'console_server')
63
64
if not fs.existsSync(process.env['SMC'])
65
fs.mkdirSync(process.env['SMC'])
66
if not fs.existsSync(DATA)
67
fs.mkdirSync(DATA)
68
69
##################################################################
70
# Read the secret token file.
71
#
72
# This file is created by the local_hub process, which is started at
73
# the same time as the console_server. So, we try for up to 5 seconds
74
# until this file appears.
75
##################################################################
76
77
misc = require('smc-util/misc')
78
79
80
secret_token = undefined
81
82
read_token = (cb) ->
83
f = (cb) ->
84
fs.exists secret_token_filename(), (exists) ->
85
if not exists
86
cb("secret token file does not exist")
87
else
88
secret_token = fs.readFileSync(secret_token_filename()).toString()
89
cb()
90
misc.retry_until_success
91
f : f
92
max_time : 30000
93
cb : cb
94
95
start_session = (socket, mesg) ->
96
winston.info "start_session #{to_json(mesg)}"
97
98
if not mesg.params? # for connecting to an existing session.
99
mesg.params = {}
100
101
opts = defaults mesg.params,
102
rows : 24
103
cols : 80
104
command : 'bash'
105
args : []
106
path : required
107
filename : required
108
109
opts.path = abspath(opts.path) # important since console server is started in some random location
110
opts.filename = abspath(opts.filename)
111
112
init_fn = misc.console_init_filename(opts.filename)
113
if fs.existsSync(init_fn) and opts.command == 'bash'
114
opts.args = ['--init-file', "#{init_fn}"]
115
116
if process.env['USER'] == 'root'
117
if not mesg.project_id? or mesg.project_id.length != 36
118
winston.debug("suspicious project_id (=#{mesg.project_id}) -- bailing")
119
return
120
121
winston.debug "start_session opts = #{to_json(opts)}"
122
123
# Ensure that the given user exists. If not, send an error. The
124
# hub should always ensure the user exists before starting a session.
125
async.series([
126
(cb) ->
127
# Fork off a child process that does all further work to
128
# handle a connection. We prefer the .js file if it is there,
129
# e.g., in kucalc, but can also work purely with .coffee, e.g.,
130
# for smc-in-smc development. NOTE: things will break if
131
# just console_server_child.js exists, but not all other .js files.
132
module = __dirname + '/console_server_child'
133
if fs.existsSync(module + '.js')
134
module += '.js'
135
else
136
module += '.coffee'
137
child = child_process.fork(module, [])
138
139
# Send the pid of the child to the client (the connected hub)
140
socket.write_mesg('json', message.session_description(pid:child.pid))
141
142
# Disable use of the socket for sending/receiving messages, since
143
# it will be only used for raw xterm stuff hence.
144
misc_node.disable_mesg(socket)
145
146
# Give the socket to the child, along with the options.
147
child.send(opts, socket)
148
149
cb()
150
], (err) ->
151
if err
152
# TODO: change protocol to allow for checking for an error message.
153
winston.debug("ERROR - #{err}")
154
)
155
156
handle_client = (socket, mesg) ->
157
try
158
switch mesg.event
159
when 'start_session', 'connect_to_session'
160
start_session(socket, mesg)
161
when 'send_signal'
162
switch mesg.signal
163
when 2
164
signal = 'SIGINT'
165
when 3
166
signal = 'SIGQUIT'
167
when 9
168
signal = 'SIGKILL'
169
else
170
throw("only signals 2 (SIGINT), 3 (SIGQUIT), and 9 (SIGKILL) are supported")
171
process.kill(mesg.pid, signal)
172
if mesg.id?
173
socket.write_mesg('json', message.signal_sent(id:mesg.id))
174
else
175
if mesg.id?
176
err = message.error(id:mesg.id, error:"Console server received an invalid mesg type '#{mesg.event}'")
177
socket.write_mesg('json', err)
178
catch e
179
winston.error "ERROR: '#{e}' handling message '#{to_json(mesg)}'"
180
181
server = net.createServer (socket) ->
182
winston.debug "PARENT: received connection"
183
misc_node.unlock_socket socket, secret_token, (err) ->
184
if not err
185
# Receive a single message:
186
misc_node.enable_mesg(socket)
187
socket.on 'mesg', (type, mesg) ->
188
winston.debug "received control mesg #{to_json(mesg)}"
189
handle_client(socket, mesg)
190
191
# Start listening for connections on the socket.
192
start_server = (cb) ->
193
async.series([
194
(cb) ->
195
# read the secret token
196
read_token(cb)
197
(cb) ->
198
# start listening for incoming connections on localhost only.
199
server.listen(program.port ? 0, '127.0.0.1', cb)
200
(cb) ->
201
# write port that we are listening on to port file
202
fs.writeFile(port_manager.port_file('console'), server.address().port, cb)
203
], (err) ->
204
if err
205
cb(err)
206
else
207
winston.info("listening on port #{server.address().port}")
208
cb()
209
)
210
211
program.usage('[?] [options]')
212
.option('--port <n>', 'TCP server port to listen on (default: 0 = os assigned)', ((n)->parseInt(n)), 0)
213
.option('--kucalc', "Running in the kucalc environment")
214
.parse(process.argv)
215
216
if program.kucalc
217
require('./kucalc').IN_KUCALC = true
218
219
start_server (err) ->
220
if err
221
winston.debug("failed to start console server")
222
223