pytty 0.2.0 → 0.3.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 +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