import fcntl, sys, os, time, logging import queue import signal, atexit import subprocess import contextlib from threading import Thread import argparse import i3_lemonbar_config as config import i3_lemonbar_common as common import i3_lemonbar_modules as modules import i3_lemonbar_parser as lemonparser def assert_only_instance(): """ If PID file exists: Look for process with given PID If found: Exit program If not found: Delete PID file and continue Look for fifo file If exists: Delete it """ try: pid = None with open(config.pid_file, 'r') as fp: pid = int(fp.read()) except IOError: common.logger.debug('Could not open PID file. Assuming non existent') except ValueError: common.logger.debug('''PID file contents broken''') try: os.remove(config.pid_file) common.logger.debug('''Deleted old PID file, continuing as usual''') except OSError: common.logger.debug('''Failed deleting old PID file.''') os._exit(1) if pid is not None: try: common.logger.debug('''Found old PID file. Looking for owner''') os.kill(pid, 0) common.logger.debug('''Owner exists''') common.logger.debug('''Failed, another instance of the launcher is running (PID {})'''.format(pid)) os._exit(1) except ProcessLookupError: common.logger.debug('''Owner does not exist''') try: os.remove(config.pid_file) common.logger.debug('''Deleted old PID file, continuing as usual''') except OSError: common.logger.debug('''Failed deleting old PID file.''') os._exit(1) with open(config.pid_file, 'w+') as fp: fp.write('{:d}'.format(os.getpid())) common.logger.debug('''Created and wrote to PID file''') common.create_new_fifo(config.fifo_file_status) def handle_exit(signum, frame): common.logger.info('Signal handler called with signal {}'.format(signum)) common.logger.info('Calling os._exit(0)') os._exit(0) # Terminates process p nicely def nice_term(p): if p is not None: p.terminate() def nice_delete(f): with contextlib.suppress(FileNotFoundError): os.remove(f) def clean_up(): common.logger.debug('Cleaning up') nice_delete(config.pid_file) nice_delete(config.fifo_file_status) modules.stop_all() os._exit(0) def keep_fifo_open(): with open(config.fifo_file_status, 'w', buffering=1) as fifo_write: while True: fifo_write.write('HEARTBEAT\n') time.sleep(30) class LemonbarWrapper: def __init__(self, only_instance = True): assert only_instance, 'Multiple instances not supported' # TODO assert_only_instance() # Creates pid file and fifo file self.buffer_in = common.Shelf() self.all_threads = [] # Start writing and reading threads # Create readers before writers self.p_lemonbar = subprocess.Popen(config.lemonbar_args, stdin=subprocess.PIPE , stdout=subprocess.PIPE, text=True) self.start_thread(target = self.exec_commands, desc='Execute commands thread') self.start_thread(target = self.write_parsed, desc='Write parsed status thread') self.start_thread(target = keep_fifo_open, desc='Keep fifo open thread') self.start_thread(target = self.put_fifo_in_queue, desc='Put fifo entries in queue thread') modules.start_all(self) common.logger.debug('Threads started') # Helpers to start threads def start_thread(self, target, desc): # Wrapper around arbitrary target def run_thread(target, desc): target() common.health_logger.info('"%s" reached end', desc) thread = Thread(target = run_thread, args=(target, desc)) self.all_threads.append(thread) common.health_logger.info('"%s" starting', desc) thread.start() def join(self): for thread in self.all_threads: thread.join() # Thread targets def write_parsed(self): # Write parse entries in queue to lemonbar while True: data = self.buffer_in.get() # Blocking read self.p_lemonbar.stdin.write(data + '\n') self.p_lemonbar.stdin.flush() #common.logger.debug('Read: "{0}"'.format(data)) #common.logger.debug('Parsed "{0}"'.format(psd)) def exec_commands(self): while True: data = self.p_lemonbar.stdout.readline() if not data: common.logger.debug('Lemonbar closed, exiting') common.health_logger.info('Lemonbar closed, exiting') clean_up() break common.logger.debug('Trying reading: "{0}"'.format(data.strip('\n'))) try: modules.do_action(data) except Exception as e: common.logger.debug('Exception occured executing command\n Line in: {}'.format(data)) common.logger.debug(str(e)) if len(data) == 0: common.logger.debug("Lemonbar output closed") break common.logger.debug('Read: "{0}"'.format(data.strip('\n'))) def put_fifo_in_queue(self): with open(config.fifo_file_status, 'r', buffering=1) as fifo_read: # Let parser read from fifo common.logger.debug("FIFO {} opened for reading".format(config.fifo_file_status)) while True: try: data = fifo_read.readline() # Blocking read if len(data) == 0: common.logger.debug("Writer closed") break data = data.rstrip() # Remove trailing newlines self.buffer_in.put(lemonparser.parse_line(data)) except BrokenPipeError: common.logger.debug('Broken pipe in parse status thread, exiting') common.health_logger.info('Broken pipe in parse status thread, exiting') clean_up() except Exception as e: common.logger.debug('Unknown exception in parse status thread, exiting') common.health_logger.info('Unknown exception in parse status thread, exiting') raise common.logger.debug(str(e)) clean_up() if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('--debug', action='store_true') args = parser.parse_args() if(args.debug): debuglvl = logging.DEBUG else: debuglvl = logging.INFO # Setup logger to stdout formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') handler = logging.StreamHandler(sys.stdout) handler.setFormatter(formatter) common.logger = logging.getLogger('Normal logger') common.logger.setLevel(debuglvl) common.logger.addHandler(handler) # Setup health logger to file in tmp formatter = logging.Formatter('%(asctime)s %(message)s') handler = logging.FileHandler(config.health_file) handler.setFormatter(formatter) common.health_logger = logging.getLogger('Health logger') common.health_logger.addHandler(handler) # Ensure clean up on exit atexit.register(clean_up) signal.signal(signal.SIGTERM, handle_exit) signal.signal(signal.SIGINT, handle_exit) # Start the lemonbar wrapper LemonbarWrapper().join() common.logger.debug('Reached end')