diff --git a/pywalfox/__main__.py b/pywalfox/__main__.py index 34ef376..5f0e5f8 100755 --- a/pywalfox/__main__.py +++ b/pywalfox/__main__.py @@ -1,5 +1,6 @@ import os import sys +import atexit import logging import argparse import subprocess @@ -14,10 +15,29 @@ from .channel.unix.client import Client parser = argparse.ArgumentParser(description='Pywalfox - Native messaging host') -parser.add_argument('action', nargs='?', default=None, help='available options are setup, update, daemon, log and uninstall') -parser.add_argument('--verbose', dest='verbose', action='store_true', help='runs the daemon in verbose mode with debugging output') -parser.add_argument('-p', '--print', dest='print_mode', action='store_true', help='prints the debugging output instead of writing to logfile') -parser.add_argument('-v', '--version', dest='version', action='store_true', help='displays the current version of the daemon') +setup_group = parser.add_argument_group('install/uninstall') +start_group = parser.add_argument_group('start') +parser.add_argument('action', + nargs='?', + default=None, + metavar='ACTION', + help='available actions are install, start, update, log and uninstall') +parser.add_argument('-v', '--version', + dest='version', + action='store_true', + help='displays the current version of the daemon') +start_group.add_argument('-p', '--print', + dest='print_mode', + action='store_true', + help='writes debugging output from the native messaging host to stdout') +start_group.add_argument('--verbose', + dest='verbose', + action='store_true', + help='runs the native messaging host in verbose mode') +setup_group.add_argument('-g', '--global', + dest='global_install', + action='store_true', + help='installs/uninstalls the native host manifest globally') def get_python_version(): """Gets the current python version and checks if it is supported.""" @@ -34,10 +54,11 @@ def get_python_version(): def send_update_action(): """Sends the update command to the socket server.""" client = Client() - connected = client.start() - if connected is True: - client.send_message('update') + for host in client.hosts: + connected = client.connect(host) + if connected is True: + client.send_message('update') def open_log_file(): """Opens the daemon log file in an editor.""" @@ -57,11 +78,9 @@ def print_version(): def run_daemon(): """Starts the daemon.""" - python_version = get_python_version() - - daemon = Daemon(python_version.major) + daemon = Daemon(get_python_version().major) + atexit.register(daemon.close) daemon.start() - daemon.close() def handle_args(args): """Handles CLI arguments.""" @@ -73,23 +92,22 @@ def handle_args(args): send_update_action() sys.exit(0) - if args.action == 'daemon': + if args.action == 'start': setup_logging(args.verbose, args.print_mode) run_daemon() - sys.exit(0) if args.action == 'log': open_log_file() sys.exit(0) - if args.action == 'setup': + if args.action == 'install': from pywalfox.install import start_setup - start_setup(True) + start_setup(args.global_install) sys.exit(0) if args.action == 'uninstall': from pywalfox.install import start_uninstall - start_uninstall(True) + start_uninstall(args.global_install) sys.exit(0) # If no action was specified diff --git a/pywalfox/assets/css/userChrome.css b/pywalfox/assets/css/userChrome.css index ddaec0d..69998ee 100644 --- a/pywalfox/assets/css/userChrome.css +++ b/pywalfox/assets/css/userChrome.css @@ -2,9 +2,10 @@ #main-window { --pywalfox-font-size: 13px; --pywalfox-font-size-sm: calc(var(--pywalfox-font-size) * 0.9); - --pywalfox-background: var(--lwt-toolbarbutton-active-background); + --pywalfox-background: var(--lwt-accent-color); --pywalfox-background-light: var(--arrowpanel-background); --pywalfox-text: var(--arrowpanel-color); + --pywalfox-text-focus: var(--toolbar-color); --pywalfox-unselected-tab-opacity: 0.8; --pywalfox-darker-background: rgba(0, 0, 0, 0.4); --pywalfox-padding: 4px 8px; @@ -24,7 +25,8 @@ button, search-textbox, menuseparator { /* Background color on hover in right-click context menus */ menu[_moz-menuactive="true"], menuitem[_moz-menuactive="true"] { -moz-appearance: none !important; - background-color: rgba(249, 249, 250, 0.1) !important; + background-color: var(--pywalfox-background) !important; + color: var(--pywalfox-text-focus) !important; padding: 4px 4px !important; } @@ -69,5 +71,16 @@ textbox, panelview, .tabbrowser-tab, #sidebar-header, panelmultiview { font-size: var(--pywalfox-font-size-sm) !important; } -/********************* END PYWALFOX CUSTOM CSS *********************/ +/* Change the grey background color seen e.g. when opening a bookmark in a newtab */ +#tabbrowser-tabpanels { + background-color: var(--pywalfox-background) !important; +} + +/* Theme the status panel at the bottom */ +#statuspanel-label { + background: var(--pywalfox-background-light) !important; + border-color: var(--pywalfox-background) !important; + color: var(--pywalfox-text) !important; +} +/********************* END PYWALFOX CUSTOM CSS *********************/ diff --git a/pywalfox/bin/main.sh b/pywalfox/bin/main.sh index 5fc21de..a5e4036 100755 --- a/pywalfox/bin/main.sh +++ b/pywalfox/bin/main.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -pywalfox daemon +python -m pywalfox start || python3 -m pywalfox start || python2.7 -m pywalfox start diff --git a/pywalfox/bin/win.bat b/pywalfox/bin/win.bat index 023c049..83ce038 100644 --- a/pywalfox/bin/win.bat +++ b/pywalfox/bin/win.bat @@ -1,3 +1,3 @@ @echo off -pywalfox daemon +python -m pywalfox start || python3 -m pywalfox start || python2.7 -m pywalfox start diff --git a/pywalfox/channel/connector.py b/pywalfox/channel/connector.py index 8e9ad2b..67fc6a1 100644 --- a/pywalfox/channel/connector.py +++ b/pywalfox/channel/connector.py @@ -1,7 +1,7 @@ +import os import socket import logging -from ..config import UNIX_SOCKET_PATH, WIN_SOCKET_HOST - +from ..config import UNIX_SOCKET_PATH, WIN_SOCKET_HOST, UNIX_SOCKET_PATH_ALT, WIN_SOCKET_HOST_ALT class Connector: """ @@ -10,17 +10,61 @@ class Connector: since UNIX-sockets are not properly supported on Windows. :param platform_id str: the current platform identifier, e.g. win32 + :param validate_host bool: check if the socket host is available before binding """ - def __init__(self, platform_id): + def __init__(self, platform_id, validate_host=True): if platform_id == 'win32': - self.host = WIN_SOCKET_HOST + if validate_host is True: + self.host = self.get_win_socket_host() + + self.hosts = [WIN_SOCKET_HOST, WIN_SOCKET_HOST_ALT] self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) logging.debug('Setup socket server using AF_INET (win32)') else: - self.host = UNIX_SOCKET_PATH + if validate_host is True: + self.host = self.get_unix_socket_path() + + self.hosts = [UNIX_SOCKET_PATH, UNIX_SOCKET_PATH_ALT] self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) logging.debug('Setup socket server using AF_UNIX (linux/darwin)') + def get_unix_socket_path(self): + """ + Get an available path to bind the UNIX-socket to. + + :return: the path to be used when binding the UNIX-socket + :rType: str + """ + if os.path.exists(UNIX_SOCKET_PATH): + logging.debug('Default UNIX-socket is already in use') + return UNIX_SOCKET_PATH_ALT + + return UNIX_SOCKET_PATH + + def get_win_socket_host(self): + """ + Get an available host and port to bind the UDP-socket to. + + :return: the host and port to be used when binding the UDP-socket + :rType: (host, port) + """ + is_valid = True + test_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + test_socket.bind(WIN_SOCKET_HOST) + test_socket.close() + except OSError as e: + is_valid = False + if e.errno == 98: # errno 98 means that address is already bound + logging.debug('Default UDP-socket host is already in use') + else: + logging.error('Failed to test UDP-socket host availability: %s' % str(e)) + + if is_valid is True: + return WIN_SOCKET_HOST + + return WIN_SOCKET_HOST_ALT + def encode_message(self, message): """ Encodes a message to be sent using the socket. diff --git a/pywalfox/channel/unix/client.py b/pywalfox/channel/unix/client.py index 1b81ece..802c8b5 100644 --- a/pywalfox/channel/unix/client.py +++ b/pywalfox/channel/unix/client.py @@ -6,23 +6,23 @@ class Client(Connector): """UNIX-socket client used to communicate with the daemon.""" def __init__(self): - Connector.__init__(self, 'unix') + Connector.__init__(self, 'unix', False) - def start(self): + def connect(self, host): """ Connects to the UNIX-socket if it exists. :return: if the connection to the socket was successfull :rType: bool """ - if os.path.exists(self.host): + if os.path.exists(host): try: - self.socket.connect(self.host) - logging.debug('Successfully connected to UNIX socket at: %s' % self.host) + self.socket.connect(host) + logging.debug('Successfully connected to UNIX socket at: %s' % host) return True except OSError as e: logging.error('Failed to connect to socket: %s' % e.strerror) else: - logging.error('Could not find socket: %s' % self.host) + logging.debug('Could not find socket: %s' % host) return False diff --git a/pywalfox/channel/unix/server.py b/pywalfox/channel/unix/server.py index 758c2dd..219e1c4 100644 --- a/pywalfox/channel/unix/server.py +++ b/pywalfox/channel/unix/server.py @@ -33,4 +33,14 @@ def start(self): def close(self): """Unbinds the socket and deletes the file.""" self.socket.close() - os.remove(self.host) + + try: + """ + UNIX-sockets can be overwritten by other processes even if another process + is already using it. This may lead to the file not existing and will + cause a crash if not handled properly. + """ + os.remove(self.host) + logging.debug('UNIX-socket deleted') + except OSError as e: + logging.debug('UNIX-socket has already been deleted, skipping') diff --git a/pywalfox/channel/win/client.py b/pywalfox/channel/win/client.py index 5c068f8..6fe673a 100644 --- a/pywalfox/channel/win/client.py +++ b/pywalfox/channel/win/client.py @@ -5,9 +5,9 @@ class Client(Connector): """UDP-socket client used to communicate with the daemon.""" def __init__(self): - Connector.__init__(self, 'win32') + Connector.__init__(self, 'win32', False) - def start(self): + def connect(self, host): """ Connects to the UDP socket. @@ -15,10 +15,10 @@ def start(self): :rType: bool """ try: - self.socket.connect(self.host) - logging.debug('Successfully connected to UDP socket at: %s:%s' % (self.host[0], self.host[1])) + self.socket.connect(host) + logging.debug('Successfully connected to UDP socket at: %s:%s' % (host[0], host[1])) return True except Exception as e: - logging.error('Failed to connect to socket: %s' % str(e)) + logging.debug('Failed to connect to socket: %s' % str(e)) return False diff --git a/pywalfox/config.py b/pywalfox/config.py index 774db36..5ba6299 100644 --- a/pywalfox/config.py +++ b/pywalfox/config.py @@ -1,9 +1,11 @@ import os -DAEMON_VERSION = '2.4' +DAEMON_VERSION = '2.7' UNIX_SOCKET_PATH = '/tmp/pywalfox_socket' +UNIX_SOCKET_PATH_ALT = '/tmp/pywalfox_socket_alt' WIN_SOCKET_HOST = ('127.0.0.1', 56744) +WIN_SOCKET_HOST_ALT = ('127.0.0.1', 56745) SUPPORTED_BROWSERS = ['firefox'] @@ -12,19 +14,19 @@ PYWAL_COLORS_PATH = os.path.join(XDG_CACHE_DIR, 'wal/colors.json') APP_PATH = os.path.dirname(os.path.abspath(__file__)) -BIN_PATH_UNIX = os.path.join(APP_PATH, 'bin/main.sh') -BIN_PATH_WIN = os.path.join(APP_PATH, 'bin/win.bat') CSS_PATH = os.path.join(APP_PATH, 'assets/css') +BIN_PATH_WIN = os.path.join(APP_PATH, 'bin/win.bat') +BIN_PATH_UNIX = os.path.join(APP_PATH, 'bin/main.sh') FIREFOX_PROFILES_PATH_LINUX = os.path.join(HOME_PATH, '.mozilla/firefox') FIREFOX_PROFILES_PATH_WIN = os.path.join(HOME_PATH, 'AppData/Roaming/Mozilla/Firefox') FIREFOX_PROFILES_PATH_DARWIN = os.path.join(HOME_PATH, 'Library/Application Support/Firefox') -LOG_FILE_PATH = os.path.join(XDG_CACHE_DIR, 'pywalfox.log') LOG_FILE_COUNT = 1 LOG_FILE_MAX_SIZE = 1000*200 # 0.2 mb -LOG_FILE_FORMAT = '[%(asctime)s] %(levelname)s:%(message)s' LOG_FILE_DATE_FORMAT = '%m-%d-%Y %I:%M:%S' +LOG_FILE_FORMAT = '[%(asctime)s] %(levelname)s:%(message)s' +LOG_FILE_PATH = os.path.join(XDG_CACHE_DIR, 'pywalfox.log') ACTIONS = { 'VERSION': 'debug:version', diff --git a/pywalfox/daemon.py b/pywalfox/daemon.py index 77cabad..70ba8ee 100644 --- a/pywalfox/daemon.py +++ b/pywalfox/daemon.py @@ -198,16 +198,15 @@ def start(self): """Starts the daemon and listens for incoming messages.""" self.is_running = True self.start_socket_server() - try: - while True: - message = self.messenger.get_message() - logging.debug('Received message from extension: %s' % message) - self.handle_message(message) - except KeyboardInterrupt: - return + + while self.is_running: + message = self.messenger.get_message() + logging.debug('Received message from extension: %s' % message) + self.handle_message(message) def close(self): """Application cleanup.""" - self.socket_server.close() + logging.debug('Running cleanup') self.is_running = False - logging.debug('Cleanup') + self.socket_server.close() + sys.exit(0) diff --git a/pywalfox/install.py b/pywalfox/install.py index 50b16be..4991775 100644 --- a/pywalfox/install.py +++ b/pywalfox/install.py @@ -37,7 +37,7 @@ def create_hosts_directory(hosts_path): if not os.path.exists(hosts_path): os.makedirs(hosts_path) -def remove_existing_manifest(full_path): +def remove_existing_manifest(full_path, print_errors=True): """ Removes an existing manifest. @@ -46,9 +46,24 @@ def remove_existing_manifest(full_path): try: if os.path.isfile(full_path): os.remove(full_path) + print('Successfully removed manifest at: %s' % full_path) + else: + if print_errors is True: + print('No manifest is installed at: %s' % full_path) except Exception as e: - print('Could not remove existing manifest at: %s\n\t%s' % (full_path, str(e))) - sys.exit(1) + if print_errors is True: + if e.errno == 13: # permission error + print('Permission denied when trying to remove the manifest.') + print('If you are trying to install it globally, rerun this script with admin privileges.') + print('') + print('If you installed Pywalfox for your user only, you must probably use something like this:') + print('sudo python -m pywalfox uninstall') + else: + print('Could not remove existing manifest at: %s\n\t%s' % (full_path, str(e))) + + sys.exit(1) + else: + raise e def normalize_path(target_path): """ @@ -85,14 +100,22 @@ def copy_manifest(target_path, bin_path): :param bin_path str: the path to the daemon executable """ full_path = get_full_manifest_path(target_path) - create_hosts_directory(target_path) - remove_existing_manifest(full_path) try: + create_hosts_directory(target_path) + remove_existing_manifest(full_path, False) shutil.copyfile(MANIFEST_SRC_PATH, full_path) print('Copied manifest to: %s' % full_path) except Exception as e: - print('Could not copy manifest to: %s\n\t%s' % (full_path, str(e))) + if e.errno == 13: # permission error + print('Permission denied when trying to install the manifest.') + print('If you are trying to install it globally, rerun this script with admin privileges.') + print('') + print('If you installed Pywalfox for your user only, you must probably use something like this:') + print('sudo python -m pywalfox install') + else: + print('Failed to install manifest: %s:\n%s' % (full_path, str(e))) + sys.exit(1) set_daemon_path(full_path, bin_path) @@ -115,19 +138,19 @@ def set_executable_permissions(bin_path): print('Try setting the permissions manually using: chmod +x') sys.exit(1) -def get_target_path_key(user_only): +def get_target_path_key(global_install): """ Gets the path key for the 'native-messaging-hosts' directory based on if the manifest should be installed locally or globally. - :param user_only bool: if the manifest should be installed for the current user only + :param global_install bool: if the manifest should be installed for all users :return: the key in MANIFEST_TARGET_PATHS_* corresponding to the manifest path :rType: str """ - if user_only is True: - return 'FIREFOX_USER' - else: + if global_install is True: return 'FIREFOX' + else: + return 'FIREFOX_USER' def setup_register(manifest_path_key): """ @@ -192,13 +215,21 @@ def darwin_setup(manifest_path_key): copy_manifest(manifest_path, BIN_PATH_UNIX) set_executable_permissions(BIN_PATH_UNIX) -def start_setup(user_only): +def validate_permissions(global_install): + if os.geteuid() == 0 and global_install is False: + print('You are running the script as root, but did not specify the --global option.') + selection = input('Things may not work as expected, continue? (y/N): ') + if selection.lower() != 'y' and selection.lower() != 'yes': + sys.exit(1) + +def start_setup(global_install): """ Installs the native messaging host manifest. - :param user_only bool: if the manifest should be installed for the current user only + :param global_install bool: if the manifest should be installed for all users """ - manifest_path_key = get_target_path_key(user_only) + validate_permissions(global_install) + manifest_path_key = get_target_path_key(global_install) if sys.platform.startswith('win32'): win_setup(manifest_path_key) @@ -207,13 +238,14 @@ def start_setup(user_only): else: linux_setup(manifest_path_key) -def start_uninstall(user_only): +def start_uninstall(global_install): """ Tries to remove an existing manifest and delete registry keys (win32). - :param user_only bool: if the manifest should be uninstalled for the current user only + :param global_install bool: if the manifest should be uninstalled for all users """ - manifest_path_key = get_target_path_key(user_only) + validate_permissions(global_install) + manifest_path_key = get_target_path_key(global_install) if sys.platform.startswith('win32'): manifest_path = MANIFEST_TARGET_PATH_WIN