-
Notifications
You must be signed in to change notification settings - Fork 64
Expand file tree
/
Copy pathno_notebook.py
More file actions
executable file
·408 lines (338 loc) · 14.6 KB
/
Copy pathno_notebook.py
File metadata and controls
executable file
·408 lines (338 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
from .vpython import GlowWidget, baseObj, vector, canvas, _browsertype
from ._notebook_helpers import _in_spyder, _undo_vpython_import_in_spyder
from http.server import BaseHTTPRequestHandler, HTTPServer
import os
import platform
import sys
import threading
import json
import webbrowser as _webbrowser
import asyncio
from autobahn.asyncio.websocket import WebSocketServerProtocol, WebSocketServerFactory
import txaio
import copy
import socket
import multiprocessing
import time
import signal
from urllib.parse import unquote
from .rate_control import rate
makeDaemonic = (platform.system() == "Windows")
# Redefine `Thread.run` to not show a traceback for Spyder when stopping
# the server by raising a KeyboardInterrupt or SystemExit.
if _in_spyder:
def install_thread_stopped_message():
"""
Workaround to prevent showing a traceback when VPython server stops.
See:
https://bugs.python.org/issue1230540
"""
run_old = threading.Thread.run
def run(*args, **kwargs):
try:
run_old(*args, **kwargs)
except (KeyboardInterrupt, SystemExit):
pass
# ("VPython server stopped.")
except:
raise
threading.Thread.run = run
install_thread_stopped_message()
# Check for Ctrl+C. SIGINT will also be sent by our code if WServer is closed.
def signal_handler(signal, frame):
#print("in signal handler, calling stop server")
stop_server()
signal.signal(signal.SIGINT, signal_handler)
# Requests from client to http server can be the following:
# get glowcomm.html, library .js files, images, or font files
def find_free_port():
s = socket.socket()
s.bind(('', 0)) # find an available port
return s.getsockname()[1]
if "VPYTHON_HTTP_PORT" in os.environ:
__HTTP_PORT = int(os.environ["VPYTHON_HTTP_PORT"])
else:
__HTTP_PORT = find_free_port()
__SOCKET_PORT = find_free_port()
try:
if platform.python_implementation() == 'PyPy':
# use port number between 9000 and 9999 for PyPy
__SOCKET_PORT = 9000 + __SOCKET_PORT % 1000
except:
pass
# try: # machinery for reusing ports
# fd = open('free_ports')
# __HTTP_PORT = int(fd.readline())
# __SOCKET_PORT = int(fd.readline())
# except:
# __HTTP_PORT = find_free_port()
# __SOCKET_PORT = find_free_port()
# fd = open('free_ports', 'w') # this writes to user program's directory
# fd.write(str(__HTTP_PORT))
# fd.write('\n')
# fd.write(str(__SOCKET_PORT))
# Make it possible for glowcomm.html to find out what the websocket port is:
js = __file__.replace(
'no_notebook.py', 'vpython_libraries' + os.sep + 'glowcomm.html')
with open(js) as fd:
glowcomm_raw = fd.read()
def glowcomm_with_socket_port(port):
global glowcomm_raw
# provide glowcomm.html with socket number
return glowcomm_raw.replace('XXX', str(port))
glowcomm = glowcomm_with_socket_port(__SOCKET_PORT)
httpserving = False
websocketserving = False
class serveHTTP(BaseHTTPRequestHandler):
serverlib = __file__.replace('no_notebook.py', 'vpython_libraries')
serverdata = __file__.replace('no_notebook.py', 'vpython_data')
mimes = {'html': ['text/html', serverlib],
'js': ['application/javascript', serverlib],
'css': ['text/css', serverlib],
'jpg': ['image/jpg', serverdata],
'png': ['image/png', serverlib],
'otf': ['application/x-font-otf', serverdata],
'ttf': ['application/x-font-ttf', serverdata],
'ico': ['image/x-icon', serverdata]}
def do_GET(self):
global httpserving
httpserving = True
html = False
if self.path == "/":
self.path = 'glowcomm.html'
html = True
elif self.path[0] == "/":
self.path = os.sep + self.path[1:]
f = self.path.rfind('.')
fext = None
if f > 0:
fext = self.path[f + 1:]
if fext in self.mimes:
mime = self.mimes[fext]
# For example, mime[0] is image/jpg,
# mime[1] is C:\Users\Bruce\Anaconda3\lib\site-packages\vpython\vpython_data
self.send_response(200)
self.send_header('Content-type', mime[0])
self.end_headers()
if not html:
path = unquote(self.path) # convert %20 to space, for example
# Now path can be for example \Fig 4.6.jpg
# user current working directory, e.g. D:\Documents\0GlowScriptWork\LocalServer
cwd = os.getcwd()
loc = cwd + path
if not os.path.isfile(loc):
loc = mime[1] + path # look in vpython_data
fd = open(loc, 'rb')
self.wfile.write(fd.read())
else:
# string.encode() is not available in Python 2.7, but neither is async
self.wfile.write(glowcomm.encode('utf-8'))
def log_message(self, format, *args): # this overrides server stderr output
return
# Requests from client to websocket server can be the following:
# trigger event; return data (constructors, attributes, methods)
# other event; pause, waitfor, pick, compound
class WSserver(WebSocketServerProtocol):
# Data sent and received must be type "bytes", so use string.encode and string.decode
connection = None
def onConnect(self, request):
self.connection = self
def onOpen(self):
global websocketserving
websocketserving = True
# For Python 3.5 and later, the newer syntax eliminates "@asyncio.coroutine"
# in favor of "async def onMessage...", and "yield from" with "await".
# Attempting to use the older Python 3.4 syntax was not successful, so this
# no-notebook version of VPython requires Python 3.5.3 or later.
# @asyncio.coroutine
# def onMessage(self, data, isBinary): # data includes canvas update, events, pick, compound
# data includes canvas update, events, pick, compound
async def onMessage(self, data, isBinary):
baseObj.handle_attach() # attach arrow and attach trail
baseObj.sent = False # tell main thread that we're preparing to send data to browser
while True:
try:
objdata = copy.deepcopy(baseObj.updates)
attrdata = copy.deepcopy(baseObj.attrs)
baseObj.initialize() # reinitialize baseObj.updates
break
except:
pass
for a in attrdata: # a is [idx, attr]
idx, attr = a
val = getattr(baseObj.object_registry[idx], attr)
if type(val) is vector:
val = [val.x, val.y, val.z]
if idx in objdata['attrs']:
objdata['attrs'][idx][attr] = val
else:
objdata['attrs'][idx] = {attr: val}
objdata = baseObj.package(objdata)
jdata = json.dumps(objdata, separators=(',', ':')).encode('utf_8')
self.sendMessage(jdata, isBinary=False)
baseObj.sent = True
if data != b'trigger': # b'trigger' just asks for updates
d = json.loads(data.decode("utf_8")) # update_canvas info
for m in d:
# Must send events one at a time to GW.handle_msg because bound events need the loop code:
# message format used by notebook
msg = {'content': {'data': [m]}}
loop = asyncio.get_event_loop()
try:
await loop.run_in_executor(None, GW.handle_msg, msg)
except:
#
# this will throw a runtime exception after the main Thread
# has stopped, but we don't really case since the main thread
# is no longer there to do anything anyway.
if threading.main_thread().is_alive():
raise
else:
pass
def onClose(self, wasClean, code, reason):
"""Called when browser tab is closed."""
global websocketserving
self.connection = None
# We r done serving, let everyone else know...
websocketserving = False
# The cleanest way to get a fresh browser tab going in spyder
# is to force vpython to be reimported each time the code is run.
#
# Even though this code is repeated in stop_server below we also
# need it here because in spyder the script may have stopped on its
# own ( because it has no infinite loop in it ) so the only signal
# that the tab has been closed comes via the websocket.
if _in_spyder:
_undo_vpython_import_in_spyder()
# We want to exit, but the main thread is running.
# Only the main thread can properly call sys.exit, so have a signal
# handler call it on the main thread's behalf.
if platform.system() == 'Windows':
if threading.main_thread().is_alive() and not _in_spyder:
# On windows, if we get here then this signal won't be caught
# by our signal handler. Just call it ourselves.
os.kill(os.getpid(), signal.CTRL_C_EVENT)
else:
stop_server()
else:
os.kill(os.getpid(), signal.SIGINT)
try:
no_launch = os.environ.get("VPYTHON_NO_LAUNCH_BROWSER", False)
if no_launch=="0":
no_launch=False
if platform.python_implementation() == 'PyPy':
server_address = ('', 0) # let HTTPServer choose a free port
__server = HTTPServer(server_address, serveHTTP)
port = __server.server_port # get the chosen port
# Change the global variable to store the actual port used
__HTTP_PORT = port
if not no_launch:
_webbrowser.open('http://localhost:{}'.format(port)
) # or webbrowser.open_new_tab()
else:
__server = HTTPServer(('', __HTTP_PORT), serveHTTP)
# or webbrowser.open_new_tab()
if not no_launch:
if _browsertype == 'default': # uses default browser
_webbrowser.open('http://localhost:{}'.format(__HTTP_PORT))
except:
pass
if _browsertype == 'pyqt':
if platform.python_implementation() == 'PyPy':
raise RuntimeError('The pyqt browser cannot be used PyPy. Please use '
'the default browser instead by removing '
'set_browser("pyqt") from your code.')
elif sys.platform.startswith('win'):
raise RuntimeError('The pyqt browser cannot be used on Windows. '
'Please use the default browser instead by '
'removing set_browser("pyqt") from your code.')
elif sys.version_info.major == 3 and sys.version_info.minor >= 8:
raise RuntimeError('The pyqt browser cannot be used on Python 3.8. '
'Please use the default browser instead by '
'removing set_browser("pyqt") from your code.')
def start_Qapp(port):
# creates a python browser with PyQt5
# runs qtbrowser.py in a separate process
filepath = os.path.dirname(__file__)
filename = filepath + '/qtbrowser.py'
os.system('python ' + filename + ' http://localhost:{}'.format(port))
# create a browser in its own process
if _browsertype == 'pyqt':
__m = multiprocessing.Process(target=start_Qapp, args=(__HTTP_PORT,))
__m.start()
__w = threading.Thread(target=__server.serve_forever, daemon=makeDaemonic)
__w.start()
def start_websocket_server():
"""
Function to get the websocket server going and run the event loop
that feeds it.
"""
# We need a new loop in case some other process has already started the
# main loop. In principle we might be able to do a check for a running
# loop but this works whether or not a loop is running.
__interact_loop = asyncio.new_event_loop()
# Need to do two things before starting the server factory:
#
# 1. Set our loop to be the default event loop on this thread
asyncio.set_event_loop(__interact_loop)
# 2. Line below is courtesy of
# https://github.com/crossbario/autobahn-python/issues/1007#issuecomment-391541322
txaio.config.loop = __interact_loop
# Now create the factory, start the server then run the event loop forever.
__factory = WebSocketServerFactory(u"ws://localhost:{}/".format(__SOCKET_PORT))
__factory.protocol = WSserver
__coro = __interact_loop.create_server(__factory, '0.0.0.0', __SOCKET_PORT)
__interact_loop.run_until_complete(__coro)
__interact_loop.run_forever()
# Put the websocket server in a separate thread running its own event loop.
# That works even if some other program (e.g. spyder) already running an
# async event loop.
__t = threading.Thread(target=start_websocket_server, daemon=makeDaemonic)
__t.start()
def stop_server():
"""Shuts down all threads and exits cleanly."""
#print("in stop server")
global __server
__server.shutdown()
event_loop = txaio.config.loop
event_loop.stop()
# We've told the event loop to stop, but it won't shut down until we poke
# it with a simple scheduled task.
event_loop.call_soon_threadsafe(lambda: None)
# If we are in spyder, undo our import. This gets done in the websocket
# server onClose above if the browser tab is closed but is not done
# if the user stops the kernel instead.
if _in_spyder:
_undo_vpython_import_in_spyder()
# We don't want Ctrl-C to try to sys.exit inside spyder, i.e.
# in an ipython console with a separate python kernel running.
if _in_spyder:
raise KeyboardInterrupt
if threading.main_thread().is_alive():
#print("main is alive...")
sys.exit(0)
else:
#
# check to see if the event loop is still going, if so join it.
#
#print("main is dead..")
if __t.is_alive():
#print("__t is alive still")
if threading.get_ident() != __t.ident:
#print("but it's not my thread, so I'll join...")
__t.join()
else:
#print("__t is alive, but that's my thread! So skip it.")
pass
else:
if makeDaemonic:
sys.exit(0)
# If the main thread has already stopped, the python interpreter
# is likely just running .join on the two remaining threads (in
# python/threading.py:_shutdown). Since we just stopped those threads,
# we'll now exit.
GW = GlowWidget()
while not (httpserving and websocketserving): # try to make sure setup is complete
time.sleep(0.1)
# Dummy variable to import
_ = None