proclib 0.1.4 → 0.2.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.
- 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
|