rerun 0.11.0 → 0.12.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.
@@ -1,119 +1,142 @@
1
- require 'optparse'
2
- require 'pathname'
3
- require 'rerun/watcher'
4
-
5
- libdir = "#{File.expand_path(File.dirname(File.dirname(__FILE__)))}"
6
-
7
- $spec = Gem::Specification.load(File.join(libdir, "..", "rerun.gemspec"))
8
-
9
- module Rerun
10
- class Options
11
- DEFAULT_PATTERN = "**/*.{rb,js,coffee,css,scss,sass,erb,html,haml,ru,yml,slim,md,feature}"
12
- DEFAULT_DIRS = ["."]
13
-
14
- DEFAULTS = {
15
- :pattern => DEFAULT_PATTERN,
16
- :signal => "TERM",
17
- :notify => true,
18
- :name => Pathname.getwd.basename.to_s.capitalize,
19
- :ignore => [],
20
- :dir => DEFAULT_DIRS
21
- }
22
-
23
- def self.parse args = ARGV
24
-
25
- default_options = DEFAULTS.dup
26
- options = {
27
- ignore: []
28
- }
29
-
30
- opts = OptionParser.new("", 24, ' ') do |opts|
31
- opts.banner = "Usage: rerun [options] [--] cmd"
32
-
33
- opts.separator ""
34
- opts.separator "Launches an app, and restarts it when the filesystem changes."
35
- opts.separator "See http://github.com/alexch/rerun for more info."
36
- opts.separator "Version: #{$spec.version}"
37
- opts.separator ""
38
- opts.separator "Options:"
39
-
40
- opts.on("-d dir", "--dir dir", "directory to watch, default = \"#{DEFAULT_DIRS}\". Specify multiple paths with ',' or separate '-d dir' option pairs.") do |dir|
41
- elements = dir.split(",")
42
- options[:dir] = (options[:dir] || []) + elements
43
- end
44
-
45
- # todo: rename to "--watch"
46
- opts.on("-p pattern", "--pattern pattern", "file glob to watch, default = \"#{DEFAULTS[:pattern]}\"") do |pattern|
47
- options[:pattern] = pattern
48
- end
49
-
50
- opts.on("-i pattern", "--ignore pattern", "file glob to ignore (can be set many times). To ignore a directory, you must append '/*' e.g. --ignore 'coverage/*'") do |pattern|
51
- options[:ignore] += [pattern]
52
- end
53
-
54
- opts.on("-s signal", "--signal signal", "terminate process using this signal, default = \"#{DEFAULTS[:signal]}\"") do |signal|
55
- options[:signal] = signal
56
- end
57
-
58
- opts.on("-r", "--restart", "expect process to restart itself (uses the HUP signal unless overridden using --signal)") do |signal|
59
- options[:restart] = true
60
- default_options[:signal] = "HUP"
61
- end
62
-
63
- opts.on("-c", "--clear", "clear screen before each run") do
64
- options[:clear] = true
65
- end
66
-
67
- opts.on("-x", "--exit", "expect the program to exit. With this option, rerun checks the return value; without it, rerun checks that the process is running.") do |dir|
68
- options[:exit] = true
69
- end
70
-
71
- opts.on("-b", "--background", "disable on-the-fly commands, allowing the process to be backgrounded") do
72
- options[:background] = true
73
- end
74
-
75
- opts.on("-n name", "--name name", "name of app used in logs and notifications, default = \"#{DEFAULTS[:name]}\"") do |name|
76
- options[:name] = name
77
- end
78
-
79
- opts.on("--no-growl", "don't use growl [OBSOLETE]") do
80
- options[:growl] = false
81
- $stderr.puts "--no-growl is obsolete; use --no-notify instead"
82
- return
83
- end
84
-
85
- opts.on("--[no-]notify [notifier]", "send messages through growl (requires growlnotify) or osx (requires terminal-notifier gem)") do |notifier|
86
- notifier = true if notifier.nil?
87
- options[:notify] = notifier
88
- end
89
-
90
- opts.on_tail("-h", "--help", "--usage", "show this message") do
91
- puts opts
92
- return
93
- end
94
-
95
- opts.on_tail("--version", "show version") do
96
- puts $spec.version
97
- return
98
- end
99
-
100
- opts.on_tail ""
101
- opts.on_tail "On top of --pattern and --ignore, we ignore any changes to files and dirs starting with a dot."
102
-
103
- end
104
-
105
- if args.empty?
106
- puts opts
107
- nil
108
- else
109
- opts.parse! args
110
- default_options[:cmd] = args.join(" ")
111
-
112
- options = default_options.merge(options)
113
-
114
- options
115
- end
116
- end
117
-
118
- end
119
- end
1
+ require 'optparse'
2
+ require 'pathname'
3
+ require 'rerun/watcher'
4
+ require 'rerun/system'
5
+
6
+ libdir = "#{File.expand_path(File.dirname(File.dirname(__FILE__)))}"
7
+
8
+ $spec = Gem::Specification.load(File.join(libdir, "..", "rerun.gemspec"))
9
+
10
+ module Rerun
11
+ class Options
12
+
13
+ extend Rerun::System
14
+
15
+ # If you change the default pattern, please update the README.md file -- the list appears twice therein, which at the time of this comment are lines 17 and 119
16
+ DEFAULT_PATTERN = "**/*.{rb,js,coffee,css,scss,sass,erb,html,haml,ru,yml,slim,md,feature,c,h}"
17
+ DEFAULT_DIRS = ["."]
18
+
19
+ DEFAULTS = {
20
+ :pattern => DEFAULT_PATTERN,
21
+ :signal => (windows? ? "TERM,KILL" : "TERM,INT,KILL"),
22
+ :wait => 2,
23
+ :notify => true,
24
+ :quiet => false,
25
+ :verbose => false,
26
+ :name => Pathname.getwd.basename.to_s.capitalize,
27
+ :ignore => [],
28
+ :dir => DEFAULT_DIRS,
29
+ :force_polling => false,
30
+ }
31
+
32
+ def self.parse args = ARGV
33
+
34
+ default_options = DEFAULTS.dup
35
+ options = {
36
+ ignore: []
37
+ }
38
+
39
+ opts = OptionParser.new("", 24, ' ') do |opts|
40
+ opts.banner = "Usage: rerun [options] [--] cmd"
41
+
42
+ opts.separator ""
43
+ opts.separator "Launches an app, and restarts it when the filesystem changes."
44
+ opts.separator "See http://github.com/alexch/rerun for more info."
45
+ opts.separator "Version: #{$spec.version}"
46
+ opts.separator ""
47
+ opts.separator "Options:"
48
+
49
+ opts.on("-d dir", "--dir dir", "directory to watch, default = \"#{DEFAULT_DIRS}\". Specify multiple paths with ',' or separate '-d dir' option pairs.") do |dir|
50
+ elements = dir.split(",")
51
+ options[:dir] = (options[:dir] || []) + elements
52
+ end
53
+
54
+ # todo: rename to "--watch"
55
+ opts.on("-p pattern", "--pattern pattern", "file glob to watch, default = \"#{DEFAULTS[:pattern]}\"") do |pattern|
56
+ options[:pattern] = pattern
57
+ end
58
+
59
+ opts.on("-i pattern", "--ignore pattern", "file glob to ignore (can be set many times). To ignore a directory, you must append '/*' e.g. --ignore 'coverage/*'") do |pattern|
60
+ options[:ignore] += [pattern]
61
+ end
62
+
63
+ opts.on("-s signal", "--signal signal", "terminate process using this signal. To try several signals in series, use a comma-delimited list. Default: \"#{DEFAULTS[:signal]}\"") do |signal|
64
+ options[:signal] = signal
65
+ end
66
+
67
+ opts.on("-w sec", "--wait sec", "after asking the process to terminate, wait this long (in seconds) before either aborting, or trying the next signal in series. Default: #{DEFAULTS[:wait]} sec")
68
+
69
+ opts.on("-r", "--restart", "expect process to restart itself, so just send a signal and continue watching. Uses the HUP signal unless overridden using --signal") do |signal|
70
+ options[:restart] = true
71
+ default_options[:signal] = "HUP"
72
+ end
73
+
74
+ opts.on("-x", "--exit", "expect the program to exit. With this option, rerun checks the return value; without it, rerun checks that the process is running.") do |dir|
75
+ options[:exit] = true
76
+ end
77
+
78
+ opts.on("-c", "--clear", "clear screen before each run") do
79
+ options[:clear] = true
80
+ end
81
+
82
+ opts.on("-b", "--background", "disable on-the-fly commands, allowing the process to be backgrounded") do
83
+ options[:background] = true
84
+ end
85
+
86
+ opts.on("-n name", "--name name", "name of app used in logs and notifications, default = \"#{DEFAULTS[:name]}\"") do |name|
87
+ options[:name] = name
88
+ end
89
+
90
+ opts.on("--force-polling", "use polling instead of a native filesystem scan (useful for Vagrant)") do
91
+ options[:force_polling] = true
92
+ end
93
+
94
+ opts.on("--no-growl", "don't use growl [OBSOLETE]") do
95
+ options[:growl] = false
96
+ $stderr.puts "--no-growl is obsolete; use --no-notify instead"
97
+ return
98
+ end
99
+
100
+ opts.on("--[no-]notify [notifier]", "send messages through a desktop notification application. Supports growl (requires growlnotify), osx (requires terminal-notifier gem), and notify-send on GNU/Linux (notify-send must be installed)") do |notifier|
101
+ notifier = true if notifier.nil?
102
+ options[:notify] = notifier
103
+ end
104
+
105
+ opts.on("-q", "--quiet", "don't output any logs") do
106
+ options[:quiet] = true
107
+ end
108
+
109
+ opts.on("--verbose", "log extra stuff like PIDs (unless you also specified `--quiet`") do
110
+ options[:verbose] = true
111
+ end
112
+
113
+ opts.on_tail("-h", "--help", "--usage", "show this message") do
114
+ puts opts
115
+ return
116
+ end
117
+
118
+ opts.on_tail("--version", "show version") do
119
+ puts $spec.version
120
+ return
121
+ end
122
+
123
+ opts.on_tail ""
124
+ opts.on_tail "On top of --pattern and --ignore, we ignore any changes to files and dirs starting with a dot."
125
+
126
+ end
127
+
128
+ if args.empty?
129
+ puts opts
130
+ nil
131
+ else
132
+ opts.parse! args
133
+ default_options[:cmd] = args.join(" ")
134
+
135
+ options = default_options.merge(options)
136
+
137
+ options
138
+ end
139
+ end
140
+
141
+ end
142
+ end
@@ -1,334 +1,375 @@
1
- require 'timeout'
2
- require 'io/wait'
3
-
4
- module Rerun
5
- class Runner
6
-
7
- def self.keep_running(cmd, options)
8
- runner = new(cmd, options)
9
- runner.start
10
- runner.join
11
- # apparently runner doesn't keep running anymore (as of Listen 2) so we have to sleep forever :-(
12
- sleep 10000 while true # :-(
13
- end
14
-
15
- include System
16
-
17
- def initialize(run_command, options = {})
18
- @run_command, @options = run_command, options
19
- @run_command = "ruby #{@run_command}" if @run_command.split(' ').first =~ /\.rb$/
20
- end
21
-
22
- def start_keypress_thread
23
- return if @options[:background]
24
-
25
- @keypress_thread = Thread.new do
26
- while true
27
- if c = key_pressed
28
- case c.downcase
29
- when 'c'
30
- say "Clearing screen"
31
- clear_screen
32
- when 'r'
33
- say "Restarting"
34
- restart
35
- when 'p'
36
- toggle_pause if watcher_running?
37
- when 'x', 'q'
38
- die
39
- break # the break will stop this thread, in case the 'die' doesn't
40
- else
41
- puts "\n#{c.inspect} pressed inside rerun"
42
- puts [["c", "clear screen"],
43
- ["r", "restart"],
44
- ["p", "toggle pause"],
45
- ["x or q", "stop and exit"]
46
- ].map{|key, description| " #{key} -- #{description}"}.join("\n")
47
- puts
48
- end
49
- end
50
- sleep 1 # todo: use select instead of polling somehow?
51
- end
52
- end
53
- @keypress_thread.run
54
- end
55
-
56
- def stop_keypress_thread
57
- @keypress_thread.kill if @keypress_thread
58
- @keypress_thread = nil
59
- end
60
-
61
- def restart
62
- @restarting = true
63
- if @options[:restart]
64
- restart_with_signal(@options[:signal])
65
- else
66
- stop
67
- start
68
- end
69
- @restarting = false
70
- end
71
-
72
- def watcher_running?
73
- @watcher && @watcher.running?
74
- end
75
-
76
- def toggle_pause
77
- unless @pausing
78
- say "Pausing. Press 'p' again to resume."
79
- @watcher.pause
80
- @pausing = true
81
- else
82
- say "Resuming."
83
- @watcher.unpause
84
- @pausing = false
85
- end
86
- end
87
-
88
- def unpause
89
- @watcher.unpause
90
- end
91
-
92
- def dir
93
- @options[:dir]
94
- end
95
-
96
- def dirs
97
- @options[:dir] || "."
98
- end
99
-
100
- def pattern
101
- @options[:pattern]
102
- end
103
-
104
- def ignore
105
- @options[:ignore] || []
106
- end
107
-
108
- def clear?
109
- @options[:clear]
110
- end
111
-
112
- def exit?
113
- @options[:exit]
114
- end
115
-
116
- def app_name
117
- @options[:name]
118
- end
119
-
120
- def restart_with_signal(restart_signal)
121
- if @pid && (@pid != 0)
122
- notify "restarting", "We will be with you shortly."
123
- signal(restart_signal)
124
- end
125
- end
126
-
127
- def start
128
- if windows?
129
- raise "Sorry, Rerun does not work on Windows."
130
- end
131
-
132
- if @already_running
133
- taglines = [
134
- "Here we go again!",
135
- "Keep on trucking.",
136
- "Once more unto the breach, dear friends, once more!",
137
- "The road goes ever on and on, down from the door where it began.",
138
- ]
139
- notify "restarted", taglines[rand(taglines.size)]
140
- else
141
- taglines = [
142
- "To infinity... and beyond!",
143
- "Charge!",
144
- ]
145
- notify "launched", taglines[rand(taglines.size)]
146
- @already_running = true
147
- end
148
-
149
- clear_screen if clear?
150
- start_keypress_thread unless @keypress_thread
151
-
152
- @pid = Kernel.fork do
153
- begin
154
- exec(@run_command)
155
- rescue => e
156
- puts "#{e.class}: #{e.message}"
157
- exit
158
- end
159
- end
160
- status_thread = Process.detach(@pid) # so if the child exits, it dies
161
-
162
- Signal.trap("INT") do # INT = control-C -- allows user to stop the top-level rerun process
163
- die
164
- end
165
-
166
- Signal.trap("TERM") do # TERM is the polite way of terminating a process
167
- die
168
- end
169
-
170
- begin
171
- sleep 2
172
- rescue Interrupt => e
173
- # in case someone hits control-C immediately ("oops!")
174
- die
175
- end
176
-
177
- if exit?
178
- status = status_thread.value
179
- if status.success?
180
- notify "succeeded", ""
181
- else
182
- notify "failed", "Exit status #{status.exitstatus}"
183
- end
184
- else
185
- if !running?
186
- notify "Launch Failed", "See console for error output"
187
- @already_running = false
188
- end
189
- end
190
-
191
- unless @watcher
192
-
193
- watcher = Watcher.new(:directory => dirs, :pattern => pattern, :ignore => ignore) do |changes|
194
-
195
- message = change_message(changes)
196
-
197
- say "Change detected: #{message}"
198
- restart unless @restarting
199
- end
200
- watcher.start
201
- @watcher = watcher
202
- say "Watching #{dir.join(', ')} for #{pattern}" +
203
- (ignore.empty? ? "" : " (ignoring #{ignore.join(',')})")
204
- end
205
- end
206
-
207
- def change_message(changes)
208
- message = [:modified, :added, :removed].map do |change|
209
- count = changes[change] ? changes[change].size : 0
210
- if count > 0
211
- "#{count} #{change}"
212
- end
213
- end.compact.join(", ")
214
-
215
- changed_files = changes.values.flatten
216
- if changed_files.count > 0
217
- message += ": "
218
- message += changes.values.flatten[0..3].map { |path| path.split('/').last }.join(', ')
219
- if changed_files.count > 3
220
- message += ", ..."
221
- end
222
- end
223
- message
224
- end
225
-
226
- def die
227
- #stop_keypress_thread # don't do this since we're probably *in* the keypress thread
228
- stop # stop the child process if it exists
229
- exit 0 # todo: status code param
230
- end
231
-
232
- def join
233
- @watcher.join
234
- end
235
-
236
- def running?
237
- signal(0)
238
- end
239
-
240
- def signal(signal)
241
- say "Sending signal #{signal} to #{@pid}" unless signal == 0
242
- Process.kill(signal, @pid)
243
- true
244
- rescue
245
- false
246
- end
247
-
248
- # todo: test escalation
249
- def stop
250
- default_signal = @options[:signal] || "TERM"
251
- if @pid && (@pid != 0)
252
- notify "stopping", "All good things must come to an end." unless @restarting
253
- begin
254
- timeout(5) do # todo: escalation timeout setting
255
- # start with a polite SIGTERM
256
- signal(default_signal) && Process.wait(@pid)
257
- end
258
- rescue Timeout::Error
259
- begin
260
- timeout(5) do
261
- # escalate to SIGINT aka control-C since some foolish process may be ignoring SIGTERM
262
- signal("INT") && Process.wait(@pid)
263
- end
264
- rescue Timeout::Error
265
- # escalate to SIGKILL aka "kill -9" which cannot be ignored
266
- signal("KILL") && Process.wait(@pid)
267
- end
268
- end
269
- end
270
- rescue => e
271
- false
272
- end
273
-
274
- def git_head_changed?
275
- old_git_head = @git_head
276
- read_git_head
277
- @git_head and old_git_head and @git_head != old_git_head
278
- end
279
-
280
- def read_git_head
281
- git_head_file = File.join(dir, '.git', 'HEAD')
282
- @git_head = File.exists?(git_head_file) && File.read(git_head_file)
283
- end
284
-
285
- def notify(title, body, background = true)
286
- Notification.new(title, body, @options).send(background) if @options[:notify]
287
- puts
288
- say "#{app_name} #{title}"
289
- end
290
-
291
- def say msg
292
- puts "#{Time.now.strftime("%T")} [rerun] #{msg}"
293
- end
294
-
295
- # non-blocking stdin reader.
296
- # returns a 1-char string if a key was pressed; otherwise nil
297
- #
298
- def key_pressed
299
- begin
300
- # this "raw input" nonsense is because unix likes waiting for linefeeds before sending stdin
301
-
302
- # 'raw' means turn raw input on
303
-
304
- # restore proper output newline handling -- see stty.rb and "man stty" and /usr/include/sys/termios.h
305
- # looks like "raw" flips off the OPOST bit 0x00000001 /* enable following output processing */
306
- # which disables #define ONLCR 0x00000002 /* map NL to CR-NL (ala CRMOD) */
307
- # so this sets it back on again since all we care about is raw input, not raw output
308
- system("stty raw opost")
309
-
310
- c = nil
311
- if $stdin.ready?
312
- c = $stdin.getc
313
- end
314
- c.chr if c
315
- ensure
316
- system "stty -raw" # turn raw input off
317
- end
318
-
319
- # note: according to 'man tty' the proper way restore the settings is
320
- # tty_state=`stty -g`
321
- # ensure
322
- # system 'stty "#{tty_state}'
323
- # end
324
- # but this way seems fine and less confusing
325
-
326
- end
327
-
328
- def clear_screen
329
- # see http://ascii-table.com/ansi-escape-sequences-vt-100.php
330
- $stdout.print "\033[H\033[2J"
331
- end
332
-
333
- end
334
- end
1
+ require 'timeout'
2
+ require 'io/wait'
3
+
4
+ module Rerun
5
+ class Runner
6
+
7
+ def self.keep_running(cmd, options)
8
+ runner = new(cmd, options)
9
+ runner.start
10
+ runner.join
11
+ # apparently runner doesn't keep running anymore (as of Listen 2) so we have to sleep forever :-(
12
+ sleep 10000 while true # :-(
13
+ end
14
+
15
+ include System
16
+ include ::Timeout
17
+
18
+ def initialize(run_command, options = {})
19
+ @run_command, @options = run_command, options
20
+ @run_command = "ruby #{@run_command}" if @run_command.split(' ').first =~ /\.rb$/
21
+ end
22
+
23
+ def start_keypress_thread
24
+ return if @options[:background]
25
+
26
+ @keypress_thread = Thread.new do
27
+ while true
28
+ if c = key_pressed
29
+ case c.downcase
30
+ when 'c'
31
+ say "Clearing screen"
32
+ clear_screen
33
+ when 'r'
34
+ say "Restarting"
35
+ restart
36
+ when 'f'
37
+ say "Stopping and starting"
38
+ restart(false)
39
+ when 'p'
40
+ toggle_pause if watcher_running?
41
+ when 'x', 'q'
42
+ die
43
+ break # the break will stop this thread, in case the 'die' doesn't
44
+ else
45
+ puts "\n#{c.inspect} pressed inside rerun"
46
+ puts [["c", "clear screen"],
47
+ ["r", "restart"],
48
+ ["f", "forced restart (stop and start)"],
49
+ ["p", "toggle pause"],
50
+ ["x or q", "stop and exit"]
51
+ ].map {|key, description| " #{key} -- #{description}"}.join("\n")
52
+ puts
53
+ end
54
+ end
55
+ sleep 1 # todo: use select instead of polling somehow?
56
+ end
57
+ end
58
+ @keypress_thread.run
59
+ end
60
+
61
+ def stop_keypress_thread
62
+ @keypress_thread.kill if @keypress_thread
63
+ @keypress_thread = nil
64
+ end
65
+
66
+ def restart(with_signal = true)
67
+ @restarting = true
68
+ if @options[:restart] && with_signal
69
+ restart_with_signal(@options[:signal])
70
+ else
71
+ stop
72
+ start
73
+ end
74
+ @restarting = false
75
+ end
76
+
77
+ def watcher_running?
78
+ @watcher && @watcher.running?
79
+ end
80
+
81
+ def toggle_pause
82
+ unless @pausing
83
+ say "Pausing. Press 'p' again to resume."
84
+ @watcher.pause
85
+ @pausing = true
86
+ else
87
+ say "Resuming."
88
+ @watcher.unpause
89
+ @pausing = false
90
+ end
91
+ end
92
+
93
+ def unpause
94
+ @watcher.unpause
95
+ end
96
+
97
+ def dir
98
+ @options[:dir]
99
+ end
100
+
101
+ def dirs
102
+ @options[:dir] || "."
103
+ end
104
+
105
+ def pattern
106
+ @options[:pattern]
107
+ end
108
+
109
+ def ignore
110
+ @options[:ignore] || []
111
+ end
112
+
113
+ def clear?
114
+ @options[:clear]
115
+ end
116
+
117
+ def quiet?
118
+ @options[:quiet]
119
+ end
120
+
121
+ def verbose?
122
+ @options[:verbose]
123
+ end
124
+
125
+ def exit?
126
+ @options[:exit]
127
+ end
128
+
129
+ def app_name
130
+ @options[:name]
131
+ end
132
+
133
+ def restart_with_signal(restart_signal)
134
+ if @pid && (@pid != 0)
135
+ notify "restarting", "We will be with you shortly."
136
+ send_signal(restart_signal)
137
+ end
138
+ end
139
+
140
+ def force_polling
141
+ @options[:force_polling]
142
+ end
143
+
144
+ def start
145
+ if @already_running
146
+ taglines = [
147
+ "Here we go again!",
148
+ "Keep on trucking.",
149
+ "Once more unto the breach, dear friends, once more!",
150
+ "The road goes ever on and on, down from the door where it began.",
151
+ ]
152
+ notify "restarted", taglines[rand(taglines.size)]
153
+ else
154
+ taglines = [
155
+ "To infinity... and beyond!",
156
+ "Charge!",
157
+ ]
158
+ notify "launched", taglines[rand(taglines.size)]
159
+ @already_running = true
160
+ end
161
+
162
+ clear_screen if clear?
163
+ start_keypress_thread unless @keypress_thread
164
+
165
+ begin
166
+ @pid = run @run_command
167
+ say "Rerun (#{$PID}) running #{app_name} (#{@pid})"
168
+ rescue => e
169
+ puts "#{e.class}: #{e.message}"
170
+ exit
171
+ end
172
+
173
+ status_thread = Process.detach(@pid) # so if the child exits, it dies
174
+
175
+ Signal.trap("INT") do # INT = control-C -- allows user to stop the top-level rerun process
176
+ die
177
+ end
178
+
179
+ Signal.trap("TERM") do # TERM is the polite way of terminating a process
180
+ die
181
+ end
182
+
183
+ begin
184
+ sleep 2
185
+ rescue Interrupt => e
186
+ # in case someone hits control-C immediately ("oops!")
187
+ die
188
+ end
189
+
190
+ if exit?
191
+ status = status_thread.value
192
+ if status.success?
193
+ notify "succeeded", ""
194
+ else
195
+ notify "failed", "Exit status #{status.exitstatus}"
196
+ end
197
+ else
198
+ if !running?
199
+ notify "Launch Failed", "See console for error output"
200
+ @already_running = false
201
+ end
202
+ end
203
+
204
+ unless @watcher
205
+
206
+ watcher = Watcher.new(:directory => dirs, :pattern => pattern, :ignore => ignore, :force_polling => force_polling) do |changes|
207
+
208
+ message = change_message(changes)
209
+
210
+ say "Change detected: #{message}"
211
+ restart unless @restarting
212
+ end
213
+ watcher.start
214
+ @watcher = watcher
215
+ say "Watching #{dir.join(', ')} for #{pattern}" +
216
+ (ignore.empty? ? "" : " (ignoring #{ignore.join(',')})") +
217
+ (watcher.adapter.nil? ? "" : " with #{watcher.adapter_name} adapter")
218
+ end
219
+ end
220
+
221
+ def run command
222
+ Kernel.spawn command
223
+ end
224
+
225
+ def change_message(changes)
226
+ message = [:modified, :added, :removed].map do |change|
227
+ count = changes[change] ? changes[change].size : 0
228
+ if count > 0
229
+ "#{count} #{change}"
230
+ end
231
+ end.compact.join(", ")
232
+
233
+ changed_files = changes.values.flatten
234
+ if changed_files.count > 0
235
+ message += ": "
236
+ message += changes.values.flatten[0..3].map {|path| path.split('/').last}.join(', ')
237
+ if changed_files.count > 3
238
+ message += ", ..."
239
+ end
240
+ end
241
+ message
242
+ end
243
+
244
+ def die
245
+ #stop_keypress_thread # don't do this since we're probably *in* the keypress thread
246
+ stop # stop the child process if it exists
247
+ exit 0 # todo: status code param
248
+ end
249
+
250
+ def join
251
+ @watcher.join
252
+ end
253
+
254
+ def running?
255
+ send_signal(0)
256
+ end
257
+
258
+ # Send the signal to process @pid and wait for it to die.
259
+ # @returns true if the process dies
260
+ # @returns false if either sending the signal fails or the process fails to die
261
+ def signal_and_wait(signal)
262
+
263
+ signal_sent = if windows?
264
+ force_kill = (signal == 'KILL')
265
+ system("taskkill /T #{'/F' if force_kill} /PID #{@pid}")
266
+ else
267
+ send_signal(signal)
268
+ end
269
+
270
+ if signal_sent
271
+ # the signal was successfully sent, so wait for the process to die
272
+ begin
273
+ timeout(@options[:wait]) do
274
+ Process.wait(@pid)
275
+ end
276
+ process_status = $?
277
+ say "Process ended: #{process_status}" if verbose?
278
+ true
279
+ rescue Timeout::Error
280
+ false
281
+ end
282
+ else
283
+ false
284
+ end
285
+ end
286
+
287
+ # Send the signal to process @pid.
288
+ # @returns true if the signal is sent
289
+ # @returns false if sending the signal fails
290
+ # If sending the signal fails, the exception will be swallowed
291
+ # (and logged if verbose is true) and this method will return false.
292
+ #
293
+ def send_signal(signal)
294
+ say "Sending signal #{signal} to #{@pid}" unless signal == 0 if verbose?
295
+ Process.kill(signal, @pid)
296
+ true
297
+ rescue => e
298
+ say "Signal #{signal} failed: #{e.class}: #{e.message}" if verbose?
299
+ false
300
+ end
301
+
302
+ # todo: test escalation
303
+ def stop
304
+ if @pid && (@pid != 0)
305
+ notify "stopping", "All good things must come to an end." unless @restarting
306
+ @options[:signal].split(',').each do |signal|
307
+ success = signal_and_wait(signal)
308
+ return true if success
309
+ end
310
+ end
311
+ rescue => e
312
+ false
313
+ end
314
+
315
+ def git_head_changed?
316
+ old_git_head = @git_head
317
+ read_git_head
318
+ @git_head and old_git_head and @git_head != old_git_head
319
+ end
320
+
321
+ def read_git_head
322
+ git_head_file = File.join(dir, '.git', 'HEAD')
323
+ @git_head = File.exists?(git_head_file) && File.read(git_head_file)
324
+ end
325
+
326
+ def notify(title, body, background = true)
327
+ Notification.new(title, body, @options).send(background) if @options[:notify]
328
+ puts
329
+ say "#{app_name} #{title}"
330
+ end
331
+
332
+ def say msg
333
+ puts "#{Time.now.strftime("%T")} [rerun] #{msg}" unless quiet?
334
+ end
335
+
336
+ # non-blocking stdin reader.
337
+ # returns a 1-char string if a key was pressed; otherwise nil
338
+ #
339
+ def key_pressed
340
+ begin
341
+ # this "raw input" nonsense is because unix likes waiting for linefeeds before sending stdin
342
+
343
+ # 'raw' means turn raw input on
344
+
345
+ # restore proper output newline handling -- see stty.rb and "man stty" and /usr/include/sys/termios.h
346
+ # looks like "raw" flips off the OPOST bit 0x00000001 /* enable following output processing */
347
+ # which disables #define ONLCR 0x00000002 /* map NL to CR-NL (ala CRMOD) */
348
+ # so this sets it back on again since all we care about is raw input, not raw output
349
+ system("stty raw opost")
350
+
351
+ c = nil
352
+ if $stdin.ready?
353
+ c = $stdin.getc
354
+ end
355
+ c.chr if c
356
+ ensure
357
+ system "stty -raw" # turn raw input off
358
+ end
359
+
360
+ # note: according to 'man tty' the proper way restore the settings is
361
+ # tty_state=`stty -g`
362
+ # ensure
363
+ # system 'stty "#{tty_state}'
364
+ # end
365
+ # but this way seems fine and less confusing
366
+
367
+ end
368
+
369
+ def clear_screen
370
+ # see http://ascii-table.com/ansi-escape-sequences-vt-100.php
371
+ $stdout.print "\033[H\033[2J"
372
+ end
373
+
374
+ end
375
+ end