mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-06 16:05:05 +02:00
kitten @ run: A new remote control command to run a process on the machine kitty is running on and get its output
Fixes #7429
This commit is contained in:
@@ -53,6 +53,8 @@ Detailed list of changes
|
||||
0.35.0 [future]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- kitten @ run: A new remote control command to run a process on the machine kitty is running on and get its output (:iss:`7429`)
|
||||
|
||||
- :opt:`notify_on_cmd_finish`: Show the actual command that was finished (:iss:`7420`)
|
||||
|
||||
- Shell integration: Make the currently executing cmdline available as a window variable in kitty
|
||||
|
||||
@@ -30,9 +30,30 @@ class LaunchSpec(NamedTuple):
|
||||
args: List[str]
|
||||
|
||||
|
||||
remote_control_password_docs = '''\
|
||||
type=list
|
||||
Restrict the actions remote control is allowed to take. This works like
|
||||
:opt:`remote_control_password`. You can specify a password and list of actions
|
||||
just as for :opt:`remote_control_password`. For example::
|
||||
|
||||
--remote-control-password '"my passphrase" get-* set-colors'
|
||||
|
||||
This password will be in effect for this window only.
|
||||
Note that any passwords you have defined for :opt:`remote_control_password`
|
||||
in :file:`kitty.conf` are also in effect. You can override them by using the same password here.
|
||||
You can also disable all :opt:`remote_control_password` global passwords for this window, by using::
|
||||
|
||||
--remote-control-password '!'
|
||||
|
||||
This option only takes effect if :option:`--allow-remote-control`
|
||||
is also specified. Can be specified multiple times to create multiple passwords.
|
||||
This option was added to kitty in version 0.26.0
|
||||
'''
|
||||
|
||||
|
||||
@run_once
|
||||
def options_spec() -> str:
|
||||
return '''
|
||||
return f'''
|
||||
--window-title --title
|
||||
The title to set for the new window. By default, title is controlled by the
|
||||
child process. The special value :code:`current` will copy the title from the
|
||||
@@ -171,24 +192,7 @@ remote control.
|
||||
|
||||
|
||||
--remote-control-password
|
||||
type=list
|
||||
Restrict the actions remote control is allowed to take. This works like
|
||||
:opt:`remote_control_password`. You can specify a password and list of actions
|
||||
just as for :opt:`remote_control_password`. For example::
|
||||
|
||||
--remote-control-password '"my passphrase" get-* set-colors'
|
||||
|
||||
This password will be in effect for this window only.
|
||||
Note that any passwords you have defined for :opt:`remote_control_password`
|
||||
in :file:`kitty.conf` are also in effect. You can override them by using the same password here.
|
||||
You can also disable all :opt:`remote_control_password` global passwords for this window, by using::
|
||||
|
||||
--remote-control-password '!'
|
||||
|
||||
This option only takes effect if :option:`--allow-remote-control`
|
||||
is also specified. Can be specified multiple times to create multiple passwords.
|
||||
This option was added to kitty in version 0.26.0
|
||||
|
||||
{remote_control_password_docs}
|
||||
|
||||
--stdin-source
|
||||
type=choices
|
||||
|
||||
123
kitty/rc/run.py
Normal file
123
kitty/rc/run.py
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import sys
|
||||
from base64 import standard_b64decode, standard_b64encode
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from kitty.launch import remote_control_password_docs
|
||||
from kitty.types import AsyncResponse
|
||||
|
||||
from .base import (
|
||||
ArgsType,
|
||||
Boss,
|
||||
CmdGenerator,
|
||||
ParsingOfArgsFailed,
|
||||
PayloadGetType,
|
||||
PayloadType,
|
||||
RCOptions,
|
||||
RemoteCommand,
|
||||
ResponseType,
|
||||
Window,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from kitty.cli_stub import RunRCOptions as CLIOptions
|
||||
|
||||
|
||||
class Run(RemoteCommand):
|
||||
protocol_spec = __doc__ = '''
|
||||
data+/str: Chunk of STDIN data, base64 encoded no more than 4096 bytes. Must send an empty chunk to indicate end of data.
|
||||
cmdline+/list.str: The command line to run
|
||||
allow_remote_control/bool: A boolean indicating whether to allow remote control
|
||||
remote_control_password/list.str: A list of remote control passwords
|
||||
'''
|
||||
|
||||
short_desc = 'Run a program on the computer in which kitty is running and get the output'
|
||||
desc = (
|
||||
'Run the specified program on the computer in which kitty is running. When STDIN is not a TTY it is forwarded'
|
||||
' to the program as its STDIN. STDOUT and STDERR from the the program are forwarded here. The exit status of this'
|
||||
' invocation will be the exit status of the executed program. If you wish to just run a program without wiating for a response, '
|
||||
' use @ launch --type=background instead.'
|
||||
)
|
||||
|
||||
options_spec = f'''\n
|
||||
--allow-remote-control
|
||||
type=bool-set
|
||||
The executed program will have privileges to run remote control commands in kitty.
|
||||
|
||||
|
||||
--remote-control-password
|
||||
{remote_control_password_docs}
|
||||
'''
|
||||
args = RemoteCommand.Args(
|
||||
spec='CMD ...', json_field='data', special_parse='+cmdline:!read_run_data(io_data, args, &payload)', minimum_count=1,
|
||||
completion=RemoteCommand.CompletionSpec.from_string('type:special group:cli.CompleteExecutableFirstArg')
|
||||
)
|
||||
reads_streaming_data = True
|
||||
is_asynchronous = True
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
if not args:
|
||||
self.fatal('Must specify command to run')
|
||||
import secrets
|
||||
ret = {
|
||||
'stream_id': secrets.token_urlsafe(),
|
||||
'cmdline': args,
|
||||
'allow_remote_control': opts.allow_remote_control,
|
||||
'remote_control_password': opts.remote_control_password,
|
||||
'data': '',
|
||||
}
|
||||
def pipe() -> CmdGenerator:
|
||||
if sys.stdin.isatty():
|
||||
yield ret
|
||||
else:
|
||||
limit = 4096
|
||||
while True:
|
||||
data = sys.stdin.buffer.read(limit)
|
||||
if not data:
|
||||
break
|
||||
ret['data'] = standard_b64encode(data).decode("ascii")
|
||||
yield ret
|
||||
return pipe()
|
||||
|
||||
def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType:
|
||||
import os
|
||||
import tempfile
|
||||
data = payload_get('data')
|
||||
q = self.handle_streamed_data(standard_b64decode(data) if data else b'', payload_get)
|
||||
if isinstance(q, AsyncResponse):
|
||||
return q
|
||||
stdin_data = q.getvalue()
|
||||
from kitty.launch import parse_remote_control_passwords
|
||||
cmdline = payload_get('cmdline')
|
||||
allow_remote_control = payload_get('allow_remote_control')
|
||||
pw = payload_get('remote_control_password')
|
||||
rcp = parse_remote_control_passwords(allow_remote_control, pw)
|
||||
if not cmdline:
|
||||
raise ParsingOfArgsFailed('No cmdline to run specified')
|
||||
responder = self.create_async_responder(payload_get, window)
|
||||
stdout, stderr = tempfile.TemporaryFile(), tempfile.TemporaryFile()
|
||||
|
||||
def on_death(exit_status: int, err: Optional[Exception]) -> None:
|
||||
with stdout, stderr:
|
||||
if err:
|
||||
responder.send_error(f'Failed to run: {cmdline} with err: {err}')
|
||||
else:
|
||||
exit_code = os.waitstatus_to_exitcode(exit_status)
|
||||
stdout.seek(0)
|
||||
stderr.seek(0)
|
||||
responder.send_data({
|
||||
'stdout': standard_b64encode(stdout.read()).decode('ascii'),
|
||||
'stderr': standard_b64encode(stderr.read()).decode('ascii'),
|
||||
'exit_code': exit_code, 'exit_status': exit_status,
|
||||
})
|
||||
|
||||
boss.run_background_process(
|
||||
cmdline, stdin=stdin_data, stdout=stdout.fileno(), stderr=stderr.fileno(), notify_on_death=on_death,
|
||||
remote_control_passwords=rcp, allow_remote_control=allow_remote_control
|
||||
)
|
||||
return AsyncResponse()
|
||||
|
||||
|
||||
run = Run()
|
||||
80
tools/cmd/at/run.go
Normal file
80
tools/cmd/at/run.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package at
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"kitty/tools/tty"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
type run_response_data struct {
|
||||
Stdout string `json:"stdout"`
|
||||
Stderr string `json:"stderr"`
|
||||
Exit_code int `json:"exit_code"`
|
||||
Exit_status int `json:"exit_status"`
|
||||
}
|
||||
|
||||
func run_handle_response(data []byte) error {
|
||||
var r run_response_data
|
||||
if err := json.Unmarshal(data, &r); err != nil {
|
||||
return err
|
||||
}
|
||||
if stdout, err := base64.StdEncoding.DecodeString(r.Stdout); err == nil {
|
||||
_, _ = os.Stdout.Write(stdout)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
if stderr, err := base64.StdEncoding.DecodeString(r.Stderr); err == nil {
|
||||
_, _ = os.Stderr.Write(stderr)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
if r.Exit_code != 0 {
|
||||
return &exit_error{r.Exit_code}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func read_run_data(io_data *rc_io_data, args []string, payload *run_json_type) (func(io_data *rc_io_data) (bool, error), error) {
|
||||
is_first_call := true
|
||||
is_tty := tty.IsTerminal(os.Stdin.Fd())
|
||||
buf := make([]byte, 4096)
|
||||
cmdline := make([]escaped_string, len(args))
|
||||
for i, s := range args {
|
||||
cmdline[i] = escaped_string(s)
|
||||
}
|
||||
payload.Cmdline = cmdline
|
||||
io_data.handle_response = run_handle_response
|
||||
|
||||
return func(io_data *rc_io_data) (bool, error) {
|
||||
if is_first_call {
|
||||
is_first_call = false
|
||||
} else {
|
||||
io_data.rc.Stream = false
|
||||
}
|
||||
buf = buf[:cap(buf)]
|
||||
var n int
|
||||
var err error
|
||||
if is_tty {
|
||||
buf = buf[:0]
|
||||
err = io.EOF
|
||||
} else {
|
||||
n, err = os.Stdin.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return false, err
|
||||
}
|
||||
buf = buf[:n]
|
||||
}
|
||||
set_payload_data(io_data, base64.StdEncoding.EncodeToString(buf))
|
||||
if err == io.EOF {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}, nil
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user