pytty 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +7 -7
- data/README.md +1 -1
- data/lib/pytty/client/api.rb +5 -0
- data/lib/pytty/client/api/attach.rb +17 -6
- data/lib/pytty/client/api/port.rb +21 -0
- data/lib/pytty/client/api/rm.rb +18 -0
- data/lib/pytty/client/api/status.rb +19 -0
- data/lib/pytty/client/api/stderr.rb +19 -0
- data/lib/pytty/client/api/stdout.rb +1 -1
- data/lib/pytty/client/cli.rb +3 -0
- data/lib/pytty/client/cli/port_command.rb +24 -0
- data/lib/pytty/client/cli/rm_command.rb +2 -7
- data/lib/pytty/client/cli/root_command.rb +3 -1
- data/lib/pytty/client/cli/run_command.rb +14 -5
- data/lib/pytty/client/cli/status_command.rb +26 -0
- data/lib/pytty/client/cli/stderr_command.rb +28 -0
- data/lib/pytty/client/cli/stdout_command.rb +7 -1
- data/lib/pytty/client/process_yield.rb +6 -0
- data/lib/pytty/daemon/api/router.rb +30 -13
- data/lib/pytty/daemon/components/yield_handler.rb +41 -3
- data/lib/pytty/daemon/process_yield.rb +133 -35
- data/lib/pytty/version.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ac7cf59162fd0dccf371c30835205439e1e5886e1205046a7e8bfeba356ec66d
|
4
|
+
data.tar.gz: 91514548de9aee6be4f3f4ae8fdf1cad590ca4af1696b50dcd94075747bd5569
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 932b0d858592fc7674e29735b6fc97e0244e0348a8c0cc944008bf10c137085aba44fa52ea9cfed2d92d161f549eef8243817363193d9d100bbc435a8bfec8eb
|
7
|
+
data.tar.gz: b467fead672a215988f55b57225f6ba2167e3ca8aae23d3849f4bb69b056159ed3d5cd505e579909d78995bf40ff2ef434c0ced8fdb0cdb1988d39b9d58add12
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pytty (0.
|
4
|
+
pytty (0.5.0)
|
5
5
|
async-websocket
|
6
6
|
clamp (~> 1.3)
|
7
7
|
falcon
|
@@ -10,18 +10,18 @@ PATH
|
|
10
10
|
GEM
|
11
11
|
remote: https://rubygems.org/
|
12
12
|
specs:
|
13
|
-
async (1.
|
13
|
+
async (1.14.0)
|
14
14
|
nio4r (~> 2.3)
|
15
15
|
timers (~> 4.1)
|
16
16
|
async-container (0.8.1)
|
17
17
|
async (~> 1.0)
|
18
18
|
async-io (~> 1.4)
|
19
|
-
async-http (0.37.
|
20
|
-
async (~> 1.
|
21
|
-
async-io (~> 1.
|
19
|
+
async-http (0.37.9)
|
20
|
+
async (~> 1.14)
|
21
|
+
async-io (~> 1.18)
|
22
22
|
http-protocol (~> 0.10)
|
23
|
-
async-io (1.
|
24
|
-
async (~> 1.
|
23
|
+
async-io (1.18.1)
|
24
|
+
async (~> 1.14)
|
25
25
|
async-websocket (0.6.1)
|
26
26
|
async-io
|
27
27
|
websocket-driver (~> 0.7.0)
|
data/README.md
CHANGED
data/lib/pytty/client/api.rb
CHANGED
@@ -3,4 +3,9 @@ require_relative "api/yield"
|
|
3
3
|
require_relative "api/attach"
|
4
4
|
require_relative "api/spawn"
|
5
5
|
require_relative "api/stdout"
|
6
|
+
require_relative "api/stderr"
|
7
|
+
require_relative "api/status"
|
8
|
+
|
6
9
|
require_relative "api/signal"
|
10
|
+
require_relative "api/rm"
|
11
|
+
require_relative "api/port"
|
@@ -15,10 +15,9 @@ module Pytty
|
|
15
15
|
# }.to_json
|
16
16
|
# response = internet.post("#{Pytty::Client.host_url}/v1/stdin/#{id}", headers, [stdin_body])
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
end
|
18
|
+
$stdin.raw!
|
19
|
+
$stdin.echo = false
|
20
|
+
|
22
21
|
async_stdin = Async::IO::Stream.new(
|
23
22
|
Async::IO::Generic.new($stdin)
|
24
23
|
)
|
@@ -60,14 +59,26 @@ module Pytty
|
|
60
59
|
|
61
60
|
response = internet.post("#{Pytty::Client.host_url}/v1/attach/#{id}", headers, [body])
|
62
61
|
response.body.each do |c|
|
63
|
-
|
62
|
+
stream = if c[0] == "1"
|
63
|
+
$stdout
|
64
|
+
else
|
65
|
+
$stderr
|
66
|
+
end
|
67
|
+
|
68
|
+
case c[1..-1]
|
69
|
+
when "\n"
|
70
|
+
stream.print "#{c}\r"
|
71
|
+
else
|
72
|
+
stream.print c[1..-1]
|
73
|
+
end
|
64
74
|
end
|
65
75
|
rescue Async::Wrapper::Cancelled => ex
|
66
76
|
p ["rescued", ex]
|
67
77
|
ensure
|
68
78
|
stdin_task.stop
|
69
|
-
|
70
79
|
internet.close
|
80
|
+
`stty sane`
|
81
|
+
#$stdout.print "\r" #stdin.raw!
|
71
82
|
end
|
72
83
|
end
|
73
84
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Pytty
|
2
|
+
module Client
|
3
|
+
module Api
|
4
|
+
class Port
|
5
|
+
def self.run(id:, from:, to:)
|
6
|
+
internet = Async::HTTP::Internet.new
|
7
|
+
headers = [['accept', 'application/json']]
|
8
|
+
body = {
|
9
|
+
from: from,
|
10
|
+
to: to
|
11
|
+
}.to_json
|
12
|
+
|
13
|
+
response = internet.post("#{Pytty::Client.host_url}/v1/port/#{id}", headers, [body])
|
14
|
+
[response, JSON.parse(response.body.read)]
|
15
|
+
ensure
|
16
|
+
internet.close
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Pytty
|
2
|
+
module Client
|
3
|
+
module Api
|
4
|
+
class Rm
|
5
|
+
def self.run(id:)
|
6
|
+
internet = Async::HTTP::Internet.new
|
7
|
+
headers = [['accept', 'application/json']]
|
8
|
+
body = {}.to_json
|
9
|
+
|
10
|
+
response = internet.post("#{Pytty::Client.host_url}/v1/rm/#{id}", headers, [body])
|
11
|
+
[response, JSON.parse(response.body.read)]
|
12
|
+
ensure
|
13
|
+
internet.close
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Pytty
|
2
|
+
module Client
|
3
|
+
module Api
|
4
|
+
class Status
|
5
|
+
def self.run(id:)
|
6
|
+
internet = Async::HTTP::Internet.new
|
7
|
+
headers = [['accept', 'application/json']]
|
8
|
+
body = {
|
9
|
+
}.to_json
|
10
|
+
|
11
|
+
response = internet.post("#{Pytty::Client.host_url}/v1/status/#{id}", headers, [body])
|
12
|
+
[response, response.read]
|
13
|
+
ensure
|
14
|
+
internet.close
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Pytty
|
2
|
+
module Client
|
3
|
+
module Api
|
4
|
+
class Stderr
|
5
|
+
def self.run(id:)
|
6
|
+
internet = Async::HTTP::Internet.new
|
7
|
+
headers = [['accept', 'application/json']]
|
8
|
+
body = {
|
9
|
+
}.to_json
|
10
|
+
|
11
|
+
response = internet.post("#{Pytty::Client.host_url}/v1/stderr/#{id}", headers, [body])
|
12
|
+
[response, response.body]
|
13
|
+
ensure
|
14
|
+
internet.close
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/pytty/client/cli.rb
CHANGED
@@ -11,6 +11,9 @@ require_relative "cli/rm_command"
|
|
11
11
|
require_relative "cli/signal_command"
|
12
12
|
require_relative "cli/attach_command"
|
13
13
|
require_relative "cli/stdout_command"
|
14
|
+
require_relative "cli/stderr_command"
|
15
|
+
require_relative "cli/status_command"
|
16
|
+
require_relative "cli/port_command"
|
14
17
|
|
15
18
|
require_relative "cli/run_command"
|
16
19
|
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'async'
|
3
|
+
require 'async/http'
|
4
|
+
require 'async/http/internet'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Pytty
|
8
|
+
module Client
|
9
|
+
module Cli
|
10
|
+
class PortCommand < Clamp::Command
|
11
|
+
parameter "ID", "id"
|
12
|
+
parameter "PORTS", "ports"
|
13
|
+
|
14
|
+
def execute
|
15
|
+
Async.run do
|
16
|
+
from,to = ports.split(":")
|
17
|
+
Pytty::Client::Api::Port.run id: id, from: from, to: to
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -24,12 +24,9 @@ module Pytty
|
|
24
24
|
end
|
25
25
|
|
26
26
|
Async.run do
|
27
|
-
internet = Async::HTTP::Internet.new
|
28
|
-
headers = [['accept', 'application/json']]
|
29
|
-
body = {}.to_json
|
30
|
-
|
31
27
|
for id in ids do
|
32
|
-
response =
|
28
|
+
response, body = Pytty::Client::Api::Rm.run id: id
|
29
|
+
|
33
30
|
if response.status == 200
|
34
31
|
puts id
|
35
32
|
else
|
@@ -37,8 +34,6 @@ module Pytty
|
|
37
34
|
exit 1
|
38
35
|
end
|
39
36
|
end
|
40
|
-
ensure
|
41
|
-
internet.close
|
42
37
|
end
|
43
38
|
end
|
44
39
|
end
|
@@ -20,6 +20,9 @@ module Pytty
|
|
20
20
|
subcommand ["signal"], "signal", SignalCommand
|
21
21
|
subcommand ["attach"], "attach", AttachCommand
|
22
22
|
subcommand ["stdout"], "stdout", StdoutCommand
|
23
|
+
subcommand ["stderr"], "stderr", StderrCommand
|
24
|
+
subcommand ["status"], "status", StatusCommand
|
25
|
+
subcommand ["port"], "port", PortCommand
|
23
26
|
|
24
27
|
def self.run
|
25
28
|
super
|
@@ -31,4 +34,3 @@ module Pytty
|
|
31
34
|
end
|
32
35
|
end
|
33
36
|
end
|
34
|
-
|
@@ -11,8 +11,10 @@ module Pytty
|
|
11
11
|
parameter "CMD ...", "command"
|
12
12
|
option ["-i","--interactive"], :flag, "interactive"
|
13
13
|
option ["-t","--tty"], :flag, "tty"
|
14
|
+
|
14
15
|
option ["-d","--detach"], :flag, "detach"
|
15
16
|
option ["--name"], "name", "name"
|
17
|
+
option ["--rm"], :flag, "rm"
|
16
18
|
|
17
19
|
def execute
|
18
20
|
Async.run do |task|
|
@@ -22,16 +24,23 @@ module Pytty
|
|
22
24
|
exit 1
|
23
25
|
end
|
24
26
|
process_yield = Pytty::Client::ProcessYield.from_json body
|
25
|
-
|
27
|
+
|
28
|
+
attach_task = unless detach?
|
26
29
|
task.async do
|
27
30
|
process_yield.attach interactive: interactive?
|
31
|
+
process_yield.rm
|
28
32
|
end
|
29
33
|
end
|
30
34
|
|
31
|
-
process_yield.spawn tty: tty?, interactive: interactive?
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
+
response, body = process_yield.spawn tty: tty?, interactive: interactive?
|
36
|
+
if response.status == 200
|
37
|
+
if detach?
|
38
|
+
puts process_yield.id
|
39
|
+
end
|
40
|
+
else
|
41
|
+
puts body
|
42
|
+
attach_task.stop
|
43
|
+
print "\r" unless detach?
|
35
44
|
end
|
36
45
|
end
|
37
46
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pytty
|
4
|
+
module Client
|
5
|
+
module Cli
|
6
|
+
class StatusCommand < Clamp::Command
|
7
|
+
parameter "ID ...", "id"
|
8
|
+
|
9
|
+
def execute
|
10
|
+
Async.run do
|
11
|
+
for id in id_list do
|
12
|
+
response, body = Pytty::Client::Api::Status.run id: id
|
13
|
+
if response.status == 200
|
14
|
+
puts body
|
15
|
+
else
|
16
|
+
puts body
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'async'
|
3
|
+
require 'async/http'
|
4
|
+
require 'async/http/internet'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Pytty
|
8
|
+
module Client
|
9
|
+
module Cli
|
10
|
+
class StderrCommand < Clamp::Command
|
11
|
+
parameter "ID", "id"
|
12
|
+
|
13
|
+
def execute
|
14
|
+
Async.run do
|
15
|
+
response, body = Pytty::Client::Api::Stderr.run id: id
|
16
|
+
if response.status == 200
|
17
|
+
puts body.read
|
18
|
+
else
|
19
|
+
puts body.read
|
20
|
+
exit 1
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -12,7 +12,13 @@ module Pytty
|
|
12
12
|
|
13
13
|
def execute
|
14
14
|
Async.run do
|
15
|
-
Pytty::Client::Api::Stdout.run id: id
|
15
|
+
response, body = Pytty::Client::Api::Stdout.run id: id
|
16
|
+
if response.status == 200
|
17
|
+
puts body.read
|
18
|
+
else
|
19
|
+
puts body.read
|
20
|
+
exit 1
|
21
|
+
end
|
16
22
|
end
|
17
23
|
end
|
18
24
|
end
|
@@ -20,9 +20,11 @@ module Pytty
|
|
20
20
|
status: json.fetch("status")
|
21
21
|
})
|
22
22
|
end
|
23
|
+
|
23
24
|
def running?
|
24
25
|
!@pid.nil?
|
25
26
|
end
|
27
|
+
|
26
28
|
def to_s
|
27
29
|
fields = []
|
28
30
|
fields << @id
|
@@ -32,6 +34,10 @@ module Pytty
|
|
32
34
|
fields.join("\t")
|
33
35
|
end
|
34
36
|
|
37
|
+
def rm
|
38
|
+
Pytty::Client::Api::Rm.run id: @id
|
39
|
+
end
|
40
|
+
|
35
41
|
def spawn(tty:, interactive:)
|
36
42
|
Pytty::Client::Api::Spawn.run id: @id, tty: tty, interactive: interactive
|
37
43
|
end
|
@@ -15,20 +15,29 @@ module Pytty
|
|
15
15
|
end
|
16
16
|
|
17
17
|
resp = case params["component"]
|
18
|
-
when "stdout"
|
18
|
+
when "stdout","stderr"
|
19
19
|
process_yield = Pytty::Daemon.yields[params["id"]]
|
20
20
|
return [404, {'content-type' => 'text/html; charset=utf-8'}, ["does not exist"]] unless process_yield
|
21
21
|
|
22
|
-
|
22
|
+
stream_path = if params["component"] == "stdout"
|
23
|
+
process_yield.stdout_path
|
24
|
+
else
|
25
|
+
process_yield.stderr_path
|
26
|
+
end
|
27
|
+
|
28
|
+
stream = Async::IO::Stream.new(
|
29
|
+
File.open stream_path, "r"
|
30
|
+
)
|
23
31
|
|
32
|
+
body = Async::HTTP::Body::Writable.new
|
24
33
|
begin
|
25
|
-
|
26
|
-
while c = our_stdout.read
|
34
|
+
while c = stream.read
|
27
35
|
body.write c
|
28
36
|
end
|
29
37
|
rescue Exception => ex
|
30
|
-
p ex
|
38
|
+
p "read", ex
|
31
39
|
ensure
|
40
|
+
stream.close if stream
|
32
41
|
body.close
|
33
42
|
end
|
34
43
|
|
@@ -37,17 +46,25 @@ module Pytty
|
|
37
46
|
process_yield = Pytty::Daemon.yields[params["id"]]
|
38
47
|
return [404, {'content-type' => 'text/html; charset=utf-8'}, ["does not exist"]] unless process_yield
|
39
48
|
|
40
|
-
puts "got attach: #{req.object_id}"
|
41
49
|
body = Async::HTTP::Body::Writable.new
|
42
50
|
|
43
|
-
Async::Task.current.async do |task|
|
51
|
+
stderr_done = Async::Task.current.async do |task|
|
52
|
+
notification = process_yield.add_stderr body
|
53
|
+
notification.wait
|
54
|
+
end
|
55
|
+
|
56
|
+
stdout_done = Async::Task.current.async do |task|
|
44
57
|
notification = process_yield.add_stdout body
|
45
58
|
notification.wait
|
46
|
-
|
47
|
-
|
48
|
-
|
59
|
+
end
|
60
|
+
|
61
|
+
Async::Task.current.async do |task|
|
62
|
+
stderr_done.wait
|
63
|
+
p ["stderr done"]
|
64
|
+
stdout_done.wait
|
65
|
+
p ["stderr done"]
|
49
66
|
ensure
|
50
|
-
|
67
|
+
p ["closing attach:", req.object_id]
|
51
68
|
body.close
|
52
69
|
end
|
53
70
|
|
@@ -55,7 +72,7 @@ module Pytty
|
|
55
72
|
when "ps","yield"
|
56
73
|
status, output = Pytty::Daemon::Components::Handler.handle component: params["component"], params: body
|
57
74
|
[status, {"Content-Type" => "application/json"}, [output.to_json]]
|
58
|
-
when "spawn","rm","signal","stdin"
|
75
|
+
when "spawn","rm","signal","stdin","status","port"
|
59
76
|
status, output = Pytty::Daemon::Components::YieldHandler.handle component: params["component"], id: params["id"], params: body
|
60
77
|
[status, {"Content-Type" => "application/json"}, [output.to_json]]
|
61
78
|
when "ws"
|
@@ -74,4 +91,4 @@ module Pytty
|
|
74
91
|
end
|
75
92
|
end
|
76
93
|
end
|
77
|
-
end
|
94
|
+
end
|
@@ -9,10 +9,48 @@ module Pytty
|
|
9
9
|
return [404, "not found"] unless process_yield
|
10
10
|
|
11
11
|
return case component
|
12
|
+
when "port"
|
13
|
+
endpoint = Async::IO::Endpoint.tcp('0.0.0.0', params["from"].to_i)
|
14
|
+
endpoint.accept do |client|
|
15
|
+
process_yield.spawn unless process_yield.running?
|
16
|
+
p ["port accept", client.object_id]
|
17
|
+
upstream = Async::IO::Endpoint.tcp('0.0.0.0', params["to"].to_i)
|
18
|
+
peer = nil
|
19
|
+
upstream.connect do |peer|
|
20
|
+
Async::Task.current.async do |task|
|
21
|
+
while rata = peer.read(1)
|
22
|
+
client.write rata
|
23
|
+
end
|
24
|
+
client.close
|
25
|
+
end
|
26
|
+
|
27
|
+
while data = client.read(2)
|
28
|
+
p data
|
29
|
+
peer.write(data)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
rescue Errno::ECONNREFUSED
|
33
|
+
sleep 0.1
|
34
|
+
p ["port upstream retry"]
|
35
|
+
retry
|
36
|
+
# rescue Async::Wrapper::Cancelled
|
37
|
+
# # ???
|
38
|
+
rescue Exception => ex
|
39
|
+
p ["accex", ex]
|
40
|
+
ensure
|
41
|
+
peer.close
|
42
|
+
client.close
|
43
|
+
end
|
44
|
+
|
45
|
+
[200, "ok"]
|
12
46
|
when "stdin"
|
13
47
|
process_yield.stdin.enqueue params["c"]
|
14
48
|
|
15
49
|
[200, "ok"]
|
50
|
+
when "status"
|
51
|
+
return [500, "still running"] if process_yield.running?
|
52
|
+
|
53
|
+
[200, process_yield.status]
|
16
54
|
when "signal"
|
17
55
|
return [500, "not running"] unless process_yield.running?
|
18
56
|
|
@@ -21,8 +59,7 @@ module Pytty
|
|
21
59
|
[200, "ok"]
|
22
60
|
when "spawn"
|
23
61
|
return [500, "already running"] if process_yield.running?
|
24
|
-
|
25
|
-
if process_yield.spawn
|
62
|
+
if process_yield.spawn tty: params["tty"], interactive: params["interactive"]
|
26
63
|
Pytty::Daemon.dump
|
27
64
|
else
|
28
65
|
return [500, "could not spawn"]
|
@@ -31,6 +68,8 @@ module Pytty
|
|
31
68
|
[200, "ok"]
|
32
69
|
when "rm"
|
33
70
|
process_yield.signal("KILL") if process_yield.running?
|
71
|
+
process_yield.cleanup
|
72
|
+
|
34
73
|
Pytty::Daemon.yields.delete process_yield.id
|
35
74
|
Pytty::Daemon.dump
|
36
75
|
|
@@ -43,4 +82,3 @@ module Pytty
|
|
43
82
|
end
|
44
83
|
end
|
45
84
|
end
|
46
|
-
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "securerandom"
|
3
3
|
require "pty"
|
4
|
+
require "open3"
|
4
5
|
|
5
6
|
module Pytty
|
6
7
|
module Daemon
|
@@ -13,12 +14,17 @@ module Pytty
|
|
13
14
|
@status = nil
|
14
15
|
@id = id || SecureRandom.uuid
|
15
16
|
|
16
|
-
@stdout = nil
|
17
17
|
@stdouts = {}
|
18
|
+
@stderrs = {}
|
19
|
+
|
20
|
+
@stdout_path = File.join Pytty::Daemon.pytty_path, "#{@id}.stdout"
|
21
|
+
@stderr_path = File.join Pytty::Daemon.pytty_path, "#{@id}.stderr"
|
22
|
+
|
18
23
|
@stdin = Async::Queue.new
|
19
24
|
end
|
20
25
|
|
21
|
-
attr_reader :id, :cmd, :pid, :status
|
26
|
+
attr_reader :id, :cmd, :pid, :status
|
27
|
+
attr_reader :stdout_path, :stderr_path
|
22
28
|
attr_accessor :stdin
|
23
29
|
|
24
30
|
def add_stdout(stdout)
|
@@ -26,8 +32,14 @@ module Pytty
|
|
26
32
|
@stdouts[notification] = stdout
|
27
33
|
notification
|
28
34
|
end
|
35
|
+
def add_stderr(stderr)
|
36
|
+
notification = Async::Notification.new
|
37
|
+
@stderrs[notification] = stderr
|
38
|
+
notification
|
39
|
+
end
|
29
40
|
|
30
41
|
def running?
|
42
|
+
# test by killing?
|
31
43
|
!@pid.nil?
|
32
44
|
end
|
33
45
|
|
@@ -42,86 +54,172 @@ module Pytty
|
|
42
54
|
}.to_json
|
43
55
|
end
|
44
56
|
|
45
|
-
def
|
46
|
-
return false if running?
|
57
|
+
def cleanup
|
47
58
|
@status = nil
|
48
59
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
60
|
+
File.unlink @stdout_path if File.exist? @stdout_path
|
61
|
+
File.unlink @stderr_path if File.exist? @stderr_path
|
62
|
+
end
|
63
|
+
|
64
|
+
def spawn(tty: false, interactive: false)
|
65
|
+
return false if running?
|
66
|
+
cleanup
|
53
67
|
|
54
|
-
stdout_path = File.join(Pytty::Daemon.pytty_path, @id)
|
55
|
-
File.unlink stdout_path if File.exist? stdout_path
|
56
68
|
stdout_appender = Async::IO::Stream.new(
|
57
|
-
File.open stdout_path, "a"
|
69
|
+
File.open @stdout_path, "a"
|
58
70
|
)
|
59
|
-
|
60
|
-
File.open
|
71
|
+
stderr_appender = Async::IO::Stream.new(
|
72
|
+
File.open @stderr_path, "a"
|
61
73
|
)
|
74
|
+
|
75
|
+
executable, *args = @cmd
|
76
|
+
# @env.merge!({
|
77
|
+
# "TERM" => "xterm"
|
78
|
+
# })
|
79
|
+
|
62
80
|
Async::Task.current.async do |task|
|
63
|
-
real_stdout, real_stdin,
|
64
|
-
|
81
|
+
real_stdout, real_stdin, real_stderr, @pid, wait_thr = begin
|
82
|
+
if tty
|
83
|
+
stderr_reader, stderr_writer = IO.pipe
|
84
|
+
p ["spawn", "PTY"]
|
85
|
+
#TODO: if bash is started, no prompt is written in stderr or stdout
|
86
|
+
p_stdout, p_stdin, pid = PTY.spawn @env, executable, *args, err: stderr_writer
|
87
|
+
|
88
|
+
[p_stdout, p_stdin, stderr_reader, pid]
|
89
|
+
else
|
90
|
+
p_stdin, p_stdout, p_stderr, p_wait_thr = Open3.popen3 @env, executable, *args, {}
|
91
|
+
[p_stdout, p_stdin, p_stderr, p_wait_thr.pid, p_wait_thr]
|
92
|
+
end
|
93
|
+
rescue Errno::ENOENT => ex
|
94
|
+
raise unless ex.message == "No such file or directory - #{executable}"
|
95
|
+
end
|
96
|
+
|
97
|
+
next unless @pid #command failed to spawn
|
98
|
+
|
65
99
|
async_stdout = Async::IO::Generic.new real_stdout
|
66
100
|
async_stdin = Async::IO::Generic.new real_stdin
|
101
|
+
async_stderr = Async::IO::Generic.new real_stderr
|
67
102
|
|
68
|
-
task_stdin_writer =
|
69
|
-
|
70
|
-
|
103
|
+
task_stdin_writer = if interactive
|
104
|
+
task.async do |subtask|
|
105
|
+
p ["task_stdin_writer", "started"]
|
106
|
+
while c = @stdin.dequeue do
|
107
|
+
async_stdin.write c
|
108
|
+
end
|
109
|
+
rescue Async::Stop => ex
|
110
|
+
puts "async_stdin#write Async::Stop"
|
111
|
+
rescue Exception => ex
|
112
|
+
puts "async_stdin#write: #{ex.inspect}"
|
113
|
+
ensure
|
114
|
+
async_stdin.close
|
115
|
+
p ["async_stdin", "closed"]
|
71
116
|
end
|
72
|
-
|
117
|
+
else
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
|
121
|
+
task_stderr_writer = task.async do
|
122
|
+
p ["task_stderr_writer", "started"]
|
123
|
+
while c = async_stderr.read(1) do
|
124
|
+
stderr_appender.write c
|
125
|
+
stderr_appender.flush
|
126
|
+
|
127
|
+
@stderrs.each do |notification, stderr|
|
128
|
+
begin
|
129
|
+
stderr.write "2#{c}"
|
130
|
+
rescue Async::HTTP::Body::Writable::Closed
|
131
|
+
puts "signaling error"
|
132
|
+
notification.signal
|
133
|
+
@stderrs.delete notification
|
134
|
+
rescue => ex
|
135
|
+
raise ex
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
p ["task_stderr_writer", "async_stderr has no more read"]
|
73
140
|
rescue Exception => ex
|
74
|
-
|
141
|
+
p ["async_stderr ex:", ex]
|
142
|
+
ensure
|
143
|
+
stderr_appender.flush
|
144
|
+
stderr_appender.close
|
145
|
+
puts "stderr_appender closed"
|
146
|
+
async_stderr.close
|
147
|
+
puts "async_stderr closed"
|
75
148
|
end
|
76
149
|
|
77
|
-
task_stdout_writer = task.async do
|
150
|
+
task_stdout_writer = task.async do
|
151
|
+
p ["task_stdout_writer", "started"]
|
152
|
+
|
78
153
|
while c = async_stdout.read(1)
|
79
154
|
stdout_appender.write c
|
80
155
|
stdout_appender.flush
|
81
156
|
|
82
157
|
@stdouts.each do |notification, stdout|
|
83
158
|
begin
|
84
|
-
stdout.write c
|
159
|
+
stdout.write "1#{c}"
|
85
160
|
rescue Errno::EPIPE, Errno::EPROTOTYPE => ex
|
86
161
|
notification.signal
|
87
162
|
@stdouts.delete notification
|
88
163
|
end
|
89
164
|
end
|
90
165
|
end
|
166
|
+
p ["task_stdout_writer", "stopped"]
|
91
167
|
rescue Async::Stop
|
92
168
|
signal "kill"
|
93
169
|
rescue Exception => ex
|
94
|
-
p ["async_stdout
|
170
|
+
p ["async_stdout", ex]
|
95
171
|
ensure
|
96
|
-
|
97
|
-
|
98
|
-
@status = if $?.exitstatus
|
99
|
-
$?.exitstatus
|
172
|
+
process_status = if wait_thr
|
173
|
+
wait_thr.value
|
100
174
|
else
|
101
|
-
|
175
|
+
begin
|
176
|
+
Process.wait(@pid)
|
177
|
+
$?
|
178
|
+
rescue Errno::ECHILD => ex
|
179
|
+
raise ex unless ex.message == "No child processes"
|
180
|
+
puts "NO CHILD PROCESS"
|
181
|
+
nil
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
@status = if process_status && process_status.exitstatus
|
186
|
+
process_status.exitstatus
|
187
|
+
else
|
188
|
+
Signal.signame process_status.termsig
|
102
189
|
end
|
103
|
-
puts "exited #{@id} with status: #{@status}"
|
104
190
|
|
191
|
+
puts "exited #{@id} with status: #{@status}"
|
105
192
|
@pid = nil
|
106
|
-
|
193
|
+
task_stdin_writer.stop if task_stdin_writer
|
107
194
|
|
108
195
|
@stdouts.each do |notification, stdout|
|
109
196
|
notification.signal
|
110
197
|
@stdouts.delete notification
|
111
198
|
end
|
199
|
+
@stderrs.each do |notification, stderr|
|
200
|
+
notification.signal
|
201
|
+
@stdouts.delete notification
|
202
|
+
end
|
112
203
|
Pytty::Daemon.dump
|
113
204
|
end
|
114
|
-
end
|
115
|
-
|
116
|
-
|
117
|
-
|
205
|
+
end
|
206
|
+
|
207
|
+
if @pid
|
208
|
+
p ["spawned", id]
|
209
|
+
return true
|
210
|
+
else
|
211
|
+
p ["failed to spawn"]
|
212
|
+
@status = 127
|
213
|
+
return false
|
214
|
+
end
|
118
215
|
end
|
119
216
|
|
120
217
|
def signal(sig)
|
121
218
|
return unless @pid
|
122
219
|
Process.kill(sig.upcase, @pid)
|
220
|
+
rescue Errno::ESRCH => ex
|
221
|
+
raise ex unless ex.message == "No such process"
|
123
222
|
end
|
124
223
|
end
|
125
224
|
end
|
126
225
|
end
|
127
|
-
|
data/lib/pytty/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pytty
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matti Paksula
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-01-
|
11
|
+
date: 2019-01-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: clamp
|
@@ -190,19 +190,26 @@ files:
|
|
190
190
|
- lib/pytty/client.rb
|
191
191
|
- lib/pytty/client/api.rb
|
192
192
|
- lib/pytty/client/api/attach.rb
|
193
|
+
- lib/pytty/client/api/port.rb
|
193
194
|
- lib/pytty/client/api/ps.rb
|
195
|
+
- lib/pytty/client/api/rm.rb
|
194
196
|
- lib/pytty/client/api/signal.rb
|
195
197
|
- lib/pytty/client/api/spawn.rb
|
198
|
+
- lib/pytty/client/api/status.rb
|
199
|
+
- lib/pytty/client/api/stderr.rb
|
196
200
|
- lib/pytty/client/api/stdout.rb
|
197
201
|
- lib/pytty/client/api/yield.rb
|
198
202
|
- lib/pytty/client/cli.rb
|
199
203
|
- lib/pytty/client/cli/attach_command.rb
|
204
|
+
- lib/pytty/client/cli/port_command.rb
|
200
205
|
- lib/pytty/client/cli/ps_command.rb
|
201
206
|
- lib/pytty/client/cli/rm_command.rb
|
202
207
|
- lib/pytty/client/cli/root_command.rb
|
203
208
|
- lib/pytty/client/cli/run_command.rb
|
204
209
|
- lib/pytty/client/cli/signal_command.rb
|
205
210
|
- lib/pytty/client/cli/spawn_command.rb
|
211
|
+
- lib/pytty/client/cli/status_command.rb
|
212
|
+
- lib/pytty/client/cli/stderr_command.rb
|
206
213
|
- lib/pytty/client/cli/stdout_command.rb
|
207
214
|
- lib/pytty/client/cli/stream_command.rb
|
208
215
|
- lib/pytty/client/cli/yield_command.rb
|