-
Notifications
You must be signed in to change notification settings - Fork 13
/
__init__.py
384 lines (314 loc) · 11.2 KB
/
__init__.py
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
import os
# Enable asyncio debug logging
os.environ['PYTHONASYNCIODEBUG'] = '1'
import ast
import functools
import pkgutil
import importlib
import asyncio
import threading
import atexit
import gta_native
from gta import ui, exceptions, enums
from gta.exceptions import *
from gta.enums import *
__author__ = 'Lennart Grahl <lennart.grahl@gmail.com>'
__status__ = 'Development'
__version__ = '0.10.16'
__all__ = exceptions.__all__ + enums.__all__
def _reset_globals():
"""
Set global attributes.
"""
global _utils, _thread, _loop, _tasks, _names
_utils = None
_thread = None
_loop = None
_tasks = []
_names = []
def _reset_futures(loop):
"""
Set global futures.
Arguments:
- `loop`: The loop the futures will be assigned to.
"""
global _tick_future, _key_future
try:
_tick_future.cancel()
_key_future.cancel()
except NameError:
pass
_tick_future = asyncio.Future(loop=loop)
_key_future = asyncio.Future(loop=loop)
def _reset_viewport():
"""
Set global UI view port.
"""
ui.reset()
def _init(console=False):
"""
Run startup function in another thread.
Arguments:
- `console`: Use console logging instead of file logging.
"""
_reset_globals()
global _thread
# Start thread
_thread = threading.Thread(target=_start, args=(console,), daemon=True)
_thread.start()
def _start(console):
"""
Initialise requirements and startup scripts in an event loop.
Arguments:
- `console`: Use console logging instead of file logging.
"""
global _utils, _loop, _names, _tasks
# Import utils
# Note: This needs to be done here because the logging module binds
# some vars to the thread which causes exceptions when
# pip invokes the logger.
from gta import utils
_utils = utils
# Setup logging
_utils.setup_logging(console)
logger = _utils.get_logger()
# Create event loop
_loop = asyncio.new_event_loop()
asyncio.set_event_loop(_loop)
# Reset futures and viewport
_reset_futures(_loop)
_reset_viewport()
# Print some debug information
logger.info('Started')
logger.info('Version: {}', __version__)
logger.info('Natives Date: {}', gta_native.__version__)
# Start scripts
_names, _tasks = _start_scripts(_loop)
if len(_tasks) > 0:
try:
_loop.run_until_complete(asyncio.wait(_tasks))
except RuntimeError:
bad_scripts = [(name, task) for name, task in zip(_names, _tasks)
if not task.done()]
# Mark bad behaving scripts as done
for name, task in bad_scripts:
# Note: We log the task so scripters can see in which line their script
# was running when cancelled
logger.error('Script "{}" did not stop in time, Task: {}', name, task)
# Note: At this point, the task is marked as done but callbacks will
# not be called anymore. We are just doing this to comfort asyncio
# to not throw any exceptions because the task wasn't marked done
task.set_result(BadBehavingScriptError())
# Report bad behaving scripts
scripts = ', '.join(('"{}"'.format(name) for name, task in bad_scripts))
logger.warning('Enforced stopping loop, caused by script(s): {}', scripts)
logger.info('Complete')
def _tick():
"""
Handle a game tick event.
"""
if _loop is not None and not _loop.is_closed():
def __tick():
global _tick_future
_tick_future.set_result(None)
_tick_future = asyncio.Future(loop=_loop)
#ui.draw()
_loop.call_soon_threadsafe(__tick)
def _key(code, down, **modifiers):
"""
Handle a key event.
Arguments:
- `code`: The key code represented as an integer.
- `down`: `True` if the key is pressed, `False` if the key was just released.
- `modifiers`: Modifier keys pressed.
"""
if _loop is not None and not _loop.is_closed():
# Translate key code
code = Key(code)
# logger = _utils.get_logger()
# logger.debug("Key '{}', down: {}, modifiers: {}", code, down, modifiers)
def __key():
global _key_future
_key_future.set_result((code, down, modifiers))
_key_future = asyncio.Future(loop=_loop)
_loop.call_soon_threadsafe(__key)
def _exit():
"""
Schedule stopping scripts.
"""
if _loop is not None and not _loop.is_closed():
logger = _utils.get_logger()
logger.debug('Scheduling script termination')
# Schedule stop routine
def __stop(loop):
logger.debug('Stopping scripts')
loop.create_task(_stop(loop))
_loop.call_soon_threadsafe(__stop, _loop)
@atexit.register
def _join():
"""
Try to join the event loop thread.
"""
# Note: _utils might be none when _init wasn't called
if _utils is None:
return
logger = _utils.get_logger()
# Wait until the thread of the event loop terminates
if _thread is not None:
logger.debug('Joining')
_thread.join(timeout=1.1)
if _thread.is_alive():
logger.error('Joining timed out, terminating ungracefully')
# Reset globals and exit
_reset_globals()
logger.info('Exiting')
@asyncio.coroutine
def _stop(loop, seconds=1.0):
"""
Stop scripts, wait for tasks to clean up or until a timeout occurs
and stop the loop.
Arguments:
- `loop`: The :class:`asyncio.BaseEventLoop` that is being used.
- `seconds`: The maximum amount of seconds to wait.
"""
logger = _utils.get_logger()
# Stop scripts
_stop_scripts(_tasks)
# Wait for scripts to clean up
logger.debug('Waiting for scripts to stop')
yield from asyncio.wait(_tasks, timeout=seconds)
# Stop loop
logger.debug('Stopping loop')
loop.stop()
def _start_scripts(loop):
"""
Run the main function of all scripts from the `scripts` package.
Arguments:
- `loop`: The :class:`asyncio.BaseEventLoop` that is going to be used.
Return a tuple containing a list of imported script names and
another list that maps the script names to:class:`asyncio.Task`
instances.
"""
logger = _utils.get_logger()
logger.info('Starting scripts')
# Start each script as a coroutine
names = []
tasks = []
for name, script in _import_scripts():
logger.info('Starting script "{}"', name)
task = loop.create_task(script())
task.add_done_callback(functools.partial(_script_done, name=name))
names.append(name)
tasks.append(task)
logger.info('Scripts started')
return names, tasks
def _stop_scripts(tasks):
"""
Cancel scripts that are still running.
Arguments:
- `tasks`: A list of :class:`asyncio.Task` instances.
"""
logger = _utils.get_logger()
logger.info('Cancelling scripts')
for task in tasks:
task.cancel()
logger.info('Scripts cancelled')
def _import_scripts():
"""
Import all scripts from the `scripts` package and install
dependencies.
Return a list containing tuples of each scripts name and the
callback to the main function of the script.
"""
logger = _utils.get_logger()
# Import parent package
parent_package = 'scripts'
importlib.import_module(parent_package, __name__)
# Import scripts from package
path = os.path.join(_utils.get_directory(), parent_package)
scripts = []
for importer, name, is_package in pkgutil.iter_modules([path]):
try:
try:
# Get meta data
metadata = _scrape_metadata(path, name, is_package)
logger.debug('Script "{}" metadata: {}', name, metadata)
# Get dependencies from meta data
dependencies = metadata.get('dependencies', ())
# Make to tuple if string
if isinstance(dependencies, str):
dependencies = (dependencies,)
except AttributeError:
dependencies = ()
try:
# Install dependencies
for dependency in dependencies:
_utils.install_dependency(dependency)
except TypeError as exc:
raise ScriptError() from exc
try:
# Import script
logger.debug('Importing script "{}"', name)
module = importlib.import_module('.' + name, parent_package)
main = getattr(module, 'main')
# Make sure that main is a co-routine
if not asyncio.iscoroutinefunction(main):
raise ScriptError(
'Main function of script "{}" is not a co-routine'.format(name))
scripts.append((name, main))
except (ImportError, AttributeError) as exc:
raise ImportScriptError(name) from exc
except ScriptError as exc:
# Note: We are not re-raising here because script errors should not
# affect other scripts that run fine
logger.exception(exc)
# Return scripts list
return scripts
def _scrape_metadata(path, name, is_package):
# Update path
if is_package:
path = os.path.join(path, name, '__init__.py')
else:
path = os.path.join(path, name + '.py')
# Open script path
metadata = {}
with open(path) as file:
for line in file:
# Find metadata strings
if line.startswith('__'):
try:
# Store key and value
key, value = line.split('=', maxsplit=1)
key = key.strip().strip('__')
# Note: Literal eval tries to retrieve a value, assignments,
# calls, etc. are not possible
value = ast.literal_eval(value.strip())
metadata[key] = value
except (ValueError, SyntaxError) as exc:
raise ImportScriptError(name) from exc
return metadata
def _script_done(task, name=None):
"""
Log the result or the exception of a script that returned.
Arguments:
- `task`: The :class:`asyncio.Future` instance of the script.
- `name`: The name of the script.
"""
logger = _utils.get_logger()
try:
try:
# Check for exception or result
script_exc = task.exception()
if script_exc is not None:
raise ScriptExecutionError(name) from script_exc
else:
result = task.result()
result = ' with result "{}"'.format(result) if result is not None else ''
logger.info('Script "{}" returned{}', name, result)
except asyncio.CancelledError:
logger.info('Script "{}" cancelled', name)
except asyncio.InvalidStateError as exc:
raise ScriptError('Script "{}" done callback called but script is not done'
''.format(name)) from exc
except ScriptError as exc:
logger.exception(exc)