proclib 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []