rerun 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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