pytty 0.3.0 → 0.4.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.
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
-