proclib 0.1.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +12 -2
- data/lib/proclib.rb +25 -45
- data/lib/proclib/channel.rb +44 -0
- data/lib/proclib/command.rb +129 -0
- data/lib/proclib/command_monitor.rb +29 -0
- data/lib/proclib/errors.rb +3 -0
- data/lib/proclib/executor.rb +66 -31
- data/lib/proclib/invocation.rb +116 -0
- data/lib/proclib/output_handler.rb +57 -8
- data/lib/proclib/process.rb +18 -58
- data/lib/proclib/result.rb +31 -0
- data/lib/proclib/version.rb +1 -1
- data/proclib.gemspec +3 -0
- metadata +36 -4
- data/examples/main.rb +0 -31
- data/lib/proclib/event_emitter.rb +0 -71
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e65c3f6fff0184132ee293d9481f36307f033e50
|
4
|
+
data.tar.gz: dd211651a7ffe1b7a273223d05b939becb9933f0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a6c87b7026897592f982245ec772b7329a81eb91e2239816231dee82d351f49379866599504c6efffb4fbfc790907f99a2b26f812cbb8a50ac7e7dce6d7cd627
|
7
|
+
data.tar.gz: 365c9d78bfed861ec49d8c8c86908cf766c553e224a44090144c0afad16f75869b579cf04193a6feb68c3a62b50dc1086d8717f29ff6cd36aeb2aa4f28c71aaa
|
data/README.md
CHANGED
@@ -8,16 +8,26 @@ I use Proclib as a suport library for a couple of systems utilities that I'm
|
|
8
8
|
slowly preparing for a proper open-source release. Proclib is one of
|
9
9
|
several extracted libraries that I'll be maturing into their own projects.
|
10
10
|
|
11
|
-
|
11
|
+
**Disclaimer**
|
12
|
+
|
13
|
+
Proclib is currently beta quality at best. The code is written in a
|
14
|
+
fairly unmaintainable fashion at the moment, and I definitely don't
|
15
|
+
suggest anyone not interested in putting some work into the library
|
16
|
+
use this in its current state.
|
12
17
|
|
13
18
|
## Usage
|
14
19
|
|
15
|
-
See `
|
20
|
+
See acceptance specs in `spec/acceptance`.
|
16
21
|
|
17
22
|
Currenty, Proclib doesn't do anything on exit to kill child processes, so you'll
|
18
23
|
need to trap the appropriate signals yourself to keep from leaving orphaned processes
|
19
24
|
around.
|
20
25
|
|
26
|
+
## Issues
|
27
|
+
|
28
|
+
- Even when caching output, Proclib expects short lines meant for user consumption
|
29
|
+
in output from its processes, and will crash on more than a few KB per line.
|
30
|
+
|
21
31
|
## License
|
22
32
|
|
23
33
|
[MIT License](http://opensource.org/licenses/MIT).
|
data/lib/proclib.rb
CHANGED
@@ -1,53 +1,33 @@
|
|
1
|
-
require 'open3'
|
2
|
-
require 'thread'
|
3
|
-
|
4
1
|
require 'proclib/version'
|
5
|
-
|
6
|
-
require 'proclib/event_emitter'
|
7
|
-
require 'proclib/process'
|
8
|
-
require 'proclib/process_group'
|
9
2
|
require 'proclib/executor'
|
3
|
+
require 'proclib/invocation'
|
10
4
|
|
11
5
|
module Proclib
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
raise ArgumentError, "Unexpected type for `cmd`: #{cmd.class}. \n"\
|
34
|
-
"Expected String or Hash"
|
35
|
-
end
|
36
|
-
|
37
|
-
unless on_output.nil? || on_output.kind_of?(Proc) || on_ouptut.kind_of?(Lambda)
|
38
|
-
raise ArgumentError, "Expected :on_output to be a proc or lambda if given"
|
39
|
-
end
|
40
|
-
|
41
|
-
executor = Executor.new(runnable,
|
42
|
-
log_to_console: log_to_console,
|
43
|
-
on_output: on_output,
|
44
|
-
cache_output: capture_output)
|
45
|
-
|
46
|
-
executor.run_sync
|
6
|
+
def self.run(cmd,
|
7
|
+
tag: nil,
|
8
|
+
log_to_console: false,
|
9
|
+
capture_output: true,
|
10
|
+
env: {},
|
11
|
+
on_output: nil,
|
12
|
+
cwd: nil,
|
13
|
+
ssh: nil
|
14
|
+
)
|
15
|
+
|
16
|
+
inv = Invocation.new(cmd,
|
17
|
+
tag: tag,
|
18
|
+
env: env,
|
19
|
+
cwd: cwd,
|
20
|
+
ssh: ssh)
|
21
|
+
|
22
|
+
executor = Executor.new(inv.commands,
|
23
|
+
log_to_console: log_to_console,
|
24
|
+
cache_output: capture_output
|
25
|
+
).tap do |ex|
|
26
|
+
ex.on_output(&on_output) unless on_output.nil?
|
47
27
|
end
|
48
|
-
end
|
49
28
|
|
50
|
-
|
51
|
-
|
29
|
+
executor.run_sync
|
30
|
+
rescue Invocation::Invalid => e
|
31
|
+
raise ArgumentError, e.message
|
52
32
|
end
|
53
33
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Proclib
|
4
|
+
# Simple thread-safe communication mechanism
|
5
|
+
class Channel
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
Message = Struct.new(:type, :data)
|
9
|
+
UnexpectedMessageType = Class.new(StandardError)
|
10
|
+
|
11
|
+
attr_reader :allowed_types
|
12
|
+
|
13
|
+
def initialize(*types)
|
14
|
+
@allowed_types = types
|
15
|
+
end
|
16
|
+
|
17
|
+
def emit(type, data = nil)
|
18
|
+
unless allowed_types.include?(type)
|
19
|
+
raise UnexpectedMessageType,
|
20
|
+
"Message type expected to be one of `#{allowed_types}`. "\
|
21
|
+
"Got: `#{type}`"
|
22
|
+
end
|
23
|
+
|
24
|
+
queue.push(Message.new(type, data))
|
25
|
+
end
|
26
|
+
|
27
|
+
def close
|
28
|
+
queue.push(:done)
|
29
|
+
end
|
30
|
+
|
31
|
+
def each
|
32
|
+
raise(ArgumentError, 'Block Expected!') unless block_given?
|
33
|
+
|
34
|
+
while msg = queue.pop
|
35
|
+
break if msg == :done
|
36
|
+
yield msg
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def queue
|
41
|
+
@queue ||= Queue.new
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
require 'proclib/errors'
|
5
|
+
|
6
|
+
require 'net/ssh'
|
7
|
+
|
8
|
+
module Proclib
|
9
|
+
module Commands
|
10
|
+
class Command
|
11
|
+
NotYetRunning = Class.new(Error)
|
12
|
+
NotYetTerminated = Class.new(Error)
|
13
|
+
|
14
|
+
attr_reader :tag, :cmdline, :env, :run_dir
|
15
|
+
|
16
|
+
def initialize(tag: nil, cmdline:, env: {} , run_dir: nil)
|
17
|
+
@env = env.map {|k,v| [k.to_s, v.to_s]}.to_h
|
18
|
+
@cmdline = cmdline
|
19
|
+
@tag = tag || cmdline[0..20]
|
20
|
+
@run_dir = run_dir
|
21
|
+
end
|
22
|
+
|
23
|
+
def pipes
|
24
|
+
@pipes ||= OpenStruct.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def spawn
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
|
31
|
+
def wait
|
32
|
+
raise NotImplementedError
|
33
|
+
end
|
34
|
+
|
35
|
+
def result
|
36
|
+
@result || raise(NotYetTerminated)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
private_constant :Command
|
40
|
+
|
41
|
+
class LocalCommand < Command
|
42
|
+
def spawn
|
43
|
+
spawn = -> do
|
44
|
+
pipes.stdin, pipes.stdout, pipes.stderr, @wait_thread = Open3.popen3(env, cmdline)
|
45
|
+
end
|
46
|
+
|
47
|
+
if run_dir
|
48
|
+
Dir.chdir(run_dir) { spawn.call }
|
49
|
+
else
|
50
|
+
spawn.call
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def wait
|
55
|
+
@result ||= wait_thread.value.to_i
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def wait_thread
|
61
|
+
@wait_thread || raise(NotYetRunning)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class SshCommand < Command
|
66
|
+
attr_reader :ssh_opts
|
67
|
+
|
68
|
+
SSHError = Class.new(Error)
|
69
|
+
|
70
|
+
def initialize(ssh:, **args)
|
71
|
+
@ssh_opts = ssh.clone
|
72
|
+
super(**args)
|
73
|
+
end
|
74
|
+
|
75
|
+
def spawn
|
76
|
+
write_pipes
|
77
|
+
|
78
|
+
open_channel do |ch|
|
79
|
+
ch.exec(cmdline) do |_, success|
|
80
|
+
raise SSHError, "Command Failed" unless success
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def wait
|
86
|
+
ssh_session.loop
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def open_channel
|
92
|
+
ssh_session.open_channel do |channel|
|
93
|
+
channel.on_open_failed do |ch, code, desc, lang|
|
94
|
+
raise SSHError, desc
|
95
|
+
end
|
96
|
+
|
97
|
+
channel.on_data {|_, data| write_pipes[:stdout].write(data) }
|
98
|
+
|
99
|
+
channel.on_extended_data {|_, data| write_pipes[:stderr].write(data) }
|
100
|
+
|
101
|
+
channel.on_request("exit-status") do |_, data|
|
102
|
+
write_pipes.each {|k,v| v.close }
|
103
|
+
@result = data.read_long
|
104
|
+
end
|
105
|
+
|
106
|
+
yield channel
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def write_pipes
|
111
|
+
@write_pipes ||= %i(stdout stderr).map do |type|
|
112
|
+
read, write = IO.pipe
|
113
|
+
pipes[type] = read
|
114
|
+
[type, write]
|
115
|
+
end.to_h
|
116
|
+
end
|
117
|
+
|
118
|
+
def ssh_session
|
119
|
+
@ssh_session ||= Net::SSH.start(*ssh_params, ssh_opts).tap do |session|
|
120
|
+
session.chdir(run_dir) unless run_dir.nil?
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def ssh_params
|
125
|
+
%i(host user).map {|i| ssh_opts.delete(i)}.compact
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'proclib/output_handler'
|
2
|
+
|
3
|
+
module Proclib
|
4
|
+
# Watches the given command, emitting the appropriate events
|
5
|
+
# on the given channel when the command does something
|
6
|
+
class CommandMonitor
|
7
|
+
attr_reader :command, :channel
|
8
|
+
|
9
|
+
def initialize(command, channel:)
|
10
|
+
@command, @channel = command, channel
|
11
|
+
end
|
12
|
+
|
13
|
+
def start
|
14
|
+
io_handlers.each(&:start)
|
15
|
+
end
|
16
|
+
|
17
|
+
def wait
|
18
|
+
io_handlers.each(&:wait)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def io_handlers
|
24
|
+
@io_handlers ||= %i(stderr stdout).map do |type|
|
25
|
+
OutputHandler.new(type, command, channel: channel)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/proclib/executor.rb
CHANGED
@@ -1,52 +1,83 @@
|
|
1
|
-
require 'proclib/event_emitter'
|
2
1
|
require 'proclib/loggers/console'
|
3
2
|
require 'proclib/output_cache'
|
3
|
+
require 'proclib/channel'
|
4
|
+
require 'proclib/process'
|
5
|
+
require 'proclib/result'
|
4
6
|
|
5
7
|
module Proclib
|
6
|
-
# Runs a
|
7
|
-
#
|
8
|
+
# Runs a list of commands simultaenously, providing callbacks on their output
|
9
|
+
# lines and exits, as well as optional output logging and caching.
|
8
10
|
class Executor
|
9
|
-
attr_reader :
|
11
|
+
attr_reader :log_to_console, :cache_output, :commands, :callbacks
|
12
|
+
alias_method :log_to_console?, :log_to_console
|
13
|
+
alias_method :cache_output?, :cache_output
|
10
14
|
|
11
|
-
def initialize(
|
12
|
-
@
|
13
|
-
|
15
|
+
def initialize(commands, log_to_console: false, cache_output: false)
|
16
|
+
@commands, @log_to_console, @cache_output =
|
17
|
+
commands, log_to_console, cache_output
|
18
|
+
@callbacks = Struct.new(:exit, :output).new([], [])
|
14
19
|
end
|
15
20
|
|
16
|
-
def
|
17
|
-
|
21
|
+
def run_sync
|
22
|
+
start
|
23
|
+
wait
|
18
24
|
end
|
19
25
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
26
|
+
def start
|
27
|
+
processes.each(&:spawn)
|
28
|
+
end
|
29
|
+
|
30
|
+
def wait
|
31
|
+
channel.each do |message|
|
32
|
+
handle_exit(message) if message.type == :exit
|
33
|
+
handle_output(message) if message.type == :output
|
34
|
+
end
|
35
|
+
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
def on_exit(&blk)
|
40
|
+
callbacks.exit << blk
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_output(&blk)
|
44
|
+
callbacks.output << blk
|
45
|
+
end
|
46
|
+
|
47
|
+
def exit_states
|
48
|
+
@exit_states ||= Array.new
|
25
49
|
end
|
26
50
|
|
27
51
|
private
|
28
52
|
|
29
|
-
|
53
|
+
def result
|
54
|
+
Result.new(
|
55
|
+
exit_code: aggregate_exit_code,
|
56
|
+
output_cache: (output_cache if cache_output?)
|
57
|
+
)
|
58
|
+
end
|
30
59
|
|
31
|
-
def
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
60
|
+
def aggregate_exit_code
|
61
|
+
if exit_states.all? {|c| c == 0}
|
62
|
+
0
|
63
|
+
elsif exit_states.size == 1
|
64
|
+
exit_states.first
|
65
|
+
else
|
66
|
+
1
|
36
67
|
end
|
37
|
-
configure_output
|
38
68
|
end
|
39
69
|
|
40
|
-
def
|
41
|
-
|
42
|
-
channel.
|
70
|
+
def handle_exit(message)
|
71
|
+
exit_states << message.data
|
72
|
+
channel.close if exit_states.size == processes.size
|
73
|
+
callbacks.exit.each {|c| c[message.data] }
|
74
|
+
end
|
43
75
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
end
|
76
|
+
def handle_output(message)
|
77
|
+
callbacks.output.each {|c| c[message.data]}
|
78
|
+
|
79
|
+
console_logger << message.data if log_to_console?
|
80
|
+
output_cache << message.data if cache_output?
|
50
81
|
end
|
51
82
|
|
52
83
|
def output_cache
|
@@ -58,7 +89,11 @@ module Proclib
|
|
58
89
|
end
|
59
90
|
|
60
91
|
def channel
|
61
|
-
@channel ||=
|
92
|
+
@channel ||= Channel.new(:output, :exit)
|
93
|
+
end
|
94
|
+
|
95
|
+
def processes
|
96
|
+
commands.map {|c| Process.new(c, channel: channel) }
|
62
97
|
end
|
63
98
|
end
|
64
99
|
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
require 'proclib/errors'
|
4
|
+
require 'proclib/command'
|
5
|
+
|
6
|
+
module Proclib
|
7
|
+
class Invocation
|
8
|
+
Invalid = Class.new(Error)
|
9
|
+
|
10
|
+
def initialize(cmd,
|
11
|
+
tag: nil,
|
12
|
+
env: {},
|
13
|
+
cwd: nil,
|
14
|
+
ssh: nil
|
15
|
+
)
|
16
|
+
@cmd = cmd
|
17
|
+
@tag = tag
|
18
|
+
@env = env
|
19
|
+
@cwd = cwd
|
20
|
+
@ssh = ssh
|
21
|
+
end
|
22
|
+
|
23
|
+
def commands
|
24
|
+
if validated_cmd.is_a?(String)
|
25
|
+
[ make_command(validated_cmd) ]
|
26
|
+
else
|
27
|
+
validated_cmd.map do |tag, cmdline|
|
28
|
+
make_command(cmdline, tag: tag)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def make_command(cmdline, tag: nil)
|
36
|
+
command_class.new(**command_args)
|
37
|
+
end
|
38
|
+
|
39
|
+
def command_args
|
40
|
+
@command_args ||= {
|
41
|
+
tag: @tag,
|
42
|
+
env: validated_env,
|
43
|
+
run_dir: validated_cwd,
|
44
|
+
cmdline: validated_cmd
|
45
|
+
}.tap do |args|
|
46
|
+
args[:ssh] = validated_ssh if !validated_ssh.nil?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def command_class
|
51
|
+
if validated_ssh.nil?
|
52
|
+
Commands::LocalCommand
|
53
|
+
else
|
54
|
+
Commands::SshCommand
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def validated_env
|
59
|
+
if !@env.kind_of?(Hash)
|
60
|
+
raise Invalid, "`env` must be a Hash if given"
|
61
|
+
end
|
62
|
+
|
63
|
+
@env.each do |*args|
|
64
|
+
args.each do |v|
|
65
|
+
unless [String, Symbol].any? {|c| v.kind_of?(c) }
|
66
|
+
raise Invalid "`env` must be a hash in the form of "\
|
67
|
+
"[String|Symbol] => [String|Symbol] if given"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def validated_cwd
|
74
|
+
return nil if @cwd.nil?
|
75
|
+
|
76
|
+
@validated_cwd ||= @cwd.tap do |cwd|
|
77
|
+
unless [Pathname, String].any? {|c| cwd.kind_of?(c) }
|
78
|
+
raise Invalid, "`cwd` must be a Pathname or String if given"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def validated_ssh
|
84
|
+
return if @ssh.nil?
|
85
|
+
|
86
|
+
@validated_ssh ||= begin
|
87
|
+
%i(host user).each do |k|
|
88
|
+
if @ssh[k].nil?
|
89
|
+
raise Invalid, ":ssh options must contain key `#{k}` if given"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
@validated_ssh
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def validated_cmd
|
98
|
+
@validated_cmd ||= begin
|
99
|
+
if ![String, Hash].any?{|c| @cmd.kind_of?(c)}
|
100
|
+
raise Invalid, "Expected cmd to be either a String or a Hash"
|
101
|
+
end
|
102
|
+
|
103
|
+
if @cmd.kind_of?(Hash)
|
104
|
+
@cmd.each do |key, value|
|
105
|
+
if ! [String, Symbol].include?(key) or !value.kind_of?(String)
|
106
|
+
raise Invalid, "If cmd is a list of commands it must be in "\
|
107
|
+
"the form of `[String|Symbol] => String`"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
@cmd
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -1,14 +1,54 @@
|
|
1
1
|
require 'thread'
|
2
|
+
require 'proclib/errors'
|
2
3
|
|
3
4
|
module Proclib
|
4
5
|
# Emits events for the given io pipe with relevant tagging info
|
5
6
|
class OutputHandler
|
6
|
-
|
7
|
+
# Calls its given callback with each line in the input written
|
8
|
+
# to the buffer
|
9
|
+
class LineBuffer
|
10
|
+
NEWLINE = "\n"
|
11
|
+
MAX_SIZE = 1024 * 10
|
12
|
+
SIZE_ERROR_MESSAGE = "A line of greater than #{MAX_SIZE} bytes was " \
|
13
|
+
"encountered from a process."
|
14
|
+
|
15
|
+
MaxSizeExceeded = Class.new(Error)
|
16
|
+
|
17
|
+
def initialize(&blk)
|
18
|
+
@buf = String.new
|
19
|
+
@callback = blk
|
20
|
+
end
|
21
|
+
|
22
|
+
def write(str)
|
23
|
+
buf << str
|
24
|
+
|
25
|
+
while buf.include?(NEWLINE)
|
26
|
+
idx = buf.index(NEWLINE)
|
27
|
+
callback.call(buf[0..(idx - 1)] + NEWLINE)
|
28
|
+
self.buf = (buf[(idx + 1)..-1] || String.new)
|
29
|
+
end
|
30
|
+
|
31
|
+
if buf.bytesize > MAX_SIZE
|
32
|
+
raise MaxSizeExceeded, SIZE_ERROR_MESSAGE
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def flush
|
37
|
+
callback.call(buf + "\n") unless buf.empty?
|
38
|
+
end
|
7
39
|
|
8
|
-
|
40
|
+
private
|
9
41
|
|
10
|
-
|
11
|
-
|
42
|
+
attr_accessor :buf
|
43
|
+
attr_reader :callback
|
44
|
+
end
|
45
|
+
|
46
|
+
READ_SIZE = 1024
|
47
|
+
Message = Class.new(Struct.new(:process_tag, :pipe_name, :line))
|
48
|
+
|
49
|
+
attr_reader :type, :command, :channel
|
50
|
+
def initialize(type, command, channel:)
|
51
|
+
@type, @command, @channel = type, command, channel
|
12
52
|
end
|
13
53
|
|
14
54
|
def start
|
@@ -25,13 +65,22 @@ module Proclib
|
|
25
65
|
|
26
66
|
private
|
27
67
|
|
28
|
-
|
68
|
+
def pipe
|
69
|
+
@pipe ||= command.pipes[type]
|
70
|
+
end
|
29
71
|
|
30
72
|
def monitor
|
31
|
-
pipe.
|
32
|
-
|
73
|
+
while s = pipe.read(READ_SIZE)
|
74
|
+
line_buffer.write(s)
|
75
|
+
end
|
76
|
+
|
77
|
+
line_buffer.flush
|
78
|
+
end
|
79
|
+
|
80
|
+
def line_buffer
|
81
|
+
@line_buffer ||= LineBuffer.new do |line|
|
82
|
+
channel.emit(:output, Message.new(command.tag, type, line))
|
33
83
|
end
|
34
|
-
emit(:end_of_output)
|
35
84
|
end
|
36
85
|
end
|
37
86
|
end
|
data/lib/proclib/process.rb
CHANGED
@@ -1,82 +1,42 @@
|
|
1
|
-
require '
|
2
|
-
require 'ostruct'
|
1
|
+
require 'thread'
|
3
2
|
|
4
|
-
require 'proclib/
|
3
|
+
require 'proclib/command_monitor'
|
5
4
|
|
6
5
|
module Proclib
|
7
|
-
# Runs a
|
6
|
+
# Runs a command, emitting output, state changes and
|
7
|
+
# exit status to the given channel
|
8
8
|
class Process
|
9
|
-
|
10
|
-
|
11
|
-
attr_reader :cmdline, :tag, :env, :run_dir
|
9
|
+
attr_reader :command, :channel
|
12
10
|
|
13
11
|
Error = Class.new(StandardError)
|
14
12
|
|
15
|
-
def initialize(
|
16
|
-
@
|
17
|
-
@tag = tag
|
18
|
-
@env = env.map {|k,v| [k.to_s, v.to_s]}.to_h
|
19
|
-
@state = :ready
|
20
|
-
@io_handlers = OpenStruct.new
|
21
|
-
@pipes = OpenStruct.new
|
22
|
-
@run_dir = run_dir
|
13
|
+
def initialize(command, channel:)
|
14
|
+
@command, @channel, @state = command, channel, :ready
|
23
15
|
end
|
24
16
|
|
25
17
|
def spawn
|
26
|
-
raise(Error, "Already started process")
|
27
|
-
|
28
|
-
spawn = -> do
|
29
|
-
pipes.stdin, pipes.stdout, pipes.stderr, @wait_thread = Open3.popen3(env, cmdline)
|
30
|
-
end
|
18
|
+
raise(Error, "Already started process") if @state != :ready
|
31
19
|
|
32
|
-
|
33
|
-
|
34
|
-
else
|
35
|
-
spawn.call
|
36
|
-
end
|
20
|
+
@state = :started
|
21
|
+
command.spawn
|
37
22
|
|
38
|
-
|
39
|
-
start_output_emitters
|
23
|
+
output_emitter.start
|
40
24
|
start_watch_thread
|
41
25
|
end
|
42
26
|
|
43
|
-
def complete?
|
44
|
-
@state == :complete
|
45
|
-
end
|
46
|
-
|
47
27
|
private
|
48
|
-
attr_reader :wait_thread, :io_handlers
|
28
|
+
attr_reader :wait_thread, :io_handlers
|
49
29
|
|
50
30
|
def start_watch_thread
|
51
|
-
Thread.new do
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
emit(:exit, result)
|
56
|
-
emit(:complete)
|
31
|
+
@watch_thread ||= Thread.new do
|
32
|
+
command.wait
|
33
|
+
output_emitter.wait
|
34
|
+
channel.emit(:exit, command.result)
|
57
35
|
end
|
58
36
|
end
|
59
37
|
|
60
|
-
def
|
61
|
-
|
62
|
-
io_handlers[type] = OutputHandler.new(tag, type, pipes[type]).tap do |handler|
|
63
|
-
bubble_events_for(handler)
|
64
|
-
handler.start
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def check_started
|
70
|
-
if wait_thread.nil?
|
71
|
-
raise Error, "Process `#{tag}` is not yet started!"
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def output_buffer
|
76
|
-
check_started
|
77
|
-
@output_buffer ||= OutputBuffer.new(tag, stdout_pipe, stderr_pipe).tap do |buffer|
|
78
|
-
bubble_events_for(buffer)
|
79
|
-
end
|
38
|
+
def output_emitter
|
39
|
+
@output_emitter ||= CommandMonitor.new(command, channel: channel)
|
80
40
|
end
|
81
41
|
end
|
82
42
|
private_constant :Process
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'proclib/errors'
|
2
|
+
|
3
|
+
module Proclib
|
4
|
+
class Result
|
5
|
+
OutputNotAvailable = Class.new(Error)
|
6
|
+
|
7
|
+
attr_reader :exit_code
|
8
|
+
def initialize(exit_code:, output_cache: nil)
|
9
|
+
@exit_code, @output_cache = exit_code, output_cache
|
10
|
+
end
|
11
|
+
|
12
|
+
%i(stdin stdout).each do |type|
|
13
|
+
define_method(type) do
|
14
|
+
if @output_cache.nil?
|
15
|
+
raise OutputNotAvailable, "`#{type}` not cached for this process. "\
|
16
|
+
"ensure output caching is enabled when invoking."
|
17
|
+
end
|
18
|
+
|
19
|
+
@output_cache.pipe_aggregate(type)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def success?
|
24
|
+
exit_code == 0
|
25
|
+
end
|
26
|
+
|
27
|
+
def failure?
|
28
|
+
!success?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/proclib/version.rb
CHANGED
data/proclib.gemspec
CHANGED
@@ -22,8 +22,11 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
23
|
spec.require_paths = ["lib"]
|
24
24
|
|
25
|
+
spec.add_dependency "net-ssh", "~> 4.2"
|
26
|
+
|
25
27
|
spec.add_development_dependency "bundler", "~> 1.13"
|
26
28
|
spec.add_development_dependency "rake", "~> 10.0"
|
27
29
|
spec.add_development_dependency "rspec", "~> 3.0"
|
30
|
+
spec.add_development_dependency "docker-api", "~> 1.0"
|
28
31
|
spec.add_development_dependency "pry"
|
29
32
|
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: proclib
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jack Forrest
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-01-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: net-ssh
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: bundler
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +66,20 @@ dependencies:
|
|
52
66
|
- - "~>"
|
53
67
|
- !ruby/object:Gem::Version
|
54
68
|
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: docker-api
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.0'
|
55
83
|
- !ruby/object:Gem::Dependency
|
56
84
|
name: pry
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -85,15 +113,19 @@ files:
|
|
85
113
|
- Rakefile
|
86
114
|
- bin/console
|
87
115
|
- bin/setup
|
88
|
-
- examples/main.rb
|
89
116
|
- lib/proclib.rb
|
90
|
-
- lib/proclib/
|
117
|
+
- lib/proclib/channel.rb
|
118
|
+
- lib/proclib/command.rb
|
119
|
+
- lib/proclib/command_monitor.rb
|
120
|
+
- lib/proclib/errors.rb
|
91
121
|
- lib/proclib/executor.rb
|
122
|
+
- lib/proclib/invocation.rb
|
92
123
|
- lib/proclib/loggers/console.rb
|
93
124
|
- lib/proclib/output_cache.rb
|
94
125
|
- lib/proclib/output_handler.rb
|
95
126
|
- lib/proclib/process.rb
|
96
127
|
- lib/proclib/process_group.rb
|
128
|
+
- lib/proclib/result.rb
|
97
129
|
- lib/proclib/string_formatting.rb
|
98
130
|
- lib/proclib/version.rb
|
99
131
|
- proclib.gemspec
|
data/examples/main.rb
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
require 'pry'
|
2
|
-
require 'pathname'
|
3
|
-
|
4
|
-
Thread.abort_on_exception = true
|
5
|
-
|
6
|
-
$LOAD_PATH.unshift Pathname.new(File.expand_path(__FILE__)).join('..', '..', 'lib').to_s
|
7
|
-
|
8
|
-
require_relative '../lib/proclib'
|
9
|
-
|
10
|
-
# Run a quick command with output logged to the console
|
11
|
-
|
12
|
-
Proclib.run("echo herorooo >&2", tag: :test, log_to_console: true)
|
13
|
-
|
14
|
-
# Pass env vars to subprocess
|
15
|
-
|
16
|
-
Proclib.run("echo $FOO", env: {FOO: 'hi'}, log_to_console: true)
|
17
|
-
|
18
|
-
_, stdout, _ = Proclib.run("ls /tmp/", capture_output: true)
|
19
|
-
|
20
|
-
puts "Files in /tmp"
|
21
|
-
puts stdout.join
|
22
|
-
|
23
|
-
cmd = "seq 1 5 | while read n; do echo $n; sleep 0.5; done"
|
24
|
-
|
25
|
-
Proclib.run({one: cmd, two: cmd}, log_to_console: true)
|
26
|
-
|
27
|
-
output_callback = -> (line, tag, pipe_name) {
|
28
|
-
STDOUT.printf("%s:%s:%s", tag, pipe_name, line)
|
29
|
-
}
|
30
|
-
|
31
|
-
Proclib.run(cmd, tag: :count_things, on_output: output_callback)
|
@@ -1,71 +0,0 @@
|
|
1
|
-
module Proclib
|
2
|
-
# Async event utils
|
3
|
-
module EventEmitter
|
4
|
-
Event = Struct.new(:name, :sender, :data)
|
5
|
-
Error = Class.new(StandardError)
|
6
|
-
|
7
|
-
# Provides callbacks on events from bound producers
|
8
|
-
class Channel
|
9
|
-
COMPLETE = Object.new
|
10
|
-
|
11
|
-
def initialize
|
12
|
-
@queue = ::Queue.new
|
13
|
-
@handlers = Hash.new
|
14
|
-
end
|
15
|
-
|
16
|
-
def push(msg)
|
17
|
-
unless msg.kind_of?(Event)
|
18
|
-
raise(Error, "EventEmitter::Queue should only handle messages of type EventEmitter::Event")
|
19
|
-
end
|
20
|
-
|
21
|
-
@queue.push(msg)
|
22
|
-
end
|
23
|
-
|
24
|
-
def on(name, &handler)
|
25
|
-
(handlers[name] ||= Array.new) << handler
|
26
|
-
end
|
27
|
-
|
28
|
-
def watch
|
29
|
-
while ev = @queue.pop
|
30
|
-
break if ev == COMPLETE
|
31
|
-
handlers[ev.name].each {|h| h.call(ev)} if handlers[ev.name]
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def finalize
|
36
|
-
@queue.push(COMPLETE)
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
attr_reader :handlers
|
41
|
-
end
|
42
|
-
|
43
|
-
# Emits messages to bound channel
|
44
|
-
module Producer
|
45
|
-
def bind_to(queue)
|
46
|
-
@event_queue = queue
|
47
|
-
end
|
48
|
-
|
49
|
-
def bound?
|
50
|
-
! @event_queue.nil?
|
51
|
-
end
|
52
|
-
|
53
|
-
private
|
54
|
-
|
55
|
-
def bubble_events_for(child)
|
56
|
-
if bound?
|
57
|
-
child.bind_to(@event_queue)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
def emit(name, data = nil)
|
62
|
-
push(Event.new(name, self, data))
|
63
|
-
end
|
64
|
-
|
65
|
-
def push(event)
|
66
|
-
@event_queue.push(event) if bound?
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
private_constant :EventEmitter
|
71
|
-
end
|