invoker 0.0.2
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 +15 -0
- data/.gitignore +4 -0
- data/.travis.yml +6 -0
- data/Gemfile +6 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +5 -0
- data/bin/invoker +14 -0
- data/invoker.gemspec +30 -0
- data/lib/invoker.rb +19 -0
- data/lib/invoker/command_listener.rb +3 -0
- data/lib/invoker/command_listener/client.rb +34 -0
- data/lib/invoker/command_listener/server.rb +31 -0
- data/lib/invoker/command_worker.rb +30 -0
- data/lib/invoker/commander.rb +165 -0
- data/lib/invoker/config.rb +19 -0
- data/lib/invoker/errors.rb +14 -0
- data/lib/invoker/reactor.rb +61 -0
- data/lib/invoker/runner.rb +105 -0
- data/readme.md +57 -0
- data/spec/invoker/command_listener/client_spec.rb +57 -0
- data/spec/invoker/command_worker_spec.rb +4 -0
- data/spec/invoker/commander_spec.rb +92 -0
- data/spec/spec_helper.rb +29 -0
- metadata +169 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ODk1Y2NiOGY1ODllNzlkMDI3ZTAxYzg4MzBiOTQ1Nzg4MTQ3NjM3OQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ODE0MmVhZDAyNzQ0NWIwZTdmYjVlOWRmYzFjZDdiM2I2NzViYjE4NQ==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MDAyNDNhZmYzMmE5NTg0NzFhMWRmNTEwMTM5MjdhNzA0YzAwMTZhY2QxYzRm
|
10
|
+
ZWRiZmRhZjk0NGFmODU4NTMwMDgyNGNkNGVjNDA5M2I4NWEyOTYwZmEyYTVh
|
11
|
+
OWMwNGU2ZmU3NTUyNGNmMGU2YzJhNTA2NzU5M2JkZWQ3NDFhOGY=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MDVmZmQ1OWMxMGM1NTRjM2YzNzJjMjBiNjIzNDA1ZThhZDZmZDI0M2VkM2I4
|
14
|
+
NDNmYjM1ZGZmYTkyOWZmNjg0NjJjOWI3ZTlmMmQ4N2RjNjU0MGVlOGFhMDZi
|
15
|
+
YjE3NWUyOGU2ZjY5MjU0OWRjMmZhNmZlMjc2Njk3MWUzMTliZDI=
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013-2014 Hemant Kumar
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/bin/invoker
ADDED
data/invoker.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{invoker}
|
5
|
+
s.version = "0.0.2"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Hemant Kumar"]
|
9
|
+
s.date = %q{2013-05-04}
|
10
|
+
s.description = %q{Something small for process management}
|
11
|
+
s.email = %q{hemant@codemancers.com}
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {spec,features}/*`.split("\n")
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
s.homepage = %q{http://github.com/code-mancers/invoker}
|
19
|
+
s.licenses = ["MIT"]
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
s.summary = %q{Something small for Process management}
|
22
|
+
s.add_dependency("slop")
|
23
|
+
s.add_dependency("iniparse")
|
24
|
+
s.add_dependency("colored")
|
25
|
+
s.add_development_dependency("bacon")
|
26
|
+
s.add_development_dependency("mocha")
|
27
|
+
s.add_development_dependency("mocha-on-bacon")
|
28
|
+
s.add_development_dependency("rake")
|
29
|
+
end
|
30
|
+
|
data/lib/invoker.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
$: << File.dirname(__FILE__) unless $:.include?(File.expand_path(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
module Invoker
|
4
|
+
VERSION = "0.0.2"
|
5
|
+
end
|
6
|
+
|
7
|
+
require "colored"
|
8
|
+
require_relative "invoker/runner"
|
9
|
+
require_relative "invoker/command_listener"
|
10
|
+
require_relative "invoker/errors"
|
11
|
+
require_relative "invoker/config"
|
12
|
+
require_relative "invoker/commander"
|
13
|
+
require_relative "invoker/command_worker"
|
14
|
+
require_relative "invoker/reactor"
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Invoker
|
2
|
+
module CommandListener
|
3
|
+
class Client
|
4
|
+
attr_accessor :client_socket
|
5
|
+
def initialize(client_socket)
|
6
|
+
@client_socket = client_socket
|
7
|
+
end
|
8
|
+
|
9
|
+
def read_and_execute
|
10
|
+
command_info = client_socket.read()
|
11
|
+
if command_info && !command_info.empty?
|
12
|
+
worker_command, command_label, rest_args = command_info.strip.split(" ")
|
13
|
+
if worker_command && command_label
|
14
|
+
run_command(worker_command, command_label, rest_args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
client_socket.close()
|
18
|
+
end
|
19
|
+
|
20
|
+
def run_command(worker_command, command_label, rest_args = nil)
|
21
|
+
case worker_command
|
22
|
+
when 'add'
|
23
|
+
Invoker::COMMANDER.add_command_by_label(command_label)
|
24
|
+
when 'remove'
|
25
|
+
Invoker::COMMANDER.remove_command(command_label, rest_args)
|
26
|
+
when 'reload'
|
27
|
+
Invoker::COMMANDER.reload_command(command_label)
|
28
|
+
else
|
29
|
+
$stdout.puts("\n Invalid command".red)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
module Invoker
|
4
|
+
module CommandListener
|
5
|
+
class Server
|
6
|
+
SOCKET_PATH = "/tmp/invoker"
|
7
|
+
def initialize
|
8
|
+
@open_clients = []
|
9
|
+
clean_old_socket()
|
10
|
+
UNIXServer.open(SOCKET_PATH) do |client|
|
11
|
+
loop do
|
12
|
+
client_socket = client.accept
|
13
|
+
process_client(client_socket)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def clean_old_socket
|
19
|
+
if File.exists?(SOCKET_PATH)
|
20
|
+
FileUtils.rm(SOCKET_PATH, :force => true)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def process_client(client_socket)
|
25
|
+
client = Invoker::CommandListener::Client.new(client_socket)
|
26
|
+
client.read_and_execute
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Invoker
|
2
|
+
class CommandWorker
|
3
|
+
attr_accessor :command_label, :pipe_end, :pid, :color
|
4
|
+
|
5
|
+
def initialize(command_label, pipe_end, pid, color)
|
6
|
+
@command_label = command_label
|
7
|
+
@pipe_end = pipe_end
|
8
|
+
@pid = pid
|
9
|
+
@color = color
|
10
|
+
end
|
11
|
+
|
12
|
+
# Copied verbatim from Eventmachine code
|
13
|
+
def receive_data data
|
14
|
+
(@buf ||= '') << data
|
15
|
+
|
16
|
+
while @buf.slice!(/(.*?)\r?\n/)
|
17
|
+
receive_line($1)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def unbind
|
22
|
+
# $stdout.print(".")
|
23
|
+
end
|
24
|
+
|
25
|
+
# Print the lines received over the network
|
26
|
+
def receive_line(line)
|
27
|
+
$stdout.puts "#{@command_label.send(color)} : #{line}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require "io/console"
|
2
|
+
require 'pty'
|
3
|
+
|
4
|
+
module Invoker
|
5
|
+
class Commander
|
6
|
+
MAX_PROCESS_COUNT = 10
|
7
|
+
LABEL_COLORS = ['green', 'yellow', 'blue', 'magenta', 'cyan']
|
8
|
+
attr_accessor :reactor, :workers, :thread_group, :open_pipes
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
# mapping between open pipes and worker classes
|
12
|
+
@open_pipes = {}
|
13
|
+
|
14
|
+
# mapping between command label and worker classes
|
15
|
+
@workers = {}
|
16
|
+
|
17
|
+
@thread_group = ThreadGroup.new()
|
18
|
+
@worker_mutex = Mutex.new()
|
19
|
+
@reactor = Invoker::Reactor.new
|
20
|
+
Thread.abort_on_exception = true
|
21
|
+
end
|
22
|
+
|
23
|
+
def start_manager
|
24
|
+
if !Invoker::CONFIG.processes || Invoker::CONFIG.processes.empty?
|
25
|
+
raise Invoker::Errors::InvalidConfig.new("No processes configured in config file")
|
26
|
+
end
|
27
|
+
install_interrupt_handler()
|
28
|
+
unix_server_thread = Thread.new { Invoker::CommandListener::Server.new() }
|
29
|
+
thread_group.add(unix_server_thread)
|
30
|
+
Invoker::CONFIG.processes.each { |process_info| add_command(process_info) }
|
31
|
+
reactor.start
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_command(process_info)
|
35
|
+
m, s = PTY.open
|
36
|
+
s.raw! # disable newline conversion.
|
37
|
+
|
38
|
+
pid = run_command(process_info, s)
|
39
|
+
|
40
|
+
s.close()
|
41
|
+
|
42
|
+
selected_color = LABEL_COLORS.shift()
|
43
|
+
LABEL_COLORS.push(selected_color)
|
44
|
+
worker = Invoker::CommandWorker.new(process_info.label, m, pid, selected_color)
|
45
|
+
|
46
|
+
add_worker(worker)
|
47
|
+
wait_on_pid(process_info.label,pid)
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_command_by_label(command_label)
|
51
|
+
process_info = Invoker::CONFIG.processes.detect {|pconfig|
|
52
|
+
pconfig.label == command_label
|
53
|
+
}
|
54
|
+
if process_info
|
55
|
+
add_command(process_info)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def reload_command(command_label)
|
60
|
+
remove_command(command_label)
|
61
|
+
add_command_by_label(command_label)
|
62
|
+
end
|
63
|
+
|
64
|
+
def remove_command(command_label, rest_args)
|
65
|
+
worker = workers[command_label]
|
66
|
+
signal_to_use = rest_args ? Array(rest_args).first : 'INT'
|
67
|
+
|
68
|
+
if worker
|
69
|
+
$stdout.puts("Removing #{command_label} with signal #{signal_to_use}".red)
|
70
|
+
process_kill(worker.pid, signal_to_use)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_worker_from_fd(fd)
|
75
|
+
open_pipes[fd.fileno]
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_worker_from_label(label)
|
79
|
+
workers[label]
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
def process_kill(pid, signal_to_use)
|
84
|
+
if signal_to_use.to_i == 0
|
85
|
+
Process.kill(signal_to_use, pid)
|
86
|
+
else
|
87
|
+
Process.kill(signal_to_use.to_i, pid)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Remove worker from all collections
|
92
|
+
def remove_worker(command_label)
|
93
|
+
@worker_mutex.synchronize do
|
94
|
+
worker = @workers[command_label]
|
95
|
+
if worker
|
96
|
+
@open_pipes.delete(worker.pipe_end.fileno)
|
97
|
+
@reactor.remove_from_monitoring(worker.pipe_end)
|
98
|
+
@workers.delete(command_label)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# add worker to global collections
|
104
|
+
def add_worker(worker)
|
105
|
+
@worker_mutex.synchronize do
|
106
|
+
@open_pipes[worker.pipe_end.fileno] = worker
|
107
|
+
@workers[worker.command_label] = worker
|
108
|
+
@reactor.add_to_monitor(worker.pipe_end)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def run_command(process_info, write_pipe)
|
113
|
+
if defined?(Bundler)
|
114
|
+
Bundler.with_clean_env do
|
115
|
+
spawn(process_info.cmd,
|
116
|
+
:chdir => process_info.dir || "/", :out => write_pipe, :err => write_pipe
|
117
|
+
)
|
118
|
+
end
|
119
|
+
else
|
120
|
+
spawn(process_info.cmd,
|
121
|
+
:chdir => process_info.dir || "/", :out => write_pipe, :err => write_pipe
|
122
|
+
)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def wait_on_pid(command_label,pid)
|
127
|
+
raise Invoker::Errors::ToomanyOpenConnections if @thread_group.enclosed?
|
128
|
+
thread = Thread.new do
|
129
|
+
Process.wait(pid)
|
130
|
+
message = "Process with command #{command_label} exited with status #{$?.exitstatus}"
|
131
|
+
$stdout.puts("\n#{message}".red)
|
132
|
+
notify_user(message)
|
133
|
+
remove_worker(command_label)
|
134
|
+
end
|
135
|
+
@thread_group.add(thread)
|
136
|
+
end
|
137
|
+
|
138
|
+
def notify_user(message)
|
139
|
+
if defined?(Bundler)
|
140
|
+
Bundler.with_clean_env do
|
141
|
+
check_and_notify_with_terminal_notifier(message)
|
142
|
+
end
|
143
|
+
else
|
144
|
+
check_and_notify_with_terminal_notifier(message)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def check_and_notify_with_terminal_notifier(message)
|
149
|
+
return unless RUBY_PLATFORM.downcase.include?("darwin")
|
150
|
+
|
151
|
+
command_path = `which terminal-notifier`
|
152
|
+
if command_path && !command_path.empty?
|
153
|
+
system("terminal-notifier -message '#{message}' -title Invoker")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def install_interrupt_handler
|
158
|
+
Signal.trap("INT") do
|
159
|
+
@workers.each {|key,worker| Process.kill("INT", worker.pid) }
|
160
|
+
exit(0)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require 'iniparse'
|
3
|
+
|
4
|
+
module Invoker
|
5
|
+
class Config
|
6
|
+
attr_accessor :processes
|
7
|
+
def initialize(filename)
|
8
|
+
@ini_content = File.read(filename)
|
9
|
+
@processes = process_ini(@ini_content)
|
10
|
+
end
|
11
|
+
|
12
|
+
def process_ini(ini_content)
|
13
|
+
document = IniParse.parse(ini_content)
|
14
|
+
document.map do |section|
|
15
|
+
OpenStruct.new(label: section.key, dir: section["directory"], cmd: section["command"])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Invoker
|
2
|
+
module Errors
|
3
|
+
class ToomanyOpenConnections < StandardError; end
|
4
|
+
class ProcessTerminated < StandardError
|
5
|
+
attr_accessor :message, :ready_fd
|
6
|
+
def initialize(ready_fd, message)
|
7
|
+
@ready_fd = ready_fd
|
8
|
+
@message = message
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class InvalidConfig < StandardError; end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Invoker
|
2
|
+
class Reactor
|
3
|
+
attr_accessor :monitored_fds
|
4
|
+
def initialize
|
5
|
+
@monitored_fds = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def add_to_monitor(fd)
|
9
|
+
@monitored_fds << fd
|
10
|
+
end
|
11
|
+
|
12
|
+
def remove_from_monitoring(fd)
|
13
|
+
@monitored_fds.delete(fd)
|
14
|
+
end
|
15
|
+
|
16
|
+
def start
|
17
|
+
loop do
|
18
|
+
watch_on_pipe
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def watch_on_pipe
|
23
|
+
ready_read_fds,ready_write_fds,read_error_fds = select(monitored_fds,[],[],0.05)
|
24
|
+
|
25
|
+
if ready_read_fds && !ready_read_fds.empty?
|
26
|
+
handle_read_event(ready_read_fds)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def handle_read_event(ready_read_fds)
|
31
|
+
ready_fds = ready_read_fds.flatten.compact
|
32
|
+
ready_fds.each {|ready_fd| process_read(ready_fd) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def process_read(ready_fd)
|
36
|
+
command_worker = Invoker::COMMANDER.get_worker_from_fd(ready_fd)
|
37
|
+
begin
|
38
|
+
data = read_data(ready_fd)
|
39
|
+
command_worker.receive_data(data)
|
40
|
+
rescue Invoker::Errors::ProcessTerminated
|
41
|
+
command_worker.unbind()
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def read_data(ready_fd)
|
46
|
+
sock_data = []
|
47
|
+
begin
|
48
|
+
while(t_data = ready_fd.read_nonblock(64))
|
49
|
+
sock_data << t_data
|
50
|
+
end
|
51
|
+
rescue Errno::EAGAIN
|
52
|
+
return sock_data.join
|
53
|
+
rescue Errno::EWOULDBLOCK
|
54
|
+
return sock_data.join
|
55
|
+
rescue
|
56
|
+
raise Invoker::Errors::ProcessTerminated.new(ready_fd,sock_data.join)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require "slop"
|
2
|
+
require "ostruct"
|
3
|
+
require "socket"
|
4
|
+
|
5
|
+
module Invoker
|
6
|
+
class Runner
|
7
|
+
def self.run(args)
|
8
|
+
|
9
|
+
selected_command = nil
|
10
|
+
|
11
|
+
opts = Slop.parse(args, help: true) do
|
12
|
+
on :v, "Print the version" do
|
13
|
+
$stdout.puts Invoker::VERSION
|
14
|
+
end
|
15
|
+
|
16
|
+
command 'start' do
|
17
|
+
banner "Usage : invoker start config.ini \n Start Invoker Process Manager"
|
18
|
+
run do |cmd_opts, cmd_args|
|
19
|
+
selected_command = OpenStruct.new(:command => 'start', :file => cmd_args.first)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
command 'add' do
|
24
|
+
banner "Usage : invoker add process_label \n Start the process with given process_label"
|
25
|
+
run do |cmd_opts, cmd_args|
|
26
|
+
selected_command = OpenStruct.new(:command => 'add', :command_key => cmd_args.first)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
command 'remove' do
|
31
|
+
banner "Usage : invoker remove process_label \n Stop the process with given label"
|
32
|
+
on :s, :signal=, "Signal to send for killing the process, default is SIGINT", as: String
|
33
|
+
|
34
|
+
run do |cmd_opts, cmd_args|
|
35
|
+
signal_to_use = cmd_opts.to_hash[:signal] || 'INT'
|
36
|
+
selected_command = OpenStruct.new(
|
37
|
+
:command => 'remove',
|
38
|
+
:command_key => cmd_args.first,
|
39
|
+
:signal => signal_to_use
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
unless selected_command
|
45
|
+
$stdout.puts opts.inspect
|
46
|
+
else
|
47
|
+
run_command(selected_command)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.run_command(selected_command)
|
52
|
+
return unless selected_command
|
53
|
+
case selected_command.command
|
54
|
+
when 'start'
|
55
|
+
start_server(selected_command)
|
56
|
+
when 'add'
|
57
|
+
add_command(selected_command)
|
58
|
+
when 'remove'
|
59
|
+
remove_command(selected_command)
|
60
|
+
else
|
61
|
+
$stdout.puts "Invalid command"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.start_server(selected_command)
|
66
|
+
config = Invoker::Config.new(selected_command.file)
|
67
|
+
Invoker.const_set(:CONFIG, config)
|
68
|
+
warn_about_terminal_notifier()
|
69
|
+
commander = Invoker::Commander.new()
|
70
|
+
Invoker.const_set(:COMMANDER, commander)
|
71
|
+
commander.start_manager()
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.add_command(selected_command)
|
75
|
+
socket = UNIXSocket.open(Invoker::CommandListener::Server::SOCKET_PATH)
|
76
|
+
socket.puts("add #{selected_command.command_key}")
|
77
|
+
socket.flush()
|
78
|
+
socket.close()
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.remove_command(selected_command)
|
82
|
+
socket = UNIXSocket.open(Invoker::CommandListener::Server::SOCKET_PATH)
|
83
|
+
socket.puts("remove #{selected_command.command_key} #{selected_command.signal}")
|
84
|
+
socket.flush()
|
85
|
+
socket.close()
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.refresh_command(selected_command)
|
89
|
+
socket = UNIXSocket.open(Invoker::CommandListener::Server::SOCKET_PATH)
|
90
|
+
socket.puts("reload #{selected_command.command_key}")
|
91
|
+
socket.flush()
|
92
|
+
socket.close()
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.warn_about_terminal_notifier
|
96
|
+
if RUBY_PLATFORM.downcase.include?("darwin")
|
97
|
+
command_path = `which terminal-notifier`
|
98
|
+
if !command_path || command_path.empty?
|
99
|
+
$stdout.puts("You can enable OSX notification for processes by installing terminal-notification gem".red)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
data/readme.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
Invoker is a gem for managing processes in development environment.
|
2
|
+
|
3
|
+
[](https://travis-ci.org/code-mancers/invoker)
|
4
|
+
|
5
|
+
|
6
|
+
## Usage ##
|
7
|
+
|
8
|
+
First we need to install `invoker` gem to get command line utility called `invoker`, we can do that via:
|
9
|
+
|
10
|
+
gem install invoker
|
11
|
+
|
12
|
+
Currently it only works with Ruby 1.9.3 and 2.0.
|
13
|
+
|
14
|
+
You need to start by creating a `ini` file which will define processes you want to manage using invoker. An example
|
15
|
+
`ini` file is included in the repo.
|
16
|
+
|
17
|
+
[rails]
|
18
|
+
directory = /home/gnufied/god_particle
|
19
|
+
command = zsh -c 'bundle exec rails s -p 5000'
|
20
|
+
|
21
|
+
[dj]
|
22
|
+
directory = /home/gnufied/god_particle
|
23
|
+
command = zsh -c 'bundle exec ruby script/delayed_job'
|
24
|
+
|
25
|
+
|
26
|
+
[events]
|
27
|
+
directory = /home/gnufied/god_particle
|
28
|
+
command = zsh -c 'bundle exec ruby script/event_server'
|
29
|
+
|
30
|
+
After that you can start process manager via:
|
31
|
+
|
32
|
+
~> invoker start invoker.ini
|
33
|
+
|
34
|
+
Above command will start all your processes in one terminal with their stdout/stderr merged and labelled.
|
35
|
+
|
36
|
+
Now additionally you can control individual process by,
|
37
|
+
|
38
|
+
# Will try to stop running delayed job by sending SIGINT to the process
|
39
|
+
~> invoker remove dj
|
40
|
+
|
41
|
+
# If Process can't be killed by SIGINT send a custom signal
|
42
|
+
~> invoker remove dj -s 9
|
43
|
+
|
44
|
+
# add and start running
|
45
|
+
~> invoker add dj
|
46
|
+
|
47
|
+
You can also enable OSX notifications for crashed processes by installing `terminal-notification` gem. It is not a dependency, but can be useful if something crashed and you weren't paying attention.
|
48
|
+
|
49
|
+
|
50
|
+
## Bug reports, Feature requests ##
|
51
|
+
|
52
|
+
Please use [Github Issue Tracker](https://github.com/code-mancers/invoker/issues) for feature requests or bug reports.
|
53
|
+
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Invoker::CommandListener::Client do
|
4
|
+
describe "add command" do
|
5
|
+
before do
|
6
|
+
@client_socket = mock()
|
7
|
+
@client = Invoker::CommandListener::Client.new(@client_socket)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should run if read from socket" do
|
11
|
+
invoker_commander.expects(:add_command_by_label).with("foo")
|
12
|
+
@client_socket.expects(:read).returns("add foo\n")
|
13
|
+
@client_socket.expects(:close)
|
14
|
+
|
15
|
+
@client.read_and_execute()
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "remove command" do
|
20
|
+
before do
|
21
|
+
@client_socket = mock()
|
22
|
+
@client = Invoker::CommandListener::Client.new(@client_socket)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "with specific signal" do
|
26
|
+
invoker_commander.expects(:remove_command).with("foo", "9")
|
27
|
+
@client_socket.expects(:read).returns("remove foo 9\n")
|
28
|
+
@client_socket.expects(:close)
|
29
|
+
|
30
|
+
@client.read_and_execute()
|
31
|
+
end
|
32
|
+
|
33
|
+
it "with default signal" do
|
34
|
+
invoker_commander.expects(:remove_command).with("foo",nil)
|
35
|
+
@client_socket.expects(:read).returns("remove foo\n")
|
36
|
+
@client_socket.expects(:close)
|
37
|
+
|
38
|
+
@client.read_and_execute()
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "invalid command" do
|
43
|
+
before do
|
44
|
+
@client_socket = mock()
|
45
|
+
@client = Invoker::CommandListener::Client.new(@client_socket)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should print error if read from socket" do
|
49
|
+
invoker_commander.expects(:remove_command).never()
|
50
|
+
invoker_commander.expects(:add_command_by_label).never()
|
51
|
+
@client_socket.expects(:read).returns("eugh foo\n")
|
52
|
+
@client_socket.expects(:close)
|
53
|
+
|
54
|
+
@client.read_and_execute
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "Invoker::Commander" do
|
4
|
+
|
5
|
+
describe "With no processes configured" do
|
6
|
+
before do
|
7
|
+
@commander = Invoker::Commander.new()
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should throw error" do
|
11
|
+
invoker_config.stubs(:processes).returns([])
|
12
|
+
|
13
|
+
lambda {
|
14
|
+
@commander.start_manager()
|
15
|
+
}.should.raise(Invoker::Errors::InvalidConfig)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#add_command_by_label" do
|
20
|
+
before do
|
21
|
+
@commander = Invoker::Commander.new()
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should find command by label and start it, if found" do
|
25
|
+
invoker_config.stubs(:processes).returns([OpenStruct.new(:label => "resque", :cmd => "foo", :dir => "bar")])
|
26
|
+
@commander.expects(:add_command).returns(true)
|
27
|
+
|
28
|
+
@commander.add_command_by_label("resque")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#remove_command" do
|
33
|
+
describe "when a worker is found" do
|
34
|
+
before do
|
35
|
+
@commander = Invoker::Commander.new()
|
36
|
+
@commander.workers.expects(:[]).returns(OpenStruct.new(:pid => "bogus"))
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "if a signal is specified" do
|
40
|
+
it "should use that signal to kill the worker" do
|
41
|
+
@commander.expects(:process_kill).with("bogus", "HUP").returns(true)
|
42
|
+
@commander.remove_command("resque", "HUP")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "if no signal is specified" do
|
47
|
+
it "should use INT signal" do
|
48
|
+
@commander.expects(:process_kill).with("bogus", "INT").returns(true)
|
49
|
+
@commander.remove_command("resque", nil)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "when no worker is found" do
|
55
|
+
before do
|
56
|
+
@commander = Invoker::Commander.new()
|
57
|
+
@commander.workers.expects(:[]).returns(nil)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should not kill anything" do
|
61
|
+
@commander.expects(:process_kill).never()
|
62
|
+
@commander.remove_command("resque", "HUP")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#add_command" do
|
69
|
+
before do
|
70
|
+
invoker_config.stubs(:processes).returns([OpenStruct.new(:label => "sleep", :cmd => "sleep 4", :dir => ENV['HOME'])])
|
71
|
+
@commander = Invoker::Commander.new()
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should populate workers and open_pipes" do
|
75
|
+
@commander.reactor.expects(:start).returns(true)
|
76
|
+
@commander.start_manager()
|
77
|
+
@commander.open_pipes.should.not.be.empty
|
78
|
+
@commander.workers.should.not.be.empty
|
79
|
+
|
80
|
+
worker = @commander.workers['sleep']
|
81
|
+
|
82
|
+
worker.should.not.equal nil
|
83
|
+
worker.command_label.should.equal "sleep"
|
84
|
+
worker.color.should.equal "green"
|
85
|
+
|
86
|
+
|
87
|
+
pipe_end_worker = @commander.open_pipes[worker.pipe_end.fileno]
|
88
|
+
pipe_end_worker.should.not.equal nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require "bacon"
|
2
|
+
require "mocha-on-bacon"
|
3
|
+
|
4
|
+
__LIB_PATH__ = File.join(File.dirname(__FILE__), "..")
|
5
|
+
$: << __LIB_PATH__
|
6
|
+
|
7
|
+
require "pry"
|
8
|
+
require "invoker"
|
9
|
+
|
10
|
+
|
11
|
+
def invoker_config
|
12
|
+
if Invoker.const_defined?(:CONFIG)
|
13
|
+
Invoker::CONFIG
|
14
|
+
else
|
15
|
+
Invoker.const_set(:CONFIG, mock())
|
16
|
+
Invoker::CONFIG
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def invoker_commander
|
21
|
+
if Invoker.const_defined?(:COMMANDER)
|
22
|
+
Invoker::COMMANDER
|
23
|
+
else
|
24
|
+
Invoker.const_set(:COMMANDER, mock())
|
25
|
+
Invoker::COMMANDER
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
metadata
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: invoker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Hemant Kumar
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-05-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
version_requirements: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ! '>='
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
name: slop
|
22
|
+
requirement: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
version_requirements: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
name: iniparse
|
36
|
+
requirement: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
name: colored
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
name: bacon
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ! '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
name: mocha
|
78
|
+
requirement: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ! '>='
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
name: mocha-on-bacon
|
92
|
+
requirement: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
name: rake
|
106
|
+
requirement: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: Something small for process management
|
112
|
+
email: hemant@codemancers.com
|
113
|
+
executables:
|
114
|
+
- invoker
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- .gitignore
|
119
|
+
- .travis.yml
|
120
|
+
- Gemfile
|
121
|
+
- MIT-LICENSE
|
122
|
+
- Rakefile
|
123
|
+
- bin/invoker
|
124
|
+
- invoker.gemspec
|
125
|
+
- lib/invoker.rb
|
126
|
+
- lib/invoker/command_listener.rb
|
127
|
+
- lib/invoker/command_listener/client.rb
|
128
|
+
- lib/invoker/command_listener/server.rb
|
129
|
+
- lib/invoker/command_worker.rb
|
130
|
+
- lib/invoker/commander.rb
|
131
|
+
- lib/invoker/config.rb
|
132
|
+
- lib/invoker/errors.rb
|
133
|
+
- lib/invoker/reactor.rb
|
134
|
+
- lib/invoker/runner.rb
|
135
|
+
- readme.md
|
136
|
+
- spec/invoker/command_listener/client_spec.rb
|
137
|
+
- spec/invoker/command_worker_spec.rb
|
138
|
+
- spec/invoker/commander_spec.rb
|
139
|
+
- spec/spec_helper.rb
|
140
|
+
homepage: http://github.com/code-mancers/invoker
|
141
|
+
licenses:
|
142
|
+
- MIT
|
143
|
+
metadata: {}
|
144
|
+
post_install_message:
|
145
|
+
rdoc_options: []
|
146
|
+
require_paths:
|
147
|
+
- lib
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ! '>='
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ! '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
requirements: []
|
159
|
+
rubyforge_project:
|
160
|
+
rubygems_version: 2.0.3
|
161
|
+
signing_key:
|
162
|
+
specification_version: 4
|
163
|
+
summary: Something small for Process management
|
164
|
+
test_files:
|
165
|
+
- spec/invoker/command_listener/client_spec.rb
|
166
|
+
- spec/invoker/command_worker_spec.rb
|
167
|
+
- spec/invoker/commander_spec.rb
|
168
|
+
- spec/spec_helper.rb
|
169
|
+
has_rdoc:
|