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 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: