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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6aa637a50487fd0311e29608e48813b41a7a9081
4
- data.tar.gz: 89259efc574e1ba539de23f1f3ee7114b393ae72
3
+ metadata.gz: e65c3f6fff0184132ee293d9481f36307f033e50
4
+ data.tar.gz: dd211651a7ffe1b7a273223d05b939becb9933f0
5
5
  SHA512:
6
- metadata.gz: 57c244222c681b1201f9596882daa27ddf59cff9fc2771bdc2d4304e6f04d7713b170ead9e976036276ec5c92a733383eb64f9b028ff7a6258572ccc546792ec
7
- data.tar.gz: 82a4ef26e49c2ef1782bb2b1b74204e7c441d367f77956e9c1c6ac4697384e1220e98dabe5ec039b9abf9d6cbfdf62df9ac88fb4ffbedc510a830a82c295cfe8
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
- Proclib is currently beta quality at best.
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 `examples/main.rb`
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
- module Methods
13
- def run(cmd,
14
- tag: nil,
15
- log_to_console: false,
16
- capture_output: true,
17
- env: {},
18
- on_output: nil,
19
- cwd: nil
20
- )
21
-
22
- raise(ArgumentError, "env must be a Hash") unless env.kind_of?(Hash)
23
-
24
- runnable = if cmd.kind_of? String
25
- Process.new(cmd, tag: tag || cmd[0..20], env: env, run_dir: cwd)
26
- elsif cmd.kind_of?(Hash)
27
- processes = cmd.map do |(k,v)|
28
- Process.new(v, tag: k || v[0..20], env: env, run_dir: cwd)
29
- end
30
-
31
- ProcessGroup.new(processes)
32
- else
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
- class << self
51
- include Methods
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
@@ -0,0 +1,3 @@
1
+ module Proclib
2
+ Error = Class.new(StandardError)
3
+ end
@@ -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 runnable, handling emitted events and dispatching to configured
7
- # facilities
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 :opts
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(runnable, opts = {})
12
- @runnable = runnable
13
- @opts = opts
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 on(name, &block)
17
- channel.on(name, &block)
21
+ def run_sync
22
+ start
23
+ wait
18
24
  end
19
25
 
20
- def run_sync
21
- configure
22
- runnable.spawn
23
- channel.watch
24
- return @status, *%i{stdout stderr}.map {|i| output_cache.pipe_aggregate(i) }
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
- attr_reader :runnable, :log_to_console
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 configure
32
- runnable.bind_to(channel)
33
- channel.on(:exit) do |event|
34
- @status = event.data.to_i
35
- channel.finalize
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 configure_output
41
- channel.on(:output) {|e| console_logger << e.data } if opts[:log_to_console]
42
- channel.on(:output) {|e| output_cache << e.data} if opts[:cache_output]
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
- if opts[:on_output]
45
- channel.on(:output) do |e|
46
- msg = e.data
47
- opts[:on_output].call(msg.line, msg.process_tag, msg.pipe_name)
48
- end
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 ||= EventEmitter::Channel.new
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
- Message = Class.new(Struct.new(:process_tag, :pipe_name, :line))
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
- include EventEmitter::Producer
40
+ private
9
41
 
10
- def initialize(process_tag, pipe_name, pipe)
11
- @process_tag, @pipe_name, @pipe = process_tag, pipe_name, pipe
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
- attr_reader :process_tag, :pipe_name, :pipe
68
+ def pipe
69
+ @pipe ||= command.pipes[type]
70
+ end
29
71
 
30
72
  def monitor
31
- pipe.each_line do |line|
32
- emit(:output, Message.new(process_tag, pipe_name, line))
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
@@ -1,82 +1,42 @@
1
- require 'open3'
2
- require 'ostruct'
1
+ require 'thread'
3
2
 
4
- require 'proclib/output_handler'
3
+ require 'proclib/command_monitor'
5
4
 
6
5
  module Proclib
7
- # Runs a single process, emitting output, state changes and exit status
6
+ # Runs a command, emitting output, state changes and
7
+ # exit status to the given channel
8
8
  class Process
9
- include EventEmitter::Producer
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(cmdline, tag:, env: {}, run_dir: nil)
16
- @cmdline = cmdline
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") unless @wait_thread.nil?
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
- if run_dir
33
- Dir.chdir(run_dir) { spawn.call }
34
- else
35
- spawn.call
36
- end
20
+ @state = :started
21
+ command.spawn
37
22
 
38
- @state = :running?
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, :pipes
28
+ attr_reader :wait_thread, :io_handlers
49
29
 
50
30
  def start_watch_thread
51
- Thread.new do
52
- result = wait_thread.value
53
- io_handlers.each_pair {|(_, e)| e.wait }
54
- @state = :complete
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 start_output_emitters
61
- %i(stderr stdout).map do |type|
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
@@ -1,3 +1,3 @@
1
1
  module Proclib
2
- VERSION = "0.1.4"
2
+ VERSION = "0.2.0"
3
3
  end
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.1.4
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: 2017-12-02 00:00:00.000000000 Z
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/event_emitter.rb
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