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.
- checksums.yaml +5 -5
- data/README.md +27 -14
- data/Rakefile +82 -83
- data/bin/rerun +18 -18
- data/lib/goo.rb +3 -3
- data/lib/rerun.rb +15 -15
- data/lib/rerun/notification.rb +82 -82
- data/lib/rerun/options.rb +146 -145
- data/lib/rerun/runner.rb +374 -376
- data/lib/rerun/system.rb +22 -22
- data/lib/rerun/watcher.rb +134 -131
- data/rerun.gemspec +34 -34
- metadata +6 -7
data/lib/rerun/options.rb
CHANGED
@@ -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
|
-
:
|
21
|
-
:
|
22
|
-
:
|
23
|
-
:
|
24
|
-
:
|
25
|
-
:
|
26
|
-
:
|
27
|
-
:
|
28
|
-
:
|
29
|
-
:
|
30
|
-
:
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
o.separator "
|
51
|
-
o.separator "
|
52
|
-
o.separator "
|
53
|
-
o.separator ""
|
54
|
-
o.separator "
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
o.on("-
|
82
|
-
options[:
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
end
|
111
|
-
|
112
|
-
o.on("
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
end
|
124
|
-
|
125
|
-
o.on_tail("--
|
126
|
-
puts
|
127
|
-
return
|
128
|
-
end
|
129
|
-
|
130
|
-
o.on_tail ""
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
when '
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
@keypress_thread
|
64
|
-
end
|
65
|
-
|
66
|
-
def
|
67
|
-
@
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
@
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
"
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
#
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
@git_head
|
315
|
-
end
|
316
|
-
|
317
|
-
def
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
puts
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
#
|
342
|
-
|
343
|
-
#
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
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
|