Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39550
1
###############################################################################
2
#
3
# CoCalc: Collaborative Calculation in the Cloud
4
#
5
# Copyright (C) 2014 -- 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
if not Primus?
23
alert("Library not fully built (Primus not defined) -- refresh your browser")
24
setTimeout((->window.location.reload()), 1000)
25
26
$ = window.$
27
_ = require('underscore')
28
29
client = require('smc-util/client')
30
31
misc_page = require('./misc_page')
32
33
APP_LOGO_WHITE = misc_page.APP_LOGO_WHITE
34
35
# these idle notifications were in misc_page, but importing it here failed
36
37
idle_notification_html = ->
38
{redux} = require('./smc-react')
39
customize = redux.getStore('customize')
40
"""
41
<div>
42
<img src="#{APP_LOGO_WHITE}">
43
<h1>... is on standby</h1>
44
&mdash; click to resume &mdash;
45
</div>
46
"""
47
48
idle_notification_state = undefined
49
50
idle_notification = (show) ->
51
if idle_notification_state? and idle_notification_state == show
52
return
53
$idle = $("#smc-idle-notification")
54
if show
55
if $idle.length == 0
56
box = $("<div/>", id: "smc-idle-notification" ).html(idle_notification_html())
57
$("body").append(box)
58
# quick slide up, just to properly slide down on the fist time
59
box.slideUp 0, ->
60
box.slideDown "slow"
61
else
62
$idle.slideDown "slow"
63
else
64
$idle.slideUp "slow"
65
idle_notification_state = show
66
67
# end idle notifications
68
69
auth_token = misc_page.get_query_param('auth_token')
70
71
class Connection extends client.Connection
72
constructor: (opts) ->
73
# Security note: not easily exposing this to the global scope would make it harder
74
# for an attacker who is eval'ing dangerous code in a Sage worksheet (say).
75
# However, even if it were not exposed, the attacker could just do
76
# "conn = new Primus(url, opts)"
77
# and make a Primus connection, and start sending/receiving messages. This would work,
78
# because the primus connection authenticates based on secure https cookies,
79
# which are there. So we could make everything painful and hard to program and
80
# actually get zero security gain.
81
#
82
# **CRITICAL:** If the smc object isn't defined in your Google Chrome console session,
83
# you have to change the context to *top*! See
84
# http://stackoverflow.com/questions/3275816/debugging-iframes-with-chrome-developer-tools/8581276#8581276
85
#
86
super(opts)
87
@_setup_window_smc()
88
89
# This is used by the base class for marking file use notifications.
90
@_redux = require('./smc-react').redux
91
92
setTimeout(@_init_idle, 15 * 1000)
93
94
_setup_window_smc: () =>
95
# if we are in DEBUG mode, inject the client into the global window object
96
if not DEBUG
97
return
98
window.smc ?= {}
99
window.smc.client = @
100
window.smc.misc = require('smc-util/misc')
101
window.smc.immutable = require('immutable')
102
window.smc.done = window.smc.misc.done
103
window.smc.sha1 = require('sha1')
104
window.smc.schema = require('smc-util/schema')
105
# use to enable/disable verbose synctable logging
106
window.smc.synctable_debug = require('smc-util/synctable').set_debug
107
window.smc.idle_trigger = => @emit('idle', 'away')
108
109
# Client-side testing code -- we use require.ensure so this stuff only
110
# ever gets loaded by the browser if actually used.
111
window.smc.test = (modules) ->
112
require.ensure ['./test-client/init'], ->
113
require('./test-client/init').run(modules)
114
window.smc.test_clear = () ->
115
require.ensure ['./test-client/init'], ->
116
require('./test-client/init').clear()
117
118
_init_idle: () =>
119
# Do not bother on mobile, since mobile devices already automatically disconnect themselves
120
# very aggressively to save battery life.
121
if require('./feature').IS_TOUCH
122
return
123
124
###
125
The @_init_time is a timestamp in the future.
126
It is pushed forward each time @_idle_reset is called.
127
The setInterval timer checks every minute, if the current time is past this @_init_time.
128
If so, the user is 'idle'.
129
To keep 'active', call webapp_client.idle_reset as often as you like:
130
A document.body event listener here and one for each jupyter iframe.body (see jupyter.coffee).
131
###
132
133
# 15 min default in case it isn't set (it will get set when user account settings are loaded)
134
@_idle_timeout ?= 15 * 60 * 1000
135
@_idle_reset()
136
setInterval(@_idle_check, 60 * 1000)
137
138
# call this idle_reset like a function
139
# will reset timer on *first* call and then every 15secs while being called
140
@idle_reset = _.throttle(@_idle_reset, 15 * 1000)
141
142
# activate a listener on our global body (universal sink for bubbling events, unless stopped!)
143
$(document).on('click mousemove keydown focusin touchstart', @idle_reset)
144
$('#smc-idle-notification').on('click mousemove keydown focusin touchstart', @_idle_reset)
145
146
delayed_disconnect = undefined
147
148
reconn = =>
149
if @_connection_is_totally_dead # CRITICAL: See https://github.com/primus/primus#primusopen !!!!!
150
@_conn?.open()
151
reconn = _.throttle(reconn, 10*1000) # never attempt to reconnect more than once per 10s, no matter what.
152
153
disconn = =>
154
if @_connected
155
@_conn?.end()
156
157
@on 'idle', (state) ->
158
#console.log("idle state: #{state}")
159
switch state
160
161
when "away"
162
idle_notification(true)
163
delayed_disconnect ?= setTimeout(disconn, 15 * 1000)
164
165
when "active"
166
idle_notification(false)
167
if delayed_disconnect?
168
clearTimeout(delayed_disconnect)
169
delayed_disconnect = undefined
170
reconn()
171
setTimeout(reconn, 5000) # avoid race condition???
172
173
# This is called periodically. If the user hasn't been active
174
# for @_idle_timeout ms, then we emit an idle event.
175
_idle_check: =>
176
if not @_idle_time?
177
return
178
now = (new Date()).getTime()
179
# console.log("idle: checking idle #{@_idle_time} < #{now}")
180
if @_idle_time < now
181
@emit('idle', 'away')
182
183
# Set @_idle_time to the **moment in in the future** at which the user will be
184
# considered idle, and also emit event indicator using is currently active.
185
_idle_reset: =>
186
@_idle_time = (new Date()).getTime() + @_idle_timeout + 1000
187
# console.log '_idle_reset', new Date(@_idle_time), ' _idle_timeout=', @_idle_timeout
188
@emit('idle', 'active')
189
190
# Change the standby timeout to a particular time in minutes.
191
# This gets called when the user configuration settings are set/loaded.
192
set_standby_timeout_m: (time_m) =>
193
@_idle_timeout = time_m * 60 * 1000
194
@_idle_reset()
195
196
_connect: (url, ondata) ->
197
log = (mesg) ->
198
console.log("websocket -", mesg)
199
log("connect")
200
201
@url = url
202
if @ondata?
203
# handlers already setup
204
return
205
206
@ondata = ondata
207
208
###
209
opts =
210
ping : 25000 # used for maintaining the connection and deciding when to reconnect.
211
pong : 12000 # used to decide when to reconnect
212
strategy : 'disconnect,online,timeout'
213
reconnect :
214
max : 5000
215
min : 1000
216
factor : 1.25
217
retries : 100000 # why ever stop trying if we're only trying once every 5 seconds?
218
conn = new Primus(url, opts)
219
###
220
221
conn = new Primus(url)
222
223
@_conn = conn
224
conn.on 'open', () =>
225
@_connected = true
226
@_connection_is_totally_dead = false
227
if @_conn_id?
228
conn.write(@_conn_id)
229
else
230
conn.write("XXXXXXXXXXXXXXXXXXXX")
231
protocol = if window.WebSocket? then 'websocket' else 'polling'
232
@emit("connected", protocol)
233
log("connected; protocol='#{protocol}'")
234
@_num_attempts = 0
235
236
conn.removeAllListeners('data')
237
f = (data) =>
238
@_conn_id = data.toString()
239
conn.removeListener('data',f)
240
conn.on('data', ondata)
241
conn.on("data", f)
242
243
if auth_token?
244
@sign_in_using_auth_token
245
auth_token : auth_token
246
cb : (err, resp) ->
247
auth_token = undefined
248
249
conn.on 'outgoing::open', (evt) =>
250
log("connecting")
251
@emit("connecting")
252
253
conn.on 'offline', (evt) =>
254
log("offline")
255
@_connected = false
256
@emit("disconnected", "offline")
257
258
conn.on 'online', (evt) =>
259
log("online")
260
261
conn.on 'message', (evt) =>
262
ondata(evt.data)
263
264
conn.on 'error', (err) =>
265
log("error: ", err)
266
# NOTE: we do NOT emit an error event in this case! See
267
# https://github.com/sagemathinc/cocalc/issues/1819
268
# for extensive discussion.
269
270
conn.on 'close', () =>
271
log("closed")
272
@_connected = false
273
@emit("disconnected", "close")
274
275
conn.on 'end', =>
276
@_connection_is_totally_dead = true
277
278
conn.on 'reconnect scheduled', (opts) =>
279
@_num_attempts = opts.attempt
280
@emit("disconnected", "close") # This just informs everybody that we *are* disconnected.
281
@emit("connecting")
282
conn.removeAllListeners('data')
283
log("reconnect scheduled in #{opts.scheduled} ms (attempt #{opts.attempt} out of #{opts.retries})")
284
285
conn.on 'incoming::pong', (time) =>
286
#log("pong latency=#{conn.latency}")
287
if not window.document.hasFocus? or window.document.hasFocus()
288
# networking/pinging slows down when browser not in focus...
289
@emit "ping", conn.latency
290
291
#conn.on 'outgoing::ping', () =>
292
# log(new Date() - 0, "sending a ping")
293
294
@_write = (data) =>
295
conn.write(data)
296
297
# return latest ping/pong time (latency) if connected; otherwise, return undefined
298
latency: () =>
299
if @_connected
300
return @_conn.latency
301
302
_fix_connection: (delete_cookies) =>
303
if delete_cookies
304
console.log("websocket -- deleting haproxy cookies")
305
document.cookie = 'SMCSERVERID3=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'
306
console.log("websocket --_fix_connection... ")
307
@_conn.end()
308
@_conn.open()
309
310
_cookies: (mesg) =>
311
$.ajax(url:mesg.url, data:{id:mesg.id, set:mesg.set, get:mesg.get, value:mesg.value})
312
313
alert_message: (args...) =>
314
require('./alerts').alert_message(args...)
315
316
connection = undefined
317
exports.connect = (url) ->
318
if connection?
319
return connection
320
else
321
return connection = new Connection(url)
322
323
exports.connect()
324
325