diff --git a/kitty/boss.py b/kitty/boss.py index f47e0645c..fd7733183 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -81,6 +81,7 @@ class Boss: self.pending_sequences = None self.cached_values = cached_values self.os_window_map = {} + self.os_window_death_actions = {} self.cursor_blinking = True self.shutting_down = False talk_fd = getattr(single_instance, 'socket', None) @@ -239,7 +240,9 @@ class Boss: if not os.path.isabs(args.directory): args.directory = os.path.join(msg['cwd'], args.directory) session = create_session(opts, args, respect_cwd=True) - self.add_os_window(session, wclass=args.cls, wname=args.name, opts_for_size=opts, startup_id=startup_id) + os_window_id = self.add_os_window(session, wclass=args.cls, wname=args.name, opts_for_size=opts, startup_id=startup_id) + if msg.get('notify_on_os_window_death'): + self.os_window_death_actions[os_window_id] = partial(self.notify_on_os_window_death, msg['notify_on_os_window_death']) else: log_error('Unknown message received from peer, ignoring') @@ -527,6 +530,20 @@ class Boss: tm.destroy() for window_id in tuple(w.id for w in self.window_id_map.values() if getattr(w, 'os_window_id', None) == os_window_id): self.window_id_map.pop(window_id, None) + action = self.os_window_death_actions.pop(os_window_id, None) + if action is not None: + action() + + def notify_on_os_window_death(self, address): + import socket + s = socket.socket(family=socket.AF_UNIX) + try: + s.connect(address) + s.sendall(b'c') + s.shutdown(socket.SHUT_RDWR) + s.close() + except Exception: + pass def display_scrollback(self, window, data, cmd): tab = self.active_tab diff --git a/kitty/cli.py b/kitty/cli.py index 14569c19d..74c7b1df4 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -91,6 +91,14 @@ with the same :option:`kitty --instance-group` will result in new windows being in the first :italic:`{appname}` instance within that group +--wait-for-single-instance-window-close +type=bool-set +Normally, when using :option:`--single-instance`, :talic:`{appname}` will open a new window in an existing +instance and quit immediately. With this option, it will not quit till the newly opened +window is closed. Note that if no previous instance is found, then :italic:`{appname}` will wait anyway, +regardless of this option. + + --listen-on Tell kitty to listen on the specified address for control messages. For example, :option:`{appname} --listen-on`=unix:/tmp/mykitty or diff --git a/kitty/main.py b/kitty/main.py index 123216934..8eebe7a29 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -15,17 +15,49 @@ from .constants import ( appname, config_dir, glfw_path, is_macos, is_wayland, logo_data_file ) from .fast_data_types import ( - create_os_window, free_font_data, glfw_init, glfw_terminate, - set_default_window_icon, set_options, GLFW_MOD_SUPER + GLFW_MOD_SUPER, create_os_window, free_font_data, glfw_init, + glfw_terminate, set_default_window_icon, set_options ) from .fonts.box_drawing import set_scale from .fonts.render import set_font_family from .utils import ( - detach, log_error, single_instance, startup_notification_handler + detach, log_error, single_instance, startup_notification_handler, + unix_socket_paths ) from .window import load_shader_programs +def talk_to_instance(args): + import json + import socket + data = {'cmd': 'new_instance', 'args': tuple(sys.argv), + 'startup_id': os.environ.get('DESKTOP_STARTUP_ID'), + 'cwd': os.getcwd()} + notify_socket = None + if args.wait_for_single_instance_window_close: + address = '\0{}-os-window-close-notify-{}-{}'.format(appname, os.getpid(), os.geteuid()) + notify_socket = socket.socket(family=socket.AF_UNIX) + try: + notify_socket.bind(address) + except FileNotFoundError: + for address in unix_socket_paths(address[1:], ext='.sock'): + notify_socket.bind(address) + break + data['notify_on_os_window_death'] = address + notify_socket.listen() + + data = json.dumps(data, ensure_ascii=False).encode('utf-8') + single_instance.socket.sendall(data) + single_instance.socket.shutdown(socket.SHUT_RDWR) + single_instance.socket.close() + + if args.wait_for_single_instance_window_close: + conn = notify_socket.accept()[0] + conn.recv(1) + conn.shutdown(socket.SHUT_RDWR) + conn.close() + + def load_all_shaders(semi_transparent=0): load_shader_programs(semi_transparent) load_borders_program() @@ -200,12 +232,7 @@ def _main(): if args.single_instance: is_first = single_instance(args.instance_group) if not is_first: - import json - data = {'cmd': 'new_instance', 'args': tuple(sys.argv), - 'startup_id': os.environ.get('DESKTOP_STARTUP_ID'), - 'cwd': os.getcwd()} - data = json.dumps(data, ensure_ascii=False).encode('utf-8') - single_instance.socket.sendall(data) + talk_to_instance(args) return opts = create_opts(args) if opts.editor != '.': diff --git a/kitty/utils.py b/kitty/utils.py index 632e9cc30..335f63447 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -266,8 +266,7 @@ def remove_socket_file(s, path=None): pass -def single_instance_unix(name): - import socket +def unix_socket_paths(name, ext='.lock'): import tempfile home = os.path.expanduser('~') candidates = [tempfile.gettempdir(), home] @@ -276,34 +275,39 @@ def single_instance_unix(name): candidates = [user_cache_dir(), '/Library/Caches'] for loc in candidates: if os.access(loc, os.W_OK | os.R_OK | os.X_OK): - filename = ('.' if loc == home else '') + name + '.lock' - path = os.path.join(loc, filename) - socket_path = path.rpartition('.')[0] + '.sock' - fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_TRUNC | os.O_CLOEXEC) - try: - fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - except EnvironmentError as err: - if err.errno in (errno.EAGAIN, errno.EACCES): - # Client - s = socket.socket(family=socket.AF_UNIX) - s.connect(socket_path) - single_instance.socket = s - return False - raise - s = socket.socket(family=socket.AF_UNIX) - try: + filename = ('.' if loc == home else '') + name + ext + yield os.path.join(loc, filename) + + +def single_instance_unix(name): + import socket + for path in unix_socket_paths(name): + socket_path = path.rpartition('.')[0] + '.sock' + fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_TRUNC | os.O_CLOEXEC) + try: + fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except EnvironmentError as err: + if err.errno in (errno.EAGAIN, errno.EACCES): + # Client + s = socket.socket(family=socket.AF_UNIX) + s.connect(socket_path) + single_instance.socket = s + return False + raise + s = socket.socket(family=socket.AF_UNIX) + try: + s.bind(socket_path) + except EnvironmentError as err: + if err.errno in (errno.EADDRINUSE, errno.EEXIST): + os.unlink(socket_path) s.bind(socket_path) - except EnvironmentError as err: - if err.errno in (errno.EADDRINUSE, errno.EEXIST): - os.unlink(socket_path) - s.bind(socket_path) - else: - raise - single_instance.socket = s # prevent garbage collection from closing the socket - atexit.register(remove_socket_file, s, socket_path) - s.listen() - s.set_inheritable(False) - return True + else: + raise + single_instance.socket = s # prevent garbage collection from closing the socket + atexit.register(remove_socket_file, s, socket_path) + s.listen() + s.set_inheritable(False) + return True def single_instance(group_id=None):