# Licensed under a 3-clause BSD style license - see LICENSE.rst # TODO: this file should be refactored to use a more thread-safe and # race-condition-safe lockfile mechanism. import datetime import os import stat import warnings import xmlrpc.client as xmlrpc from contextlib import suppress from pathlib import Path from urllib.parse import urlparse from astropy import log from astropy.utils.data import get_readable_fileobj from .errors import SAMPHubError, SAMPWarning def read_lockfile(lockfilename): """ Read in the lockfile given by ``lockfilename`` into a dictionary. """ # lockfilename may be a local file or a remote URL, but # get_readable_fileobj takes care of this. lockfiledict = {} with get_readable_fileobj(lockfilename) as f: for line in f: if not line.startswith("#"): kw, val = line.split("=") lockfiledict[kw.strip()] = val.strip() return lockfiledict def write_lockfile(lockfilename, lockfiledict): lockfile = open(lockfilename, "w") lockfile.close() os.chmod(lockfilename, stat.S_IREAD + stat.S_IWRITE) lockfile = open(lockfilename, "w") now_iso = datetime.datetime.now().isoformat() lockfile.write(f"# SAMP lockfile written on {now_iso}\n") lockfile.write("# Standard Profile required keys\n") for key, value in lockfiledict.items(): lockfile.write(f"{key}={value}\n") lockfile.close() def create_lock_file(lockfilename=None, mode=None, hub_id=None, hub_params=None): # Remove lock-files of dead hubs remove_garbage_lock_files() lockfiledir = "" # CHECK FOR SAMP_HUB ENVIRONMENT VARIABLE if "SAMP_HUB" in os.environ: # For the time being I assume just the std profile supported. if os.environ["SAMP_HUB"].startswith("std-lockurl:"): lockfilename = os.environ["SAMP_HUB"][len("std-lockurl:") :] lockfile_parsed = urlparse(lockfilename) if lockfile_parsed[0] != "file": warnings.warn( f"Unable to start a Hub with lockfile {lockfilename}. " "Start-up process aborted.", SAMPWarning, ) return False else: lockfilename = lockfile_parsed[2] else: # If it is a fresh Hub instance if lockfilename is None: log.debug("Running mode: " + mode) if mode == "single": lockfilename = os.path.join(Path.home(), ".samp") else: lockfiledir = os.path.join(Path.home(), ".samp-1") # If missing create .samp-1 directory try: os.mkdir(lockfiledir) except OSError: pass # directory already exists finally: os.chmod(lockfiledir, stat.S_IREAD + stat.S_IWRITE + stat.S_IEXEC) lockfilename = os.path.join(lockfiledir, f"samp-hub-{hub_id}") else: log.debug("Running mode: multiple") hub_is_running, lockfiledict = check_running_hub(lockfilename) if hub_is_running: warnings.warn( "Another SAMP Hub is already running. Start-up process aborted.", SAMPWarning, ) return False log.debug("Lock-file: " + lockfilename) write_lockfile(lockfilename, hub_params) return lockfilename def get_main_running_hub(): """ Get either the hub given by the environment variable SAMP_HUB, or the one given by the lockfile .samp in the user home directory. """ hubs = get_running_hubs() if not hubs: raise SAMPHubError("Unable to find a running SAMP Hub.") # CHECK FOR SAMP_HUB ENVIRONMENT VARIABLE if "SAMP_HUB" in os.environ: # For the time being I assume just the std profile supported. if os.environ["SAMP_HUB"].startswith("std-lockurl:"): lockfilename = os.environ["SAMP_HUB"][len("std-lockurl:") :] else: raise SAMPHubError("SAMP Hub profile not supported.") else: lockfilename = os.path.join(Path.home(), ".samp") return hubs[lockfilename] def get_running_hubs(): """ Return a dictionary containing the lock-file contents of all the currently running hubs (single and/or multiple mode). The dictionary format is: ``{: {: , ...}, ...}`` where ``{}`` is the lock-file name, ``{}`` and ``{}`` are the lock-file tokens (name and content). Returns ------- running_hubs : dict Lock-file contents of all the currently running hubs. """ hubs = {} lockfilename = "" # HUB SINGLE INSTANCE MODE # CHECK FOR SAMP_HUB ENVIRONMENT VARIABLE if "SAMP_HUB" in os.environ: # For the time being I assume just the std profile supported. if os.environ["SAMP_HUB"].startswith("std-lockurl:"): lockfilename = os.environ["SAMP_HUB"][len("std-lockurl:") :] else: lockfilename = os.path.join(Path.home(), ".samp") hub_is_running, lockfiledict = check_running_hub(lockfilename) if hub_is_running: hubs[lockfilename] = lockfiledict # HUB MULTIPLE INSTANCE MODE lockfiledir = "" lockfiledir = os.path.join(Path.home(), ".samp-1") if os.path.isdir(lockfiledir): for filename in os.listdir(lockfiledir): if filename.startswith("samp-hub"): lockfilename = os.path.join(lockfiledir, filename) hub_is_running, lockfiledict = check_running_hub(lockfilename) if hub_is_running: hubs[lockfilename] = lockfiledict return hubs def check_running_hub(lockfilename): """ Test whether a hub identified by ``lockfilename`` is running or not. Parameters ---------- lockfilename : str Lock-file name (path + file name) of the Hub to be tested. Returns ------- is_running : bool Whether the hub is running hub_params : dict If the hub is running this contains the parameters from the lockfile """ is_running = False lockfiledict = {} # Check whether a lockfile already exists try: lockfiledict = read_lockfile(lockfilename) except OSError: return is_running, lockfiledict if "samp.hub.xmlrpc.url" in lockfiledict: try: proxy = xmlrpc.ServerProxy( lockfiledict["samp.hub.xmlrpc.url"].replace("\\", ""), allow_none=1 ) proxy.samp.hub.ping() is_running = True except xmlrpc.ProtocolError: # There is a protocol error (e.g. for authentication required), # but the server is alive is_running = True except OSError: pass return is_running, lockfiledict def remove_garbage_lock_files(): lockfilename = "" # HUB SINGLE INSTANCE MODE lockfilename = os.path.join(Path.home(), ".samp") hub_is_running, lockfiledict = check_running_hub(lockfilename) if not hub_is_running: # If lockfilename belongs to a dead hub, then it is deleted if os.path.isfile(lockfilename): with suppress(OSError): os.remove(lockfilename) # HUB MULTIPLE INSTANCE MODE lockfiledir = os.path.join(Path.home(), ".samp-1") if os.path.isdir(lockfiledir): for filename in os.listdir(lockfiledir): if filename.startswith("samp-hub"): lockfilename = os.path.join(lockfiledir, filename) hub_is_running, lockfiledict = check_running_hub(lockfilename) if not hub_is_running: # If lockfilename belongs to a dead hub, then it is deleted if os.path.isfile(lockfilename): with suppress(OSError): os.remove(lockfilename)