diff --git a/tools/tuhi-live.py b/tools/tuhi-live.py new file mode 100644 index 0000000..e3a95b5 --- /dev/null +++ b/tools/tuhi-live.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# + +import argparse +import logging +import os +import pwd +import sys +import multiprocessing +from multiprocessing import reduction + +manager = None +logger = None + + +def open_uhid_process(queue_in, conn_out): + while True: + try: + pid = queue_in.get() + except KeyboardInterrupt: + return 0 + else: + fd = os.open('/dev/uhid', os.O_RDWR) + reduction.send_handle(conn_out, fd, pid) + + +def maybe_start_tuhi(queue): + sys.path + + try: + should_start, verbose = queue.get() + except KeyboardInterrupt: + return 0 + + if not should_start: + return + + sys.path.append(os.getcwd()) + + import tuhi.base + if verbose: + tuhi.base.logger.setLevel(logging.DEBUG) + t = tuhi.base.Tuhi() + while True: + try: + t.run() + except KeyboardInterrupt: + pass + + +def start_tuhi_server(args): + queue = multiprocessing.Queue() + + tuhi_process = multiprocessing.Process(target=maybe_start_tuhi, args=(queue,)) + tuhi_process.daemon = True + tuhi_process.start() + + sys.path.append(os.path.join(os.getcwd(), 'tools')) + + # import after spawning the process, or the 2 processes will fight for GLib + import kete + from gi.repository import Gio, GLib + + global logger + logger = logging.getLogger('tuhi-live') + logger.addHandler(kete.logger_handler) + logger.setLevel(logging.INFO) + + logger.debug('connecting to the bus') + + # connect to the session + try: + connection = Gio.bus_get_sync(Gio.BusType.SESSION, None) + except GLib.Error as e: + if (e.domain == 'g-io-error-quark' and + e.code == Gio.IOErrorEnum.DBUS_ERROR): + raise kete.DBusError(e.message) + else: + raise e + + logger.debug('looking for tuhi on the bus') + # attempt to connect to tuhi + try: + proxy = Gio.DBusProxy.new_sync(connection, + Gio.DBusProxyFlags.NONE, None, + kete.TUHI_DBUS_NAME, + kete.ROOT_PATH, + kete.ORG_FREEDESKTOP_TUHI1_MANAGER, + None) + except GLib.Error as e: + if (e.domain == 'g-io-error-quark' and + e.code == Gio.IOErrorEnum.DBUS_ERROR): + raise kete.DBusError(e.message) + else: + raise e + + started = proxy.get_name_owner() is not None + + if not started: + print(f'No-one is handling {kete.TUHI_DBUS_NAME}, attempting to start a daemon') + + queue.put((not started, args.verbose)) + + +def run_live(request_fd_queue, conn_fd): + import kete + from gi.repository import Gio, GLib + + def on_name_appeared(connection, name, client): + global manager + logger.info('Connected to the Tuhi daemon') + manager = kete.TuhiKeteManager() + + for device in manager.devices: + logger.info(f'starting live on {device}, please press button on the device') + request_fd_queue.put(os.getpid()) + fd = reduction.recv_handle(conn_fd) + device.start_live(fd) + + Gio.bus_watch_name(Gio.BusType.SESSION, + kete.TUHI_DBUS_NAME, + Gio.BusNameWatcherFlags.NONE, + on_name_appeared, + None) + + mainloop = GLib.MainLoop() + + connected_devices = 0 + + def on_disconnect(dev, pspec): + mainloop.quit() + + wait_for_disconnect = False + + try: + mainloop.run() + except KeyboardInterrupt: + pass + finally: + for device in manager.devices: + if device.live: + connected_devices += 1 + + for device in manager.devices: + if device.live and device.connected: + logger.info(f'stopping live on {device}') + device.connect('notify::connected', on_disconnect) + device.stop_live() + wait_for_disconnect = True + + # we re-run the mainloop to terminate the connections + if wait_for_disconnect: + try: + mainloop.run() + except KeyboardInterrupt: + pass + + +def drop_privileges(): + sys.stderr.write('dropping privileges\n') + + os.setgroups([]) + gid = int(os.getenv('SUDO_GID')) + uid = int(os.getenv('SUDO_UID')) + pwname = os.getenv('SUDO_USER') + os.setresgid(gid, gid, gid) + os.initgroups(pwname, gid) + os.setresuid(uid, uid, uid) + + pw = pwd.getpwuid(uid) + + # we completely clear the environment and start a new and controlled one + os.environ.clear() + os.environ['XDG_RUNTIME_DIR'] = f'/run/user/{uid}' + os.environ['HOME'] = pw.pw_dir + + +def parse(args): + desc = 'tool to start the live mode on all devices tuhi knows about' + parser = argparse.ArgumentParser(description=desc) + parser.add_argument('-v', '--verbose', + help='Show some debugging informations', + action='store_true', + default=False) + + return parser.parse_args(args[1:]) + + +def main(args=sys.argv): + if not os.geteuid() == 0: + sys.exit('Script must be run as root') + + args = parse(args) + + request_fd_queue = multiprocessing.Queue() + conn_in, conn_out = multiprocessing.Pipe() + + fd_process = multiprocessing.Process(target=open_uhid_process, args=(request_fd_queue, conn_out)) + fd_process.daemon = True + fd_process.start() + + drop_privileges() + + start_tuhi_server(args) + run_live(request_fd_queue, conn_in) + + +if __name__ == '__main__': + main(sys.argv)