fate 0.2.7 → 0.2.8
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/fate +7 -5
- data/lib/fate.rb +23 -53
- data/lib/fate/logger.rb +91 -0
- data/lib/fate/output.rb +77 -0
- data/lib/fate/{manager.rb → process_manager.rb} +38 -30
- data/lib/fate/repl.rb +2 -2
- data/lib/fate/service.rb +63 -0
- metadata +8 -5
data/bin/fate
CHANGED
@@ -12,7 +12,8 @@ $LOAD_PATH.unshift("#{FATE_ROOT}/lib")
|
|
12
12
|
require "fate"
|
13
13
|
|
14
14
|
options = {
|
15
|
-
:configuration => "fate.json"
|
15
|
+
:configuration => "fate.json",
|
16
|
+
:default_log => STDOUT
|
16
17
|
}
|
17
18
|
|
18
19
|
# TODO: config file flag
|
@@ -20,16 +21,17 @@ OptionParser.new do |parser|
|
|
20
21
|
parser.on("-c", "--configuration=FILE", "The origin of the events") do |file|
|
21
22
|
options[:configuration] = file
|
22
23
|
end
|
24
|
+
parser.on("-l", "--log=FILE", "Processes log to FILE") do |file|
|
25
|
+
options[:default_log] = File.new(file, "a")
|
26
|
+
end
|
23
27
|
end.parse!
|
24
28
|
|
25
29
|
|
26
30
|
string = File.read(options[:configuration])
|
27
31
|
configuration = JSON.parse(string, :symbolize_names => true)
|
28
|
-
fate = Fate.new(configuration)
|
32
|
+
fate = Fate.new(configuration, options)
|
33
|
+
|
29
34
|
|
30
|
-
trap("INT") do
|
31
|
-
exit
|
32
|
-
end
|
33
35
|
|
34
36
|
require "fate/repl"
|
35
37
|
fate.start
|
data/lib/fate.rb
CHANGED
@@ -5,44 +5,33 @@ gem "squeeze"
|
|
5
5
|
require "term/ansicolor"
|
6
6
|
require "squeeze/hash_tree"
|
7
7
|
|
8
|
-
require "fate/
|
9
|
-
require "fate/
|
8
|
+
require "fate/logger"
|
9
|
+
require "fate/service"
|
10
|
+
require "fate/output"
|
11
|
+
require "fate/process_manager"
|
10
12
|
|
11
13
|
Thread.abort_on_exception = true
|
12
14
|
|
13
15
|
class Fate
|
14
|
-
include Formatter
|
15
16
|
|
16
17
|
def self.start(specification, &block)
|
17
18
|
self.new(specification).start(&block)
|
18
19
|
end
|
19
20
|
|
20
|
-
attr_reader :
|
21
|
+
attr_reader :service, :manager, :completions, :logger
|
21
22
|
|
22
|
-
def initialize(
|
23
|
-
@
|
24
|
-
@
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
@completions = Set.new
|
33
|
-
|
34
|
-
@name_commands = {}
|
35
|
-
commands.each_path do |path, value|
|
36
|
-
key = path.join(".")
|
37
|
-
# add dot-delimited command names to the completions
|
38
|
-
@completions += path.map {|s| s.to_s }
|
39
|
-
@completions << key
|
40
|
-
# register each command under the dot-delimited name
|
41
|
-
@name_commands[key] = value
|
42
|
-
end
|
43
|
-
@command_width = @name_commands.keys.sort_by {|k| k.size }.last.size
|
23
|
+
def initialize(spec, options={})
|
24
|
+
@service = Service.new(spec, options)
|
25
|
+
@completions = @service.completions
|
26
|
+
|
27
|
+
@spec = spec
|
28
|
+
@logger = @service.logger["Fate Control"]
|
29
|
+
|
30
|
+
@manager = ProcessManager.new(@service)
|
31
|
+
end
|
44
32
|
|
45
|
-
|
33
|
+
def log(*args, &block)
|
34
|
+
@logger.log(*args, &block)
|
46
35
|
end
|
47
36
|
|
48
37
|
def run(&block)
|
@@ -54,18 +43,13 @@ class Fate
|
|
54
43
|
end
|
55
44
|
|
56
45
|
def start
|
57
|
-
manager.start_group(@
|
58
|
-
|
59
|
-
puts colorize("green", message)
|
46
|
+
manager.start_group(@service.commands)
|
47
|
+
logger.green "All commands are running."
|
60
48
|
end
|
61
49
|
|
62
50
|
def stop
|
63
|
-
|
64
|
-
|
65
|
-
# come before the dependers, we should stop the processes in reverse order,
|
66
|
-
# then start them back up again in forward order.
|
67
|
-
names = manager.running.sort_by {|name| keys.index(name) }
|
68
|
-
names.reverse.each do |name|
|
51
|
+
ordered = @service.stop_order(manager.running)
|
52
|
+
ordered.each do |name|
|
69
53
|
manager.stop_command(name)
|
70
54
|
end
|
71
55
|
end
|
@@ -77,34 +61,20 @@ class Fate
|
|
77
61
|
start
|
78
62
|
end
|
79
63
|
|
80
|
-
def resolve_commands(name)
|
81
|
-
targets = []
|
82
|
-
if @name_commands.has_key?(name)
|
83
|
-
targets << name
|
84
|
-
else
|
85
|
-
@name_commands.each do |cname, _command|
|
86
|
-
if cname.split(".").first == name
|
87
|
-
targets << cname
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
targets
|
92
|
-
end
|
93
|
-
|
94
64
|
def start_command(spec)
|
95
|
-
names = resolve_commands(spec)
|
65
|
+
names = @service.resolve_commands(spec)
|
96
66
|
if names.empty?
|
97
67
|
puts "No commands found for: #{spec}"
|
98
68
|
else
|
99
69
|
names.each do |name|
|
100
|
-
command = @
|
70
|
+
command = @service.commands[name]
|
101
71
|
manager.start_command(name, command)
|
102
72
|
end
|
103
73
|
end
|
104
74
|
end
|
105
75
|
|
106
76
|
def stop_command(spec)
|
107
|
-
names = resolve_commands(spec)
|
77
|
+
names = @service.resolve_commands(spec)
|
108
78
|
if names.empty?
|
109
79
|
puts "No commands found for: #{spec}"
|
110
80
|
else
|
data/lib/fate/logger.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
class Fate
|
2
|
+
|
3
|
+
# Simple logging class designed to interleave the output from multiple
|
4
|
+
# processes while making it obvious which lines were logged by which process.
|
5
|
+
|
6
|
+
class MultiLogger
|
7
|
+
|
8
|
+
attr_reader :width, :io
|
9
|
+
def initialize(options)
|
10
|
+
@width = options[:width]
|
11
|
+
if file = options[:file]
|
12
|
+
@io = File.new(file, "a")
|
13
|
+
elsif io = options[:io]
|
14
|
+
@io = io
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](name)
|
19
|
+
Sublogger.new(self, name)
|
20
|
+
end
|
21
|
+
|
22
|
+
class Sublogger
|
23
|
+
def initialize(master, name)
|
24
|
+
@master = master
|
25
|
+
@io = @master.io
|
26
|
+
@name = name
|
27
|
+
end
|
28
|
+
|
29
|
+
# duck typing for IO
|
30
|
+
def write(string)
|
31
|
+
num = @io.write(@master.format(@name, string))
|
32
|
+
@io.flush
|
33
|
+
num
|
34
|
+
end
|
35
|
+
|
36
|
+
def method_missing(method, *args, &block)
|
37
|
+
if @master.respond_to?(method)
|
38
|
+
# insert this logger's name into every relayed call.
|
39
|
+
@master.send(method, @name, *args, &block)
|
40
|
+
elsif @io.respond_to?(method)
|
41
|
+
@io.send(method, *args, &block)
|
42
|
+
else
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def write(name, string, color=nil)
|
49
|
+
if color
|
50
|
+
line = colorize(color, format(name, string))
|
51
|
+
else
|
52
|
+
line = format(name, string)
|
53
|
+
end
|
54
|
+
num = @io.write line
|
55
|
+
@io.flush
|
56
|
+
num
|
57
|
+
end
|
58
|
+
|
59
|
+
def error(name, string)
|
60
|
+
write(name, string, "red")
|
61
|
+
end
|
62
|
+
|
63
|
+
def green(name, string)
|
64
|
+
write(name, string, "green")
|
65
|
+
end
|
66
|
+
|
67
|
+
def info(name, string)
|
68
|
+
write(name, string, "yellow")
|
69
|
+
end
|
70
|
+
|
71
|
+
def debug(name, string)
|
72
|
+
write(name, string)
|
73
|
+
end
|
74
|
+
|
75
|
+
def format(name, string)
|
76
|
+
string.chomp!
|
77
|
+
if name == @last_identifier
|
78
|
+
"%-#{width}s - %s\n" % [nil, string]
|
79
|
+
else
|
80
|
+
@last_identifier = name
|
81
|
+
"%-#{width}s - %s\n" % [name, string]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def colorize(name, string)
|
86
|
+
[Term::ANSIColor.send(name), string, Term::ANSIColor.reset].join
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
data/lib/fate/output.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
class Fate
|
2
|
+
module Output
|
3
|
+
|
4
|
+
class Handlers
|
5
|
+
|
6
|
+
def initialize(service, handlers)
|
7
|
+
@service = service
|
8
|
+
@handlers = handlers
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](name)
|
12
|
+
if handler = @handlers[name]
|
13
|
+
handler
|
14
|
+
elsif @handlers["default"]
|
15
|
+
@handlers[name] = @handlers["default"][name]
|
16
|
+
else
|
17
|
+
@handlers[name] = @service.logger[name]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
class IOFilter
|
24
|
+
def initialize(master, name)
|
25
|
+
@master = master
|
26
|
+
@io = @master.io
|
27
|
+
@name = name
|
28
|
+
end
|
29
|
+
|
30
|
+
# duck typing for IO
|
31
|
+
def write(string)
|
32
|
+
num = @io.write(@master.format(@name, string))
|
33
|
+
@io.flush
|
34
|
+
num
|
35
|
+
end
|
36
|
+
|
37
|
+
def method_missing(method, *args, &block)
|
38
|
+
if @io.respond_to?(method)
|
39
|
+
@io.send(method, *args, &block)
|
40
|
+
else
|
41
|
+
super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class IOMux
|
47
|
+
attr_reader :io
|
48
|
+
attr_accessor :last_identifier
|
49
|
+
def initialize(options)
|
50
|
+
@last_identifier = nil
|
51
|
+
if file = options[:file]
|
52
|
+
@io = File.new(file, "a")
|
53
|
+
elsif io = options[:io]
|
54
|
+
@io = io
|
55
|
+
end
|
56
|
+
@handlers = {}
|
57
|
+
end
|
58
|
+
|
59
|
+
def [](name)
|
60
|
+
@handlers[name] ||= IOFilter.new(self, name)
|
61
|
+
end
|
62
|
+
|
63
|
+
def format(name, string)
|
64
|
+
if name == @last_identifier
|
65
|
+
string
|
66
|
+
else
|
67
|
+
@last_identifier = name
|
68
|
+
"==> #{name} <==\n#{string}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
@@ -1,7 +1,7 @@
|
|
1
|
+
require "fileutils"
|
1
2
|
require "pp"
|
2
3
|
# Cross-VM compatibility
|
3
4
|
# thanks to http://ku1ik.com/2010/09/18/open3-and-the-pid-of-the-spawn.html
|
4
|
-
# TODO: consider using systemu: https://github.com/ahoward/systemu/
|
5
5
|
if IO.respond_to?(:popen4)
|
6
6
|
def open4(*args)
|
7
7
|
IO.popen4(*args)
|
@@ -11,21 +11,28 @@ else
|
|
11
11
|
end
|
12
12
|
|
13
13
|
class Fate
|
14
|
-
class Manager
|
15
|
-
include Formatter
|
16
14
|
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
# A process management tool, concerned primarily with spawning child
|
16
|
+
# processes, tracking them by name, and handling unexpected exits and signals.
|
17
|
+
class ProcessManager
|
18
|
+
|
19
|
+
attr_reader :logger, :output_handlers
|
20
|
+
def initialize(service)
|
21
|
+
@service = service
|
22
|
+
@output_handlers = @service.output_handlers
|
23
|
+
@logger = @service.logger["Fate Manager"]
|
24
|
+
|
20
25
|
@threads = {}
|
21
26
|
@commands_by_name = {}
|
22
27
|
@names_by_pid = {}
|
23
28
|
@pids_by_name = {}
|
24
|
-
at_exit
|
29
|
+
at_exit do
|
30
|
+
stop
|
31
|
+
end
|
25
32
|
end
|
26
33
|
|
27
|
-
|
28
34
|
def stop
|
35
|
+
# FIXME: race condition involving spawn and stop at the same time
|
29
36
|
@names_by_pid.each do |pid, name|
|
30
37
|
kill(name)
|
31
38
|
end
|
@@ -44,7 +51,7 @@ class Fate
|
|
44
51
|
|
45
52
|
def start_command(name, command)
|
46
53
|
if pid = @pids_by_name[name]
|
47
|
-
|
54
|
+
logger.error "'#{name}' is already running with pid #{pid}"
|
48
55
|
else
|
49
56
|
spawn(name, command)
|
50
57
|
end
|
@@ -56,10 +63,9 @@ class Fate
|
|
56
63
|
@pids_by_name.delete(name)
|
57
64
|
@threads.delete(name)
|
58
65
|
system "kill -s INT #{pid}"
|
59
|
-
|
60
|
-
format_line("Fate Manager", "Sent a kill signal to #{name} running at #{pid}")
|
66
|
+
logger.info "Sent a kill signal to '#{name}' running at #{pid}"
|
61
67
|
else
|
62
|
-
|
68
|
+
logger.error "Could not find pid for '#{name}'"
|
63
69
|
end
|
64
70
|
end
|
65
71
|
|
@@ -70,29 +76,27 @@ class Fate
|
|
70
76
|
def spawn(name, command)
|
71
77
|
# TODO: check to see if command is already running
|
72
78
|
return Thread.new do
|
79
|
+
|
73
80
|
pid, stdin, stdout, stderr = open4(command)
|
74
|
-
|
81
|
+
logger.info "Starting '#{name}' (pid #{pid})"
|
75
82
|
@names_by_pid[pid] = name
|
76
83
|
@pids_by_name[name] = pid
|
77
84
|
|
85
|
+
io = output_handlers[name]
|
78
86
|
Thread.new do
|
79
|
-
|
80
|
-
STDERR.puts "(#{name}) #{line}"
|
81
|
-
end
|
87
|
+
IO.copy_stream(stderr, io)
|
82
88
|
end
|
83
89
|
|
84
90
|
# First line written to STDOUT is interpreted as the service
|
85
91
|
# signalling that it is ready.
|
86
92
|
line = stdout.gets
|
87
|
-
|
88
|
-
|
93
|
+
logger.info "#{name} is running."
|
94
|
+
|
95
|
+
io.write(line)
|
89
96
|
@threads[name] = Thread.current
|
90
|
-
#@threads << Thread.current
|
91
97
|
|
92
|
-
|
93
|
-
|
94
|
-
end
|
95
|
-
status = Process.wait(pid)
|
98
|
+
IO.copy_stream(stdout, io)
|
99
|
+
pid, status = Process.wait2(pid)
|
96
100
|
handle_child_termination(pid, status)
|
97
101
|
end
|
98
102
|
end
|
@@ -117,20 +121,24 @@ class Fate
|
|
117
121
|
end
|
118
122
|
|
119
123
|
def down_in_flames(name, pid, status)
|
120
|
-
|
121
|
-
|
122
|
-
|
124
|
+
if status.exitstatus
|
125
|
+
logger.error "Process '#{name}' (pid #{pid}) exited with code #{status.exitstatus}."
|
126
|
+
else
|
127
|
+
logger.info "Process '#{name}' (pid #{pid}) was sent signal #{status.termsig}."
|
128
|
+
end
|
129
|
+
logger.info "Shutting down all processes."
|
130
|
+
exit(1)
|
123
131
|
end
|
124
132
|
|
125
133
|
|
126
134
|
# ad hoc shell out, with rescuing because of some apparent bugs
|
127
135
|
# in MRI 1.8.7's ability to cope with unusual exit codes.
|
128
136
|
def system(command)
|
129
|
-
begin
|
137
|
+
#begin
|
130
138
|
Kernel.system command
|
131
|
-
rescue => error
|
132
|
-
puts "Exception raised when executing '#{command}': #{error.inspect}"
|
133
|
-
end
|
139
|
+
#rescue => error
|
140
|
+
#puts "Exception raised when executing '#{command}': #{error.inspect}"
|
141
|
+
#end
|
134
142
|
end
|
135
143
|
|
136
144
|
|
data/lib/fate/repl.rb
CHANGED
@@ -43,7 +43,7 @@ class Fate
|
|
43
43
|
end
|
44
44
|
|
45
45
|
on("commands") do
|
46
|
-
puts
|
46
|
+
puts self.service.names
|
47
47
|
end
|
48
48
|
|
49
49
|
on("running") do
|
@@ -51,7 +51,7 @@ class Fate
|
|
51
51
|
end
|
52
52
|
|
53
53
|
on("configuration", "config") do
|
54
|
-
puts JSON.pretty_generate(
|
54
|
+
puts JSON.pretty_generate(self.service.specification)
|
55
55
|
end
|
56
56
|
|
57
57
|
end
|
data/lib/fate/service.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
class Fate
|
2
|
+
|
3
|
+
class Service
|
4
|
+
|
5
|
+
attr_reader :longest_name, :names, :commands, :completions, :specification
|
6
|
+
attr_reader :output_handlers, :logger
|
7
|
+
def initialize(specification, options)
|
8
|
+
@specification = specification
|
9
|
+
@options = options
|
10
|
+
|
11
|
+
@commands = process_commands(@specification[:commands])
|
12
|
+
@names = @commands.keys
|
13
|
+
@longest_name = @commands.keys.sort_by {|k| k.size }.last.size
|
14
|
+
@logger = Fate::MultiLogger.new(:io => STDOUT, :width => @longest_name)
|
15
|
+
@output_handlers = Output::Handlers.new(self, options[:output] || {})
|
16
|
+
end
|
17
|
+
|
18
|
+
def process_commands(hash)
|
19
|
+
hash = Squeeze::HashTree[hash]
|
20
|
+
|
21
|
+
out = {}
|
22
|
+
@completions ||= Set.new
|
23
|
+
hash.each_path do |path, value|
|
24
|
+
key = path.join(".")
|
25
|
+
# add dot-delimited command names to the completions
|
26
|
+
@completions += path.map {|s| s.to_s }
|
27
|
+
@completions << key
|
28
|
+
# register each command under the dot-delimited name
|
29
|
+
out[key] = value
|
30
|
+
end
|
31
|
+
out
|
32
|
+
end
|
33
|
+
|
34
|
+
def resolve_commands(name)
|
35
|
+
targets = []
|
36
|
+
if @commands.has_key?(name)
|
37
|
+
targets << name
|
38
|
+
else
|
39
|
+
@commands.each do |cname, _command|
|
40
|
+
if cname.split(".").first == name
|
41
|
+
targets << cname
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
targets
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def start_order(command_names)
|
50
|
+
# presuming the spec file ordered the commands where the dependencies
|
51
|
+
# come before the dependers, we should stop the processes in reverse order,
|
52
|
+
# then start them back up again in forward order.
|
53
|
+
command_names.sort_by {|name| self.names.index(name) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def stop_order(command_names)
|
57
|
+
start_order(command_names).reverse
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.8
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-09-
|
12
|
+
date: 2012-09-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: json
|
@@ -101,10 +101,13 @@ files:
|
|
101
101
|
- bin/fate
|
102
102
|
- LICENSE
|
103
103
|
- README.md
|
104
|
-
- lib/fate.rb
|
105
|
-
- lib/fate/repl.rb
|
106
104
|
- lib/fate/formatter.rb
|
107
|
-
- lib/fate/
|
105
|
+
- lib/fate/logger.rb
|
106
|
+
- lib/fate/output.rb
|
107
|
+
- lib/fate/process_manager.rb
|
108
|
+
- lib/fate/repl.rb
|
109
|
+
- lib/fate/service.rb
|
110
|
+
- lib/fate.rb
|
108
111
|
homepage: https://github.com/automatthew/fate
|
109
112
|
licenses: []
|
110
113
|
post_install_message:
|