python-computer-craft/computercraft/sess.py

366 lines
9.2 KiB
Python

import asyncio
import string
import sys
from code import InteractiveConsole
from collections import deque
from contextlib import contextmanager
from functools import partial
from importlib import import_module
from importlib.abc import MetaPathFinder, Loader
from importlib.machinery import ModuleSpec
from itertools import count
from platform import python_version
from traceback import format_exc
from types import ModuleType
from greenlet import greenlet, getcurrent as get_current_greenlet
from .lua import lua_string
from . import rproc, ser
__all__ = (
'CCSession',
'get_current_session',
'eval_lua',
'lua_context_object',
)
def debug(*args):
sys.__stdout__.write(' '.join(map(str, args)) + '\n')
sys.__stdout__.flush()
DIGITS = string.digits + string.ascii_lowercase
def base36(n):
r = ''
while n:
r += DIGITS[n % 36]
n //= 36
return ser.encode(r[::-1])
def _is_global_greenlet():
return not hasattr(get_current_greenlet(), 'cc_greenlet')
def get_current_session():
try:
return get_current_greenlet().cc_greenlet._sess
except AttributeError:
raise RuntimeError('Computercraft function was called outside context')
class StdFileProxy:
def __init__(self, native, err):
self._native = native
self._err = err
def read(self, size=-1):
if _is_global_greenlet():
return self._native.read(size)
else:
raise RuntimeError(
"Computercraft environment doesn't support stdin read method")
def readline(self, size=-1):
if _is_global_greenlet():
return self._native.readline(size)
else:
if size is not None and size >= 0:
raise RuntimeError(
"Computercraft environment doesn't support "
"stdin readline method with parameter")
return eval_lua('return io.read()').take_string() + '\n'
def write(self, s):
if _is_global_greenlet():
return self._native.write(s)
else:
s = ser.dirty_encode(s)
if self._err:
return eval_lua('io.stderr:write(...)', s).take_none()
else:
return eval_lua('io.write(...)', s).take_none()
def fileno(self):
if _is_global_greenlet():
return self._native.fileno()
else:
# preventing use of gnu readline here
# https://github.com/python/cpython/blob/master/Python/bltinmodule.c#L1970
raise AttributeError
def __getattr__(self, name):
return getattr(self._native, name)
class ComputerCraftFinder(MetaPathFinder):
@staticmethod
def find_spec(fullname, path, target=None):
if fullname == 'cc':
return ModuleSpec(fullname, ComputerCraftLoader, is_package=True)
if fullname.startswith('cc.'):
return ModuleSpec(fullname, ComputerCraftLoader, is_package=False)
class ComputerCraftLoader(Loader):
@staticmethod
def create_module(spec):
sn = spec.name.split('.', 1)
assert sn[0] == 'cc'
if len(sn) == 1:
sn.append('_pkg')
rawmod = import_module('.' + sn[1], 'computercraft.subapis')
mod = ModuleType(spec.name)
for k in rawmod.__all__:
setattr(mod, k, getattr(rawmod, k))
return mod
@staticmethod
def exec_module(module):
pass
def install_import_hook():
import sys
sys.meta_path.append(ComputerCraftFinder)
install_import_hook()
sys.stdin = StdFileProxy(sys.__stdin__, False)
sys.stdout = StdFileProxy(sys.__stdout__, False)
sys.stderr = StdFileProxy(sys.__stderr__, True)
def eval_lua(lua_code, *params, immediate=False):
if isinstance(lua_code, str):
lua_code = ser.encode(lua_code)
request = (
(b'I' if immediate else b'T')
+ ser.serialize(lua_code)
+ ser.serialize(params)
)
result = get_current_session()._server_greenlet.switch(request)
rp = rproc.ResultProc(ser.deserialize(result))
if not immediate:
rp.check_bool_error()
return rp
@contextmanager
def lua_context_object(create_expr: str, create_params: tuple, finalizer_template: str = ''):
sess = get_current_session()
fid = sess.create_task_id()
var = 'temp[{}]'.format(lua_string(fid))
eval_lua('{} = {}'.format(var, create_expr), *create_params)
try:
yield var
finally:
finalizer_template += '; {e} = nil'
finalizer_template = finalizer_template.lstrip(' ;')
eval_lua(finalizer_template.format(e=var))
def eval_lua_method_factory(obj):
def method(name, *params):
code = 'return ' + obj + name + '(...)'
return eval_lua(code, *params)
return method
class CCGreenlet:
def __init__(self, body_fn, sess=None):
if sess is None:
self._sess = get_current_session()
else:
self._sess = sess
self._task_id = self._sess.create_task_id()
self._sess._greenlets[self._task_id] = self
parent_g = get_current_greenlet()
if parent_g is self._sess._server_greenlet:
self._parent = None
else:
self._parent = parent_g.cc_greenlet
self._parent._children.add(self._task_id)
self._children = set()
self._g = greenlet(body_fn)
self._g.cc_greenlet = self
def detach_children(self):
if self._children:
ch = list(self._children)
self._children.clear()
self._sess.drop(ch)
def _on_death(self, error=None):
self._sess._greenlets.pop(self._task_id, None)
self.detach_children()
if error is not None:
if error is True:
error = None
else:
error = ser.dirty_encode(error)
self._sess._sender(b'C' + ser.serialize(error))
if self._parent is not None:
self._parent._children.discard(self._task_id)
def defer_switch(self, *args, **kwargs):
asyncio.get_running_loop().call_soon(
partial(self.switch, *args, **kwargs))
def switch(self, *args, **kwargs):
# switch must be called from server greenlet
assert get_current_greenlet() is self._sess._server_greenlet
try:
task = self._g.switch(*args, **kwargs)
except SystemExit:
self._on_death(True)
return
except Exception:
self._on_death(format_exc(limit=None, chain=False))
return
# lua_eval call or simply idle
if isinstance(task, bytes):
x = self
while x._g.dead:
x = x._parent
self._sess._sender(task[0:1] + ser.serialize(x._task_id) + task[1:])
if self._g.dead:
if self._parent is None:
self._on_death(True)
else:
self._on_death()
class CCEventRouter:
def __init__(self, on_first_sub, on_last_unsub, resume_task):
self._stacks = {}
self._active = {}
self._on_first_sub = on_first_sub
self._on_last_unsub = on_last_unsub
self._resume_task = resume_task
def sub(self, task_id, event):
if event not in self._stacks:
self._stacks[event] = {}
self._on_first_sub(event)
se = self._stacks[event]
if task_id in se:
raise Exception('Same task subscribes to the same event twice')
se[task_id] = deque()
def unsub(self, task_id, event):
if event not in self._stacks:
return
self._stacks[event].pop(task_id, None)
if len(self._stacks[event]) == 0:
self._on_last_unsub(event)
del self._stacks[event]
def on_event(self, event, params):
if event not in self._stacks:
self._on_last_unsub(event)
return
for task_id, queue in self._stacks[event].items():
queue.append(params)
if self._active.get(task_id) == event:
self._set_task_status(task_id, event, False)
self._resume_task(task_id)
def get_from_stack(self, task_id, event):
queue = self._stacks[event][task_id]
try:
return queue.popleft()
except IndexError:
self._set_task_status(task_id, event, True)
return None
def _set_task_status(self, task_id, event, waits: bool):
if waits:
self._active[task_id] = event
else:
self._active.pop(task_id, None)
class CCSession:
def __init__(self, computer_id, sender):
# computer_id is unique identifier of a CCSession
self._computer_id = computer_id
self._tid_allocator = map(base36, count(start=1))
self._sender = sender
self._greenlets = {}
self._server_greenlet = get_current_greenlet()
self._program_greenlet = None
self._evr = CCEventRouter(
lambda event: self._sender(b'S' + ser.serialize(event)),
lambda event: self._sender(b'U' + ser.serialize(event)),
lambda task_id: self._greenlets[task_id].defer_switch('event'),
)
def on_task_result(self, task_id, result):
assert get_current_greenlet() is self._server_greenlet
if task_id not in self._greenlets:
# ignore for dropped tasks
return
self._greenlets[task_id].switch(result)
def on_event(self, event, params):
self._evr.on_event(event, params)
def create_task_id(self):
return next(self._tid_allocator)
def drop(self, task_ids):
def collect(task_id):
yield task_id
g = self._greenlets.pop(task_id)
for tid in g._children:
yield from collect(tid)
all_tids = []
for task_id in task_ids:
all_tids.extend(collect(task_id))
self._sender(b'D' + b''.join(ser.serialize(tid) for tid in all_tids))
def _run_sandboxed_greenlet(self, fn):
self._program_greenlet = CCGreenlet(fn, sess=self)
self._program_greenlet.switch()
def run_program(self, program, args):
def _run_program():
rp = eval_lua('''
local p = fs.combine(shell.dir(), ...)
if not fs.exists(p) then return nil end
if fs.isDir(p) then return nil end
local f = fs.open(p, 'r')
local code = f.readAll()
f.close()
return p, code
'''.lstrip(), program)
if rp.peek() is None:
print('Program not found', file=sys.stderr)
return
p = rp.take_string()
code = rp.take_string()
cc = compile(code, p, 'exec')
exec(cc, {'__file__': p, 'args': args})
self._run_sandboxed_greenlet(_run_program)
def run_repl(self):
def _repl():
InteractiveConsole(locals={}).interact(
banner='Python {}'.format(python_version()),
)
self._run_sandboxed_greenlet(_repl)