mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-07-14 02:21:56 +08:00
The issue was a closure problem where the on_child_death callback referenced a responder variable that was created after do_launch() was called. This caused the callback to fail when the child process exited because responder didn't exist in the callback's closure. The fix creates the responder before defining the callback and adds a null check in the callback to ensure it exists before using it. This allows commands like: kitten @ launch --type=overlay --wait-for-child-to-exit bash -c "echo test" to properly wait for the child process to exit and return the exit code.
167 lines
7.8 KiB
Python
167 lines
7.8 KiB
Python
#!/usr/bin/env python
|
|
# 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
|
|
|
|
if TYPE_CHECKING:
|
|
from kitty.cli_stub import LaunchRCOptions as CLIOptions
|
|
|
|
|
|
class Launch(RemoteCommand):
|
|
|
|
protocol_spec = __doc__ = '''
|
|
args+/list.str: The command line to run in the new window, as a list, use an empty list to run the default shell
|
|
match/str: The tab to open the new window in
|
|
next_to/str: The window next to which to create the new window or empty string to use active window
|
|
source_window/str: The window to use as source for data or empty string to use active window
|
|
window_title/str: Title for the new window
|
|
cwd/str: Working directory for the new window
|
|
add_to_session/str: Session name to add created window/tab to
|
|
env/list.str: List of environment variables of the form NAME=VALUE
|
|
var/list.str: List of user variables of the form NAME=VALUE
|
|
os_panel/list.str: List of panel settings
|
|
tab_title/str: Title for the new tab
|
|
type/choices.window.tab.os-window.os-panel.overlay.overlay-main.background.clipboard.primary: The type of window to open
|
|
keep_focus/bool: Boolean indicating whether the current window should retain focus or not
|
|
copy_colors/bool: Boolean indicating whether to copy the colors from the current window
|
|
copy_cmdline/bool: Boolean indicating whether to copy the cmdline from the current window
|
|
copy_env/list.str=copy_local_env: List of strings representing the local env vars
|
|
hold/bool: Boolean indicating whether to keep window open after cmd exits
|
|
location/choices.first.after.before.neighbor.last.vsplit.hsplit.split.default: Where in the tab to open the new window
|
|
allow_remote_control/bool: Boolean indicating whether to allow remote control from the new window
|
|
remote_control_password/list.str: A list of remote control passwords
|
|
stdin_source/choices.none.@selection.@screen.@screen_scrollback.@alternate.@alternate_scrollback.\
|
|
@first_cmd_output_on_screen.@last_cmd_output.@last_visited_cmd_output: Where to get stdin for the process from
|
|
stdin_add_formatting/bool: Boolean indicating whether to add formatting codes to stdin
|
|
stdin_add_line_wrap_markers/bool: Boolean indicating whether to add line wrap markers to stdin
|
|
spacing/list.str: A list of spacing specifications, see the docs for the set-spacing command
|
|
marker/str: Specification for marker for new window, for example: "text 1 ERROR"
|
|
logo/str: Path to window logo
|
|
logo_position/str: Window logo position as string or empty string to use default
|
|
logo_alpha/float: Window logo alpha or -1 to use default
|
|
self/bool: Boolean, if True use tab the command was run in
|
|
os_window_title/str: Title for OS Window
|
|
os_window_name/str: WM_NAME for OS Window
|
|
os_window_class/str: WM_CLASS for OS Window
|
|
os_window_state/choices.normal.fullscreen.maximized.minimized: The initial state for OS Window
|
|
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
|
|
hold_after_ssh/bool: Boolean indicating whether to run a local shell after exiting the ssh session cloned via cwd=current or similar
|
|
'''
|
|
|
|
short_desc = 'Run an arbitrary process in a new window/tab'
|
|
desc = (
|
|
'Prints out the id of the newly opened window. Any command line arguments'
|
|
' are assumed to be the command line used to run in the new window, if none'
|
|
' are provided, the default shell is run. For example::\n\n'
|
|
' 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 normally 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.
|
|
|
|
|
|
--self
|
|
type=bool-set
|
|
If specified the tab containing the window this command is run in is used
|
|
instead of the active tab
|
|
''' + '\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():
|
|
if key == 'copy_env':
|
|
continue
|
|
val = payload_get(key)
|
|
if val is None:
|
|
val = default_value
|
|
setattr(opts, key, val)
|
|
ceval = payload_get('copy_env')
|
|
opts.copy_env = False
|
|
base_env: dict[str, str] | None = None
|
|
if ceval:
|
|
if isinstance(ceval, list):
|
|
base_env = {}
|
|
for x in ceval:
|
|
k, v = x.partition('=')[::2]
|
|
base_env[k] = v
|
|
elif isinstance(ceval, bool):
|
|
opts.copy_env = ceval
|
|
target_tab = None
|
|
tabs = self.tabs_for_match_payload(boss, window, payload_get)
|
|
if tabs and tabs[0]:
|
|
target_tab = tabs[0]
|
|
|
|
# Create responder before defining callback to avoid closure issue
|
|
responder = None
|
|
if payload_get('wait_for_child_to_exit') and not payload_get('no_response'):
|
|
responder = self.create_async_responder(payload_get, window)
|
|
|
|
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
|
|
if responder is not None:
|
|
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)
|
|
|
|
return AsyncResponse()
|
|
|
|
def cancel_async_request(self, boss: 'Boss', window: Window | None, payload_get: PayloadGetType) -> None:
|
|
pass
|
|
|
|
launch = Launch()
|