rerun 0.13.0 → 0.13.1

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,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