fate 0.2.3 → 0.2.6
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/fate +6 -6
- data/lib/fate.rb +59 -134
- data/lib/fate/formatter.rb +18 -0
- data/lib/fate/manager.rb +139 -0
- data/lib/fate/{console.rb → repl.rb} +12 -7
- metadata +8 -6
data/bin/fate
CHANGED
@@ -6,8 +6,8 @@ require "json"
|
|
6
6
|
|
7
7
|
# set up loadpath
|
8
8
|
here = File.dirname(__FILE__)
|
9
|
-
|
10
|
-
$LOAD_PATH.unshift("#{
|
9
|
+
FATE_ROOT = File.expand_path("#{here}/..")
|
10
|
+
$LOAD_PATH.unshift("#{FATE_ROOT}/lib")
|
11
11
|
|
12
12
|
require "fate"
|
13
13
|
|
@@ -25,14 +25,14 @@ end.parse!
|
|
25
25
|
|
26
26
|
string = File.read(options[:configuration])
|
27
27
|
configuration = JSON.parse(string, :symbolize_names => true)
|
28
|
-
|
28
|
+
fate = Fate.new(configuration)
|
29
29
|
|
30
30
|
trap("INT") do
|
31
31
|
exit
|
32
32
|
end
|
33
33
|
|
34
|
-
require "fate/
|
35
|
-
|
36
|
-
|
34
|
+
require "fate/repl"
|
35
|
+
fate.start
|
36
|
+
fate.repl
|
37
37
|
|
38
38
|
|
data/lib/fate.rb
CHANGED
@@ -5,193 +5,122 @@ gem "squeeze"
|
|
5
5
|
require "term/ansicolor"
|
6
6
|
require "squeeze/hash_tree"
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
def open4(*args)
|
13
|
-
IO.popen4(*args)
|
14
|
-
end
|
15
|
-
else
|
16
|
-
require 'open4'
|
17
|
-
end
|
8
|
+
require "fate/formatter"
|
9
|
+
require "fate/manager"
|
10
|
+
|
11
|
+
Thread.abort_on_exception = true
|
18
12
|
|
19
13
|
class Fate
|
14
|
+
include Formatter
|
20
15
|
|
21
|
-
def self.start(
|
22
|
-
self.new(
|
16
|
+
def self.start(specification, &block)
|
17
|
+
self.new(specification).start(&block)
|
23
18
|
end
|
24
19
|
|
25
|
-
attr_reader :
|
20
|
+
attr_reader :manager, :specification, :completions, :name_commands
|
26
21
|
|
27
|
-
def initialize(
|
28
|
-
@
|
22
|
+
def initialize(specification, options={})
|
23
|
+
@specification = specification
|
29
24
|
@options = options
|
30
25
|
if logfile = options[:service_log]
|
31
26
|
@log = File.new(logfile, "a")
|
32
27
|
else
|
33
28
|
@log = STDOUT
|
34
29
|
end
|
35
|
-
commands = Squeeze::HashTree[@
|
30
|
+
commands = Squeeze::HashTree[@specification[:commands]]
|
36
31
|
|
37
32
|
@completions = Set.new
|
38
33
|
|
39
|
-
@
|
34
|
+
@name_commands = {}
|
40
35
|
commands.each_path do |path, value|
|
41
36
|
key = path.join(".")
|
42
|
-
|
37
|
+
# add dot-delimited command names to the completions
|
38
|
+
@completions += path.map {|s| s.to_s }
|
43
39
|
@completions << key
|
44
|
-
|
40
|
+
# register each command under the dot-delimited name
|
41
|
+
@name_commands[key] = value
|
45
42
|
end
|
43
|
+
@command_width = @name_commands.keys.sort_by {|k| k.size }.last.size
|
46
44
|
|
47
|
-
@
|
48
|
-
@pid_tracker = {}
|
49
|
-
@command_tracker = {}
|
45
|
+
@manager = Manager.new(@specification, :log => @log, :command_width => @command_width)
|
50
46
|
end
|
51
47
|
|
52
|
-
def
|
53
|
-
|
54
|
-
@command_width = commands.keys.sort_by {|k| k.size }.last.size
|
55
|
-
@commands.each do |name, command|
|
56
|
-
spawn(name, command)
|
57
|
-
end
|
58
|
-
|
59
|
-
at_exit { stop }
|
60
|
-
|
61
|
-
Thread.new do
|
62
|
-
# pid of -1 means to wait for any child process
|
63
|
-
pid, status = Process.wait2(-1)
|
64
|
-
# when we stop processes intentionally, we must remove the pid
|
65
|
-
# from the tracker
|
66
|
-
if name = @pid_tracker.delete(pid)
|
67
|
-
@command_tracker.delete(name)
|
68
|
-
command = @commands[name]
|
69
|
-
if status.exitstatus != 0
|
70
|
-
puts "Process '#{name}' (pid #{pid}) exited with code #{status}:"
|
71
|
-
puts "Shutting down all processes."
|
72
|
-
exit(status.exitstatus)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
# Command threads add themselves to the array when they believe
|
78
|
-
# their commands are ready.
|
79
|
-
until @threads.size == @commands.size
|
80
|
-
sleep 0.1
|
81
|
-
end
|
82
|
-
|
83
|
-
message = format_line("Fate", "All commands are running. ")
|
84
|
-
puts colorize("green", message)
|
85
|
-
|
48
|
+
def run(&block)
|
49
|
+
start
|
86
50
|
if block
|
87
51
|
yield(self)
|
88
|
-
stop
|
52
|
+
manager.stop
|
89
53
|
end
|
90
|
-
|
91
54
|
end
|
92
55
|
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
@pid_tracker[pid] = name
|
98
|
-
@command_tracker[name] = pid
|
99
|
-
|
100
|
-
Thread.new do
|
101
|
-
while line = stderr.gets
|
102
|
-
STDERR.puts "(#{name}) #{line}"
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
# First line written to STDOUT is interpreted as the service
|
107
|
-
# signalling that it is ready.
|
108
|
-
line = stdout.gets
|
109
|
-
@log.puts format_line(name, line)
|
110
|
-
@threads[name] = Thread.current
|
111
|
-
#@threads << Thread.current
|
112
|
-
|
113
|
-
while line = stdout.gets
|
114
|
-
@log.puts format_line(name, line)
|
115
|
-
end
|
116
|
-
end
|
56
|
+
def start
|
57
|
+
manager.start_group(@name_commands)
|
58
|
+
message = format_line("Fate Control", "All commands are running. ")
|
59
|
+
puts colorize("green", message)
|
117
60
|
end
|
118
61
|
|
119
62
|
def stop
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
63
|
+
keys = @name_commands.keys
|
64
|
+
# presuming the spec file ordered the commands where the dependencies
|
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|
|
69
|
+
manager.stop_command(name)
|
126
70
|
end
|
127
71
|
end
|
128
72
|
|
129
|
-
def
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
"%-#{@command_width}s - %s" % [identifier, line]
|
135
|
-
end
|
73
|
+
def restart
|
74
|
+
stop
|
75
|
+
# FIXME: this is here to prevent redis-server from crying
|
76
|
+
sleep 0.5
|
77
|
+
start
|
136
78
|
end
|
137
79
|
|
138
|
-
def
|
80
|
+
def resolve_commands(name)
|
139
81
|
targets = []
|
140
|
-
if
|
82
|
+
if @name_commands.has_key?(name)
|
141
83
|
targets << name
|
142
84
|
else
|
143
|
-
@
|
85
|
+
@name_commands.each do |cname, _command|
|
144
86
|
if cname.split(".").first == name
|
145
87
|
targets << cname
|
146
88
|
end
|
147
89
|
end
|
148
90
|
end
|
91
|
+
targets
|
92
|
+
end
|
149
93
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
@
|
157
|
-
|
158
|
-
@threads.delete(name)
|
159
|
-
system "kill -s INT #{pid}"
|
160
|
-
puts colorize("yellow", format_line("Fate", "Sent a kill signal to #{name} running at #{pid}"))
|
94
|
+
def start_command(spec)
|
95
|
+
names = resolve_commands(spec)
|
96
|
+
if names.empty?
|
97
|
+
puts "No commands found for: #{spec}"
|
98
|
+
else
|
99
|
+
names.each do |name|
|
100
|
+
command = @name_commands[name]
|
101
|
+
manager.start_command(name, command)
|
161
102
|
end
|
162
103
|
end
|
163
|
-
|
164
104
|
end
|
165
105
|
|
166
|
-
def
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
else
|
171
|
-
spawn(name, command)
|
172
|
-
until @threads[name]
|
173
|
-
sleep 0.1
|
174
|
-
end
|
175
|
-
puts colorize("green", format_line("Fate", "#{command} is running."))
|
176
|
-
end
|
106
|
+
def stop_command(spec)
|
107
|
+
names = resolve_commands(spec)
|
108
|
+
if names.empty?
|
109
|
+
puts "No commands found for: #{spec}"
|
177
110
|
else
|
178
|
-
|
111
|
+
names.each do |name|
|
112
|
+
manager.stop_command(name)
|
113
|
+
end
|
179
114
|
end
|
180
115
|
end
|
181
116
|
|
182
|
-
def restart
|
183
|
-
stop
|
184
|
-
start
|
185
|
-
end
|
186
|
-
|
187
117
|
def restart_command(name)
|
188
118
|
stop_command(name)
|
189
119
|
start_command(name)
|
190
120
|
end
|
191
121
|
|
192
|
-
# list currently running commands
|
193
122
|
def running
|
194
|
-
|
123
|
+
manager.running
|
195
124
|
end
|
196
125
|
|
197
126
|
# ad hoc shell out, with rescuing because of some apparent bugs
|
@@ -200,13 +129,9 @@ class Fate
|
|
200
129
|
begin
|
201
130
|
Kernel.system command
|
202
131
|
rescue => error
|
203
|
-
puts "Exception raised when
|
132
|
+
puts "Exception raised when executing '#{command}': #{error.inspect}"
|
204
133
|
end
|
205
134
|
end
|
206
135
|
|
207
|
-
def colorize(name, string)
|
208
|
-
[Term::ANSIColor.send(name), string, Term::ANSIColor.reset].join
|
209
|
-
end
|
210
|
-
|
211
136
|
end
|
212
137
|
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Fate
|
2
|
+
|
3
|
+
module Formatter
|
4
|
+
def format_line(identifier, line)
|
5
|
+
if identifier == @last_identifier
|
6
|
+
"%-#{@command_width}s - %s" % [nil, line]
|
7
|
+
else
|
8
|
+
@last_identifier = identifier
|
9
|
+
"%-#{@command_width}s - %s" % [identifier, line]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def colorize(name, string)
|
14
|
+
[Term::ANSIColor.send(name), string, Term::ANSIColor.reset].join
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
data/lib/fate/manager.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require "pp"
|
2
|
+
# Cross-VM compatibility
|
3
|
+
# 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
|
+
if IO.respond_to?(:popen4)
|
6
|
+
def open4(*args)
|
7
|
+
IO.popen4(*args)
|
8
|
+
end
|
9
|
+
else
|
10
|
+
require 'open4'
|
11
|
+
end
|
12
|
+
|
13
|
+
class Fate
|
14
|
+
class Manager
|
15
|
+
include Formatter
|
16
|
+
|
17
|
+
def initialize(specification, options)
|
18
|
+
@service_log = options[:log]
|
19
|
+
@command_width = options[:command_width]
|
20
|
+
@threads = {}
|
21
|
+
@commands_by_name = {}
|
22
|
+
@names_by_pid = {}
|
23
|
+
@pids_by_name = {}
|
24
|
+
at_exit { stop }
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def stop
|
29
|
+
@names_by_pid.each do |pid, name|
|
30
|
+
kill(name)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def start_group(hash)
|
35
|
+
hash.each do |name, command|
|
36
|
+
@commands_by_name[name] = command
|
37
|
+
start_command(name, command)
|
38
|
+
end
|
39
|
+
|
40
|
+
until @threads.size == hash.size
|
41
|
+
sleep 0.1
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def start_command(name, command)
|
46
|
+
if pid = @pids_by_name[name]
|
47
|
+
puts "#{name} is already running with pid #{pid}"
|
48
|
+
else
|
49
|
+
spawn(name, command)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def kill(name)
|
54
|
+
if pid = @pids_by_name[name]
|
55
|
+
@names_by_pid.delete(pid)
|
56
|
+
@pids_by_name.delete(name)
|
57
|
+
@threads.delete(name)
|
58
|
+
system "kill -s INT #{pid}"
|
59
|
+
puts colorize "yellow",
|
60
|
+
format_line("Fate Manager", "Sent a kill signal to #{name} running at #{pid}")
|
61
|
+
else
|
62
|
+
puts "Could not find pid for #{name}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def stop_command(name)
|
67
|
+
kill(name)
|
68
|
+
end
|
69
|
+
|
70
|
+
def spawn(name, command)
|
71
|
+
# TODO: check to see if command is already running
|
72
|
+
return Thread.new do
|
73
|
+
pid, stdin, stdout, stderr = open4(command)
|
74
|
+
puts colorize("yellow", format_line("Fate Manager", "Starting (#{pid}): #{name}"))
|
75
|
+
@names_by_pid[pid] = name
|
76
|
+
@pids_by_name[name] = pid
|
77
|
+
|
78
|
+
Thread.new do
|
79
|
+
while line = stderr.gets
|
80
|
+
STDERR.puts "(#{name}) #{line}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# First line written to STDOUT is interpreted as the service
|
85
|
+
# signalling that it is ready.
|
86
|
+
line = stdout.gets
|
87
|
+
puts colorize("yellow", format_line("Fate Manager", "#{name} is running."))
|
88
|
+
@service_log.puts format_line(name, line)
|
89
|
+
@threads[name] = Thread.current
|
90
|
+
#@threads << Thread.current
|
91
|
+
|
92
|
+
while line = stdout.gets
|
93
|
+
@service_log.puts format_line(name, line)
|
94
|
+
end
|
95
|
+
status = Process.wait(pid)
|
96
|
+
handle_child_termination(pid, status)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# list currently running commands
|
101
|
+
def running
|
102
|
+
names = @pids_by_name.map {|name, command| name }.sort
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def handle_child_termination(pid, status)
|
108
|
+
if name = @names_by_pid.delete(pid)
|
109
|
+
@pids_by_name.delete(name)
|
110
|
+
# TODO: CLI and instantiation flags for @mode
|
111
|
+
if (@mode != :production) && status.exitstatus != 0
|
112
|
+
down_in_flames(name, pid, status)
|
113
|
+
else
|
114
|
+
# Probably should notify somebody somehow
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def down_in_flames(name, pid, status)
|
120
|
+
puts "Process '#{name}' (pid #{pid}) exited with code #{status}:"
|
121
|
+
puts "Shutting down all processes."
|
122
|
+
exit(status.exitstatus)
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
# ad hoc shell out, with rescuing because of some apparent bugs
|
127
|
+
# in MRI 1.8.7's ability to cope with unusual exit codes.
|
128
|
+
def system(command)
|
129
|
+
begin
|
130
|
+
Kernel.system command
|
131
|
+
rescue => error
|
132
|
+
puts "Exception raised when executing '#{command}': #{error.inspect}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
@@ -1,12 +1,13 @@
|
|
1
|
-
|
1
|
+
gem "harp"
|
2
|
+
require "harp"
|
2
3
|
class Fate
|
3
4
|
|
4
|
-
include
|
5
|
+
include Harp
|
5
6
|
|
6
|
-
|
7
|
+
setup_repl do |repl|
|
7
8
|
|
8
9
|
on("help") do
|
9
|
-
commands =
|
10
|
+
commands = repl.commands.select {|c| c.size > 1 } + ["!"]
|
10
11
|
puts "* Available commands: " << commands.sort.join(" ")
|
11
12
|
end
|
12
13
|
|
@@ -14,12 +15,16 @@ class Fate
|
|
14
15
|
exit
|
15
16
|
end
|
16
17
|
|
17
|
-
on(
|
18
|
+
on("stop") do
|
19
|
+
self.stop
|
20
|
+
end
|
21
|
+
|
22
|
+
on(/^stop (\S+)$/) do |args|
|
18
23
|
command = args.first
|
19
24
|
self.stop_command(args.first)
|
20
25
|
end
|
21
26
|
|
22
|
-
on(
|
27
|
+
on(/^start (\S+)$/) do |args|
|
23
28
|
command = args.first
|
24
29
|
self.start_command(args.first)
|
25
30
|
end
|
@@ -38,7 +43,7 @@ class Fate
|
|
38
43
|
end
|
39
44
|
|
40
45
|
on("commands") do
|
41
|
-
puts JSON.pretty_generate(
|
46
|
+
puts JSON.pretty_generate(self.name_commands)
|
42
47
|
end
|
43
48
|
|
44
49
|
on("running") do
|
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.6
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,16 +9,16 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-09-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
15
|
+
name: harp
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version: 0.2.
|
21
|
+
version: 0.2.4
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -26,7 +26,7 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - ! '>='
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: 0.2.
|
29
|
+
version: 0.2.4
|
30
30
|
- !ruby/object:Gem::Dependency
|
31
31
|
name: open4
|
32
32
|
requirement: !ruby/object:Gem::Requirement
|
@@ -86,7 +86,9 @@ files:
|
|
86
86
|
- LICENSE
|
87
87
|
- README.md
|
88
88
|
- lib/fate.rb
|
89
|
-
- lib/fate/
|
89
|
+
- lib/fate/repl.rb
|
90
|
+
- lib/fate/formatter.rb
|
91
|
+
- lib/fate/manager.rb
|
90
92
|
homepage: https://github.com/automatthew/fate
|
91
93
|
licenses: []
|
92
94
|
post_install_message:
|