forked from vpython/vpython-jupyter
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathno_notebook.py
More file actions
312 lines (256 loc) · 11.1 KB
/
no_notebook.py
File metadata and controls
312 lines (256 loc) · 11.1 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
from .vpython import GlowWidget, baseObj, vector, canvas
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 signal
from urllib.parse import unquote
from .rate_control import rate
# Check for Ctrl+C. SIGINT will also be sent by our code if WServer is closed.
def signal_handler(signal, frame):
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]
__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()
await loop.run_in_executor(None, GW.handle_msg, msg)
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():
# 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:
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
_webbrowser.open('http://localhost:{}'.format(port)
) # or webbrowser.open_new_tab()
else:
__server = HTTPServer(('', __HTTP_PORT), serveHTTP)
# or webbrowser.open_new_tab()
_webbrowser.open('http://localhost:{}'.format(__HTTP_PORT))
except:
pass
__w = threading.Thread(target=__server.serve_forever)
__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)
__t.start()
def stop_server():
"""Shuts down all threads and exits cleanly."""
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():
sys.exit(0)
else:
pass
# 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
rate(60)
# Dummy variable to import
_ = None