fate 0.2.7 → 0.2.8

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