rerun 0.13.0 → 0.13.1

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