import fcntl, sys, os, time, logging 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: logger.debug('Could not open PID file. Assuming non existent') except ValueError: logger.debug('''PID file contents broken''') try: os.remove(config.pid_file) logger.debug('''Deleted old PID file, continuing as usual''') except OSError: logger.debug('''Failed deleting old PID file.''') os._exit(1) if pid is not None: try: logger.debug('''Found old PID file. Looking for owner''') os.kill(pid, 0) logger.debug('''Owner exists''') logger.debug('''Failed, another instance of the launcher is running (PID {})'''.format(pid)) os._exit(1) except ProcessLookupError: logger.debug('''Owner does not exist''') try: os.remove(config.pid_file) logger.debug('''Deleted old PID file, continuing as usual''') except OSError: logger.debug('''Failed deleting old PID file.''') os._exit(1) with open(config.pid_file, 'w+') as fp: fp.write('{:d}'.format(os.getpid())) logger.debug('''Created and wrote to PID file''') create_new_fifo(config.fifo_file_status) create_new_fifo(config.fifo_file_executor) def create_new_fifo(fifo_file): """ Create new fifo file, removing old one if it exists """ try: os.remove(fifo_file) logger.debug('''Removed old fifo file''') except OSError: logger.debug('''No old fifo file, good''') try: os.mkfifo(fifo_file) except OSError: logger.error('''Failed, couldn't create fifo file''') os.remove(config.pid_file) # Clean up own PID file sys.exit(0) def handle_exit(signum, frame): logger.info('Signal handler called with signal {}'.format(signum)) 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(): logger.debug('Cleaning up') nice_delete(config.pid_file) nice_delete(config.fifo_file_status) nice_delete(config.fifo_file_executor) nice_term(p_conky_slow) nice_term(p_conky_fast) nice_term(common.p_screen) 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 with open(config.fifo_file_status, 'w') as fifo: p_conky_slow = subprocess.Popen(['conky', '-c', config.path+'conky_slow'], # Use communicate stdout=fifo, stderr=fifo) logger.debug('Started conky slow') p_conky_fast = subprocess.Popen(['conky', '-c', config.path+'conky_fast'], stdout=fifo, stderr=fifo) logger.debug('Started conky fast') i3_ws_obj = wspaces.i3ws(fifo_file=config.fifo_file_status) def parse_status(): global p_lemonbar with open(config.fifo_file_executor, 'w') as fifo_write: p_lemonbar = subprocess.Popen(config.lemonbar_args ,stdin=subprocess.PIPE, stdout=fifo_write, text=True) with open(config.fifo_file_status, 'r', buffering=1) as fifo_read: # Let parser read from fifo logger.debug("FIFO {} opened for reading".format(config.fifo_file_status)) lemonparser.parse_line('WSPINA1___main INA2___web FOC5___terms INA6___stats ') # TODO modular while True: try: data = fifo_read.readline() # Blocking read if len(data) == 0: logger.debug("Writer closed") break psd = lemonparser.parse_line(data) p_lemonbar.stdin.write(lemonparser.parse_line(data) + '\n') p_lemonbar.stdin.flush() #logger.debug('Read: "{0}"'.format(data)) #logger.debug('Parsed "{0}"'.format(psd)) except BrokenPipeError: logger.debug('Broken pipe in parse status thread, exiting') health_logger.info('Broken pipe in parse status thread, exiting') clean_up() except: logger.debug('Unknown exception in parse status thread, exiting') health_logger.info('Unknown exception in parse status thread, exiting') clean_up() def exec_commands(): with open(config.fifo_file_executor, 'r', buffering=1) as fifo_read: logger.debug("FIFO {} opened for reading".format(config.fifo_file_executor)) while True: try: data = fifo_read.readline() 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: logger.debug('Exception occured executing command\n Line in: {}\n Data: {}'.format(line_in, line_in[l:].split())) if len(data) == 0: logger.debug("Lemonbar output closed") break logger.debug('Read: "{0}"'.format(data.strip('\n'))) except BrokenPipeError: logger.debug('Broken pipe in exec commands thread, exiting') health_logger.info('Broken pipe in exec commands thread, exiting') clean_up() except: health_logger.info('Unknown exception in exec commands thread, exiting') raise clean_up() def strip_ansi_unicode(s): # ANSI escape sequences are for colors in terminal and similar ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') strip_ansi = ansi_escape.sub('', s) strip_unicode = (strip_ansi.encode('ascii', 'ignore')).decode('utf-8') return strip_unicode def user_screen(): # Attach to screen session, creating if needed, in UTF-8 mode common.p_screen = subprocess.Popen('screen -D -R -U -S lemonbar' # TODO lemonbar_user ,stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True) for line in common.p_screen.stdout: #try: logger.debug('Screen read line {}'.format(line.strip('\n'))) # with open(config.fifo_file_status, 'w') as fifo: # clean_line = strip_ansi_unicode(line) # if (not clean_line.startswith('[bluetooth]') # and not clean_line.startswith('[CHG]') # and not clean_line.isspace()): # fifo.write('RESP [bluetoothctl] ' + clean_line) #except: # raise 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) health_logger.info('"%s" starting', desc) self.thread.start() # Wrapper around the target def run(target, desc): target() 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) logger = logging.getLogger('Normal logger') logger.setLevel(debuglvl) 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) health_logger = logging.getLogger('Health logger') health_logger.addHandler(handler) # 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) # 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 = write_sys_status, desc='Write sys status thread') i3_thread(target = user_screen, desc='Screen thread for user commands') logger.debug('Threads started') i3_thread.join_threads() logger.debug('Reached end')