diff options
Diffstat (limited to 'rwa/support/sessionservice/service.py')
-rwxr-xr-x | rwa/support/sessionservice/service.py | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/rwa/support/sessionservice/service.py b/rwa/support/sessionservice/service.py new file mode 100755 index 0000000..ae634c8 --- /dev/null +++ b/rwa/support/sessionservice/service.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python3 + +# This file is part of Remote Support Desktop +# https://gitlab.das-netzwerkteam.de/RemoteWebApp/rwa.support.sessionservice +# Copyright 2020, 2021 Jonathan Weth <dev@jonathanweth.de> +# Copyright 2020 Daniel Teichmann <daniel.teichmann@das-netzwerkteam.de> +# Copyright 2020 Mike Gabriel <mike.gabriel@das-netzwerkteam.de> +# SPDX-License-Identifier: GPL-2.0-or-later +# +# 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. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the +# Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +import argparse +import json +import logging +import os +import signal +import tempfile +import time +from threading import Thread +from typing import Union, Any + +import dbus +import dbus.mainloop.glib +import dbus.service +from gi.repository import GLib + +from .lock import is_locked, lock, unlock +from .session import Session +from .trigger import TriggerServerThread + +ALLOW_ONLY_ONE_SESSION = True + + +class RWASupportSessionService(dbus.service.Object): + """D-Bus Session Service for RWA.Support. + + D-Bus namespace: ``org.ArcticaProject.RWASupportSessionService`` + + D-Bus object name: ``/RWASupportSessionService`` + + :param loop: GLib main loop running the service + :param mockup_mode: Starts the service in mock up mode + """ + + def __init__( + self, loop: GLib.MainLoop, mockup_mode: bool = False, one_time: bool = False + ): + self.loop = loop + self.mockup_mode = mockup_mode + self.one_time = one_time + + self.bus = dbus.SessionBus() + name = dbus.service.BusName("org.ArcticaProject.RWASupportSessionService", bus=self.bus) + + self.check_lock_thread = Thread(target=self._check_lock) + self.check_lock_thread.start() + + self.trigger_service = TriggerServerThread(self._trigger) + self.trigger_service.start() + + self.update_service_running = False + self.sessions = {} + super().__init__(name, "/RWASupportSessionService") + + logging.info("D-Bus service has been started.") + + @dbus.service.method("org.ArcticaProject.RWASupportSessionService", out_signature="s") + def start(self) -> str: + """Start a new remote session and register it in RWA.Support.WebApp. + + :return: Result as JSON (D-Bus string) + + **Structure of returned JSON (success):** + + :: + + {"status": "success", "id": <pid>, "url": "<url>", "pin": <pin>} + + **Structure of returned JSON (error):** + + :: + + {"status": "error", "type": "<type>"} + + **Possible choices for error types:** ``multiple``, ``connection`` + """ + if ALLOW_ONLY_ONE_SESSION and len(self.sessions.values()) > 0: + logging.warning( + "There is already one session running and the service is configured to allow only one " + "session, so this session won't be started." + ) + return json.dumps({"status": "error", "type": "multiple"}) + + # Start session + try: + session = Session(self.trigger_service.port, mockup_mode) + + # Add session to sessions list + self.sessions[session.pid] = session + + # Start session update service + self._ensure_update_service() + + return_json = session.client_meta + return_json["status"] = "success" + + logging.info( + f"New session #{session.pid} was started with meta {return_json}." + ) + + return json.dumps(return_json) + except ConnectionError: + pass + + return json.dumps({"status": "error", "type": "connection"}) + + @dbus.service.method("org.ArcticaProject.RWASupportSessionService", in_signature="i", out_signature="s") + def status(self, pid: int) -> str: + """Return the status of a session. + + .. note:: + + This uses the last status version got by the update service in the background. + + :param pid: (Process) ID of session (D-Bus integer) + :return: Session status as JSON (D-Bus string) + + **Structure of returned JSON:** + + :: + + {"id": <pid>, "status": <status>} + + **Possible status options:** + + ============ ====================== + ``running`` The session is running and ready for connecting. + ``active`` The session is running and a the remote connected to the session. + ``stopped`` The session was stopped. + ``dead`` There was a problem, so that the session is dead. + ============ ====================== + """ + return self._get_status(pid) + + @dbus.service.method("org.ArcticaProject.RWASupportSessionService", in_signature="i", out_signature="s") + def refresh_status(self, pid: int) -> str: + """Same as :meth:`status`, but updates status from RWA.WebApp before returning it here. + """ + self._update_session(pid) + return self._get_status(pid) + + @dbus.service.method("org.ArcticaProject.RWASupportSessionService", in_signature="i", out_signature="s") + def stop(self, pid: int) -> str: + """Stop a remote session. + + :param pid: (Process) ID of session (D-Bus integer) + :return: Session status as JSON (D-Bus string) + + **Structure of returned JSON:** + + :: + + {"id": <pid>, "status": "stopped"} + """ + try: + session = self.sessions[pid] + except KeyError: + return json.dumps({"pid": pid, "status": "stopped"}, sort_keys=True) + session.stop() + return json.dumps({"id": pid, "status": "stopped"}, sort_keys=True) + + def _get_status(self, pid: int) -> str: + try: + session = self.sessions[pid] + except KeyError: + return json.dumps({"id": pid, "status": "dead"}, sort_keys=True) + return json.dumps(session.status) + + def _ensure_update_service(self): + """Start session update thread if it isn't already running.""" + if not self.update_service_running: + self.update_thread = Thread(target=self._update_sessions) + self.update_thread.start() + + def _update_session(self, pid: int): + """Update the status of a session.""" + + try: + session = self.sessions[pid] + except KeyError: + logging.info(f"Update status for session #{pid} …") + logging.warning(" Session is dead.") + return + + # Check if VNC process is still running + running = session.vnc_process_running + if running: + pass + elif session.status_text == "stopped" and session.pid in self.sessions: + logging.info(f"Update status for session #{pid} …") + logging.warning(" Session is dead.") + + del self.sessions[session.pid] + else: + logging.info(f"Update status for session #{pid} …") + logging.warning(" VNC was stopped, so session is dead.") + + session.stop() + del self.sessions[session.pid] + + def _update_sessions(self): + """Go through all running sessions and update their status using ``_update_session``.""" + logging.info("Started update service for sessions.") + while len(self.sessions.values()) > 0: + for session in list(self.sessions.values()): + self._update_session(session.pid) + + time.sleep(2) + + self.update_service_running = False + logging.info("Stopped update service for sessions.") + if self.one_time: + self._stop_all() + + def _trigger(self, session_id: int, data: dict, method: str = "trigger") -> Union[dict, bool]: + """Trigger a specific session via trigger token.""" + logging.info(f"Triggered with session ID {session_id} and {data}") + + for session in self.sessions.values(): + if session.session_id == session_id: + r = session.trigger(data, method) + logging.info(f"Session #{session.pid} matches the ID: {r}") + return r + + logging.warning(" No matching session found for this ID.") + return False + + def _stop_all(self): + """Stop all sessions and this daemon.""" + logging.info("Stop all sessions and exit service.") + for session in list(self.sessions.values()): + session.stop() + del self.sessions[session.pid] + self.trigger_service.shutdown() + self.loop.quit() + + def _check_lock(self): + """Check if lock file exists.""" + while True: + if not is_locked(): + logging.error("The lock file was removed, so stop this service.") + self._stop_all() + break + time.sleep(1) + + +def str2bool(v: Union[str, bool, int]) -> bool: + """Return true or false if the given string can be interpreted as a boolean otherwise raise an exception.""" + if isinstance(v, bool): + return v + if v.lower() in ("yes", "true", "t", "y", "1", 1): + return True + elif v.lower() in ("no", "false", "f", "n", "0", 0): + return False + else: + raise argparse.ArgumentTypeError("Boolean value expected.") + +def main(): + # Check for lock file + if is_locked(): + logging.error("The service is already running.") + exit(1) + + # Create lock file + lock() + + parser = argparse.ArgumentParser(description="D-Bus Session Service for RWA.Support") + parser.add_argument( + "-m", + "--mockup-mode", + type=str2bool, + nargs="?", + const=True, + default=False, + help="Activates mock up mode. Acts like the real session service but don't do changes or call RWA.", + ) + parser.add_argument( + "-o", + "--one-time", + type=str2bool, + nargs="?", + const=True, + default=False, + help="Runs as one-time-service. Stops after one session.", + ) + + args = parser.parse_args() + mockup_mode = args.mockup_mode + one_time = args.one_time + + if mockup_mode: + logging.warning( + "All API responses are faked and should NOT BE USED IN PRODUCTION!" + ) + + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + + loop = GLib.MainLoop() + object = RWASupportSessionService(loop, mockup_mode, one_time) + + def signal_handler(sig, frame): + logging.info("Service was terminated.") + object._stop_all() + + signal.signal(signal.SIGINT, signal_handler) + + loop.run() + + logging.info("Remove lock file ...") + unlock() + + +if __name__ == "__main__": + main()
\ No newline at end of file |