rerun 0.13.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rerun/runner.rb CHANGED
@@ -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.exist?(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