foreman 0.31.0 → 0.50.0

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.
Files changed (73) hide show
  1. data/README.md +46 -0
  2. data/bin/foreman-runner +32 -0
  3. data/bin/taskman +8 -0
  4. data/data/example/Procfile +4 -2
  5. data/data/example/spawnee +14 -0
  6. data/data/example/spawner +7 -0
  7. data/data/example/utf8 +11 -0
  8. data/data/export/bluepill/master.pill.erb +11 -10
  9. data/data/export/launchd/launchd.plist.erb +22 -0
  10. data/data/export/runit/log/run.erb +7 -0
  11. data/data/export/runit/run.erb +2 -2
  12. data/data/export/supervisord/app.conf.erb +27 -0
  13. data/data/export/upstart/master.conf.erb +2 -2
  14. data/data/export/upstart/process.conf.erb +3 -3
  15. data/lib/foreman/cli.rb +83 -34
  16. data/lib/foreman/engine/cli.rb +105 -0
  17. data/lib/foreman/engine.rb +232 -140
  18. data/lib/foreman/env.rb +27 -0
  19. data/lib/foreman/export/base.rb +108 -6
  20. data/lib/foreman/export/bluepill.rb +4 -20
  21. data/lib/foreman/export/inittab.rb +11 -16
  22. data/lib/foreman/export/launchd.rb +15 -0
  23. data/lib/foreman/export/runit.rb +15 -41
  24. data/lib/foreman/export/supervisord.rb +16 -0
  25. data/lib/foreman/export/upstart.rb +10 -28
  26. data/lib/foreman/export.rb +23 -0
  27. data/lib/foreman/helpers.rb +45 -0
  28. data/lib/foreman/process.rb +80 -46
  29. data/lib/foreman/procfile.rb +68 -14
  30. data/lib/foreman/version.rb +1 -1
  31. data/lib/foreman.rb +12 -7
  32. data/man/foreman.1 +29 -3
  33. data/spec/foreman/cli_spec.rb +54 -60
  34. data/spec/foreman/engine_spec.rb +64 -46
  35. data/spec/foreman/export/base_spec.rb +19 -0
  36. data/spec/foreman/export/bluepill_spec.rb +24 -6
  37. data/spec/foreman/export/inittab_spec.rb +40 -0
  38. data/spec/foreman/export/launchd_spec.rb +21 -0
  39. data/spec/foreman/export/runit_spec.rb +23 -22
  40. data/spec/foreman/export/supervisord_spec.rb +36 -0
  41. data/spec/foreman/export/upstart_spec.rb +49 -16
  42. data/spec/foreman/export_spec.rb +22 -0
  43. data/spec/foreman/helpers_spec.rb +26 -0
  44. data/spec/foreman/process_spec.rb +48 -2
  45. data/spec/foreman/procfile_spec.rb +41 -0
  46. data/spec/foreman_spec.rb +3 -20
  47. data/spec/helper_spec.rb +18 -0
  48. data/spec/resources/Procfile +4 -0
  49. data/spec/resources/bin/echo +2 -0
  50. data/spec/resources/bin/env +2 -0
  51. data/spec/resources/bin/test +2 -0
  52. data/spec/resources/bin/utf8 +2 -0
  53. data/spec/resources/export/bluepill/app-concurrency.pill +49 -0
  54. data/spec/resources/export/bluepill/app.pill +6 -25
  55. data/spec/resources/export/inittab/inittab.concurrency +4 -0
  56. data/spec/resources/export/inittab/inittab.default +4 -0
  57. data/spec/resources/export/launchd/launchd-a.default +22 -0
  58. data/spec/resources/export/launchd/launchd-b.default +22 -0
  59. data/spec/resources/export/supervisord/app-alpha-1.conf +24 -0
  60. data/spec/resources/export/supervisord/app-alpha-2.conf +24 -0
  61. data/spec/spec_helper.rb +93 -10
  62. metadata +72 -76
  63. data/README.markdown +0 -49
  64. data/bin/runner +0 -2
  65. data/data/export/runit/log_run.erb +0 -7
  66. data/lib/foreman/procfile_entry.rb +0 -22
  67. data/lib/foreman/utils.rb +0 -15
  68. /data/spec/resources/export/runit/{app-alpha-1-log-run → app-alpha-1/log/run} +0 -0
  69. /data/spec/resources/export/runit/{app-alpha-1-run → app-alpha-1/run} +0 -0
  70. /data/spec/resources/export/runit/{app-alpha-2-log-run → app-alpha-2/log/run} +0 -0
  71. /data/spec/resources/export/runit/{app-alpha-2-run → app-alpha-2/run} +0 -0
  72. /data/spec/resources/export/runit/{app-bravo-1-log-run → app-bravo-1/log/run} +0 -0
  73. /data/spec/resources/export/runit/{app-bravo-1-run → app-bravo-1/run} +0 -0
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Foreman
2
+
3
+ Manage Procfile-based applications
4
+
5
+ <table>
6
+ <tr>
7
+ <th>If you have...</th>
8
+ <th>Install with...</th>
9
+ </tr>
10
+ <tr>
11
+ <td>Ruby (MRI, JRuby, Windows)</td>
12
+ <td><pre>$ gem install foreman</pre></td>
13
+ </tr>
14
+ <tr>
15
+ <td>Mac OS X</td>
16
+ <td><a href="http://assets.foreman.io/foreman/foreman.pkg">foreman.pkg</a></td>
17
+ </tr>
18
+ </table>
19
+
20
+ ## Getting Started
21
+
22
+ * http://blog.daviddollar.org/2011/05/06/introducing-foreman.html
23
+
24
+ ## Documentation
25
+
26
+ * [man page](http://ddollar.github.com/foreman)
27
+ * [wiki](http://github.com/ddollar/foreman/wiki)
28
+ * [changelog](https://github.com/ddollar/foreman/blob/master/Changelog.md)
29
+
30
+ ## Ports
31
+
32
+ * [shoreman](https://github.com/hecticjeff/shoreman) - shell
33
+ * [honcho](https://github.com/nickstenning/honcho) - python
34
+ * [norman](https://github.com/josh/norman) - node.js
35
+
36
+ ## Authors
37
+
38
+ #### Created and maintained by
39
+ David Dollar
40
+
41
+ #### Patches contributed by
42
+ [Contributor List](https://github.com/ddollar/foreman/contributors)
43
+
44
+ ## License
45
+
46
+ MIT
@@ -0,0 +1,32 @@
1
+ #!/bin/sh
2
+ #
3
+ #/ Usage: foreman-runner [-d <dir>] <command> [<args>...]
4
+ #/
5
+ #/ Run a command with exec, optionally changing directory first
6
+
7
+ set -e
8
+
9
+ error() {
10
+ echo $@ >&2
11
+ exit 1
12
+ }
13
+
14
+ usage() {
15
+ cat $0 | grep '^#/' | cut -c4-
16
+ exit
17
+ }
18
+
19
+ while getopts ":hd:" OPT; do
20
+ case $OPT in
21
+ d) cd "$OPTARG" ;;
22
+ h) usage ;;
23
+ \?) error "invalid option: -$OPTARG" ;;
24
+ :) error "option -$OPTARG requires an argument" ;;
25
+ esac
26
+ done
27
+
28
+ shift $((OPTIND-1))
29
+
30
+ [ -z "$1" ] && usage
31
+
32
+ exec "$@"
data/bin/taskman ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.expand_path("../../lib", __FILE__)
4
+
5
+ require "foreman/cli"
6
+
7
+ Foreman::CLI.engine_class = Foreman::TmuxEngine
8
+ Foreman::CLI.start
@@ -1,2 +1,4 @@
1
- ticker: ruby ./ticker $PORT
2
- error: ruby ./error
1
+ ticker: ruby ./ticker $PORT
2
+ error: ruby ./error
3
+ utf8: ruby ./utf8
4
+ spawner: ./spawner
@@ -0,0 +1,14 @@
1
+ #!/bin/sh
2
+
3
+ NAME="$1"
4
+
5
+ sigterm() {
6
+ echo "$NAME: got sigterm"
7
+ }
8
+
9
+ #trap sigterm SIGTERM
10
+
11
+ while true; do
12
+ echo "$NAME: ping $$"
13
+ sleep 1
14
+ done
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+
3
+ ./spawnee A &
4
+ ./spawnee B &
5
+ ./spawnee C &
6
+
7
+ wait
data/data/example/utf8 ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: BINARY
3
+
4
+ $stdout.sync = true
5
+
6
+ while true
7
+ puts "\u65e5\u672c\u8a9e\u6587\u5b57\u5217"
8
+ puts "\u0915\u0932\u094d\u0907\u0928\u0643\u0637\u0628\u041a\u0430\u043b\u0438\u043d\u0430"
9
+ puts "\xff\x03"
10
+ sleep 1
11
+ end
@@ -3,24 +3,25 @@ Bluepill.application("<%= app %>", :foreground => false, :log_file => "/var/log/
3
3
  app.uid = "<%= user %>"
4
4
  app.gid = "<%= user %>"
5
5
 
6
- <% engine.procfile.entries.each do |process| %>
7
- <% 1.upto(concurrency[process.name]) do |num| %>
8
- <% port = engine.port_for(process, num, options[:port]) %>
9
- app.process("<%= process.name %>-<%=num%>") do |process|
10
- process.start_command = "<%= process.command.gsub("$PORT", port.to_s) %>"
6
+ <% engine.each_process do |name, process| %>
7
+ <% 1.upto(engine.formation[name]) do |num| %>
8
+ <% port = engine.port_for(process, num) %>
9
+ app.process("<%= name %>-<%= num %>") do |process|
10
+ process.start_command = "<%= process.command %>"
11
11
 
12
- process.working_dir = "<%= engine.directory %>"
12
+ process.working_dir = "<%= engine.root %>"
13
13
  process.daemonize = true
14
- process.environment = {"PORT" => "<%= port %>"}
14
+ process.environment = <%= engine.env.merge("PORT" => port.to_s).inspect %>
15
15
  process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill]
16
+ process.stop_grace_time = 45.seconds
16
17
 
17
- process.stdout = process.stderr = "<%= log_root %>/<%= app %>-<%= process.name %>-<%=num%>.log"
18
+ process.stdout = process.stderr = "<%= log %>/<%= app %>-<%= name %>-<%= num %>.log"
18
19
 
19
20
  process.monitor_children do |children|
20
- children.stop_command "kill -QUIT {{PID}}"
21
+ children.stop_command "kill {{PID}}"
21
22
  end
22
23
 
23
- process.group = "<%= app %>-<%= process.name %>"
24
+ process.group = "<%= app %>-<%= name %>"
24
25
  end
25
26
  <% end %>
26
27
  <% end %>
@@ -0,0 +1,22 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string><%= "#{app}-#{name}-#{num}" %></string>
7
+ <key>ProgramArguments</key>
8
+ <array>
9
+ <string><%= process.command %></string>
10
+ </array>
11
+ <key>KeepAlive</key>
12
+ <true/>
13
+ <key>RunAtLoad</key>
14
+ <true/>
15
+ <key>StandardErrorPath</key>
16
+ <string><%= log %>/<%= app %>-<%= name %>-<%=num%>.log</string>
17
+ <key>UserName</key>
18
+ <string><%= user %></string>
19
+ <key>WorkingDirectory</key>
20
+ <string><%= engine.root %></string>
21
+ </dict>
22
+ </plist>
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ LOG=<%= log %>/<%= name %>-<%= num %>
5
+
6
+ test -d "$LOG" || mkdir -p m2750 "$LOG" && chown <%= user %> "$LOG"
7
+ exec chpst -u <%= user %> svlogd "$LOG"
@@ -1,3 +1,3 @@
1
1
  #!/bin/sh
2
- cd <%= engine.directory %>
3
- exec chpst -u <%= user %> -e <%= process_env_directory %> <%= process.command %>
2
+ cd <%= engine.root %>
3
+ exec chpst -u <%= user %> -e <%= File.join(location, "#{process_directory}/env") %> <%= process.command %>
@@ -0,0 +1,27 @@
1
+ <%
2
+ app_names = []
3
+ engine.each_process do |name, process|
4
+ 1.upto(engine.formation[name]) do |num|
5
+ port = engine.port_for(process, num)
6
+ full_name = "#{app}-#{name}-#{num}"
7
+ environment = engine.env.merge("PORT" => port.to_s).map do |key, value|
8
+ "#{key}=#{shell_quote(value)}"
9
+ end
10
+ app_names << full_name
11
+ %>
12
+ [program:<%= full_name %>]
13
+ command=<%= process.command %>
14
+ autostart=true
15
+ autorestart=true
16
+ stopsignal=QUIT
17
+ stdout_logfile=<%= log %>/<%= name %>-<%= num %>.log
18
+ stderr_logfile=<%= log %>/<%= name %>-<%= num %>.error.log
19
+ user=<%= user %>
20
+ directory=<%= engine.root %>
21
+ environment=<%= environment.join(',') %><%
22
+ end
23
+ end
24
+ %>
25
+
26
+ [group:<%= app %>]
27
+ programs=<%= app_names.join(',') %>
@@ -1,8 +1,8 @@
1
1
  pre-start script
2
2
 
3
3
  bash << "EOF"
4
- mkdir -p <%= log_root %>
5
- chown -R <%= user %> <%= log_root %>
4
+ mkdir -p <%= log %>
5
+ chown -R <%= user %> <%= log %>
6
6
  EOF
7
7
 
8
8
  end script
@@ -1,5 +1,5 @@
1
- start on starting <%= app %>-<%= process.name %>
2
- stop on stopping <%= app %>-<%= process.name %>
1
+ start on starting <%= app %>-<%= name %>
2
+ stop on stopping <%= app %>-<%= name %>
3
3
  respawn
4
4
 
5
- exec su - <%= user %> -c 'cd <%= engine.directory %>; export PORT=<%= port %>;<% engine.environment.each_pair do |var,env| %> export <%= var.upcase %>=<%= env %>; <% end %> <%= process.command %> >> <%= log_root %>/<%=process.name%>-<%=num%>.log 2>&1'
5
+ exec su - <%= user %> -c 'cd <%= engine.root %>; export PORT=<%= port %>;<% engine.env.each_pair do |var,env| %> export <%= var.upcase %>=<%= shell_quote(env) %>; <% end %> <%= process.command %> >> <%= log %>/<%=name%>-<%=num%>.log 2>&1'
data/lib/foreman/cli.rb CHANGED
@@ -1,21 +1,42 @@
1
1
  require "foreman"
2
+ require "foreman/helpers"
2
3
  require "foreman/engine"
4
+ require "foreman/engine/cli"
3
5
  require "foreman/export"
4
- require "thor"
6
+ require "foreman/version"
7
+ require "shellwords"
5
8
  require "yaml"
9
+ require "thor"
6
10
 
7
11
  class Foreman::CLI < Thor
8
12
 
13
+ include Foreman::Helpers
14
+
15
+ map ["-v", "--version"] => :version
16
+
9
17
  class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile"
18
+ class_option :root, :type => :string, :aliases => "-d", :desc => "Default: Procfile directory"
10
19
 
11
- desc "start", "Start the application"
20
+ desc "start [PROCESS]", "Start the application (or a specific PROCESS)"
12
21
 
13
- method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
14
- method_option :port, :type => :numeric, :aliases => "-p"
15
- method_option :concurrency, :type => :string, :aliases => "-c", :banner => '"alpha=5,bar=3"'
22
+ method_option :color, :type => :boolean, :aliases => "-c", :desc => "Force color to be enabled"
23
+ method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
24
+ method_option :formation, :type => :string, :aliases => "-m", :banner => '"alpha=5,bar=3"'
25
+ method_option :port, :type => :numeric, :aliases => "-p"
26
+
27
+ class << self
28
+ # Hackery. Take the run method away from Thor so that we can redefine it.
29
+ def is_thor_reserved_word?(word, type)
30
+ return false if word == "run"
31
+ super
32
+ end
33
+ end
16
34
 
17
- def start
35
+ def start(process=nil)
18
36
  check_procfile!
37
+ load_environment!
38
+ engine.load_procfile(procfile)
39
+ engine.options[:formation] = "#{process}=1" if process
19
40
  engine.start
20
41
  end
21
42
 
@@ -27,22 +48,14 @@ class Foreman::CLI < Thor
27
48
  method_option :port, :type => :numeric, :aliases => "-p"
28
49
  method_option :user, :type => :string, :aliases => "-u"
29
50
  method_option :template, :type => :string, :aliases => "-t"
30
- method_option :concurrency, :type => :string, :aliases => "-c",
31
- :banner => '"alpha=5,bar=3"'
51
+ method_option :concurrency, :type => :string, :aliases => "-c", :banner => '"alpha=5,bar=3"'
32
52
 
33
53
  def export(format, location=nil)
34
54
  check_procfile!
35
-
36
- formatter = case format
37
- when "inittab" then Foreman::Export::Inittab
38
- when "upstart" then Foreman::Export::Upstart
39
- when "bluepill" then Foreman::Export::Bluepill
40
- when "runit" then Foreman::Export::Runit
41
- else error "Unknown export format: #{format}."
42
- end
43
-
44
- formatter.new(engine).export(location, options)
45
-
55
+ load_environment!
56
+ engine.load_procfile(procfile)
57
+ formatter = Foreman::Export.formatter(format)
58
+ formatter.new(location, engine, options).export
46
59
  rescue Foreman::Export::Exception => ex
47
60
  error ex.message
48
61
  end
@@ -50,41 +63,77 @@ class Foreman::CLI < Thor
50
63
  desc "check", "Validate your application's Procfile"
51
64
 
52
65
  def check
53
- error "no processes defined" unless engine.procfile.entries.length > 0
54
- display "valid procfile detected (#{engine.procfile.process_names.join(', ')})"
66
+ check_procfile!
67
+ engine.load_procfile(procfile)
68
+ error "no processes defined" unless engine.processes.length > 0
69
+ puts "valid procfile detected (#{engine.process_names.join(', ')})"
55
70
  end
56
71
 
57
- private ######################################################################
72
+ desc "run COMMAND [ARGS...]", "Run a command using your application's environment"
58
73
 
59
- def check_procfile!
60
- error("#{procfile} does not exist.") unless File.exist?(procfile)
61
- end
74
+ method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
62
75
 
63
- def engine
64
- @engine ||= Foreman::Engine.new(procfile, options)
76
+ def run(*args)
77
+ load_environment!
78
+ begin
79
+ exec engine.env, args.shelljoin
80
+ rescue Errno::EACCES
81
+ error "not executable: #{args.first}"
82
+ rescue Errno::ENOENT
83
+ error "command not found: #{args.first}"
84
+ end
65
85
  end
66
86
 
67
- def procfile
68
- options[:procfile] || "Procfile"
87
+ desc "version", "Display Foreman gem version"
88
+
89
+ def version
90
+ puts Foreman::VERSION
69
91
  end
70
92
 
71
- def display(message)
72
- puts message
93
+ no_tasks do
94
+ def engine
95
+ @engine ||= begin
96
+ engine_class = Foreman::Engine::CLI
97
+ engine = engine_class.new(options)
98
+ engine
99
+ end
100
+ end
73
101
  end
74
102
 
103
+ private ######################################################################
104
+
75
105
  def error(message)
76
106
  puts "ERROR: #{message}"
77
107
  exit 1
78
108
  end
79
109
 
80
- def procfile_exists?(procfile)
81
- File.exist?(procfile)
110
+ def check_procfile!
111
+ error("#{procfile} does not exist.") unless File.exist?(procfile)
112
+ end
113
+
114
+ def load_environment!
115
+ if options[:env]
116
+ options[:env].split(",").each do |file|
117
+ engine.load_env file
118
+ end
119
+ else
120
+ default_env = File.join(engine.root, ".env")
121
+ engine.load_env default_env if File.exists?(default_env)
122
+ end
123
+ end
124
+
125
+ def procfile
126
+ case
127
+ when options[:procfile] then options[:procfile]
128
+ when options[:root] then File.expand_path(File.join(options[:app_root], "Procfile"))
129
+ else "Procfile"
130
+ end
82
131
  end
83
132
 
84
133
  def options
85
134
  original_options = super
86
135
  return original_options unless File.exists?(".foreman")
87
- defaults = YAML::load_file(".foreman") || {}
136
+ defaults = ::YAML::load_file(".foreman") || {}
88
137
  Thor::CoreExt::HashWithIndifferentAccess.new(defaults.merge(original_options))
89
138
  end
90
139
 
@@ -0,0 +1,105 @@
1
+ require "foreman/engine"
2
+
3
+ class Foreman::Engine::CLI < Foreman::Engine
4
+
5
+ module Color
6
+
7
+ ANSI = {
8
+ :reset => 0,
9
+ :black => 30,
10
+ :red => 31,
11
+ :green => 32,
12
+ :yellow => 33,
13
+ :blue => 34,
14
+ :magenta => 35,
15
+ :cyan => 36,
16
+ :white => 37,
17
+ :bright_black => 30,
18
+ :bright_red => 31,
19
+ :bright_green => 32,
20
+ :bright_yellow => 33,
21
+ :bright_blue => 34,
22
+ :bright_magenta => 35,
23
+ :bright_cyan => 36,
24
+ :bright_white => 37,
25
+ }
26
+
27
+ def self.enable(io, force=false)
28
+ io.extend(self)
29
+ @@color_force = force
30
+ end
31
+
32
+ def color?
33
+ return true if @@color_force
34
+ return true if Foreman.windows?
35
+ return false unless self.respond_to?(:isatty)
36
+ self.isatty && ENV["TERM"]
37
+ end
38
+
39
+ def color(name)
40
+ return "" unless color?
41
+ return "" unless ansi = ANSI[name.to_sym]
42
+ "\e[#{ansi}m"
43
+ end
44
+
45
+ end
46
+
47
+ FOREMAN_COLORS = %w( cyan yellow green magenta red blue intense_cyan intense_yellow
48
+ intense_green intense_magenta intense_red, intense_blue )
49
+
50
+ def startup
51
+ @colors = map_colors
52
+ proctitle "foreman: master"
53
+ require "win32console" if Foreman.windows?
54
+ Color.enable($stdout, options[:color]) unless $stdout.respond_to?(:color?)
55
+ end
56
+
57
+ def output(name, data)
58
+ data.to_s.chomp.split("\n").each do |message|
59
+ output = ""
60
+ output += $stdout.color(@colors[name.split(".").first].to_sym)
61
+ output += "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(name)} | "
62
+ output += $stdout.color(:reset)
63
+ output += message
64
+ $stdout.puts output
65
+ $stdout.flush
66
+ end
67
+ rescue Errno::EPIPE
68
+ terminate_gracefully
69
+ end
70
+
71
+ def shutdown
72
+ end
73
+
74
+ private
75
+
76
+ def name_padding
77
+ @name_padding ||= begin
78
+ index_padding = @names.values.map { |n| formation[n] }.max.to_s.length + 1
79
+ name_padding = @names.values.map { |n| n.length + index_padding }.sort.last
80
+ [ 6, name_padding ].max
81
+ end
82
+ end
83
+
84
+ def pad_process_name(name)
85
+ name.ljust(name_padding, " ")
86
+ end
87
+
88
+ def map_colors
89
+ colors = Hash.new("white")
90
+ @names.values.each_with_index do |name, index|
91
+ colors[name] = FOREMAN_COLORS[index % FOREMAN_COLORS.length]
92
+ end
93
+ colors["system"] = "intense_white"
94
+ colors
95
+ end
96
+
97
+ def proctitle(title)
98
+ $0 = title
99
+ end
100
+
101
+ def termtitle(title)
102
+ printf("\033]0;#{title}\007") unless Foreman.windows?
103
+ end
104
+
105
+ end