pytty 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -4
  3. data/README.md +2 -0
  4. data/build/linux-debian-deps.sh +7 -0
  5. data/build/linux.sh +7 -0
  6. data/build/macos-deps.sh +11 -0
  7. data/build/macos.sh +8 -0
  8. data/lib/pytty/client.rb +7 -0
  9. data/lib/pytty/client/api.rb +2 -0
  10. data/lib/pytty/client/api/attach.rb +23 -12
  11. data/lib/pytty/client/api/ps.rb +1 -1
  12. data/lib/pytty/client/api/signal.rb +20 -0
  13. data/lib/pytty/client/api/spawn.rb +2 -2
  14. data/lib/pytty/client/api/stdout.rb +19 -0
  15. data/lib/pytty/client/api/yield.rb +3 -2
  16. data/lib/pytty/client/cli.rb +1 -0
  17. data/lib/pytty/client/cli/attach_command.rb +2 -1
  18. data/lib/pytty/client/cli/kill_command.rb +4 -2
  19. data/lib/pytty/client/cli/ps_command.rb +5 -4
  20. data/lib/pytty/client/cli/rm_command.rb +7 -2
  21. data/lib/pytty/client/cli/root_command.rb +1 -0
  22. data/lib/pytty/client/cli/run_command.rb +9 -4
  23. data/lib/pytty/client/cli/signal_command.rb +6 -9
  24. data/lib/pytty/client/cli/spawn_command.rb +7 -1
  25. data/lib/pytty/client/cli/stdout_command.rb +22 -0
  26. data/lib/pytty/client/cli/yield_command.rb +3 -7
  27. data/lib/pytty/client/process_yield.rb +10 -4
  28. data/lib/pytty/daemon.rb +5 -1
  29. data/lib/pytty/daemon/api/router.rb +28 -17
  30. data/lib/pytty/daemon/cli/root_command.rb +5 -1
  31. data/lib/pytty/daemon/cli/serve_command.rb +8 -7
  32. data/lib/pytty/daemon/components.rb +1 -5
  33. data/lib/pytty/daemon/components/handler.rb +4 -42
  34. data/lib/pytty/daemon/components/yield_handler.rb +46 -0
  35. data/lib/pytty/daemon/process_yield.rb +62 -36
  36. data/lib/pytty/version.rb +1 -1
  37. metadata +10 -7
  38. data/bin/build +0 -15
  39. data/lib/pytty/daemon/components/http_handler.rb +0 -54
  40. data/lib/pytty/daemon/components/run.rb +0 -19
  41. data/lib/pytty/daemon/components/stream.rb +0 -44
  42. data/lib/pytty/daemon/components/web_socket_handler.rb +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd905d25dba15816b22209e67dfd888c3379e3f0ef01d18611b86c21e49a6666
4
- data.tar.gz: 7b215e1046ea4a67b3bc619ccb6603ba8f540a8473a6060b6f47d9921187296e
3
+ metadata.gz: 449a17a0c2d92d72ceddac8091af43e21e16d730d063458b58411385cd659fa2
4
+ data.tar.gz: db5e5b5446c3d18b16d886fcd5409380fcd49ed1f6d120bdc7f29f3e6740146a
5
5
  SHA512:
6
- metadata.gz: 19c80222d3401ca8e1b84878cba3a2fd9f858de56382388104533a03a9ec88ac8cb83594984218273467b8c83e9f94c640b28985462b1f54e76a1fda562dded9
7
- data.tar.gz: 1056958148e94f81ba23f50970e4fc399300fc93fe2854518f3a551f46b1128c61da8a9166fab9349bb6ebaa67bfa66b1eb46e64c10ee9c71e5aca0320a58e76
6
+ metadata.gz: cc1f6302b922e313c85327d3348d116905d2f889260695f680c946eedcc769c84bf510a3b0439ed499e3f8528f2c8c8bde89cce0a5bb2e29a600ab04987a7631
7
+ data.tar.gz: 59553a444c16d811403ce3e288aaf44d454ed1bb100896b4b6a30645664554b6004dfc8e748ee66d81227ce7792a1a43c4c58d6d63a549f43a2ea45b879e2811
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pytty (0.3.0)
4
+ pytty (0.4.0)
5
5
  async-websocket
6
6
  clamp (~> 1.3)
7
7
  falcon
@@ -10,7 +10,7 @@ PATH
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
- async (1.12.0)
13
+ async (1.13.0)
14
14
  nio4r (~> 2.3)
15
15
  timers (~> 4.1)
16
16
  async-container (0.8.1)
@@ -62,7 +62,6 @@ GEM
62
62
  http-hpack (0.1.1)
63
63
  http-protocol (0.10.1)
64
64
  http-hpack (~> 0.1.0)
65
- kommando (0.1.2)
66
65
  listen (3.1.5)
67
66
  rb-fsevent (~> 0.9, >= 0.9.4)
68
67
  rb-inotify (~> 0.9, >= 0.9.7)
@@ -121,7 +120,6 @@ DEPENDENCIES
121
120
  guard-bundler
122
121
  guard-process
123
122
  guard-rspec
124
- kommando
125
123
  pytty!
126
124
  rake (~> 10.0)
127
125
  rspec (~> 3.0)
data/README.md CHANGED
@@ -1 +1,3 @@
1
1
  # pytty
2
+
3
+ Process Yield TTY
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env sh
2
+ set -e
3
+
4
+ apt-get update && apt-get install -y squashfs-tools build-essential bison curl
5
+
6
+ curl -L https://github.com/kontena/ruby-packer/releases/download/0.5.0%2Bextra7/rubyc-0.5.0+extra7-linux-amd64.gz | gunzip > /usr/local/bin/rubyc
7
+ chmod +x /usr/local/bin/rubyc
data/build/linux.sh ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env sh
2
+ set -e
3
+
4
+ version=${TRAVIS_TAG#"v"}
5
+ package="tmp/pyttyd-linux-amd64-${version}"
6
+ rubyc -o "$package" -d /tmp/pytty-build --make-args="-j16 --silent" pyttyd
7
+ ./"$package" --version
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env sh
2
+ set -e
3
+
4
+ export HOMEBREW_NO_AUTO_UPDATE=1
5
+ brew install squashfs || brew upgrade squashfs || true
6
+ brew install openssl || brew upgrade openssl || true
7
+
8
+ curl -sL https://curl.haxx.se/ca/cacert.pem > /usr/local/etc/openssl/cacert.pem
9
+
10
+ curl -sL https://dl.bintray.com/kontena/ruby-packer/0.5.0-dev/rubyc-0.5.0-extra2-darwin-amd64.gz | gunzip > /usr/local/bin/rubyc
11
+ chmod +x /usr/local/bin/rubyc
data/build/macos.sh ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env sh
2
+ set -ue
3
+
4
+ version=${TRAVIS_TAG#"v"}
5
+ package="tmp/pyttyd-darwin-amd64-${version}"
6
+
7
+ rubyc --openssl-dir=/usr/local/etc/openssl -o "$package" -d /tmp/pytty-build-macos --make-args="-j16 --silent" pyttyd
8
+ ./"$package" --version
data/lib/pytty/client.rb CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  module Pytty
4
4
  module Client
5
+ def self.host_url
6
+ if ENV["PYTTY_HOST"]
7
+ ENV["PYTTY_HOST"]
8
+ else
9
+ "http://localhost:1234"
10
+ end
11
+ end
5
12
  end
6
13
  end
7
14
 
@@ -2,3 +2,5 @@ require_relative "api/ps"
2
2
  require_relative "api/yield"
3
3
  require_relative "api/attach"
4
4
  require_relative "api/spawn"
5
+ require_relative "api/stdout"
6
+ require_relative "api/signal"
@@ -2,8 +2,8 @@ module Pytty
2
2
  module Client
3
3
  module Api
4
4
  class Attach
5
- def self.run(id:)
6
- Async::Task.current.async do |stdin_task|
5
+ def self.run(id:, interactive:)
6
+ stdin_task = Async::Task.current.async do
7
7
  internet = Async::HTTP::Internet.new
8
8
  headers = [['accept', 'application/json']]
9
9
  body = {}.to_json
@@ -17,27 +17,35 @@ module Pytty
17
17
  stdin_body = {
18
18
  c: "\f"
19
19
  }.to_json
20
- response = internet.post("http://localhost:1234/v1/stdin/#{id}", headers, [stdin_body])
20
+ response = internet.post("#{Pytty::Client.host_url}/v1/stdin/#{id}", headers, [stdin_body])
21
21
 
22
22
  detach_sequence_started = false
23
23
  while c = async_stdin.read(1) do
24
+ detach = false
24
25
  case c
25
26
  when "\x10"
26
27
  detach_sequence_started = true
27
28
  next
28
29
  when "\x11"
29
- if detach_sequence_started
30
- detach_sequence_started = false
31
- exit 0
32
- end
30
+ detach = true if detach_sequence_started
31
+ when "\x03"
32
+ detach = true unless interactive
33
+ end
34
+
35
+ if detach
36
+ puts ""
37
+ puts "detached.\r"
38
+ exit 0
33
39
  end
34
40
 
35
41
  detach_sequence_started = false
36
42
 
37
- stdin_body = {
38
- c: c
39
- }.to_json
40
- response = internet.post("http://localhost:1234/v1/stdin/#{id}", headers, [stdin_body])
43
+ if interactive
44
+ stdin_body = {
45
+ c: c
46
+ }.to_json
47
+ response = internet.post("#{Pytty::Client.host_url}/v1/stdin/#{id}", headers, [stdin_body])
48
+ end
41
49
  end
42
50
  end
43
51
 
@@ -46,12 +54,15 @@ module Pytty
46
54
  headers = [['accept', 'application/json']]
47
55
  body = {}.to_json
48
56
 
49
- response = internet.post("http://localhost:1234/v1/attach/#{id}", headers, [body])
57
+ response = internet.post("#{Pytty::Client.host_url}/v1/attach/#{id}", headers, [body])
50
58
  response.body.each do |c|
51
59
  print c
52
60
  end
53
61
  rescue Async::Wrapper::Cancelled => ex
62
+ p ["rescued", ex]
54
63
  ensure
64
+ stdin_task.stop
65
+
55
66
  internet.close
56
67
  end
57
68
  end
@@ -7,7 +7,7 @@ module Pytty
7
7
  headers = [['accept', 'application/json']]
8
8
  body = {}.to_json
9
9
 
10
- response = internet.post("http://localhost:1234/v1/ps", headers, [body])
10
+ response = internet.post("#{Pytty::Client.host_url}/v1/ps", headers, [body])
11
11
  JSON.parse(response.body.read)
12
12
  ensure
13
13
  internet.close
@@ -0,0 +1,20 @@
1
+ module Pytty
2
+ module Client
3
+ module Api
4
+ class Signal
5
+ def self.run(id:, signal:)
6
+ internet = Async::HTTP::Internet.new
7
+ headers = [['accept', 'application/json']]
8
+ body = {
9
+ signal: signal
10
+ }.to_json
11
+
12
+ response = internet.post("#{Pytty::Client.host_url}/v1/signal/#{id}", headers, [body])
13
+ [response, JSON.parse(response.read)]
14
+ ensure
15
+ internet.close
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -10,8 +10,8 @@ module Pytty
10
10
  interactive: interactive
11
11
  }.to_json
12
12
 
13
- response = internet.post("http://localhost:1234/v1/spawn/#{id}", headers, [body])
14
- JSON.parse(response.read)
13
+ response = internet.post("#{Pytty::Client.host_url}/v1/spawn/#{id}", headers, [body])
14
+ [response, JSON.parse(response.read)]
15
15
  ensure
16
16
  internet.close
17
17
  end
@@ -0,0 +1,19 @@
1
+ module Pytty
2
+ module Client
3
+ module Api
4
+ class Stdout
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/stdout/#{id}", headers, [body])
12
+ puts response.read
13
+ ensure
14
+ internet.close
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -2,7 +2,7 @@ module Pytty
2
2
  module Client
3
3
  module Api
4
4
  class Yield
5
- def self.run(cmd:, env:)
5
+ def self.run(cmd:, id:, env:)
6
6
  internet = Async::HTTP::Internet.new
7
7
  headers = [['accept', 'application/json']]
8
8
 
@@ -12,11 +12,12 @@ module Pytty
12
12
  }.merge env
13
13
 
14
14
  body = {
15
+ id: id,
15
16
  cmd: cmd,
16
17
  env: term_env
17
18
  }.to_json
18
19
 
19
- response = internet.post("http://localhost:1234/v1/yield", headers, [body])
20
+ response = internet.post("#{Pytty::Client.host_url}/v1/yield", headers, [body])
20
21
  JSON.parse(response.body.read)
21
22
  end
22
23
  end
@@ -10,5 +10,6 @@ require_relative "cli/rm_command"
10
10
  require_relative "cli/spawn_command"
11
11
  require_relative "cli/signal_command"
12
12
  require_relative "cli/attach_command"
13
+ require_relative "cli/stdout_command"
13
14
 
14
15
  require_relative "cli/root_command"
@@ -9,10 +9,11 @@ module Pytty
9
9
  module Cli
10
10
  class AttachCommand < Clamp::Command
11
11
  parameter "ID", "id"
12
+ option ["-i","--interactive"], :flag, "interactive"
12
13
 
13
14
  def execute
14
15
  Async.run do
15
- Pytty::Client::Api::Attach.run id: id
16
+ Pytty::Client::Api::Attach.run id: id, interactive: interactive?
16
17
  end
17
18
  end
18
19
  end
@@ -14,10 +14,12 @@ module Pytty
14
14
  Async.run do
15
15
  internet = Async::HTTP::Internet.new
16
16
  headers = [['accept', 'application/json']]
17
- body = {}.to_json
17
+ body = {
18
+ signal: "KILL"
19
+ }.to_json
18
20
 
19
21
  for id in id_list do
20
- response = internet.post("http://localhost:1234/v1/kill/#{id}", headers, [body])
22
+ response = internet.post("#{Pytty::Client.host_url}/v1/signal/#{id}", headers, [body])
21
23
  puts response.read
22
24
  end
23
25
  ensure
@@ -11,17 +11,18 @@ module Pytty
11
11
  option ["-q","--quiet"], :flag, "quiet"
12
12
 
13
13
  def execute
14
- process_yields = Async.run do
14
+ process_yield_jsons = Async.run do
15
15
  Pytty::Client::Api::Ps.run
16
16
  end.wait
17
17
 
18
18
  unless quiet?
19
- puts "id cmd"
19
+ puts "id\trunning\tcmd"
20
20
  puts "-"*40
21
21
  end
22
- for process_yield in process_yields do
22
+ for process_yield_json in process_yield_jsons do
23
+ process_yield = Pytty::Client::ProcessYield.from_json process_yield_json
23
24
  if quiet?
24
- puts process_yield.fetch "id"
25
+ puts process_yield.id
25
26
  else
26
27
  puts process_yield
27
28
  end
@@ -29,8 +29,13 @@ module Pytty
29
29
  body = {}.to_json
30
30
 
31
31
  for id in ids do
32
- response = internet.post("http://localhost:1234/v1/rm/#{id}", headers, [body])
33
- puts id
32
+ response = internet.post("#{Pytty::Client.host_url}/v1/rm/#{id}", headers, [body])
33
+ if response.status == 200
34
+ puts id
35
+ else
36
+ puts response.read
37
+ exit 1
38
+ end
34
39
  end
35
40
  ensure
36
41
  internet.close
@@ -21,6 +21,7 @@ module Pytty
21
21
  subcommand ["spawn"], "spawn", SpawnCommand
22
22
  subcommand ["signal"], "signal", SignalCommand
23
23
  subcommand ["attach"], "attach", AttachCommand
24
+ subcommand ["stdout"], "stdout", StdoutCommand
24
25
 
25
26
  def self.run
26
27
  super
@@ -12,17 +12,22 @@ module Pytty
12
12
  option ["-i","--interactive"], :flag, "interactive"
13
13
  option ["-t","--tty"], :flag, "tty"
14
14
  option ["-d","--detach"], :flag, "detach"
15
+ option ["--name"], "name", "name"
15
16
 
16
17
  def execute
17
- Async.run do
18
- json = Pytty::Client::Api::Yield.run cmd: cmd_list, env: {}
18
+ Async.run do |task|
19
+ json = Pytty::Client::Api::Yield.run id: name, cmd: cmd_list, env: {}
19
20
  process_yield = Pytty::Client::ProcessYield.from_json json
21
+ unless detach?
22
+ task.async do
23
+ process_yield.attach interactive: interactive?
24
+ end
25
+ end
26
+
20
27
  process_yield.spawn tty: tty?, interactive: interactive?
21
28
 
22
29
  if detach?
23
30
  puts process_yield.id
24
- else
25
- process_yield.attach
26
31
  end
27
32
  end
28
33
  end
@@ -13,17 +13,14 @@ module Pytty
13
13
 
14
14
  def execute
15
15
  Async.run do
16
- internet = Async::HTTP::Internet.new
17
- headers = [['accept', 'application/json']]
18
- body = {}.to_json
19
-
20
16
  for id in id_list do
21
- #TODO /v1/process/:id/signal ?
22
- response = internet.post("http://localhost:1234/v1/signal/#{signal}/#{id}", headers, [body])
23
- p response.read
17
+ response, body = Pytty::Client::Api::Signal.run id: id, signal: signal
18
+ if response.status == 200
19
+ puts id
20
+ else
21
+ puts body
22
+ end
24
23
  end
25
- ensure
26
- internet.close
27
24
  end
28
25
  end
29
26
  end
@@ -13,7 +13,13 @@ module Pytty
13
13
  def execute
14
14
  Async.run do
15
15
  for id in id_list
16
- Pytty::Client::Api::Spawn.run id: id
16
+ response, body = Pytty::Client::Api::Spawn.run id: id, tty:true, interactive:true
17
+ unless response.status == 200
18
+ puts body
19
+ exit 1
20
+ else
21
+ puts id
22
+ end
17
23
  end
18
24
  end
19
25
  end
@@ -0,0 +1,22 @@
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 StdoutCommand < Clamp::Command
11
+ parameter "ID", "id"
12
+
13
+ def execute
14
+ Async.run do
15
+ Pytty::Client::Api::Stdout.run id: id
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
@@ -9,19 +9,15 @@ module Pytty
9
9
  module Cli
10
10
  class YieldCommand < Clamp::Command
11
11
  parameter "CMD ...", "command"
12
- option ["-q", "--quiet"], :flag, "quiet"
12
+ option ["--name"], "NAME", "name"
13
13
 
14
14
  def execute
15
15
  process_yield = Async.run do
16
- json = Pytty::Client::Api::Yield.run cmd: cmd_list, env: {}
16
+ json = Pytty::Client::Api::Yield.run cmd: cmd_list, id: name, env: {}
17
17
  ::Pytty::Client::ProcessYield.from_json json
18
18
  end.wait
19
19
 
20
- if quiet?
21
- puts process_yield.id
22
- else
23
- puts process_yield
24
- end
20
+ puts process_yield.id
25
21
  end
26
22
  end
27
23
  end
@@ -18,17 +18,23 @@ module Pytty
18
18
  pid: json.fetch("pid")
19
19
  })
20
20
  end
21
-
21
+ def running?
22
+ !@pid.nil?
23
+ end
22
24
  def to_s
23
- "#{@id} #{@cmd}"
25
+ fields = []
26
+ fields << @id
27
+ fields << running?
28
+ fields << @cmd.join(" ")
29
+ fields.join("\t")
24
30
  end
25
31
 
26
32
  def spawn(tty:, interactive:)
27
33
  Pytty::Client::Api::Spawn.run id: @id, tty: tty, interactive: interactive
28
34
  end
29
35
 
30
- def attach
31
- Pytty::Client::Api::Attach.run id: @id
36
+ def attach(interactive:)
37
+ Pytty::Client::Api::Attach.run id: @id, interactive: interactive
32
38
  end
33
39
  end
34
40
  end
data/lib/pytty/daemon.rb CHANGED
@@ -28,8 +28,12 @@ module Pytty
28
28
 
29
29
  end
30
30
 
31
+ def self.pytty_path
32
+ File.join Dir.home, ".pytty"
33
+ end
34
+
31
35
  def self.yields_json
32
- File.join(Dir.home,".pytty","yields.json")
36
+ File.join(pytty_path,"yields.json")
33
37
  end
34
38
  end
35
39
  end
@@ -15,26 +15,34 @@ module Pytty
15
15
  end
16
16
 
17
17
  resp = case params["component"]
18
- when "stdin"
19
- c = body["c"]
20
- Pytty::Daemon.yields[params["id"]].stdin.enqueue c
21
- [200, {"Content-Type" => "text/html"}, ["ok"]]
22
- when "stream"
23
- if env["HTTP_UPGRADE"] == "websocket"
24
- handler = Pytty::Daemon::Components::WebSocketHandler.new(env)
25
- handler.handle
18
+ when "stdout"
19
+ process_yield = Pytty::Daemon.yields[params["id"]]
20
+ return [404, {'content-type' => 'text/html; charset=utf-8'}, ["does not exist"]] unless process_yield
21
+
22
+ body = Async::HTTP::Body::Writable.new
23
+
24
+ begin
25
+ our_stdout = process_yield.stdout.dup
26
+ while c = our_stdout.read
27
+ body.write c
28
+ end
29
+ rescue Exception => ex
30
+ p ex
31
+ ensure
32
+ puts "closing body"
33
+ body.close
26
34
  end
27
35
 
28
- [404, {"Content-Type" => "text/html"}, ["websocket only"]]
36
+ [200, {'content-type' => 'text/html; charset=utf-8'}, body]
29
37
  when "attach"
30
- task = Async::Task.current
38
+ process_yield = Pytty::Daemon.yields[params["id"]]
31
39
  body = Async::HTTP::Body::Writable.new
32
40
 
33
- task.async do |task|
34
- Pytty::Daemon.yields[params["id"]].stdouts << body
35
- loop do
36
- task.sleep 0.1
37
- end
41
+ Async::Task.current.async do |task|
42
+ notification = process_yield.add_stdout body
43
+ p ["blocking", notification]
44
+ notification.wait
45
+ p "got notification"
38
46
  rescue Exception => ex
39
47
  p ex
40
48
  ensure
@@ -43,8 +51,11 @@ module Pytty
43
51
  end
44
52
 
45
53
  [200, {'content-type' => 'text/html; charset=utf-8'}, body]
46
- when "run","yield","ps","rm","kill","spawn","signal"
47
- status, output = Pytty::Daemon::Components::Handler.handle component: params["component"], id: params["id"], params: body
54
+ when "ps","yield"
55
+ status, output = Pytty::Daemon::Components::Handler.handle component: params["component"], params: body
56
+ [status, {"Content-Type" => "application/json"}, [output.to_json]]
57
+ when "spawn","rm","signal","stdin"
58
+ status, output = Pytty::Daemon::Components::YieldHandler.handle component: params["component"], id: params["id"], params: body
48
59
  [status, {"Content-Type" => "application/json"}, [output.to_json]]
49
60
  when "ws"
50
61
  if env["HTTP_UPGRADE"] == "websocket"
@@ -15,7 +15,11 @@ module Pytty
15
15
  subcommand ["serve"], "serve", ServeCommand
16
16
 
17
17
  def self.run
18
- super
18
+ if ARGV.size == 0
19
+ ServeCommand.run
20
+ else
21
+ super
22
+ end
19
23
  rescue StandardError => exc
20
24
  warn exc.message
21
25
  warn exc.backtrace.join("\n")
@@ -5,23 +5,24 @@ module Pytty
5
5
  module Daemon
6
6
  module Cli
7
7
  class ServeCommand < Clamp::Command
8
- option ["--url"], "URL", "url"
9
-
10
8
  def execute
11
9
  puts "🚽 pyttyd #{Pytty::VERSION}"
12
10
 
13
11
  url_parts = ["http://"]
14
- url_parts << if bind = ENV.get("PYTTY_BIND")
15
- bind
12
+ url_parts << if ENV["PYTTY_BIND"]
13
+ ENV["PYTTY_BIND"]
16
14
  else
17
15
  "127.0.0.1"
18
16
  end
19
- url_parts << if port = ENV.get("PYTTY_PORT")
20
- if port == "PORT"
17
+ url_parts << ":"
18
+ url_parts << if ENV["PYTTY_PORT"]
19
+ if ENV["PYTTY_PORT"] == "PORT"
21
20
  ENV.fetch "PORT"
22
21
  else
23
- "1234"
22
+ ENV["PYTTY_PORT"]
24
23
  end
24
+ else
25
+ "1234"
25
26
  end
26
27
 
27
28
  Async::Reactor.run do
@@ -1,7 +1,3 @@
1
1
 
2
- require_relative "components/run"
3
- require_relative "components/stream"
4
-
5
- require_relative "components/http_handler"
6
- require_relative "components/web_socket_handler"
7
2
  require_relative "components/handler"
3
+ require_relative "components/yield_handler"
@@ -1,28 +1,11 @@
1
1
  # frozen_string_literal: true
2
- require "rack/request"
3
- require "mustermann"
4
2
 
5
3
  module Pytty
6
4
  module Daemon
7
5
  module Components
8
6
  module Handler
9
- def self.handle(component:, id:, params:)
7
+ def self.handle(component:, params:)
10
8
  return case component
11
- when "signal"
12
- process_yield = Pytty::Daemon.yields[id]
13
- return [404, "not found"] unless process_yield
14
- return [500, "not running"] unless process_yield.running?
15
- p params
16
- process_yield.signal params["signal"]
17
- [200, "ok"]
18
- when "spawn"
19
- process_yield = Pytty::Daemon.yields[id]
20
- return [404, "not found"] unless process_yield
21
-
22
- process_yield.spawn
23
- Pytty::Daemon.dump
24
-
25
- [200, "ok"]
26
9
  when "ps"
27
10
  output = []
28
11
  Pytty::Daemon.yields.each do |id, process_yield|
@@ -32,33 +15,12 @@ module Pytty
32
15
  when "yield"
33
16
  cmd = params.dig "cmd"
34
17
  env = params.dig "env"
35
- process_yield = Pytty::Daemon::ProcessYield.new cmd, env: env
36
- Pytty::Daemon.yields[process_yield.id] = process_yield
37
- Pytty::Daemon.dump
18
+ id = params.dig "id"
38
19
 
39
- [200, process_yield]
40
- when "rm"
41
- process_yield = Pytty::Daemon.yields[id]
42
- p Pytty::Daemon.yields
43
- p id
44
- return [404, "not found"] unless process_yield
45
- if process_yield.running?
46
- process_yield.kill
47
- end
48
- Pytty::Daemon.yields.delete process_yield.id
20
+ process_yield = Pytty::Daemon::ProcessYield.new cmd, id: id, env: env
21
+ Pytty::Daemon.yields[process_yield.id] = process_yield
49
22
  Pytty::Daemon.dump
50
23
 
51
- [200, nil]
52
- when "spawn"
53
- process_yield = Pytty::Daemon.yields[id]
54
- return [404, "not found"] unless process_yield
55
-
56
- pipe = IO.pipe
57
- stderr_reader = Async::IO::Generic.new(pipe.first)
58
- stderr_writer = Async::IO::Generic.new(pipe.last)
59
-
60
- process_yield.spawn stdout: $stdout, stderr: stderr_writer, stdin: $stdin
61
- stderr_reader.close
62
24
  [200, process_yield]
63
25
  else
64
26
  raise "unknown: #{component}"
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pytty
4
+ module Daemon
5
+ module Components
6
+ module YieldHandler
7
+ def self.handle(component:, id:, params:)
8
+ process_yield = Pytty::Daemon.yields[id]
9
+ return [404, "not found"] unless process_yield
10
+
11
+ return case component
12
+ when "stdin"
13
+ process_yield.stdin.enqueue params["c"]
14
+
15
+ [200, "ok"]
16
+ when "signal"
17
+ return [500, "not running"] unless process_yield.running?
18
+
19
+ process_yield.signal params["signal"]
20
+
21
+ [200, "ok"]
22
+ when "spawn"
23
+ return [500, "already running"] if process_yield.running?
24
+
25
+ if process_yield.spawn
26
+ Pytty::Daemon.dump
27
+ else
28
+ return [500, "could not spawn"]
29
+ end
30
+
31
+ [200, "ok"]
32
+ when "rm"
33
+ process_yield.signal("KILL") if process_yield.running?
34
+ Pytty::Daemon.yields.delete process_yield.id
35
+ Pytty::Daemon.dump
36
+
37
+ [200, id]
38
+ else
39
+ raise "unknown: #{component} with id: #{id}"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require "securerandom"
3
+ require "pty"
3
4
 
4
5
  module Pytty
5
6
  module Daemon
@@ -9,14 +10,22 @@ module Pytty
9
10
  @env = env
10
11
 
11
12
  @pid = nil
13
+ @status = nil
12
14
  @id = id || SecureRandom.uuid
13
15
 
14
- @stdouts = []
16
+ @stdout = nil
17
+ @stdouts = {}
15
18
  @stdin = Async::Queue.new
16
19
  end
17
20
 
18
- attr_reader :id, :cmd, :pid
19
- attr_accessor :stdouts, :stdin
21
+ attr_reader :id, :cmd, :pid, :status, :stdout
22
+ attr_accessor :stdin
23
+
24
+ def add_stdout(stdout)
25
+ notification = Async::Notification.new
26
+ @stdouts[notification] = stdout
27
+ notification
28
+ end
20
29
 
21
30
  def running?
22
31
  !@pid.nil?
@@ -26,6 +35,7 @@ module Pytty
26
35
  {
27
36
  id: @id,
28
37
  pid: @pid,
38
+ status: @status,
29
39
  cmd: @cmd,
30
40
  env: @env,
31
41
  running: running?
@@ -33,11 +43,21 @@ module Pytty
33
43
  end
34
44
 
35
45
  def spawn
36
- executable, args = @cmd
37
- @env.merge!({
38
- "TERM" => "vt100"
39
- })
46
+ return false if running?
40
47
 
48
+ executable, args = @cmd
49
+ # @env.merge!({
50
+ # "TERM" => "xterm"
51
+ # })
52
+
53
+ stdout_path = File.join(Pytty::Daemon.pytty_path, @id)
54
+ File.unlink stdout_path if File.exist? stdout_path
55
+ stdout_appender = Async::IO::Stream.new(
56
+ File.open stdout_path, "a"
57
+ )
58
+ @stdout = Async::IO::Stream.new(
59
+ File.open stdout_path, "r"
60
+ )
41
61
  Async::Task.current.async do |task|
42
62
  p ["spawn", executable, args, @env]
43
63
 
@@ -46,46 +66,52 @@ module Pytty
46
66
  async_stdout = Async::IO::Generic.new real_stdout
47
67
  async_stdin = Async::IO::Generic.new real_stdin
48
68
 
49
- task.async do |subtask|
69
+ task_stdin_writer = task.async do |subtask|
50
70
  while c = @stdin.dequeue do
51
- p c
52
71
  async_stdin.write c
53
72
  end
73
+ rescue Exception => ex
74
+ puts "async_stdin.write: #{ex.inspect}"
54
75
  end
55
76
 
56
- while c = async_stdout.read(1)
57
- @stdouts.each do |s|
58
- begin
59
- s.write c
60
- rescue Errno::EPIPE => ex
61
- puts "cannnot write, popping"
62
- @stdouts.pop
77
+ task_stdout_writer = task.async do |subtask|
78
+ while c = async_stdout.read(1)
79
+ stdout_appender.write c
80
+ stdout_appender.flush
81
+ @stdouts.each do |notification, stdout|
82
+ begin
83
+ stdout.write c
84
+ rescue Errno::EPIPE => ex
85
+ notification.signal
86
+ @stdouts.delete notification
87
+ end
63
88
  end
64
89
  end
90
+ ensure
91
+ task_stdin_writer.stop
92
+ Process.wait(@pid)
93
+ @status = $?.exitstatus
94
+ @pid = nil
95
+ stdout_appender.close
96
+
97
+ @stdouts.each do |notification, stdout|
98
+ p ["notifying: #{notification}"]
99
+ notification.signal
100
+ @stdouts.delete notification
101
+ end
102
+ p ["exited", @cmd, "status", @status]
65
103
  end
66
- puts "CLOSED"
67
- #stdout.close
68
- end
69
-
70
- end
71
- def signal(sig)
72
- Process.kill(sig, @pid)
73
- end
74
-
75
- def tstp
76
- Process.kill("TSTP", @pid)
77
- end
104
+ end.wait
78
105
 
79
- def cont
80
- Process.kill("CONT", @pid)
106
+ puts "spawned"
107
+ p ["@stdouts", @stdouts]
108
+ return true
81
109
  end
82
110
 
83
- def kill
84
- Process.kill("KILL", @pid)
85
- end
86
-
87
- def term
88
- Process.kill("TERM", @pid)
111
+ def signal(sig)
112
+ sig_upcased = sig.upcase
113
+ p ["signaling", sig_upcased, "to", @pid]
114
+ Process.kill(sig_upcased, @pid)
89
115
  end
90
116
  end
91
117
  end
data/lib/pytty/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pytty
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
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.3.0
4
+ version: 0.4.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-13 00:00:00.000000000 Z
11
+ date: 2019-01-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: clamp
@@ -178,9 +178,12 @@ files:
178
178
  - LICENSE.txt
179
179
  - README.md
180
180
  - Rakefile
181
- - bin/build
182
181
  - bin/console
183
182
  - bin/setup
183
+ - build/linux-debian-deps.sh
184
+ - build/linux.sh
185
+ - build/macos-deps.sh
186
+ - build/macos.sh
184
187
  - exe/pytty
185
188
  - exe/pyttyd
186
189
  - lib/pytty.rb
@@ -188,7 +191,9 @@ files:
188
191
  - lib/pytty/client/api.rb
189
192
  - lib/pytty/client/api/attach.rb
190
193
  - lib/pytty/client/api/ps.rb
194
+ - lib/pytty/client/api/signal.rb
191
195
  - lib/pytty/client/api/spawn.rb
196
+ - lib/pytty/client/api/stdout.rb
192
197
  - lib/pytty/client/api/yield.rb
193
198
  - lib/pytty/client/cli.rb
194
199
  - lib/pytty/client/cli/attach_command.rb
@@ -199,6 +204,7 @@ files:
199
204
  - lib/pytty/client/cli/run_command.rb
200
205
  - lib/pytty/client/cli/signal_command.rb
201
206
  - lib/pytty/client/cli/spawn_command.rb
207
+ - lib/pytty/client/cli/stdout_command.rb
202
208
  - lib/pytty/client/cli/stream_command.rb
203
209
  - lib/pytty/client/cli/yield_command.rb
204
210
  - lib/pytty/client/process_yield.rb
@@ -212,10 +218,7 @@ files:
212
218
  - lib/pytty/daemon/cli/serve_command.rb
213
219
  - lib/pytty/daemon/components.rb
214
220
  - lib/pytty/daemon/components/handler.rb
215
- - lib/pytty/daemon/components/http_handler.rb
216
- - lib/pytty/daemon/components/run.rb
217
- - lib/pytty/daemon/components/stream.rb
218
- - lib/pytty/daemon/components/web_socket_handler.rb
221
+ - lib/pytty/daemon/components/yield_handler.rb
219
222
  - lib/pytty/daemon/process_yield.rb
220
223
  - lib/pytty/version.rb
221
224
  - pytty.gemspec
data/bin/build DELETED
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env sh
2
- set -e
3
- set -x
4
- case "$1" in
5
- "gem")
6
- set +e
7
- gem uninstall -xa pytty
8
- set -e
9
- rake install
10
- ;;
11
- "binary")
12
- rubyc -c --make-args="-j16" -o pkg/pyttyd pyttyd
13
- ;;
14
- esac
15
-
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
- require "rack/request"
3
- require "mustermann"
4
-
5
- module Pytty
6
- module Daemon
7
- module Components
8
- class HttpHandler
9
- def initialize(env)
10
- @env = env
11
- end
12
-
13
- def handle
14
- req = Rack::Request.new(@env)
15
- body = begin
16
- JSON.parse(req.body.read)
17
- rescue
18
- end
19
-
20
- params = Mustermann.new('/:component(/?:id)?').params(req.path_info)
21
- case params.fetch("component")
22
- when "rm"
23
- id = params["id"]
24
-
25
- process_yield = Pytty::Daemon.yields[id]
26
- unless process_yield
27
- return [404, nil]
28
- else
29
- return [200, process_yield]
30
- end
31
- end
32
-
33
- case req.path_info
34
- when "/kill"
35
- p req
36
- return [200, {}]
37
- end
38
-
39
- obj = case req.path_info
40
- when "/run"
41
- Run.new cmd: body.dig("cmd")
42
- when "/stream"
43
- Stream.new cmd: body.dig("cmd")
44
- else
45
- raise "Unknown: #{req.path_info}"
46
- end
47
-
48
- obj.run
49
- end
50
- end
51
- end
52
- end
53
- end
54
-
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Pytty
4
- module Daemon
5
- module Components
6
- class Run
7
- def initialize(cmd:)
8
- @cmd = cmd
9
- end
10
-
11
- def run
12
- cmd_string = @cmd.join(" ")
13
- `#{cmd_string}`
14
- end
15
- end
16
- end
17
- end
18
- end
19
-
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
- require 'pty'
3
- require 'io/console'
4
-
5
- module Pytty
6
- module Daemon
7
- module Components
8
- class Stream
9
- def initialize(cmd:, env:)
10
- @cmd = cmd
11
- @env = env
12
- end
13
-
14
- def run(stream:)
15
- pipe = IO.pipe
16
- stderr = Async::IO::Generic.new(pipe.first)
17
- stderr_writer = Async::IO::Generic.new(pipe.last)
18
-
19
-
20
- cmd, args = @cmd
21
- process_stdout, process_stdin, pid = PTY.spawn(@env, cmd, *args, err: stderr_writer.fileno)
22
- stderr_writer.close
23
-
24
- stdout = Async::IO::Generic.new process_stdout
25
- # stdin = Async::IO::Generic.new process_stdin
26
- Async::Task.current.async do |task|
27
- while c = stdout.read(1)
28
- stream.write c
29
- end
30
- stream.close
31
- end
32
- # Async::Task.current.async do |task|
33
- # loop do
34
- # p "o"
35
- # task.sleep 1
36
- # end
37
- # end
38
-
39
- end
40
- end
41
- end
42
- end
43
- end
44
-
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
- require "rack/request"
3
-
4
- module Pytty
5
- module Daemon
6
- module Components
7
- class WebSocketHandler
8
- def initialize(env)
9
- @env = env
10
- end
11
-
12
- def handle
13
- req = Rack::Request.new(@env)
14
- ws = Pytty::Daemon::Api::WebSockets.new(@env)
15
- ws.handle
16
-
17
- klass = case req.path_info
18
- when "/stream"
19
- Stream
20
- else
21
- raise "Unknown: #{req.path_info}"
22
- end
23
- params = ws.read
24
- body = begin
25
- JSON.parse(params)
26
- rescue Exception => ex
27
- p ex
28
- end
29
-
30
- obj = klass.new cmd: body.dig("cmd"), env: body.dig("env")
31
- obj.run stream: ws
32
- end
33
- end
34
- end
35
- end
36
- end
37
-