fate 0.2.3 → 0.2.6
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 +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:
|