fate 0.2.7 → 0.2.8
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.
- 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:
|