fate 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +10 -0
- data/bin/fate +38 -0
- data/lib/fate.rb +210 -0
- data/lib/fate/console.rb +54 -0
- data/lib/hash_tree.rb +313 -0
- metadata +118 -0
data/LICENSE
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
Copyright (c) 2012 Matthew King
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
8
|
+
|
9
|
+
|
10
|
+
|
data/bin/fate
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require "rubygems"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
# set up loadpath
|
8
|
+
here = File.dirname(__FILE__)
|
9
|
+
SPAWN_CONTROL_ROOT = File.expand_path("#{here}/..")
|
10
|
+
$LOAD_PATH.unshift("#{SPAWN_CONTROL_ROOT}/lib")
|
11
|
+
|
12
|
+
require "fate"
|
13
|
+
|
14
|
+
options = {
|
15
|
+
:configuration => "fate.json"
|
16
|
+
}
|
17
|
+
|
18
|
+
# TODO: config file flag
|
19
|
+
OptionParser.new do |parser|
|
20
|
+
parser.on("-c", "--configuration=FILE", "The origin of the events") do |file|
|
21
|
+
options[:configuration] = file
|
22
|
+
end
|
23
|
+
end.parse!
|
24
|
+
|
25
|
+
|
26
|
+
string = File.read(options[:configuration])
|
27
|
+
configuration = JSON.parse(string, :symbolize_names => true)
|
28
|
+
spawner = Fate.new(configuration)
|
29
|
+
|
30
|
+
trap("INT") do
|
31
|
+
exit
|
32
|
+
end
|
33
|
+
|
34
|
+
require "fate/console"
|
35
|
+
spawner.start
|
36
|
+
spawner.console
|
37
|
+
|
38
|
+
|
data/lib/fate.rb
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
require "term/ansicolor"
|
4
|
+
|
5
|
+
require "hash_tree"
|
6
|
+
# Cross-VM compatibility
|
7
|
+
# thanks to http://ku1ik.com/2010/09/18/open3-and-the-pid-of-the-spawn.html
|
8
|
+
# TODO: consider using systemu: https://github.com/ahoward/systemu/
|
9
|
+
if IO.respond_to?(:popen4)
|
10
|
+
def open4(*args)
|
11
|
+
IO.popen4(*args)
|
12
|
+
end
|
13
|
+
else
|
14
|
+
require 'open4'
|
15
|
+
end
|
16
|
+
|
17
|
+
class Fate
|
18
|
+
|
19
|
+
def self.start(configuration, &block)
|
20
|
+
self.new(configuration).start(&block)
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :commands, :configuration, :completions
|
24
|
+
|
25
|
+
def initialize(configuration, options={})
|
26
|
+
@configuration = configuration
|
27
|
+
@options = options
|
28
|
+
if logfile = options[:service_log]
|
29
|
+
@log = File.new(logfile, "a")
|
30
|
+
else
|
31
|
+
@log = STDOUT
|
32
|
+
end
|
33
|
+
commands = HashTree[@configuration[:commands]]
|
34
|
+
|
35
|
+
@completions = Set.new
|
36
|
+
|
37
|
+
@commands = {}
|
38
|
+
commands.each_path do |path, value|
|
39
|
+
key = path.join(".")
|
40
|
+
@completions += path
|
41
|
+
@completions << key
|
42
|
+
@commands[key] = value
|
43
|
+
end
|
44
|
+
|
45
|
+
@threads = {}
|
46
|
+
@pid_tracker = {}
|
47
|
+
@command_tracker = {}
|
48
|
+
end
|
49
|
+
|
50
|
+
def start(&block)
|
51
|
+
@running = []
|
52
|
+
@command_width = commands.keys.sort_by {|k| k.size }.last.size
|
53
|
+
@commands.each do |name, command|
|
54
|
+
spawn(name, command)
|
55
|
+
end
|
56
|
+
|
57
|
+
at_exit { stop }
|
58
|
+
|
59
|
+
Thread.new do
|
60
|
+
# pid of -1 means to wait for any child process
|
61
|
+
pid, status = Process.wait2(-1)
|
62
|
+
# when we stop processes intentionally, we must remove the pid
|
63
|
+
# from the tracker
|
64
|
+
if name = @pid_tracker.delete(pid)
|
65
|
+
@command_tracker.delete(name)
|
66
|
+
command = @commands[name]
|
67
|
+
if status.exitstatus != 0
|
68
|
+
puts "Process '#{name}' (pid #{pid}) exited with code #{status}:"
|
69
|
+
puts "Shutting down all processes."
|
70
|
+
exit(status.exitstatus)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Command threads add themselves to the array when they believe
|
76
|
+
# their commands are ready.
|
77
|
+
until @threads.size == @commands.size
|
78
|
+
sleep 0.1
|
79
|
+
end
|
80
|
+
|
81
|
+
message = format_line("Fate", "All commands are running. ")
|
82
|
+
puts colorize("green", message)
|
83
|
+
|
84
|
+
if block
|
85
|
+
yield(self)
|
86
|
+
stop
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
def spawn(name, command)
|
92
|
+
return Thread.new do
|
93
|
+
pid, stdin, stdout, stderr = open4(command)
|
94
|
+
puts colorize("yellow", format_line("Fate", "Starting (#{pid}): #{command}"))
|
95
|
+
@pid_tracker[pid] = name
|
96
|
+
@command_tracker[name] = pid
|
97
|
+
|
98
|
+
Thread.new do
|
99
|
+
while line = stderr.gets
|
100
|
+
STDERR.puts "(#{name}) #{line}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# First line written to STDOUT is interpreted as the service
|
105
|
+
# signalling that it is ready.
|
106
|
+
line = stdout.gets
|
107
|
+
@log.puts format_line(name, line)
|
108
|
+
@threads[name] = Thread.current
|
109
|
+
#@threads << Thread.current
|
110
|
+
|
111
|
+
while line = stdout.gets
|
112
|
+
@log.puts format_line(name, line)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def stop
|
118
|
+
if @pid_tracker.size != 0
|
119
|
+
command = "kill #{@pid_tracker.keys.join(' ')}"
|
120
|
+
system command
|
121
|
+
@pid_tracker.clear
|
122
|
+
@command_tracker.clear
|
123
|
+
@threads.clear
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def format_line(identifier, line)
|
128
|
+
if identifier == @last_identifier
|
129
|
+
"%-#{@command_width}s - %s" % [nil, line]
|
130
|
+
else
|
131
|
+
@last_identifier = identifier
|
132
|
+
"%-#{@command_width}s - %s" % [identifier, line]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def stop_command(name)
|
137
|
+
targets = []
|
138
|
+
if command = @commands[name]
|
139
|
+
targets << name
|
140
|
+
else
|
141
|
+
@commands.each do |cname, _command|
|
142
|
+
if cname.split(".").first == name
|
143
|
+
targets << cname
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
if targets.empty?
|
149
|
+
puts "No such command registered: #{name}"
|
150
|
+
end
|
151
|
+
|
152
|
+
targets.each do |name|
|
153
|
+
if pid = @command_tracker[name]
|
154
|
+
@pid_tracker.delete(pid)
|
155
|
+
@command_tracker.delete(name)
|
156
|
+
@threads.delete(name)
|
157
|
+
system "kill -s INT #{pid}"
|
158
|
+
puts colorize("yellow", format_line("Fate", "Sent a kill signal to #{name} running at #{pid}"))
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
def start_command(name)
|
165
|
+
if command = @commands[name]
|
166
|
+
if pid = @command_tracker[name]
|
167
|
+
puts "#{name} is already running with pid #{pid}"
|
168
|
+
else
|
169
|
+
spawn(name, command)
|
170
|
+
until @threads[name]
|
171
|
+
sleep 0.1
|
172
|
+
end
|
173
|
+
puts colorize("green", format_line("Fate", "#{command} is running."))
|
174
|
+
end
|
175
|
+
else
|
176
|
+
puts "No such command registered: #{name}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def restart
|
181
|
+
stop
|
182
|
+
start
|
183
|
+
end
|
184
|
+
|
185
|
+
def restart_command(name)
|
186
|
+
stop_command(name)
|
187
|
+
start_command(name)
|
188
|
+
end
|
189
|
+
|
190
|
+
# list currently running commands
|
191
|
+
def running
|
192
|
+
names = @command_tracker.map {|name, command| name }
|
193
|
+
end
|
194
|
+
|
195
|
+
# ad hoc shell out, with rescuing because of some apparent bugs
|
196
|
+
# in MRI 1.8.7's ability to cope with unusual exit codes.
|
197
|
+
def system(command)
|
198
|
+
begin
|
199
|
+
Kernel.system command
|
200
|
+
rescue => error
|
201
|
+
puts "Exception raised when shelling out: #{error.inspect}"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def colorize(name, string)
|
206
|
+
[Term::ANSIColor.send(name), string, Term::ANSIColor.reset].join
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
|
data/lib/fate/console.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require "consolize"
|
2
|
+
class Fate
|
3
|
+
|
4
|
+
include Consolize
|
5
|
+
|
6
|
+
setup_console do |console|
|
7
|
+
|
8
|
+
on("help") do
|
9
|
+
commands = console.commands.select {|c| c.size > 1 } + ["!"]
|
10
|
+
puts "* Available commands: " << commands.sort.join(" ")
|
11
|
+
end
|
12
|
+
|
13
|
+
on("quit", "q", "exit") do
|
14
|
+
exit
|
15
|
+
end
|
16
|
+
|
17
|
+
on(/stop (\S+)$/) do |args|
|
18
|
+
command = args.first
|
19
|
+
self.stop_command(args.first)
|
20
|
+
end
|
21
|
+
|
22
|
+
on(/start (\S+)$/) do |args|
|
23
|
+
command = args.first
|
24
|
+
self.start_command(args.first)
|
25
|
+
end
|
26
|
+
|
27
|
+
on("restart") do
|
28
|
+
self.restart
|
29
|
+
end
|
30
|
+
|
31
|
+
on(/restart (\S+)$/) do |args|
|
32
|
+
command = args.first
|
33
|
+
self.restart_command(args.first)
|
34
|
+
end
|
35
|
+
|
36
|
+
on_bang do |args|
|
37
|
+
self.system args.first
|
38
|
+
end
|
39
|
+
|
40
|
+
on("commands") do
|
41
|
+
puts JSON.pretty_generate(commands)
|
42
|
+
end
|
43
|
+
|
44
|
+
on("running") do
|
45
|
+
puts self.running
|
46
|
+
end
|
47
|
+
|
48
|
+
on("configuration", "config") do
|
49
|
+
puts JSON.pretty_generate(configuration)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
data/lib/hash_tree.rb
ADDED
@@ -0,0 +1,313 @@
|
|
1
|
+
module Traversable
|
2
|
+
|
3
|
+
# Follow or create the path specified by the signature and assign
|
4
|
+
# the value as a terminating leaf node.
|
5
|
+
#
|
6
|
+
# h.set([:a, :b, :c], "This is a retrievable value")
|
7
|
+
#
|
8
|
+
def set(sig, val)
|
9
|
+
raise ArgumentError if sig.empty?
|
10
|
+
create_path(sig) do |node, key|
|
11
|
+
node[key] = val
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def reduce(sig, base=0)
|
16
|
+
create_path(sig) do |node, key|
|
17
|
+
node[key] = base unless node.has_key?(key)
|
18
|
+
node[key] = yield node[key]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def increment(sig, val=1)
|
23
|
+
val = yield if block_given?
|
24
|
+
create_path(sig) do |node, key|
|
25
|
+
if node.has_key?(key)
|
26
|
+
node[key] = node[key] + val
|
27
|
+
else
|
28
|
+
node[key] = val
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Usage:
|
34
|
+
# a = ht.reducer([:a, :b, :c], 0) {|acc, v| acc + v }
|
35
|
+
# a[1]
|
36
|
+
def reducer(sig, base, &block)
|
37
|
+
p = nil
|
38
|
+
create_path(sig) do |node, key|
|
39
|
+
unless node.has_key?(key)
|
40
|
+
node[key] = base
|
41
|
+
end
|
42
|
+
p = lambda do |newval|
|
43
|
+
node[key] = block.call(node[key], newval)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
p
|
47
|
+
end
|
48
|
+
|
49
|
+
def sum(*args)
|
50
|
+
out = 0
|
51
|
+
retrieve(*args) { |v| out += v }
|
52
|
+
out
|
53
|
+
end
|
54
|
+
|
55
|
+
def count(*args)
|
56
|
+
args = args + [:_count]
|
57
|
+
sum(*args)
|
58
|
+
end
|
59
|
+
|
60
|
+
def unique(*args)
|
61
|
+
out = 0
|
62
|
+
filter(*args) { |v| out += v.size }
|
63
|
+
out
|
64
|
+
end
|
65
|
+
|
66
|
+
# like retrieve, but will return any kind of node
|
67
|
+
def filter(*sig)
|
68
|
+
results = []
|
69
|
+
search(sig) do |node|
|
70
|
+
results << node
|
71
|
+
yield(node) if block_given?
|
72
|
+
end
|
73
|
+
results
|
74
|
+
end
|
75
|
+
|
76
|
+
# Given a signature array, attempt to retrieve matching leaf values.
|
77
|
+
def retrieve(*sig)
|
78
|
+
results = []
|
79
|
+
search(sig) do |node|
|
80
|
+
results << node unless node.respond_to?(:children)
|
81
|
+
yield(node) if block_given?
|
82
|
+
end
|
83
|
+
results
|
84
|
+
end
|
85
|
+
|
86
|
+
# Generic tree search method
|
87
|
+
def search(sig)
|
88
|
+
current_nodes = [self]
|
89
|
+
|
90
|
+
while !current_nodes.empty?
|
91
|
+
next_nodes = []
|
92
|
+
matcher = sig.shift
|
93
|
+
if matcher
|
94
|
+
current_nodes.each do |node|
|
95
|
+
if node.respond_to?(:children)
|
96
|
+
next_nodes += node.children(matcher)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
else
|
100
|
+
current_nodes.each {|n| yield(n) }
|
101
|
+
end
|
102
|
+
current_nodes = next_nodes
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def traverse
|
107
|
+
current_nodes = [self]
|
108
|
+
while !current_nodes.empty?
|
109
|
+
next_nodes = []
|
110
|
+
current_nodes.each do |node|
|
111
|
+
if node.respond_to?(:children)
|
112
|
+
next_nodes += node.children(true)
|
113
|
+
yield(node)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
current_nodes = next_nodes
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
class HashTree < Hash
|
124
|
+
include Traversable
|
125
|
+
|
126
|
+
# Override the constructor to provide a default_proc
|
127
|
+
# NOTE: there's a better way to do this in 1.9.2, it seems.
|
128
|
+
# See Hash#default_proc=
|
129
|
+
def self.new()
|
130
|
+
hash = Hash.new { |h,k| h[k] = HashTree.new }
|
131
|
+
super.replace(hash)
|
132
|
+
end
|
133
|
+
|
134
|
+
def self.[](hash)
|
135
|
+
ht = self.new
|
136
|
+
ht << hash
|
137
|
+
ht
|
138
|
+
end
|
139
|
+
|
140
|
+
def _dump(depth)
|
141
|
+
h = Hash[self]
|
142
|
+
h.delete_if {|k,v| v.is_a? Proc }
|
143
|
+
Marshal.dump(h)
|
144
|
+
end
|
145
|
+
|
146
|
+
def self._load(*args)
|
147
|
+
h = Marshal.load(*args)
|
148
|
+
ht = self.new
|
149
|
+
ht.replace(h)
|
150
|
+
ht
|
151
|
+
end
|
152
|
+
|
153
|
+
# Follow the path specified, creating new nodes where necessary.
|
154
|
+
# Returns the value at the end of the path. If a block is supplied,
|
155
|
+
# it will be called with the last node and the last key as parameters,
|
156
|
+
# analogous to Hash.new's default proc. This is necessary to allow
|
157
|
+
# setting a value at the end of the path. See the implementation of #insert.
|
158
|
+
def create_path(sig)
|
159
|
+
final_key = sig.pop
|
160
|
+
hash = self
|
161
|
+
sig.each do |a|
|
162
|
+
hash = hash[a]
|
163
|
+
end
|
164
|
+
yield(hash, final_key) if block_given?
|
165
|
+
hash[final_key]
|
166
|
+
end
|
167
|
+
|
168
|
+
# Attempt to retrieve the value at the end of the path specified,
|
169
|
+
# without creating new nodes. Returns nil on failure.
|
170
|
+
# TODO: consider whether splatting the signature is wise.
|
171
|
+
def find(sig)
|
172
|
+
stage = self
|
173
|
+
sig.each do |a|
|
174
|
+
if stage.has_key?(a)
|
175
|
+
stage = stage[a]
|
176
|
+
else
|
177
|
+
return nil
|
178
|
+
end
|
179
|
+
end
|
180
|
+
stage
|
181
|
+
end
|
182
|
+
|
183
|
+
def remove(*sig)
|
184
|
+
stage = self
|
185
|
+
s2 = sig.slice(0..-2)
|
186
|
+
s2.each do |a|
|
187
|
+
if stage.has_key?(a)
|
188
|
+
stage = stage[a]
|
189
|
+
else
|
190
|
+
return nil
|
191
|
+
end
|
192
|
+
end
|
193
|
+
stage.delete(sig.last)
|
194
|
+
end
|
195
|
+
|
196
|
+
def children(matcher=true)
|
197
|
+
next_keys = self.keys.select do |key|
|
198
|
+
match?(matcher, key)
|
199
|
+
end
|
200
|
+
self.values_at(*next_keys)
|
201
|
+
end
|
202
|
+
|
203
|
+
def +(other)
|
204
|
+
out = HashTree.new
|
205
|
+
_plus(other, out)
|
206
|
+
out
|
207
|
+
end
|
208
|
+
|
209
|
+
def _plus(ht2, out)
|
210
|
+
self.each do |k1,v1|
|
211
|
+
v1 = v1.respond_to?(:dup) ? v1 : v1.dup
|
212
|
+
if ht2.has_key?(k1)
|
213
|
+
v2 = ht2[k1]
|
214
|
+
if v1.respond_to?(:_plus)
|
215
|
+
out[k1] = v1
|
216
|
+
v1._plus(v2, out[k1])
|
217
|
+
elsif v2.respond_to?(:_plus)
|
218
|
+
raise ArgumentError,
|
219
|
+
"Can't merge leaf with non-leaf:\n#{v1.inspect}\n#{v2.inspect}"
|
220
|
+
else
|
221
|
+
if v2.is_a?(Numeric) && v1.is_a?(Numeric)
|
222
|
+
out[k1] = v1 + v2
|
223
|
+
else
|
224
|
+
out[k1] = [v1, ht2[k1]]
|
225
|
+
end
|
226
|
+
end
|
227
|
+
else
|
228
|
+
# should anything happen here?
|
229
|
+
end
|
230
|
+
end
|
231
|
+
ht2.each do |k,v|
|
232
|
+
if self.has_key?(k)
|
233
|
+
# should anything happen here?
|
234
|
+
else
|
235
|
+
v = v.respond_to?(:dup) ? v : v.dup
|
236
|
+
out[k] = v
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def <<(other)
|
242
|
+
other.each do |k,v1|
|
243
|
+
if self.has_key?(k)
|
244
|
+
v2 = self[k]
|
245
|
+
if v1.respond_to?(:has_key?) && v2.respond_to?(:has_key?)
|
246
|
+
v2 << v1
|
247
|
+
elsif v1.is_a?(Numeric) && v2.is_a?(Numeric)
|
248
|
+
self[k] = v1 + v2
|
249
|
+
else
|
250
|
+
raise ArgumentError,
|
251
|
+
"Can't merge leaf with non-leaf:\n#{v1.inspect}\n#{v2.inspect}"
|
252
|
+
end
|
253
|
+
else
|
254
|
+
if v1.respond_to?(:has_key?)
|
255
|
+
self[k] << v1
|
256
|
+
else
|
257
|
+
self[k] = v1
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def match?(val, key)
|
264
|
+
case val
|
265
|
+
when true
|
266
|
+
true
|
267
|
+
when String, Symbol
|
268
|
+
key == val
|
269
|
+
when Regexp
|
270
|
+
key =~ val
|
271
|
+
when Proc
|
272
|
+
val.call(key)
|
273
|
+
when nil
|
274
|
+
false
|
275
|
+
else
|
276
|
+
raise ArgumentError, "Unexpected matcher type: #{val.inspect}"
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def each_path(stack=[], &block)
|
281
|
+
self.each do |k, v|
|
282
|
+
stack.push(k)
|
283
|
+
if v.respond_to?(:each_path)
|
284
|
+
v.each_path(stack, &block)
|
285
|
+
else
|
286
|
+
block.call(stack, v)
|
287
|
+
end
|
288
|
+
stack.pop
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def paths
|
293
|
+
out = []
|
294
|
+
end
|
295
|
+
|
296
|
+
def each_leaf(stack=[], &block)
|
297
|
+
self.each do |k,v|
|
298
|
+
stack.push(k)
|
299
|
+
if v.respond_to?(:each_leaf)
|
300
|
+
v.each_leaf(stack, &block)
|
301
|
+
else
|
302
|
+
block.call(v)
|
303
|
+
end
|
304
|
+
stack.pop
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
end
|
309
|
+
|
310
|
+
|
311
|
+
|
312
|
+
|
313
|
+
|
metadata
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fate
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 19
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
- 2
|
10
|
+
version: 0.2.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Matthew King
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-08-29 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: consolize
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 23
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
- 2
|
33
|
+
- 0
|
34
|
+
version: 0.2.0
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: open4
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 27
|
46
|
+
segments:
|
47
|
+
- 1
|
48
|
+
- 3
|
49
|
+
- 0
|
50
|
+
version: 1.3.0
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: term-ansicolor
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 23
|
62
|
+
segments:
|
63
|
+
- 1
|
64
|
+
- 0
|
65
|
+
- 0
|
66
|
+
version: 1.0.0
|
67
|
+
type: :runtime
|
68
|
+
version_requirements: *id003
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
executables:
|
72
|
+
- fate
|
73
|
+
extensions: []
|
74
|
+
|
75
|
+
extra_rdoc_files: []
|
76
|
+
|
77
|
+
files:
|
78
|
+
- bin/fate
|
79
|
+
- LICENSE
|
80
|
+
- lib/fate.rb
|
81
|
+
- lib/hash_tree.rb
|
82
|
+
- lib/fate/console.rb
|
83
|
+
has_rdoc: true
|
84
|
+
homepage: https://github.com/automatthew/fate
|
85
|
+
licenses: []
|
86
|
+
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
hash: 3
|
98
|
+
segments:
|
99
|
+
- 0
|
100
|
+
version: "0"
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
hash: 3
|
107
|
+
segments:
|
108
|
+
- 0
|
109
|
+
version: "0"
|
110
|
+
requirements: []
|
111
|
+
|
112
|
+
rubyforge_project:
|
113
|
+
rubygems_version: 1.6.2
|
114
|
+
signing_key:
|
115
|
+
specification_version: 3
|
116
|
+
summary: Tool for running and interacting with a multi-process service
|
117
|
+
test_files: []
|
118
|
+
|