import fcntl, sys, os, time, logging import queue import signal, atexit import subprocess import contextlib from threading import Thread import argparse import re # regexp import i3_lemonbar_config as config import i3_lemonbar_common as common import i3_lemonbar_parser as lemonparser import i3_workspaces as wspaces p_conky_slow = None p_conky_fast = None p_lemonbar = None i3_ws_obj = None 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') common.screen.destroy() nice_delete(config.pid_file) nice_delete(config.fifo_file_status) nice_term(p_conky_slow) nice_term(p_conky_fast) if i3_ws_obj is not None: i3_ws_obj.quit() sys.exit(0) def write_sys_status(): global p_conky_slow, p_conky_fast, i3_ws_obj # Buffering = 1 to write entire lines with open(config.fifo_file_status, 'w', buffering=1) as fifo: p_conky_slow = subprocess.Popen(['conky', '-c', config.path+'conky_slow'], # Use communicate stdout=fifo, stderr=fifo) common.logger.debug('Started conky slow') p_conky_fast = subprocess.Popen(['conky', '-c', config.path+'conky_fast'], stdout=fifo, stderr=fifo) common.logger.debug('Started conky fast') i3_ws_obj = wspaces.i3ws(fifo_file=config.fifo_file_status) i3_ws_obj.work() def queue_parse_job(job): common.parsing_queue.put(job) def put_fifo_in_queue(): 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 queue_parse_job(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: common.logger.debug('Unknown exception in parse status thread, exiting') common.health_logger.info('Unknown exception in parse status thread, exiting') clean_up() def parse_status(): global p_lemonbar p_lemonbar = subprocess.Popen(config.lemonbar_args, stdin=subprocess.PIPE , stdout=subprocess.PIPE, text=True) lemonparser.parse_line('WSPINA1___main INA2___web FOC5___terms INA6___stats ') # TODO modular while True: data = common.parsing_queue.get() # Blocking read if data is None: common.logger.debug('Queue closed') common.health_logger.info('Queue closed') break psd = lemonparser.parse_line(data) p_lemonbar.stdin.write(psd + '\n') p_lemonbar.stdin.flush() #common.logger.debug('Read: "{0}"'.format(data)) #common.logger.debug('Parsed "{0}"'.format(psd)) def exec_commands(): global p_lemonbar # Wait until up TODO better solution while True: if p_lemonbar is not None: break while True: data = 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: for key,func in common.commands_dict.items(): l = len(key) if data[:l] == key: func(data) break except: common.logger.debug('Exception occured executing command\n Line in: {}\n Data: {}'.format(line_in, line_in[l:].split())) if len(data) == 0: common.logger.debug("Lemonbar output closed") break common.logger.debug('Read: "{0}"'.format(data.strip('\n'))) ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') def strip_ansi_unicode(s): # ANSI escape sequences are for colors in terminal and similar strip_ansi = ansi_escape.sub('', s) strip_unicode = (strip_ansi.encode('ascii', 'ignore')).decode('utf-8') return strip_unicode def user_screen(): # Create screen session, in UTF-8 mode, logging to fifo common.screen = common.LemonbarScreen() with open(config.fifo_screen_log, 'r', buffering=1) as screen_read: empty_count = 0 while empty_count < 3: try: line = screen_read.readline().strip('\n') line = strip_ansi_unicode(line) common.logger.debug('Screen read line {}'.format(line)) if (not line.startswith('[CHG]') and not line.isspace()): #common.logger.debug('put in queue {}'.format(line)) queue_parse_job('RESP {}\n'.format(line)) if not line: # End loop if many empty lines in a row empty_count = empty_count + 1 else: empty_count = 0 except: raise common.screen.destroy() nice_delete(config.fifo_screen_log) class i3_thread: """ Helper class to start and stop threads""" all_threads = [] # Static, contains all started threads def __init__(self, target, desc): self.thread = Thread(target = self.__class__.run, args=(target,desc)) self.desc = desc self.all_threads.append(self) common.health_logger.info('"%s" starting', desc) self.thread.start() # Wrapper around the target def run(target, desc): target() common.health_logger.info('"%s" reached end', desc) def join_threads(): for i3_th in i3_thread.all_threads: i3_th.thread.join() if __name__ == "__main__": parser = argparse.ArgumentParser(description='Blah blah blah.') 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) # common.logger.basicConfig(stream=sys.stdout, level=debuglvl) assert_only_instance() # Creates pid file and fifo file atexit.register(clean_up) signal.signal(signal.SIGTERM, handle_exit) signal.signal(signal.SIGINT, handle_exit) common.parsing_queue = queue.Queue() # Start writing and reading threads # Create readers before writers i3_thread(target = exec_commands, desc='Exec commands thread') i3_thread(target = parse_status, desc='Parse status thread') i3_thread(target = put_fifo_in_queue, desc='') i3_thread(target = write_sys_status, desc='Write sys status thread') i3_thread(target = user_screen, desc='Screen thread for user commands') common.logger.debug('Threads started') i3_thread.join_threads() common.logger.debug('Reached end')