@ launch: Add a --wait-for-child-exit flag to get the child processes exit code even when it is running in a window.
This commit is contained in:
parent
a4963a58e3
commit
3c3ba4a9fb
4 changed files with 64 additions and 7 deletions
|
|
@ -125,6 +125,10 @@ Detailed list of changes
|
|||
- Remote control: Allow modifying desktop panels and showing/hiding OS Windows
|
||||
using the `kitten @ resize-os-window` command (:iss:`8550`)
|
||||
|
||||
- Remote control launch: Allow waiting for a program launched in a new window
|
||||
to exit and get the exit code via the `kitty +launch
|
||||
--wait-for-child-to-exit` command line flag (:disc:`8573`)
|
||||
|
||||
- Allow starting kitty with the OS window hidden via :option:`kitty --start-as=hidden <kitty --start-as>`, useful for single instance mode (:iss:`3466`)
|
||||
|
||||
- Allow configuring the mouse unhide behavior when using :opt:`mouse_hide_wait` (:pull:`8508`)
|
||||
|
|
|
|||
|
|
@ -2782,6 +2782,10 @@ def set_update_check_process(self, process: Optional['PopenType[bytes]'] = None)
|
|||
self.update_check_process.kill()
|
||||
self.update_check_process = process
|
||||
|
||||
def monitor_pid(self, pid: int, callback: Callable[[int, Exception | None], None]) -> None:
|
||||
self.background_process_death_notify_map[pid] = callback
|
||||
monitor_pid(pid)
|
||||
|
||||
def on_monitored_pid_death(self, pid: int, exit_status: int) -> None:
|
||||
callback = self.background_process_death_notify_map.pop(pid, None)
|
||||
if callback is not None:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import shutil
|
||||
from collections.abc import Container, Iterable, Iterator, Sequence
|
||||
from contextlib import suppress
|
||||
from typing import Any, NamedTuple, TypedDict
|
||||
from typing import Any, Callable, NamedTuple, TypedDict
|
||||
|
||||
from .boss import Boss
|
||||
from .child import Child
|
||||
|
|
@ -593,6 +593,7 @@ def _launch(
|
|||
is_clone_launch: str = '',
|
||||
rc_from_window: Window | None = None,
|
||||
base_env: dict[str, str] | None = None,
|
||||
child_death_callback: Callable[[int, Exception | None], None] | None = None,
|
||||
) -> Window | None:
|
||||
source_window = boss.active_window_for_cwd
|
||||
if opts.source_window:
|
||||
|
|
@ -730,7 +731,8 @@ def _launch(
|
|||
raise ValueError('The cmd to run must be specified when running a background process')
|
||||
boss.run_background_process(
|
||||
cmd, cwd=kw['cwd'], cwd_from=kw['cwd_from'], env=env or None, stdin=kw['stdin'],
|
||||
allow_remote_control=kw['allow_remote_control'], remote_control_passwords=kw['remote_control_passwords']
|
||||
allow_remote_control=kw['allow_remote_control'], remote_control_passwords=kw['remote_control_passwords'],
|
||||
notify_on_death=child_death_callback,
|
||||
)
|
||||
elif opts.type in ('clipboard', 'primary'):
|
||||
stdin = kw.get('stdin')
|
||||
|
|
@ -741,6 +743,8 @@ def _launch(
|
|||
else:
|
||||
set_primary_selection(stdin)
|
||||
boss.handle_clipboard_loss('primary')
|
||||
if child_death_callback is not None:
|
||||
child_death_callback(0, None)
|
||||
else:
|
||||
kw['hold'] = opts.hold
|
||||
if force_target_tab and target_tab is not None:
|
||||
|
|
@ -751,6 +755,8 @@ def _launch(
|
|||
with Window.set_ignore_focus_changes_for_new_windows(opts.keep_focus):
|
||||
new_window: Window = tab.new_window(
|
||||
env=env or None, watchers=watchers or None, is_clone_launch=is_clone_launch, next_to=next_to, **kw)
|
||||
if child_death_callback is not None:
|
||||
boss.monitor_pid(new_window.child.pid or 0, child_death_callback)
|
||||
if spacing:
|
||||
patch_window_edges(new_window, spacing)
|
||||
tab.relayout()
|
||||
|
|
@ -781,12 +787,13 @@ def launch(
|
|||
is_clone_launch: str = '',
|
||||
rc_from_window: Window | None = None,
|
||||
base_env: dict[str, str] | None = None,
|
||||
child_death_callback: Callable[[int, Exception | None], None] | None = None,
|
||||
) -> Window | None:
|
||||
active = boss.active_window
|
||||
if opts.keep_focus and active:
|
||||
orig, active.ignore_focus_changes = active.ignore_focus_changes, True
|
||||
try:
|
||||
return _launch(boss, opts, args, target_tab, force_target_tab, is_clone_launch, rc_from_window, base_env)
|
||||
return _launch(boss, opts, args, target_tab, force_target_tab, is_clone_launch, rc_from_window, base_env, child_death_callback)
|
||||
finally:
|
||||
if opts.keep_focus and active:
|
||||
active.ignore_focus_changes = orig
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from kitty.cli_stub import LaunchCLIOptions
|
||||
from kitty.launch import launch as do_launch
|
||||
from kitty.launch import options_spec as launch_options_spec
|
||||
from kitty.launch import parse_launch_args
|
||||
from kitty.types import AsyncResponse
|
||||
|
||||
from .base import MATCH_TAB_OPTION, ArgsType, Boss, PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType, Window
|
||||
|
||||
|
|
@ -54,6 +56,7 @@ class Launch(RemoteCommand):
|
|||
color/list.str: list of color specifications such as foreground=red
|
||||
watcher/list.str: list of paths to watcher files
|
||||
bias/float: The bias with which to create the new window in the current layout
|
||||
wait_for_child_to_exit/bool: Boolean indicating whether to wait and return child exit code
|
||||
'''
|
||||
|
||||
short_desc = 'Run an arbitrary process in a new window/tab'
|
||||
|
|
@ -64,6 +67,21 @@ class Launch(RemoteCommand):
|
|||
' kitten @ launch --title=Email mutt'
|
||||
)
|
||||
options_spec = MATCH_TAB_OPTION + '\n\n' + '''\
|
||||
--wait-for-child-to-exit
|
||||
type=bool-set
|
||||
Wait until the launched program exits and print out its exit code. The exit code is
|
||||
printed out instead of the window id. If the program exited nromally its exit code is printed, which
|
||||
is always greater than or equal to zero. If the program was killed by a signal, the symbolic name
|
||||
of the SIGNAL is printed, if available, otherwise the signal number with a leading minus sign is printed.
|
||||
|
||||
|
||||
--response-timeout
|
||||
type=float
|
||||
default=86400
|
||||
The time in seconds to wait for the started process to exit, when using the :option:`--wait-for-child-to-exit`
|
||||
option. Defaults to one day.
|
||||
|
||||
|
||||
--no-response
|
||||
type=bool-set
|
||||
Do not print out the id of the newly created window.
|
||||
|
|
@ -76,14 +94,17 @@ class Launch(RemoteCommand):
|
|||
''' + '\n\n' + launch_options_spec().replace(':option:`launch', ':option:`kitten @ launch')
|
||||
args = RemoteCommand.Args(spec='[CMD ...]', json_field='args', completion=RemoteCommand.CompletionSpec.from_string(
|
||||
'type:special group:cli.CompleteExecutableFirstArg'))
|
||||
is_asynchronous = True
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
ans = {'args': args or []}
|
||||
for attr, val in opts.__dict__.items():
|
||||
ans[attr] = val
|
||||
# ans['wait_for_child_to_exit'] = opts.wait_for_child_to_exit
|
||||
return ans
|
||||
|
||||
def response_from_kitty(self, boss: Boss, window: Window | None, payload_get: PayloadGetType) -> ResponseType:
|
||||
# responder.send_data(getattr(w, 'id', 0))
|
||||
default_opts = parse_launch_args()[0]
|
||||
opts = LaunchCLIOptions()
|
||||
for key, default_value in default_opts.__dict__.items():
|
||||
|
|
@ -108,10 +129,31 @@ def response_from_kitty(self, boss: Boss, window: Window | None, payload_get: Pa
|
|||
tabs = self.tabs_for_match_payload(boss, window, payload_get)
|
||||
if tabs and tabs[0]:
|
||||
target_tab = tabs[0]
|
||||
elif payload_get('type') not in ('background', 'os-window', 'tab', 'window'):
|
||||
return None
|
||||
w = do_launch(boss, opts, payload_get('args') or [], target_tab=target_tab, rc_from_window=window, base_env=base_env)
|
||||
return None if payload_get('no_response') else str(getattr(w, 'id', 0))
|
||||
|
||||
def on_child_death(exit_status: int, exc: Exception | None) -> None:
|
||||
code = os.waitstatus_to_exitcode(exit_status)
|
||||
ans = str(code)
|
||||
if code < 0:
|
||||
try:
|
||||
from signal import Signals
|
||||
ans = Signals(-code).name
|
||||
except ValueError:
|
||||
pass
|
||||
responder.send_data(ans)
|
||||
|
||||
w = do_launch(
|
||||
boss, opts, payload_get('args') or [], target_tab=target_tab, rc_from_window=window, base_env=base_env,
|
||||
child_death_callback=on_child_death if payload_get('wait_for_child_to_exit') and not payload_get('no_response') else None)
|
||||
if payload_get('no_response'):
|
||||
return None
|
||||
|
||||
if not payload_get('wait_for_child_to_exit'):
|
||||
return str(0 if w is None else w.id)
|
||||
|
||||
responder = self.create_async_responder(payload_get, window)
|
||||
return AsyncResponse()
|
||||
|
||||
def cancel_async_request(self, boss: 'Boss', window: Window | None, payload_get: PayloadGetType) -> None:
|
||||
pass
|
||||
|
||||
launch = Launch()
|
||||
|
|
|
|||
Loading…
Reference in a new issue