#!/usr/bin/python3 # -*- coding: utf-8 -*- # Copyright (c) 2018-2021 Niklas Ekström import select import sys import socket import threading import time import os import struct import pty import signal import termios import fcntl import logging import json logging.basicConfig(format = '%(levelname)s, %(asctime)s, %(name)s, line %(lineno)d: %(message)s') logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) FS_CFG_FILE = 'a314/files_pi/a314fs.conf' PICMD_CFG_FILE = 'a314/files_pi/picmd.conf' volume_paths = {} search_path = '' env_vars = {} sgr_map = {} def load_cfg(): with open(FS_CFG_FILE, 'rt') as f: cfg = json.load(f) devs = cfg['devices'] for _, dev in devs.items(): volume_paths[dev['volume']] = dev['path'] global search_path search_path = os.getenv('PATH') with open(PICMD_CFG_FILE, 'rt') as f: cfg = json.load(f) if 'paths' in cfg: search_path = ':'.join(cfg['paths']) + ':' + search_path os.environ['PATH'] = search_path if 'env_vars' in cfg: for key, val in cfg['env_vars'].items(): env_vars[key] = val if 'sgr_map' in cfg: for key, val in cfg['sgr_map'].items(): sgr_map[key] = str(val) load_cfg() MSG_REGISTER_REQ = 1 MSG_REGISTER_RES = 2 MSG_DEREGISTER_REQ = 3 MSG_DEREGISTER_RES = 4 MSG_READ_MEM_REQ = 5 MSG_READ_MEM_RES = 6 MSG_WRITE_MEM_REQ = 7 MSG_WRITE_MEM_RES = 8 MSG_CONNECT = 9 MSG_CONNECT_RESPONSE = 10 MSG_DATA = 11 MSG_EOS = 12 MSG_RESET = 13 def wait_for_msg(): header = b'' while len(header) < 9: data = drv.recv(9 - len(header)) if not data: logger.error('Connection to a314d was closed, terminating.') exit(-1) header += data (plen, stream_id, ptype) = struct.unpack('=IIB', header) payload = b'' while len(payload) < plen: data = drv.recv(plen - len(payload)) if not data: logger.error('Connection to a314d was closed, terminating.') exit(-1) payload += data return (stream_id, ptype, payload) def send_register_req(name): m = struct.pack('=IIB', len(name), 0, MSG_REGISTER_REQ) + name drv.sendall(m) def send_read_mem_req(address, length): m = struct.pack('=IIBII', 8, 0, MSG_READ_MEM_REQ, address, length) drv.sendall(m) def read_mem(address, length): send_read_mem_req(address, length) stream_id, ptype, payload = wait_for_msg() if ptype != MSG_READ_MEM_RES: logger.error('Expected MSG_READ_MEM_RES but got %s. Shutting down.', ptype) exit(-1) return payload def send_write_mem_req(address, data): m = struct.pack('=IIBI', 4 + len(data), 0, MSG_WRITE_MEM_REQ, address) + data drv.sendall(m) def write_mem(address, data): send_write_mem_req(address, data) stream_id, ptype, payload = wait_for_msg() if ptype != MSG_WRITE_MEM_RES: logger.error('Expected MSG_WRITE_MEM_RES but got %s. Shutting down.', ptype) exit(-1) def send_connect_response(stream_id, result): m = struct.pack('=IIBB', 1, stream_id, MSG_CONNECT_RESPONSE, result) drv.sendall(m) def send_data(stream_id, data): m = struct.pack('=IIB', len(data), stream_id, MSG_DATA) + data drv.sendall(m) def send_eos(stream_id): m = struct.pack('=IIB', 0, stream_id, MSG_EOS) drv.sendall(m) def send_reset(stream_id): m = struct.pack('=IIB', 0, stream_id, MSG_RESET) drv.sendall(m) sessions = {} class PiCmdSession(object): def __init__(self, stream_id): self.stream_id = stream_id self.pid = 0 self.first_packet = True self.reset_after = None self.rasp_was_esc = False self.rasp_in_cs = False self.rasp_holding = '' self.amiga_in_cs = False self.amiga_holding = '' def process_amiga_ansi(self, data): data = data.decode('latin-1') out = '' for c in data: if not self.amiga_in_cs: if c == '\x9b': self.amiga_in_cs = True self.amiga_holding = '\x1b[' else: out += c else: # self.amiga_in_cs self.amiga_holding += c if c >= chr(0x40) and c <= chr(0x7e): if c == 'r': # Window Bounds Report # ESC[1;1;rows;cols r rows, cols = map(int, self.amiga_holding[6:-2].split(';')) winsize = struct.pack('HHHH', rows, cols, 0, 0) fcntl.ioctl(self.fd, termios.TIOCSWINSZ, winsize) elif c == '|': # Input Event Report # ESC[12;0;0;x;x;x;x;x| # Window resized send_data(self.stream_id, b'\x9b' + b'0 q') else: out += self.amiga_holding self.amiga_holding = '' self.amiga_in_cs = False if len(out) != 0: os.write(self.fd, out.encode('utf-8')) def process_msg_data(self, data): if self.first_packet: if len(data) != 8: send_reset(self.stream_id) del sessions[self.stream_id] else: address, length = struct.unpack('>II', data) buf = read_mem(address, length) ind = 0 rows, cols = struct.unpack('>HH', buf[ind:ind+4]) ind += 4 component_count = buf[ind] ind += 1 components = [] for _ in range(component_count): n = buf[ind] ind += 1 components.append(buf[ind:ind+n].decode('latin-1')) ind += n arg_count = buf[ind] ind += 1 args = [] for _ in range(arg_count): n = buf[ind] ind += 1 args.append(buf[ind:ind+n].decode('latin-1')) ind += n if arg_count == 0: args.append('bash') self.pid, self.fd = pty.fork() if self.pid == 0: for key, val in env_vars.items(): os.putenv(key, val) os.putenv('PATH', search_path) os.putenv('TERM', 'ansi') winsize = struct.pack('HHHH', rows, cols, 0, 0) fcntl.ioctl(sys.stdin, termios.TIOCSWINSZ, winsize) if component_count != 0 and components[0] in volume_paths: path = volume_paths[components[0]] os.chdir(os.path.join(path, *components[1:])) else: os.chdir(os.getenv('HOME', '/')) os.execvp(args[0], args) self.first_packet = False elif self.pid: self.process_amiga_ansi(data) def close(self): if self.pid: os.kill(self.pid, signal.SIGTERM) self.pid = 0 os.close(self.fd) del sessions[self.stream_id] def process_rasp_ansi(self, text): text = text.decode('utf-8') out = '' for c in text: if not self.rasp_in_cs: if not self.rasp_was_esc: if c == '\x1b': self.rasp_was_esc = True else: out += c else: # self.rasp_was_esc if c == '[': self.rasp_was_esc = False self.rasp_in_cs = True self.rasp_holding = '\x1b[' elif c == '\x1b': out += '\x1b' else: out += '\x1b' out += c self.rasp_was_esc = False else: # self.rasp_in_cs self.rasp_holding += c if c >= chr(0x40) and c <= chr(0x7e): if c == 'm': # Select Graphic Rendition # ESC[30;37m attrs = self.rasp_holding[2:-1].split(';') attrs = [sgr_map[a] if a in sgr_map else a for a in attrs] out += '\x1b[' + (';'.join(attrs)) + 'm' else: out += self.rasp_holding self.rasp_holding = '' self.rasp_in_cs = False return out.encode('latin-1', 'replace') def handle_text(self): try: text = os.read(self.fd, 1024) text = self.process_rasp_ansi(text) while len(text) > 0: take = min(len(text), 252) send_data(self.stream_id, text[:take]) text = text[take:] except: #os.close(self.fd) os.kill(self.pid, signal.SIGTERM) self.pid = 0 send_eos(self.stream_id) self.reset_after = time.time() + 10 def handle_timeout(self): if self.reset_after and self.reset_after < time.time(): send_reset(self.stream_id) del sessions[self.stream_id] def fileno(self): return self.fd def process_drv_msg(stream_id, ptype, payload): if ptype == MSG_CONNECT: if payload == b'picmd': s = PiCmdSession(stream_id) sessions[stream_id] = s send_connect_response(stream_id, 0) else: send_connect_response(stream_id, 3) elif stream_id in sessions: s = sessions[stream_id] if ptype == MSG_DATA: s.process_msg_data(payload) elif ptype == MSG_EOS: if s.pid: send_eos(s.stream_id) s.close() elif ptype == MSG_RESET: s.close() done = False try: idx = sys.argv.index('-ondemand') except ValueError: idx = -1 if idx != -1: fd = int(sys.argv[idx + 1]) drv = socket.socket(fileno=fd) else: drv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) drv.connect(('localhost', 7110)) drv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) send_register_req(b'picmd') _, _, payload = wait_for_msg() if payload[0] != 1: logger.error('Unable to register picmd with driver, shutting down') drv.close() done = True rbuf = b'' if not done: logger.info('picmd server is running') while not done: sel_fds = [drv] + [s for s in sessions.values() if s.pid] if idx == -1: sel_fds.append(sys.stdin) rfd, wfd, xfd = select.select(sel_fds, [], [], 5.0) for fd in rfd: if fd == sys.stdin: line = sys.stdin.readline() if not line or line.startswith('quit'): for s in sessions.values(): s.close() drv.close() done = True elif fd == drv: buf = drv.recv(1024) if not buf: for s in sessions.values(): s.close() drv.close() done = True else: rbuf += buf while True: if len(rbuf) < 9: break (plen, stream_id, ptype) = struct.unpack('=IIB', rbuf[:9]) if len(rbuf) < 9 + plen: break rbuf = rbuf[9:] payload = rbuf[:plen] rbuf = rbuf[plen:] process_drv_msg(stream_id, ptype, payload) else: fd.handle_text() for s in sessions.values(): s.handle_timeout()