proclib 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b8a7d4e4a70ff96e2309807bffc296bb93d9aa67
4
+ data.tar.gz: 4c09847012dfc3010071e7d589ab6802786726fe
5
+ SHA512:
6
+ metadata.gz: cead6a0fffa9ee03ae07ab6ea2c0dfd368171497e4b30f5b31d7c12bfedfde362d76bcd2bf1d6788aba12dbf05369a196ab235783507a7f9d290239f4782b1a1
7
+ data.tar.gz: 9b4385413c690e8ef80484b6d20fe986f2d23f97a1cba6a9f3157ce203db427b78620a233dffeb3ab63cdbc2d35ea80b1b4d989686a0fd9871c1574e266f3e95
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /vendor/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.3
5
+ before_install: gem install bundler -v 1.13.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in proclib.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Jack Forrest
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,23 @@
1
+ # Proclib
2
+
3
+ High-level tools for subprocess management in Ruby
4
+
5
+ ## Status
6
+
7
+ I use Proclib as a suport library for a couple of systems utilities that I'm
8
+ slowly preparing for a proper open-source release. Proclib is one of
9
+ several extracted libraries that I'll be maturing into their own projects.
10
+
11
+ Proclib is currently beta quality at best.
12
+
13
+ ## Usage
14
+
15
+ See `examples/main.rb`
16
+
17
+ Currenty, Proclib doesn't do anything on exit to kill child processes, so you'll
18
+ need to trap the appropriate signals yourself to keep from leaving orphaned processes
19
+ around.
20
+
21
+ ## License
22
+
23
+ [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "proclib"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,21 @@
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
+ _, stdout, _ = Proclib.run("ls /tmp/", capture_output: true)
15
+
16
+ puts "Files in /tmp"
17
+ puts stdout.join
18
+
19
+ cmd = "seq 1 10 | while read n; do echo $n; sleep 0.5; done"
20
+
21
+ Proclib.run({tmp: cmd, home: cmd}, log_to_console: true)
@@ -0,0 +1,34 @@
1
+ require 'open3'
2
+ require 'thread'
3
+
4
+ require 'proclib/version'
5
+
6
+ require 'proclib/event_emitter'
7
+ require 'proclib/process'
8
+ require 'proclib/process_group'
9
+ require 'proclib/executor'
10
+
11
+ module Proclib
12
+ module Methods
13
+ def run(cmd, tag: nil, log_to_console: false, capture_output: true)
14
+ runnable = if cmd.kind_of? String
15
+ Process.new(cmd, tag: tag || cmd[0..20])
16
+ elsif cmd.kind_of?(Hash)
17
+ processes = cmd.map {|(k,v)| Process.new(v, tag: k || v[0..20]) }
18
+ ProcessGroup.new(processes)
19
+ else
20
+ raise ArgumentError, "Unexpected type for `cmd`: #{cmd.class}. \n"\
21
+ "Expected String or Hash"
22
+ end
23
+
24
+ executor = Executor.new(runnable,
25
+ log_to_console: log_to_console,
26
+ cache_output: capture_output)
27
+ executor.run_sync
28
+ end
29
+ end
30
+
31
+ class << self
32
+ include Methods
33
+ end
34
+ end
@@ -0,0 +1,71 @@
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
@@ -0,0 +1,57 @@
1
+ require 'proclib/event_emitter'
2
+ require 'proclib/loggers/console'
3
+ require 'proclib/output_cache'
4
+
5
+ module Proclib
6
+ # Runs a runnable, handling emitted events and dispatching to configured
7
+ # facilities
8
+ class Executor
9
+ attr_reader :opts
10
+
11
+ def initialize(runnable, opts = {})
12
+ @runnable = runnable
13
+ @opts = opts
14
+ end
15
+
16
+ def on(name, &block)
17
+ channel.on(name, &block)
18
+ end
19
+
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) }
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :runnable, :log_to_console
30
+
31
+ def configure
32
+ runnable.bind_to(channel)
33
+ channel.on(:complete) do |event|
34
+ @status = event.data.to_i
35
+ channel.finalize
36
+ end
37
+ configure_output
38
+ end
39
+
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]
43
+ end
44
+
45
+ def output_cache
46
+ @output_cache ||= OutputCache.new
47
+ end
48
+
49
+ def console_logger
50
+ @console_logger ||= Loggers::Console.new
51
+ end
52
+
53
+ def channel
54
+ @channel ||= EventEmitter::Channel.new
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,24 @@
1
+ require 'proclib/string_formatting'
2
+
3
+ using Proclib::StringFormatting
4
+
5
+ module Proclib
6
+ module Loggers
7
+ class Console
8
+ def log(message)
9
+ STDOUT.printf("[ %-20s | %-8s ] %s",
10
+ message.process_tag.to_s.truncate_to(20).colorize(:cyan),
11
+ stylized_pipe_name(message),
12
+ message.line)
13
+ end
14
+ alias_method :<<, :log
15
+
16
+ private
17
+
18
+ def stylized_pipe_name(message)
19
+ color = ( {stdout: :blue, stderr: :yellow}[message.pipe_name] || :default )
20
+ message.pipe_name.to_s.truncate_to(8).colorize(color)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,40 @@
1
+ module Proclib
2
+ class OutputCache
3
+ class Entry < Struct.new(:process_tag, :pipe_name, :cache)
4
+ def <<(line)
5
+ entry << line
6
+ end
7
+
8
+ private
9
+
10
+ def entry
11
+ (process_cache[pipe_name] ||= Array.new)
12
+ end
13
+
14
+ def process_cache
15
+ (cache[process_tag] ||= Hash.new)
16
+ end
17
+ end
18
+
19
+ def << message
20
+ Entry.new(message.process_tag, message.pipe_name, cache) << message.line
21
+ end
22
+
23
+ def pipe_aggregate(name)
24
+ process_caches.map {|c| c[name] || []}.flatten
25
+ end
26
+
27
+ private
28
+
29
+ def process_caches
30
+ cache.values
31
+ end
32
+
33
+ # Data structure: { process_tag: { stdin: [], stdout: [] } }
34
+ def cache
35
+ @cache ||= Hash.new
36
+ end
37
+ end
38
+
39
+ private_constant :OutputCache
40
+ end
@@ -0,0 +1,37 @@
1
+ require 'thread'
2
+
3
+ module Proclib
4
+ # Emits events for the given io pipe with relevant tagging info
5
+ class OutputHandler
6
+ Message = Class.new(Struct.new(:process_tag, :pipe_name, :line))
7
+
8
+ include EventEmitter::Producer
9
+
10
+ def initialize(process_tag, pipe_name, pipe)
11
+ @process_tag, @pipe_name, @pipe = process_tag, pipe_name, pipe
12
+ end
13
+
14
+ def start
15
+ @thread = Thread.new { monitor }
16
+ end
17
+
18
+ def wait
19
+ @thread.join
20
+ end
21
+
22
+ def kill
23
+ @thread.exit
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :process_tag, :pipe_name, :pipe
29
+
30
+ def monitor
31
+ pipe.each_line do |line|
32
+ emit(:output, Message.new(process_tag, pipe_name, line))
33
+ end
34
+ emit(:end_of_output)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,72 @@
1
+ require 'open3'
2
+ require 'ostruct'
3
+
4
+ require 'proclib/output_handler'
5
+
6
+ module Proclib
7
+ # Runs a single process, emitting output, state changes and exit status
8
+ class Process
9
+ include EventEmitter::Producer
10
+
11
+ attr_reader :cmdline, :tag
12
+
13
+ Error = Class.new(StandardError)
14
+
15
+ def initialize(cmdline, tag:)
16
+ @cmdline = cmdline
17
+ @tag = tag
18
+ @state = :ready
19
+ @io_handlers = OpenStruct.new
20
+ @pipes = OpenStruct.new
21
+ end
22
+
23
+ def spawn
24
+ raise(Error, "Already started process") unless @wait_thread.nil?
25
+
26
+ pipes.stdin, pipes.stdout, pipes.stderr, @wait_thread = Open3.popen3(cmdline)
27
+ @state = :running?
28
+ start_output_emitters
29
+ start_watch_thread
30
+ end
31
+
32
+ def complete?
33
+ @state == :complete
34
+ end
35
+
36
+ private
37
+ attr_reader :wait_thread, :io_handlers, :pipes
38
+
39
+ def start_watch_thread
40
+ Thread.new do
41
+ result = wait_thread.value
42
+ io_handlers.each_pair {|(_, e)| e.wait }
43
+ @state = :complete
44
+ emit(:exit, result)
45
+ emit(:complete)
46
+ end
47
+ end
48
+
49
+ def start_output_emitters
50
+ %i(stderr stdout).map do |type|
51
+ io_handlers[type] = OutputHandler.new(tag, type, pipes[type]).tap do |handler|
52
+ bubble_events_for(handler)
53
+ handler.start
54
+ end
55
+ end
56
+ end
57
+
58
+ def check_started
59
+ if wait_thread.nil?
60
+ raise Error, "Process `#{tag}` is not yet started!"
61
+ end
62
+ end
63
+
64
+ def output_buffer
65
+ check_started
66
+ @output_buffer ||= OutputBuffer.new(tag, stdout_pipe, stderr_pipe).tap do |buffer|
67
+ bubble_events_for(buffer)
68
+ end
69
+ end
70
+ end
71
+ private_constant :Process
72
+ end
@@ -0,0 +1,41 @@
1
+ require 'proclib/event_emitter'
2
+
3
+ module Proclib
4
+ class ProcessGroup
5
+ include EventEmitter::Producer
6
+
7
+ def initialize(processes)
8
+ @processes = processes
9
+ end
10
+
11
+ def spawn
12
+ processes.each do |process|
13
+ process.bind_to(channel)
14
+ process.spawn
15
+ end
16
+
17
+ start_watch_thread
18
+ end
19
+
20
+ def kill
21
+ processes.each(&:kill)
22
+ end
23
+
24
+ private
25
+ attr_reader :processes
26
+
27
+ def start_watch_thread
28
+ Thread.new { channel.watch }
29
+ end
30
+
31
+ def channel
32
+ @channel ||= EventEmitter::Channel.new.tap do |channel|
33
+ channel.on(:output) {|ev| push(ev) }
34
+ channel.on(:exit) do |ev|
35
+ emit(:complete) if processes.all?(&:complete?)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ private_constant :ProcessGroup
41
+ end
@@ -0,0 +1,28 @@
1
+ module Proclib
2
+ module StringFormatting
3
+ refine String do
4
+ UnknownColor = Class.new(StandardError)
5
+
6
+ COLORS = {
7
+ yellow: 33,
8
+ blue: 34,
9
+ cyan: 36,
10
+ default: 0,
11
+ }
12
+
13
+ def colorize(color)
14
+ color = COLORS[color]
15
+
16
+ if color.nil?
17
+ raise(UnknownColor, "Unknown color for string: `#{color}`")
18
+ end
19
+
20
+ "\033[#{color}m#{self}\033[#{COLORS[:default]}m"
21
+ end
22
+
23
+ def truncate_to(size)
24
+ self[0..size]
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Proclib
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'proclib/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "proclib"
8
+ spec.version = Proclib::VERSION
9
+ spec.authors = ["Jack Forrest"]
10
+ spec.email = ["jack@jrforrest.net"]
11
+
12
+ spec.summary = %q{Provides tools for subprocess management}
13
+ spec.description = "Proclib allows easy management of subprocess with a "\
14
+ "very high-level interface, with niceties such as multiplexed logging of "\
15
+ "output to logfiles and the console, output capture, and signal propagation."
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.13"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ spec.add_development_dependency "pry"
29
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: proclib
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jack Forrest
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-11-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.13'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Proclib allows easy management of subprocess with a very high-level interface,
70
+ with niceties such as multiplexed logging of output to logfiles and the console,
71
+ output capture, and signal propagation.
72
+ email:
73
+ - jack@jrforrest.net
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - ".gitignore"
79
+ - ".rspec"
80
+ - ".travis.yml"
81
+ - Gemfile
82
+ - LICENSE.txt
83
+ - README.md
84
+ - Rakefile
85
+ - bin/console
86
+ - bin/setup
87
+ - examples/main.rb
88
+ - lib/proclib.rb
89
+ - lib/proclib/event_emitter.rb
90
+ - lib/proclib/executor.rb
91
+ - lib/proclib/loggers/console.rb
92
+ - lib/proclib/output_cache.rb
93
+ - lib/proclib/output_handler.rb
94
+ - lib/proclib/process.rb
95
+ - lib/proclib/process_group.rb
96
+ - lib/proclib/string_formatting.rb
97
+ - lib/proclib/version.rb
98
+ - proclib.gemspec
99
+ homepage:
100
+ licenses:
101
+ - MIT
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubyforge_project:
119
+ rubygems_version: 2.5.2
120
+ signing_key:
121
+ specification_version: 4
122
+ summary: Provides tools for subprocess management
123
+ test_files: []