fate 0.2.7 → 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
data/bin/fate CHANGED
@@ -12,7 +12,8 @@ $LOAD_PATH.unshift("#{FATE_ROOT}/lib")
12
12
  require "fate"
13
13
 
14
14
  options = {
15
- :configuration => "fate.json"
15
+ :configuration => "fate.json",
16
+ :default_log => STDOUT
16
17
  }
17
18
 
18
19
  # TODO: config file flag
@@ -20,16 +21,17 @@ OptionParser.new do |parser|
20
21
  parser.on("-c", "--configuration=FILE", "The origin of the events") do |file|
21
22
  options[:configuration] = file
22
23
  end
24
+ parser.on("-l", "--log=FILE", "Processes log to FILE") do |file|
25
+ options[:default_log] = File.new(file, "a")
26
+ end
23
27
  end.parse!
24
28
 
25
29
 
26
30
  string = File.read(options[:configuration])
27
31
  configuration = JSON.parse(string, :symbolize_names => true)
28
- fate = Fate.new(configuration)
32
+ fate = Fate.new(configuration, options)
33
+
29
34
 
30
- trap("INT") do
31
- exit
32
- end
33
35
 
34
36
  require "fate/repl"
35
37
  fate.start
@@ -5,44 +5,33 @@ gem "squeeze"
5
5
  require "term/ansicolor"
6
6
  require "squeeze/hash_tree"
7
7
 
8
- require "fate/formatter"
9
- require "fate/manager"
8
+ require "fate/logger"
9
+ require "fate/service"
10
+ require "fate/output"
11
+ require "fate/process_manager"
10
12
 
11
13
  Thread.abort_on_exception = true
12
14
 
13
15
  class Fate
14
- include Formatter
15
16
 
16
17
  def self.start(specification, &block)
17
18
  self.new(specification).start(&block)
18
19
  end
19
20
 
20
- attr_reader :manager, :specification, :completions, :name_commands
21
+ attr_reader :service, :manager, :completions, :logger
21
22
 
22
- def initialize(specification, options={})
23
- @specification = specification
24
- @options = options
25
- if logfile = options[:service_log]
26
- @log = File.new(logfile, "a")
27
- else
28
- @log = STDOUT
29
- end
30
- commands = Squeeze::HashTree[@specification[:commands]]
31
-
32
- @completions = Set.new
33
-
34
- @name_commands = {}
35
- commands.each_path do |path, value|
36
- key = path.join(".")
37
- # add dot-delimited command names to the completions
38
- @completions += path.map {|s| s.to_s }
39
- @completions << key
40
- # register each command under the dot-delimited name
41
- @name_commands[key] = value
42
- end
43
- @command_width = @name_commands.keys.sort_by {|k| k.size }.last.size
23
+ def initialize(spec, options={})
24
+ @service = Service.new(spec, options)
25
+ @completions = @service.completions
26
+
27
+ @spec = spec
28
+ @logger = @service.logger["Fate Control"]
29
+
30
+ @manager = ProcessManager.new(@service)
31
+ end
44
32
 
45
- @manager = Manager.new(@specification, :log => @log, :command_width => @command_width)
33
+ def log(*args, &block)
34
+ @logger.log(*args, &block)
46
35
  end
47
36
 
48
37
  def run(&block)
@@ -54,18 +43,13 @@ class Fate
54
43
  end
55
44
 
56
45
  def start
57
- manager.start_group(@name_commands)
58
- message = format_line("Fate Control", "All commands are running. ")
59
- puts colorize("green", message)
46
+ manager.start_group(@service.commands)
47
+ logger.green "All commands are running."
60
48
  end
61
49
 
62
50
  def stop
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|
51
+ ordered = @service.stop_order(manager.running)
52
+ ordered.each do |name|
69
53
  manager.stop_command(name)
70
54
  end
71
55
  end
@@ -77,34 +61,20 @@ class Fate
77
61
  start
78
62
  end
79
63
 
80
- def resolve_commands(name)
81
- targets = []
82
- if @name_commands.has_key?(name)
83
- targets << name
84
- else
85
- @name_commands.each do |cname, _command|
86
- if cname.split(".").first == name
87
- targets << cname
88
- end
89
- end
90
- end
91
- targets
92
- end
93
-
94
64
  def start_command(spec)
95
- names = resolve_commands(spec)
65
+ names = @service.resolve_commands(spec)
96
66
  if names.empty?
97
67
  puts "No commands found for: #{spec}"
98
68
  else
99
69
  names.each do |name|
100
- command = @name_commands[name]
70
+ command = @service.commands[name]
101
71
  manager.start_command(name, command)
102
72
  end
103
73
  end
104
74
  end
105
75
 
106
76
  def stop_command(spec)
107
- names = resolve_commands(spec)
77
+ names = @service.resolve_commands(spec)
108
78
  if names.empty?
109
79
  puts "No commands found for: #{spec}"
110
80
  else
@@ -0,0 +1,91 @@
1
+ class Fate
2
+
3
+ # Simple logging class designed to interleave the output from multiple
4
+ # processes while making it obvious which lines were logged by which process.
5
+
6
+ class MultiLogger
7
+
8
+ attr_reader :width, :io
9
+ def initialize(options)
10
+ @width = options[:width]
11
+ if file = options[:file]
12
+ @io = File.new(file, "a")
13
+ elsif io = options[:io]
14
+ @io = io
15
+ end
16
+ end
17
+
18
+ def [](name)
19
+ Sublogger.new(self, name)
20
+ end
21
+
22
+ class Sublogger
23
+ def initialize(master, name)
24
+ @master = master
25
+ @io = @master.io
26
+ @name = name
27
+ end
28
+
29
+ # duck typing for IO
30
+ def write(string)
31
+ num = @io.write(@master.format(@name, string))
32
+ @io.flush
33
+ num
34
+ end
35
+
36
+ def method_missing(method, *args, &block)
37
+ if @master.respond_to?(method)
38
+ # insert this logger's name into every relayed call.
39
+ @master.send(method, @name, *args, &block)
40
+ elsif @io.respond_to?(method)
41
+ @io.send(method, *args, &block)
42
+ else
43
+ super
44
+ end
45
+ end
46
+ end
47
+
48
+ def write(name, string, color=nil)
49
+ if color
50
+ line = colorize(color, format(name, string))
51
+ else
52
+ line = format(name, string)
53
+ end
54
+ num = @io.write line
55
+ @io.flush
56
+ num
57
+ end
58
+
59
+ def error(name, string)
60
+ write(name, string, "red")
61
+ end
62
+
63
+ def green(name, string)
64
+ write(name, string, "green")
65
+ end
66
+
67
+ def info(name, string)
68
+ write(name, string, "yellow")
69
+ end
70
+
71
+ def debug(name, string)
72
+ write(name, string)
73
+ end
74
+
75
+ def format(name, string)
76
+ string.chomp!
77
+ if name == @last_identifier
78
+ "%-#{width}s - %s\n" % [nil, string]
79
+ else
80
+ @last_identifier = name
81
+ "%-#{width}s - %s\n" % [name, string]
82
+ end
83
+ end
84
+
85
+ def colorize(name, string)
86
+ [Term::ANSIColor.send(name), string, Term::ANSIColor.reset].join
87
+ end
88
+
89
+ end
90
+
91
+ end
@@ -0,0 +1,77 @@
1
+ class Fate
2
+ module Output
3
+
4
+ class Handlers
5
+
6
+ def initialize(service, handlers)
7
+ @service = service
8
+ @handlers = handlers
9
+ end
10
+
11
+ def [](name)
12
+ if handler = @handlers[name]
13
+ handler
14
+ elsif @handlers["default"]
15
+ @handlers[name] = @handlers["default"][name]
16
+ else
17
+ @handlers[name] = @service.logger[name]
18
+ end
19
+ end
20
+
21
+ end
22
+
23
+ class IOFilter
24
+ def initialize(master, name)
25
+ @master = master
26
+ @io = @master.io
27
+ @name = name
28
+ end
29
+
30
+ # duck typing for IO
31
+ def write(string)
32
+ num = @io.write(@master.format(@name, string))
33
+ @io.flush
34
+ num
35
+ end
36
+
37
+ def method_missing(method, *args, &block)
38
+ if @io.respond_to?(method)
39
+ @io.send(method, *args, &block)
40
+ else
41
+ super
42
+ end
43
+ end
44
+ end
45
+
46
+ class IOMux
47
+ attr_reader :io
48
+ attr_accessor :last_identifier
49
+ def initialize(options)
50
+ @last_identifier = nil
51
+ if file = options[:file]
52
+ @io = File.new(file, "a")
53
+ elsif io = options[:io]
54
+ @io = io
55
+ end
56
+ @handlers = {}
57
+ end
58
+
59
+ def [](name)
60
+ @handlers[name] ||= IOFilter.new(self, name)
61
+ end
62
+
63
+ def format(name, string)
64
+ if name == @last_identifier
65
+ string
66
+ else
67
+ @last_identifier = name
68
+ "==> #{name} <==\n#{string}"
69
+ end
70
+ end
71
+
72
+
73
+ end
74
+
75
+ end
76
+ end
77
+
@@ -1,7 +1,7 @@
1
+ require "fileutils"
1
2
  require "pp"
2
3
  # Cross-VM compatibility
3
4
  # 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
5
  if IO.respond_to?(:popen4)
6
6
  def open4(*args)
7
7
  IO.popen4(*args)
@@ -11,21 +11,28 @@ else
11
11
  end
12
12
 
13
13
  class Fate
14
- class Manager
15
- include Formatter
16
14
 
17
- def initialize(specification, options)
18
- @service_log = options[:log]
19
- @command_width = options[:command_width]
15
+ # A process management tool, concerned primarily with spawning child
16
+ # processes, tracking them by name, and handling unexpected exits and signals.
17
+ class ProcessManager
18
+
19
+ attr_reader :logger, :output_handlers
20
+ def initialize(service)
21
+ @service = service
22
+ @output_handlers = @service.output_handlers
23
+ @logger = @service.logger["Fate Manager"]
24
+
20
25
  @threads = {}
21
26
  @commands_by_name = {}
22
27
  @names_by_pid = {}
23
28
  @pids_by_name = {}
24
- at_exit { stop }
29
+ at_exit do
30
+ stop
31
+ end
25
32
  end
26
33
 
27
-
28
34
  def stop
35
+ # FIXME: race condition involving spawn and stop at the same time
29
36
  @names_by_pid.each do |pid, name|
30
37
  kill(name)
31
38
  end
@@ -44,7 +51,7 @@ class Fate
44
51
 
45
52
  def start_command(name, command)
46
53
  if pid = @pids_by_name[name]
47
- puts "#{name} is already running with pid #{pid}"
54
+ logger.error "'#{name}' is already running with pid #{pid}"
48
55
  else
49
56
  spawn(name, command)
50
57
  end
@@ -56,10 +63,9 @@ class Fate
56
63
  @pids_by_name.delete(name)
57
64
  @threads.delete(name)
58
65
  system "kill -s INT #{pid}"
59
- puts colorize "yellow",
60
- format_line("Fate Manager", "Sent a kill signal to #{name} running at #{pid}")
66
+ logger.info "Sent a kill signal to '#{name}' running at #{pid}"
61
67
  else
62
- puts "Could not find pid for #{name}"
68
+ logger.error "Could not find pid for '#{name}'"
63
69
  end
64
70
  end
65
71
 
@@ -70,29 +76,27 @@ class Fate
70
76
  def spawn(name, command)
71
77
  # TODO: check to see if command is already running
72
78
  return Thread.new do
79
+
73
80
  pid, stdin, stdout, stderr = open4(command)
74
- puts colorize("yellow", format_line("Fate Manager", "Starting (#{pid}): #{name}"))
81
+ logger.info "Starting '#{name}' (pid #{pid})"
75
82
  @names_by_pid[pid] = name
76
83
  @pids_by_name[name] = pid
77
84
 
85
+ io = output_handlers[name]
78
86
  Thread.new do
79
- while line = stderr.gets
80
- STDERR.puts "(#{name}) #{line}"
81
- end
87
+ IO.copy_stream(stderr, io)
82
88
  end
83
89
 
84
90
  # First line written to STDOUT is interpreted as the service
85
91
  # signalling that it is ready.
86
92
  line = stdout.gets
87
- puts colorize("yellow", format_line("Fate Manager", "#{name} is running."))
88
- @service_log.puts format_line(name, line)
93
+ logger.info "#{name} is running."
94
+
95
+ io.write(line)
89
96
  @threads[name] = Thread.current
90
- #@threads << Thread.current
91
97
 
92
- while line = stdout.gets
93
- @service_log.puts format_line(name, line)
94
- end
95
- status = Process.wait(pid)
98
+ IO.copy_stream(stdout, io)
99
+ pid, status = Process.wait2(pid)
96
100
  handle_child_termination(pid, status)
97
101
  end
98
102
  end
@@ -117,20 +121,24 @@ class Fate
117
121
  end
118
122
 
119
123
  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)
124
+ if status.exitstatus
125
+ logger.error "Process '#{name}' (pid #{pid}) exited with code #{status.exitstatus}."
126
+ else
127
+ logger.info "Process '#{name}' (pid #{pid}) was sent signal #{status.termsig}."
128
+ end
129
+ logger.info "Shutting down all processes."
130
+ exit(1)
123
131
  end
124
132
 
125
133
 
126
134
  # ad hoc shell out, with rescuing because of some apparent bugs
127
135
  # in MRI 1.8.7's ability to cope with unusual exit codes.
128
136
  def system(command)
129
- begin
137
+ #begin
130
138
  Kernel.system command
131
- rescue => error
132
- puts "Exception raised when executing '#{command}': #{error.inspect}"
133
- end
139
+ #rescue => error
140
+ #puts "Exception raised when executing '#{command}': #{error.inspect}"
141
+ #end
134
142
  end
135
143
 
136
144
 
@@ -43,7 +43,7 @@ class Fate
43
43
  end
44
44
 
45
45
  on("commands") do
46
- puts JSON.pretty_generate(self.name_commands)
46
+ puts self.service.names
47
47
  end
48
48
 
49
49
  on("running") do
@@ -51,7 +51,7 @@ class Fate
51
51
  end
52
52
 
53
53
  on("configuration", "config") do
54
- puts JSON.pretty_generate(configuration)
54
+ puts JSON.pretty_generate(self.service.specification)
55
55
  end
56
56
 
57
57
  end
@@ -0,0 +1,63 @@
1
+ class Fate
2
+
3
+ class Service
4
+
5
+ attr_reader :longest_name, :names, :commands, :completions, :specification
6
+ attr_reader :output_handlers, :logger
7
+ def initialize(specification, options)
8
+ @specification = specification
9
+ @options = options
10
+
11
+ @commands = process_commands(@specification[:commands])
12
+ @names = @commands.keys
13
+ @longest_name = @commands.keys.sort_by {|k| k.size }.last.size
14
+ @logger = Fate::MultiLogger.new(:io => STDOUT, :width => @longest_name)
15
+ @output_handlers = Output::Handlers.new(self, options[:output] || {})
16
+ end
17
+
18
+ def process_commands(hash)
19
+ hash = Squeeze::HashTree[hash]
20
+
21
+ out = {}
22
+ @completions ||= Set.new
23
+ hash.each_path do |path, value|
24
+ key = path.join(".")
25
+ # add dot-delimited command names to the completions
26
+ @completions += path.map {|s| s.to_s }
27
+ @completions << key
28
+ # register each command under the dot-delimited name
29
+ out[key] = value
30
+ end
31
+ out
32
+ end
33
+
34
+ def resolve_commands(name)
35
+ targets = []
36
+ if @commands.has_key?(name)
37
+ targets << name
38
+ else
39
+ @commands.each do |cname, _command|
40
+ if cname.split(".").first == name
41
+ targets << cname
42
+ end
43
+ end
44
+ end
45
+ targets
46
+ end
47
+
48
+
49
+ def start_order(command_names)
50
+ # presuming the spec file ordered the commands where the dependencies
51
+ # come before the dependers, we should stop the processes in reverse order,
52
+ # then start them back up again in forward order.
53
+ command_names.sort_by {|name| self.names.index(name) }
54
+ end
55
+
56
+ def stop_order(command_names)
57
+ start_order(command_names).reverse
58
+ end
59
+
60
+ end
61
+
62
+ end
63
+
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.7
4
+ version: 0.2.8
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-04 00:00:00.000000000 Z
12
+ date: 2012-09-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: json
@@ -101,10 +101,13 @@ files:
101
101
  - bin/fate
102
102
  - LICENSE
103
103
  - README.md
104
- - lib/fate.rb
105
- - lib/fate/repl.rb
106
104
  - lib/fate/formatter.rb
107
- - lib/fate/manager.rb
105
+ - lib/fate/logger.rb
106
+ - lib/fate/output.rb
107
+ - lib/fate/process_manager.rb
108
+ - lib/fate/repl.rb
109
+ - lib/fate/service.rb
110
+ - lib/fate.rb
108
111
  homepage: https://github.com/automatthew/fate
109
112
  licenses: []
110
113
  post_install_message: