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 CHANGED
@@ -6,8 +6,8 @@ require "json"
6
6
 
7
7
  # set up loadpath
8
8
  here = File.dirname(__FILE__)
9
- SPAWN_CONTROL_ROOT = File.expand_path("#{here}/..")
10
- $LOAD_PATH.unshift("#{SPAWN_CONTROL_ROOT}/lib")
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
- spawner = Fate.new(configuration)
28
+ fate = Fate.new(configuration)
29
29
 
30
30
  trap("INT") do
31
31
  exit
32
32
  end
33
33
 
34
- require "fate/console"
35
- spawner.start
36
- spawner.console
34
+ require "fate/repl"
35
+ fate.start
36
+ fate.repl
37
37
 
38
38
 
@@ -5,193 +5,122 @@ gem "squeeze"
5
5
  require "term/ansicolor"
6
6
  require "squeeze/hash_tree"
7
7
 
8
- # Cross-VM compatibility
9
- # thanks to http://ku1ik.com/2010/09/18/open3-and-the-pid-of-the-spawn.html
10
- # TODO: consider using systemu: https://github.com/ahoward/systemu/
11
- if IO.respond_to?(:popen4)
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(configuration, &block)
22
- self.new(configuration).start(&block)
16
+ def self.start(specification, &block)
17
+ self.new(specification).start(&block)
23
18
  end
24
19
 
25
- attr_reader :commands, :configuration, :completions
20
+ attr_reader :manager, :specification, :completions, :name_commands
26
21
 
27
- def initialize(configuration, options={})
28
- @configuration = configuration
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[@configuration[:commands]]
30
+ commands = Squeeze::HashTree[@specification[:commands]]
36
31
 
37
32
  @completions = Set.new
38
33
 
39
- @commands = {}
34
+ @name_commands = {}
40
35
  commands.each_path do |path, value|
41
36
  key = path.join(".")
42
- @completions += path
37
+ # add dot-delimited command names to the completions
38
+ @completions += path.map {|s| s.to_s }
43
39
  @completions << key
44
- @commands[key] = value
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
- @threads = {}
48
- @pid_tracker = {}
49
- @command_tracker = {}
45
+ @manager = Manager.new(@specification, :log => @log, :command_width => @command_width)
50
46
  end
51
47
 
52
- def start(&block)
53
- @running = []
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 spawn(name, command)
94
- return Thread.new do
95
- pid, stdin, stdout, stderr = open4(command)
96
- puts colorize("yellow", format_line("Fate", "Starting (#{pid}): #{command}"))
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
- if @pid_tracker.size != 0
121
- command = "kill #{@pid_tracker.keys.join(' ')}"
122
- system command
123
- @pid_tracker.clear
124
- @command_tracker.clear
125
- @threads.clear
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 format_line(identifier, line)
130
- if identifier == @last_identifier
131
- "%-#{@command_width}s - %s" % [nil, line]
132
- else
133
- @last_identifier = identifier
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 stop_command(name)
80
+ def resolve_commands(name)
139
81
  targets = []
140
- if command = @commands[name]
82
+ if @name_commands.has_key?(name)
141
83
  targets << name
142
84
  else
143
- @commands.each do |cname, _command|
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
- if targets.empty?
151
- puts "No such command registered: #{name}"
152
- end
153
-
154
- targets.each do |name|
155
- if pid = @command_tracker[name]
156
- @pid_tracker.delete(pid)
157
- @command_tracker.delete(name)
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 start_command(name)
167
- if command = @commands[name]
168
- if pid = @command_tracker[name]
169
- puts "#{name} is already running with pid #{pid}"
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
- puts "No such command registered: #{name}"
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
- names = @command_tracker.map {|name, command| name }
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 shelling out: #{error.inspect}"
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
@@ -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
- require "consolize"
1
+ gem "harp"
2
+ require "harp"
2
3
  class Fate
3
4
 
4
- include Consolize
5
+ include Harp
5
6
 
6
- setup_console do |console|
7
+ setup_repl do |repl|
7
8
 
8
9
  on("help") do
9
- commands = console.commands.select {|c| c.size > 1 } + ["!"]
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(/stop (\S+)$/) do |args|
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(/start (\S+)$/) do |args|
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(commands)
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.3
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-08-30 00:00:00.000000000 Z
12
+ date: 2012-09-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: consolize
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.0
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.0
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/console.rb
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: