pytty 0.2.0 → 0.3.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 +9 -1
  3. data/bin/build +15 -0
  4. data/exe/pytty +1 -0
  5. data/exe/pyttyd +1 -0
  6. data/lib/pytty/client.rb +9 -0
  7. data/lib/pytty/client/api.rb +4 -0
  8. data/lib/pytty/client/api/attach.rb +61 -0
  9. data/lib/pytty/client/api/ps.rb +18 -0
  10. data/lib/pytty/client/api/spawn.rb +21 -0
  11. data/lib/pytty/client/api/yield.rb +25 -0
  12. data/lib/pytty/client/cli.rb +7 -0
  13. data/lib/pytty/client/cli/attach_command.rb +22 -0
  14. data/lib/pytty/client/cli/kill_command.rb +31 -0
  15. data/lib/pytty/client/cli/ps_command.rb +34 -0
  16. data/lib/pytty/client/cli/rm_command.rb +43 -0
  17. data/lib/pytty/client/cli/root_command.rb +7 -0
  18. data/lib/pytty/client/cli/run_command.rb +12 -9
  19. data/lib/pytty/client/cli/signal_command.rb +33 -0
  20. data/lib/pytty/client/cli/spawn_command.rb +24 -0
  21. data/lib/pytty/client/cli/stream_command.rb +31 -0
  22. data/lib/pytty/client/cli/yield_command.rb +30 -0
  23. data/lib/pytty/client/process_yield.rb +35 -0
  24. data/lib/pytty/daemon.rb +38 -0
  25. data/lib/pytty/daemon/api/router.rb +35 -11
  26. data/lib/pytty/daemon/api/server.rb +5 -18
  27. data/lib/pytty/daemon/cli/serve_command.rb +22 -2
  28. data/lib/pytty/daemon/components.rb +3 -2
  29. data/lib/pytty/daemon/components/handler.rb +71 -0
  30. data/lib/pytty/daemon/components/{web_handler.rb → http_handler.rb} +21 -1
  31. data/lib/pytty/daemon/components/stream.rb +8 -1
  32. data/lib/pytty/daemon/process_yield.rb +93 -0
  33. data/lib/pytty/version.rb +1 -1
  34. data/pytty.gemspec +11 -3
  35. metadata +36 -11
  36. data/.gitignore +0 -11
  37. data/.rspec +0 -3
  38. data/.ruby-gemset +0 -1
  39. data/.ruby-version +0 -1
  40. data/.travis.yml +0 -7
  41. data/Guardfile +0 -25
  42. data/lib/pytty/daemon/api/chunk.rb +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 86943cf397ea5d69b0d735243100ad08ed00629b2b1ffc34157f44acb0ff2316
4
- data.tar.gz: 5ee2c4f7c9bd1bbb9e550be3140d1c7c53f5b51f74ae3cad2f60994b1a026374
3
+ metadata.gz: fd905d25dba15816b22209e67dfd888c3379e3f0ef01d18611b86c21e49a6666
4
+ data.tar.gz: 7b215e1046ea4a67b3bc619ccb6603ba8f540a8473a6060b6f47d9921187296e
5
5
  SHA512:
6
- metadata.gz: 04b69e3162611cda3e34a33dfae91a6cbad3226ae31e928d8a77dcf1e210c7b4fd766a173f182e8be7a089c2ca26cd3f6456fb13fab420695108c706649a56c3
7
- data.tar.gz: 4a61b1ff318f39adb8918565e78a6ef7ed189c7063bcb97ce3ca9efc27593a2b73e8a9262e5542cd5c6e850eb77ddbe5c9555be8948f136f4422962d3d18c06b
6
+ metadata.gz: 19c80222d3401ca8e1b84878cba3a2fd9f858de56382388104533a03a9ec88ac8cb83594984218273467b8c83e9f94c640b28985462b1f54e76a1fda562dded9
7
+ data.tar.gz: 1056958148e94f81ba23f50970e4fc399300fc93fe2854518f3a551f46b1128c61da8a9166fab9349bb6ebaa67bfa66b1eb46e64c10ee9c71e5aca0320a58e76
data/Gemfile.lock CHANGED
@@ -1,10 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pytty (0.2.0)
4
+ pytty (0.3.0)
5
5
  async-websocket
6
6
  clamp (~> 1.3)
7
7
  falcon
8
+ mustermann
8
9
 
9
10
  GEM
10
11
  remote: https://rubygems.org/
@@ -51,6 +52,9 @@ GEM
51
52
  guard (~> 2.2)
52
53
  guard-compat (~> 1.1)
53
54
  guard-compat (1.2.1)
55
+ guard-process (1.2.1)
56
+ guard-compat (~> 1.2, >= 1.2.1)
57
+ spoon (~> 0.0.1)
54
58
  guard-rspec (4.7.3)
55
59
  guard (~> 2.1)
56
60
  guard-compat (~> 1.1)
@@ -67,6 +71,7 @@ GEM
67
71
  lumberjack (1.0.13)
68
72
  mapping (1.1.1)
69
73
  method_source (0.9.2)
74
+ mustermann (1.0.3)
70
75
  nenv (0.3.0)
71
76
  nio4r (2.3.1)
72
77
  notiffany (0.1.1)
@@ -99,6 +104,8 @@ GEM
99
104
  mapping (~> 1.0)
100
105
  rainbow (>= 2.0, < 4.0)
101
106
  shellany (0.0.1)
107
+ spoon (0.0.6)
108
+ ffi
102
109
  thor (0.20.3)
103
110
  timers (4.2.0)
104
111
  websocket-driver (0.7.0)
@@ -112,6 +119,7 @@ DEPENDENCIES
112
119
  bundler (~> 1.16)
113
120
  guard
114
121
  guard-bundler
122
+ guard-process
115
123
  guard-rspec
116
124
  kommando
117
125
  pytty!
data/bin/build ADDED
@@ -0,0 +1,15 @@
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
+
data/exe/pytty CHANGED
@@ -9,6 +9,7 @@ $LOAD_PATH.unshift lib_path unless $LOAD_PATH.include?(lib_path)
9
9
  STDOUT.sync = true
10
10
 
11
11
  require 'pytty'
12
+ require 'pytty/client'
12
13
  require 'pytty/client/cli'
13
14
 
14
15
  Pytty::Client::Cli::RootCommand.run
data/exe/pyttyd CHANGED
@@ -9,6 +9,7 @@ $LOAD_PATH.unshift lib_path unless $LOAD_PATH.include?(lib_path)
9
9
  STDOUT.sync = true
10
10
 
11
11
  require 'pytty'
12
+ require 'pytty/daemon'
12
13
  require 'pytty/daemon/cli'
13
14
 
14
15
  Pytty::Daemon::Cli::RootCommand.run
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pytty
4
+ module Client
5
+ end
6
+ end
7
+
8
+ require_relative "client/process_yield"
9
+ require_relative "client/api"
@@ -0,0 +1,4 @@
1
+ require_relative "api/ps"
2
+ require_relative "api/yield"
3
+ require_relative "api/attach"
4
+ require_relative "api/spawn"
@@ -0,0 +1,61 @@
1
+ module Pytty
2
+ module Client
3
+ module Api
4
+ class Attach
5
+ def self.run(id:)
6
+ Async::Task.current.async do |stdin_task|
7
+ internet = Async::HTTP::Internet.new
8
+ headers = [['accept', 'application/json']]
9
+ body = {}.to_json
10
+
11
+ $stdin.raw!
12
+ $stdin.echo = false
13
+ async_stdin = Async::IO::Stream.new(
14
+ Async::IO::Generic.new($stdin)
15
+ )
16
+
17
+ stdin_body = {
18
+ c: "\f"
19
+ }.to_json
20
+ response = internet.post("http://localhost:1234/v1/stdin/#{id}", headers, [stdin_body])
21
+
22
+ detach_sequence_started = false
23
+ while c = async_stdin.read(1) do
24
+ case c
25
+ when "\x10"
26
+ detach_sequence_started = true
27
+ next
28
+ when "\x11"
29
+ if detach_sequence_started
30
+ detach_sequence_started = false
31
+ exit 0
32
+ end
33
+ end
34
+
35
+ detach_sequence_started = false
36
+
37
+ stdin_body = {
38
+ c: c
39
+ }.to_json
40
+ response = internet.post("http://localhost:1234/v1/stdin/#{id}", headers, [stdin_body])
41
+ end
42
+ end
43
+
44
+ begin
45
+ internet = Async::HTTP::Internet.new
46
+ headers = [['accept', 'application/json']]
47
+ body = {}.to_json
48
+
49
+ response = internet.post("http://localhost:1234/v1/attach/#{id}", headers, [body])
50
+ response.body.each do |c|
51
+ print c
52
+ end
53
+ rescue Async::Wrapper::Cancelled => ex
54
+ ensure
55
+ internet.close
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,18 @@
1
+ module Pytty
2
+ module Client
3
+ module Api
4
+ class Ps
5
+ def self.run
6
+ internet = Async::HTTP::Internet.new
7
+ headers = [['accept', 'application/json']]
8
+ body = {}.to_json
9
+
10
+ response = internet.post("http://localhost:1234/v1/ps", headers, [body])
11
+ JSON.parse(response.body.read)
12
+ ensure
13
+ internet.close
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ module Pytty
2
+ module Client
3
+ module Api
4
+ class Spawn
5
+ def self.run(id:, tty:, interactive:)
6
+ internet = Async::HTTP::Internet.new
7
+ headers = [['accept', 'application/json']]
8
+ body = {
9
+ tty: tty,
10
+ interactive: interactive
11
+ }.to_json
12
+
13
+ response = internet.post("http://localhost:1234/v1/spawn/#{id}", headers, [body])
14
+ JSON.parse(response.read)
15
+ ensure
16
+ internet.close
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ module Pytty
2
+ module Client
3
+ module Api
4
+ class Yield
5
+ def self.run(cmd:, env:)
6
+ internet = Async::HTTP::Internet.new
7
+ headers = [['accept', 'application/json']]
8
+
9
+ term_env = {
10
+ "LINES" => IO.console.winsize.first.to_s,
11
+ "COLUMNS" => IO.console.winsize.last.to_s
12
+ }.merge env
13
+
14
+ body = {
15
+ cmd: cmd,
16
+ env: term_env
17
+ }.to_json
18
+
19
+ response = internet.post("http://localhost:1234/v1/yield", headers, [body])
20
+ JSON.parse(response.body.read)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -3,5 +3,12 @@ require "clamp"
3
3
  require_relative "../common/cli/version_command"
4
4
  require_relative "cli/run_command"
5
5
  require_relative "cli/stream_command"
6
+ require_relative "cli/yield_command"
7
+ require_relative "cli/ps_command"
8
+ require_relative "cli/kill_command"
9
+ require_relative "cli/rm_command"
10
+ require_relative "cli/spawn_command"
11
+ require_relative "cli/signal_command"
12
+ require_relative "cli/attach_command"
6
13
 
7
14
  require_relative "cli/root_command"
@@ -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 AttachCommand < Clamp::Command
11
+ parameter "ID", "id"
12
+
13
+ def execute
14
+ Async.run do
15
+ Pytty::Client::Api::Attach.run id: id
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,31 @@
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 KillCommand < Clamp::Command
11
+ parameter "ID ...", "id"
12
+
13
+ def execute
14
+ Async.run do
15
+ internet = Async::HTTP::Internet.new
16
+ headers = [['accept', 'application/json']]
17
+ body = {}.to_json
18
+
19
+ for id in id_list do
20
+ response = internet.post("http://localhost:1234/v1/kill/#{id}", headers, [body])
21
+ puts response.read
22
+ end
23
+ ensure
24
+ internet.close
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
@@ -0,0 +1,34 @@
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 PsCommand < Clamp::Command
11
+ option ["-q","--quiet"], :flag, "quiet"
12
+
13
+ def execute
14
+ process_yields = Async.run do
15
+ Pytty::Client::Api::Ps.run
16
+ end.wait
17
+
18
+ unless quiet?
19
+ puts "id cmd"
20
+ puts "-"*40
21
+ end
22
+ for process_yield in process_yields do
23
+ if quiet?
24
+ puts process_yield.fetch "id"
25
+ else
26
+ puts process_yield
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,43 @@
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 RmCommand < Clamp::Command
11
+ parameter "[ID] ...", "id"
12
+ option ["--all"], :flag, "all"
13
+
14
+ def execute
15
+ ids = if all?
16
+ process_yield_jsons = Async.run do
17
+ Pytty::Client::Api::Ps.run
18
+ end.wait
19
+ process_yield_jsons.map do |json|
20
+ json.fetch("id")
21
+ end
22
+ else
23
+ id_list
24
+ end
25
+
26
+ Async.run do
27
+ internet = Async::HTTP::Internet.new
28
+ headers = [['accept', 'application/json']]
29
+ body = {}.to_json
30
+
31
+ for id in ids do
32
+ response = internet.post("http://localhost:1234/v1/rm/#{id}", headers, [body])
33
+ puts id
34
+ end
35
+ ensure
36
+ internet.close
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
@@ -14,6 +14,13 @@ module Pytty
14
14
  subcommand ["version"], "Show version information", Pytty::Common::Cli::VersionCommand
15
15
  subcommand ["run"], "run", RunCommand
16
16
  subcommand ["stream"], "stream", StreamCommand
17
+ subcommand ["yield"], "yield", YieldCommand
18
+ subcommand ["ps"], "ps", PsCommand
19
+ subcommand ["kill"], "kill", KillCommand
20
+ subcommand ["rm"], "rm", RmCommand
21
+ subcommand ["spawn"], "spawn", SpawnCommand
22
+ subcommand ["signal"], "signal", SignalCommand
23
+ subcommand ["attach"], "attach", AttachCommand
17
24
 
18
25
  def self.run
19
26
  super
@@ -9,18 +9,21 @@ module Pytty
9
9
  module Cli
10
10
  class RunCommand < Clamp::Command
11
11
  parameter "CMD ...", "command"
12
+ option ["-i","--interactive"], :flag, "interactive"
13
+ option ["-t","--tty"], :flag, "tty"
14
+ option ["-d","--detach"], :flag, "detach"
15
+
12
16
  def execute
13
17
  Async.run do
14
- internet = Async::HTTP::Internet.new
15
- headers = [['accept', 'application/json']]
16
- body = {
17
- cmd: cmd_list
18
- }.to_json
18
+ json = Pytty::Client::Api::Yield.run cmd: cmd_list, env: {}
19
+ process_yield = Pytty::Client::ProcessYield.from_json json
20
+ process_yield.spawn tty: tty?, interactive: interactive?
19
21
 
20
- response = internet.post("http://localhost:1234/v1/run", headers, [body])
21
- puts response.read
22
- ensure
23
- internet.close
22
+ if detach?
23
+ puts process_yield.id
24
+ else
25
+ process_yield.attach
26
+ end
24
27
  end
25
28
  end
26
29
  end
@@ -0,0 +1,33 @@
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 SignalCommand < Clamp::Command
11
+ parameter "SIGNAL", "signal"
12
+ parameter "ID ...", "id"
13
+
14
+ def execute
15
+ Async.run do
16
+ internet = Async::HTTP::Internet.new
17
+ headers = [['accept', 'application/json']]
18
+ body = {}.to_json
19
+
20
+ 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
24
+ end
25
+ ensure
26
+ internet.close
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
@@ -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 SpawnCommand < Clamp::Command
11
+ parameter "ID ...", "id"
12
+
13
+ def execute
14
+ Async.run do
15
+ for id in id_list
16
+ Pytty::Client::Api::Spawn.run id: id
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
@@ -11,6 +11,37 @@ module Pytty
11
11
  class StreamCommand < Clamp::Command
12
12
  parameter "CMD ...", "command"
13
13
  def execute
14
+
15
+ $stdin.raw!
16
+ $stdin.echo = false
17
+ Async::Reactor.run do |task|
18
+ async_stdin = Async::IO::Stream.new(
19
+ Async::IO::Generic.new($stdin)
20
+ )
21
+
22
+ while c = async_stdin.read(1) do
23
+ case c
24
+ when "\x01"
25
+ print "\r"
26
+ when "\x03"
27
+ puts "\r\n\nctrl+c\n\r"
28
+ break
29
+ when "\r"
30
+ print "\n\r"
31
+ when "\e"
32
+ print c
33
+ print async_stdin.read(2)
34
+ else
35
+ print c.inspect
36
+ # print c
37
+ # p c
38
+ end
39
+ end
40
+ end
41
+
42
+ exit 0
43
+
44
+ #---------------
14
45
  env = {
15
46
  "LINES" => IO.console.winsize.first.to_s,
16
47
  "COLUMNS" => IO.console.winsize.last.to_s
@@ -0,0 +1,30 @@
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 YieldCommand < Clamp::Command
11
+ parameter "CMD ...", "command"
12
+ option ["-q", "--quiet"], :flag, "quiet"
13
+
14
+ def execute
15
+ process_yield = Async.run do
16
+ json = Pytty::Client::Api::Yield.run cmd: cmd_list, env: {}
17
+ ::Pytty::Client::ProcessYield.from_json json
18
+ end.wait
19
+
20
+ if quiet?
21
+ puts process_yield.id
22
+ else
23
+ puts process_yield
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,35 @@
1
+ module Pytty
2
+ module Client
3
+ class ProcessYield
4
+ def initialize(id:, cmd:, env:, pid:)
5
+ @cmd = cmd
6
+ @env = env
7
+ @pid = pid
8
+ @id = id
9
+ end
10
+
11
+ attr_reader :id
12
+
13
+ def self.from_json(json)
14
+ self.new({
15
+ id: json.fetch("id"),
16
+ cmd: json.fetch("cmd"),
17
+ env: json.fetch("env"),
18
+ pid: json.fetch("pid")
19
+ })
20
+ end
21
+
22
+ def to_s
23
+ "#{@id} #{@cmd}"
24
+ end
25
+
26
+ def spawn(tty:, interactive:)
27
+ Pytty::Client::Api::Spawn.run id: @id, tty: tty, interactive: interactive
28
+ end
29
+
30
+ def attach
31
+ Pytty::Client::Api::Attach.run id: @id
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pytty
4
+ module Daemon
5
+ @@yields = {}
6
+
7
+ def self.yields
8
+ @@yields
9
+ end
10
+
11
+ def self.dump
12
+ FileUtils.mkdir_p File.dirname(yields_json)
13
+ File.write yields_json, @@yields.to_json
14
+ end
15
+
16
+ def self.load
17
+ return unless File.exist? yields_json
18
+ puts "restoring from #{yields_json}"
19
+
20
+ objs = JSON.parse(File.read(yields_json))
21
+ objs.each do |k,obj|
22
+ process_yield = ProcessYield.new obj["cmd"], id: obj["id"], env: obj["env"]
23
+ @@yields[obj["id"]] = process_yield
24
+ print "spawning #{process_yield.cmd} ... "
25
+ process_yield.spawn if obj["running"]
26
+ puts "done"
27
+ end
28
+
29
+ end
30
+
31
+ def self.yields_json
32
+ File.join(Dir.home,".pytty","yields.json")
33
+ end
34
+ end
35
+ end
36
+
37
+ require_relative "daemon/process_yield"
38
+ require_relative "daemon/components"
@@ -1,7 +1,5 @@
1
- require_relative "web_sockets"
2
- require_relative "chunk"
3
-
4
- require_relative "../components"
1
+ require "mustermann"
2
+ require "json"
5
3
 
6
4
  module Pytty
7
5
  module Daemon
@@ -9,20 +7,46 @@ module Pytty
9
7
  class Router
10
8
  def call(env)
11
9
  req = Rack::Request.new(env)
12
- resp = case req.path_info
13
- when "/stream"
10
+
11
+ params = Mustermann.new('/:component(/?:id)?').params(req.path_info)
12
+ body = begin
13
+ JSON.parse(req.body.read)
14
+ rescue
15
+ end
16
+
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"
14
23
  if env["HTTP_UPGRADE"] == "websocket"
15
24
  handler = Pytty::Daemon::Components::WebSocketHandler.new(env)
16
25
  handler.handle
17
26
  end
18
27
 
19
28
  [404, {"Content-Type" => "text/html"}, ["websocket only"]]
20
- when "/run"
21
- handler = Pytty::Daemon::Components::WebHandler.new(env)
22
- output = handler.handle
29
+ when "attach"
30
+ task = Async::Task.current
31
+ body = Async::HTTP::Body::Writable.new
32
+
33
+ task.async do |task|
34
+ Pytty::Daemon.yields[params["id"]].stdouts << body
35
+ loop do
36
+ task.sleep 0.1
37
+ end
38
+ rescue Exception => ex
39
+ p ex
40
+ ensure
41
+ puts "closing body"
42
+ body.close
43
+ end
23
44
 
24
- [200, {"Content-Type" => "text/html"}, [output]]
25
- when "/ws"
45
+ [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
48
+ [status, {"Content-Type" => "application/json"}, [output.to_json]]
49
+ when "ws"
26
50
  if env["HTTP_UPGRADE"] == "websocket"
27
51
  ws = WebSockets.new env
28
52
  ws.handle
@@ -1,21 +1,12 @@
1
1
  require "falcon"
2
2
  require_relative "router"
3
- require_relative "chunk"
4
3
 
5
4
  module Pytty
6
5
  module Daemon
7
6
  module Api
8
7
  class Server
9
- def initialize
10
- end
11
-
12
- def run
8
+ def self.run(url:)
13
9
  rack_app = Rack::Builder.new do
14
- #use Rack::CommonLogger
15
-
16
- map "/chunk" do
17
- run Chunk.new
18
- end
19
10
  map "/v1" do
20
11
  run Router.new
21
12
  end
@@ -23,16 +14,12 @@ module Pytty
23
14
 
24
15
  app = Falcon::Server.middleware rack_app, verbose: true
25
16
 
26
- endpoint = Async::HTTP::URLEndpoint.parse "http://0.0.0.0:1234"
27
- bound_endpoint = Async::Reactor.run do
28
- Async::IO::SharedEndpoint.bound(endpoint)
29
- end.result
17
+ endpoint = Async::HTTP::URLEndpoint.parse url
18
+ bound_endpoint = Async::IO::SharedEndpoint.bound(endpoint)
30
19
 
31
20
  server = Falcon::Server.new(app, bound_endpoint, endpoint.protocol, endpoint.scheme)
32
- Async::Reactor.run do
33
- server.run
34
- puts "serving..."
35
- end
21
+ puts "serving at #{url}"
22
+ server.run
36
23
  end
37
24
  end
38
25
  end
@@ -5,9 +5,29 @@ module Pytty
5
5
  module Daemon
6
6
  module Cli
7
7
  class ServeCommand < Clamp::Command
8
+ option ["--url"], "URL", "url"
9
+
8
10
  def execute
9
- s = Pytty::Daemon::Api::Server.new
10
- s.run
11
+ puts "🚽 pyttyd #{Pytty::VERSION}"
12
+
13
+ url_parts = ["http://"]
14
+ url_parts << if bind = ENV.get("PYTTY_BIND")
15
+ bind
16
+ else
17
+ "127.0.0.1"
18
+ end
19
+ url_parts << if port = ENV.get("PYTTY_PORT")
20
+ if port == "PORT"
21
+ ENV.fetch "PORT"
22
+ else
23
+ "1234"
24
+ end
25
+ end
26
+
27
+ Async::Reactor.run do
28
+ Pytty::Daemon.load
29
+ Pytty::Daemon::Api::Server.run url: url_parts.join("")
30
+ end
11
31
  end
12
32
  end
13
33
  end
@@ -2,5 +2,6 @@
2
2
  require_relative "components/run"
3
3
  require_relative "components/stream"
4
4
 
5
- require_relative "components/web_handler"
6
- require_relative "components/web_socket_handler"
5
+ require_relative "components/http_handler"
6
+ require_relative "components/web_socket_handler"
7
+ require_relative "components/handler"
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+ require "rack/request"
3
+ require "mustermann"
4
+
5
+ module Pytty
6
+ module Daemon
7
+ module Components
8
+ module Handler
9
+ def self.handle(component:, id:, params:)
10
+ 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
+ when "ps"
27
+ output = []
28
+ Pytty::Daemon.yields.each do |id, process_yield|
29
+ output << process_yield
30
+ end
31
+ [200, output]
32
+ when "yield"
33
+ cmd = params.dig "cmd"
34
+ 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
38
+
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
49
+ Pytty::Daemon.dump
50
+
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
+ [200, process_yield]
63
+ else
64
+ raise "unknown: #{component}"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
  require "rack/request"
3
+ require "mustermann"
3
4
 
4
5
  module Pytty
5
6
  module Daemon
6
7
  module Components
7
- class WebHandler
8
+ class HttpHandler
8
9
  def initialize(env)
9
10
  @env = env
10
11
  end
@@ -16,6 +17,25 @@ module Pytty
16
17
  rescue
17
18
  end
18
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
+
19
39
  obj = case req.path_info
20
40
  when "/run"
21
41
  Run.new cmd: body.dig("cmd")
@@ -22,13 +22,20 @@ module Pytty
22
22
  stderr_writer.close
23
23
 
24
24
  stdout = Async::IO::Generic.new process_stdout
25
- stdin = Async::IO::Generic.new process_stdin
25
+ # stdin = Async::IO::Generic.new process_stdin
26
26
  Async::Task.current.async do |task|
27
27
  while c = stdout.read(1)
28
28
  stream.write c
29
29
  end
30
30
  stream.close
31
31
  end
32
+ # Async::Task.current.async do |task|
33
+ # loop do
34
+ # p "o"
35
+ # task.sleep 1
36
+ # end
37
+ # end
38
+
32
39
  end
33
40
  end
34
41
  end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+ require "securerandom"
3
+
4
+ module Pytty
5
+ module Daemon
6
+ class ProcessYield
7
+ def initialize(cmd, id:nil, env:{})
8
+ @cmd = cmd
9
+ @env = env
10
+
11
+ @pid = nil
12
+ @id = id || SecureRandom.uuid
13
+
14
+ @stdouts = []
15
+ @stdin = Async::Queue.new
16
+ end
17
+
18
+ attr_reader :id, :cmd, :pid
19
+ attr_accessor :stdouts, :stdin
20
+
21
+ def running?
22
+ !@pid.nil?
23
+ end
24
+
25
+ def to_json(json_generator_state=nil)
26
+ {
27
+ id: @id,
28
+ pid: @pid,
29
+ cmd: @cmd,
30
+ env: @env,
31
+ running: running?
32
+ }.to_json
33
+ end
34
+
35
+ def spawn
36
+ executable, args = @cmd
37
+ @env.merge!({
38
+ "TERM" => "vt100"
39
+ })
40
+
41
+ Async::Task.current.async do |task|
42
+ p ["spawn", executable, args, @env]
43
+
44
+ real_stdout, real_stdin, pid = PTY.spawn @env, executable, *args
45
+ @pid = pid
46
+ async_stdout = Async::IO::Generic.new real_stdout
47
+ async_stdin = Async::IO::Generic.new real_stdin
48
+
49
+ task.async do |subtask|
50
+ while c = @stdin.dequeue do
51
+ p c
52
+ async_stdin.write c
53
+ end
54
+ end
55
+
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
63
+ end
64
+ end
65
+ 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
78
+
79
+ def cont
80
+ Process.kill("CONT", @pid)
81
+ end
82
+
83
+ def kill
84
+ Process.kill("KILL", @pid)
85
+ end
86
+
87
+ def term
88
+ Process.kill("TERM", @pid)
89
+ end
90
+ end
91
+ end
92
+ end
93
+
data/lib/pytty/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pytty
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/pytty.gemspec CHANGED
@@ -18,6 +18,15 @@ Gem::Specification.new do |spec|
18
18
  spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
19
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
20
  end
21
+ ignored_files = ["Dockerfile", "docker-compose.yml",
22
+ ".dockerignore",".gitignore",
23
+ ".ruby-gemset",".ruby-version",
24
+ ".rspec",
25
+ ".travis.yml",
26
+ "Guardfile"
27
+ ]
28
+ spec.files = spec.files - ignored_files
29
+
21
30
  spec.bindir = "exe"
22
31
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
32
  spec.require_paths = ["lib"]
@@ -25,6 +34,7 @@ Gem::Specification.new do |spec|
25
34
  spec.add_runtime_dependency "clamp", "~> 1.3"
26
35
  spec.add_runtime_dependency "falcon"
27
36
  spec.add_runtime_dependency "async-websocket"
37
+ spec.add_runtime_dependency "mustermann"
28
38
 
29
39
  spec.add_development_dependency "bundler", "~> 1.16"
30
40
  spec.add_development_dependency "rake", "~> 10.0"
@@ -32,7 +42,5 @@ Gem::Specification.new do |spec|
32
42
  spec.add_development_dependency "guard"
33
43
  spec.add_development_dependency "guard-rspec"
34
44
  spec.add_development_dependency "guard-bundler"
35
-
36
- spec.add_development_dependency "kommando"
37
-
45
+ spec.add_development_dependency "guard-process"
38
46
  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.2.0
4
+ version: 0.3.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-10 00:00:00.000000000 Z
11
+ date: 2019-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: clamp
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mustermann
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: bundler
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -137,7 +151,7 @@ dependencies:
137
151
  - !ruby/object:Gem::Version
138
152
  version: '0'
139
153
  - !ruby/object:Gem::Dependency
140
- name: kommando
154
+ name: guard-process
141
155
  requirement: !ruby/object:Gem::Requirement
142
156
  requirements:
143
157
  - - ">="
@@ -159,28 +173,37 @@ executables:
159
173
  extensions: []
160
174
  extra_rdoc_files: []
161
175
  files:
162
- - ".gitignore"
163
- - ".rspec"
164
- - ".ruby-gemset"
165
- - ".ruby-version"
166
- - ".travis.yml"
167
176
  - Gemfile
168
177
  - Gemfile.lock
169
- - Guardfile
170
178
  - LICENSE.txt
171
179
  - README.md
172
180
  - Rakefile
181
+ - bin/build
173
182
  - bin/console
174
183
  - bin/setup
175
184
  - exe/pytty
176
185
  - exe/pyttyd
177
186
  - lib/pytty.rb
187
+ - lib/pytty/client.rb
188
+ - lib/pytty/client/api.rb
189
+ - lib/pytty/client/api/attach.rb
190
+ - lib/pytty/client/api/ps.rb
191
+ - lib/pytty/client/api/spawn.rb
192
+ - lib/pytty/client/api/yield.rb
178
193
  - lib/pytty/client/cli.rb
194
+ - lib/pytty/client/cli/attach_command.rb
195
+ - lib/pytty/client/cli/kill_command.rb
196
+ - lib/pytty/client/cli/ps_command.rb
197
+ - lib/pytty/client/cli/rm_command.rb
179
198
  - lib/pytty/client/cli/root_command.rb
180
199
  - lib/pytty/client/cli/run_command.rb
200
+ - lib/pytty/client/cli/signal_command.rb
201
+ - lib/pytty/client/cli/spawn_command.rb
181
202
  - lib/pytty/client/cli/stream_command.rb
203
+ - lib/pytty/client/cli/yield_command.rb
204
+ - lib/pytty/client/process_yield.rb
182
205
  - lib/pytty/common/cli/version_command.rb
183
- - lib/pytty/daemon/api/chunk.rb
206
+ - lib/pytty/daemon.rb
184
207
  - lib/pytty/daemon/api/router.rb
185
208
  - lib/pytty/daemon/api/server.rb
186
209
  - lib/pytty/daemon/api/web_sockets.rb
@@ -188,10 +211,12 @@ files:
188
211
  - lib/pytty/daemon/cli/root_command.rb
189
212
  - lib/pytty/daemon/cli/serve_command.rb
190
213
  - lib/pytty/daemon/components.rb
214
+ - lib/pytty/daemon/components/handler.rb
215
+ - lib/pytty/daemon/components/http_handler.rb
191
216
  - lib/pytty/daemon/components/run.rb
192
217
  - lib/pytty/daemon/components/stream.rb
193
- - lib/pytty/daemon/components/web_handler.rb
194
218
  - lib/pytty/daemon/components/web_socket_handler.rb
219
+ - lib/pytty/daemon/process_yield.rb
195
220
  - lib/pytty/version.rb
196
221
  - pytty.gemspec
197
222
  homepage: https://github.com/pyttyhq/pytty-ruby
data/.gitignore DELETED
@@ -1,11 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
9
-
10
- # rspec failure tracking
11
- .rspec_status
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/.ruby-gemset DELETED
@@ -1 +0,0 @@
1
- pytty
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 2.6.0
data/.travis.yml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- sudo: false
3
- language: ruby
4
- cache: bundler
5
- rvm:
6
- - 2.6.0
7
- before_install: gem install bundler -v 1.16.6
data/Guardfile DELETED
@@ -1,25 +0,0 @@
1
- guard :rspec, cmd: "bundle exec rspec" do
2
- require "guard/rspec/dsl"
3
- dsl = Guard::RSpec::Dsl.new(self)
4
-
5
- watch %r{^lib\/pytty\/(?<component>client|daemon)\/cli\/(?<command>.+_command)\.rb$} do |m|
6
- "spec/cli/#{m[:component]}/#{m[:command]}_spec.rb"
7
- end
8
-
9
- watch %r{^spec\/cli\/(?<component>client|daemon)\/(?<command>.+_command_spec)\.rb$} do |m|
10
- m.instance_variable_get(:@original_value)
11
- end
12
-
13
- end
14
-
15
- guard :bundler do
16
- require 'guard/bundler'
17
- require 'guard/bundler/verify'
18
- helper = Guard::Bundler::Verify.new
19
-
20
- files = ['Gemfile']
21
- files += Dir['*.gemspec'] if files.any? { |f| helper.uses_gemspec?(f) }
22
-
23
- # Assume files are symlinked from somewhere
24
- files.each { |file| watch(helper.real_path(file)) }
25
- end
@@ -1,28 +0,0 @@
1
- require "falcon"
2
- require_relative "router"
3
-
4
- module Pytty
5
- module Daemon
6
- module Api
7
- class Chunk
8
-
9
- def call(env)
10
- task = Async::Task.current
11
- body = Async::HTTP::Body::Writable.new
12
- task.async do |task|
13
- 10.times do
14
- body.write "hello"
15
- task.sleep 0.5
16
- end
17
- rescue Exception => ex
18
- p ex
19
- ensure
20
- body.close
21
- end
22
-
23
- [200, {'content-type' => 'text/html; charset=utf-8'}, body]
24
- end
25
- end
26
- end
27
- end
28
- end