-
-
Notifications
You must be signed in to change notification settings - Fork 992
Commit
…chine kitty is running on and get its output Fixes #7429
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, ' | ||
This comment has been minimized.
Sorry, something went wrong. |
||
' 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() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
||
} |
10 comments
on commit 38fed8b
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fantastic!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, after this and dependent commits, kitten @ launch
always ends up with Error: i/o timeout
P.S. Would you mind adding --env
support for kitten @ run
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works fine for me.
Oh, it's kitten @ send-text aaa
which gets stuck.
Should be a trivial PR for you :)
Sure! Will do it now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems kitten @ run
rarely works with unix domain sock. 2/14 work
❯ kitten @ --to unix:/tmp/kitty_sock run echo hello
Error: Did not receive expected streaming response
❯ kitten @ --to unix:/tmp/kitty_sock run echo hello
Error: Did not receive expected streaming response
❯ kitten @ --to unix:/tmp/kitty_sock run echo hello
hello
❯ kitten @ --to unix:/tmp/kitty_sock run echo hello
Error: Did not receive expected streaming response
❯ kitten @ --to unix:/tmp/kitty_sock run echo hello
Error: Did not receive expected streaming response
❯ kitten @ --to unix:/tmp/kitty_sock run echo hello
Error: Did not receive expected streaming response
❯ kitten @ --to unix:/tmp/kitty_sock run echo hello
Error: Did not receive expected streaming response
❯ kitten @ --to unix:/tmp/kitty_sock run echo hello
Error: Did not receive expected streaming response
❯ kitten @ --to unix:/tmp/kitty_sock run echo hello
Error: Did not receive expected streaming response
❯ kitten @ --to unix:/tmp/kitty_sock run echo hello
Error: Did not receive expected streaming response
❯ kitten @ --to unix:/tmp/kitty_sock run echo hello
Error: Did not receive expected streaming response
❯ kitten @ --to unix:/tmp/kitty_sock run echo hello
Error: Did not receive expected streaming response
❯ kitten @ --to unix:/tmp/kitty_sock run echo hello
Error: Did not receive expected streaming response
❯ kitten @ --to unix:/tmp/kitty_sock run echo hello
hello
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm on X11. I did some debugging. When broken the response is indeed not a stream response.
❯ kitten @ --to unix:/tmp/kitty_sock run echo 1
{"ok": true, "data": {"stdout": "MQo=", "stderr": "", "exit_code": 0, "exit_status": 0}}
Error: Did not receive expected streaming response
❯ kitten @ --to unix:/tmp/kitty_sock run echo 1
{"ok": true, "stream": true}
1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/kovidgoyal/kitty/blob/master/kitty/rc/run.py#L109
It's likely a race condition here. on_death
gets executed before AsyncResponse()
is returned to the kitten command.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if #7443 is a proper fix for this. At least it works for me :)
typo :)