poll-rerun 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,124 @@
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
+ :force_polling => false,
22
+ }
23
+
24
+ def self.parse args = ARGV
25
+
26
+ default_options = DEFAULTS.dup
27
+ options = {
28
+ ignore: []
29
+ }
30
+
31
+ opts = OptionParser.new("", 24, ' ') do |opts|
32
+ opts.banner = "Usage: rerun [options] [--] cmd"
33
+
34
+ opts.separator ""
35
+ opts.separator "Launches an app, and restarts it when the filesystem changes."
36
+ opts.separator "See http://github.com/alexch/rerun for more info."
37
+ opts.separator "Version: #{$spec.version}"
38
+ opts.separator ""
39
+ opts.separator "Options:"
40
+
41
+ opts.on("-d dir", "--dir dir", "directory to watch, default = \"#{DEFAULT_DIRS}\". Specify multiple paths with ',' or separate '-d dir' option pairs.") do |dir|
42
+ elements = dir.split(",")
43
+ options[:dir] = (options[:dir] || []) + elements
44
+ end
45
+
46
+ # todo: rename to "--watch"
47
+ opts.on("-p pattern", "--pattern pattern", "file glob to watch, default = \"#{DEFAULTS[:pattern]}\"") do |pattern|
48
+ options[:pattern] = pattern
49
+ end
50
+
51
+ 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|
52
+ options[:ignore] += [pattern]
53
+ end
54
+
55
+ opts.on("-s signal", "--signal signal", "terminate process using this signal, default = \"#{DEFAULTS[:signal]}\"") do |signal|
56
+ options[:signal] = signal
57
+ end
58
+
59
+ opts.on("-r", "--restart", "expect process to restart itself (uses the HUP signal unless overridden using --signal)") do |signal|
60
+ options[:restart] = true
61
+ default_options[:signal] = "HUP"
62
+ end
63
+
64
+ opts.on("-c", "--clear", "clear screen before each run") do
65
+ options[:clear] = true
66
+ end
67
+
68
+ 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|
69
+ options[:exit] = true
70
+ end
71
+
72
+ opts.on("-b", "--background", "disable on-the-fly commands, allowing the process to be backgrounded") do
73
+ options[:background] = true
74
+ end
75
+
76
+ opts.on("-n name", "--name name", "name of app used in logs and notifications, default = \"#{DEFAULTS[:name]}\"") do |name|
77
+ options[:name] = name
78
+ end
79
+
80
+ opts.on("--force-polling", "use polling instead of a native filesystem scan (useful for Vagrant)") do
81
+ options[:force_polling] = true
82
+ end
83
+
84
+ opts.on("--no-growl", "don't use growl [OBSOLETE]") do
85
+ options[:growl] = false
86
+ $stderr.puts "--no-growl is obsolete; use --no-notify instead"
87
+ return
88
+ end
89
+
90
+ opts.on("--[no-]notify [notifier]", "send messages through growl (requires growlnotify) or osx (requires terminal-notifier gem)") do |notifier|
91
+ notifier = true if notifier.nil?
92
+ options[:notify] = notifier
93
+ end
94
+
95
+ opts.on_tail("-h", "--help", "--usage", "show this message") do
96
+ puts opts
97
+ return
98
+ end
99
+
100
+ opts.on_tail("--version", "show version") do
101
+ puts $spec.version
102
+ return
103
+ end
104
+
105
+ opts.on_tail ""
106
+ opts.on_tail "On top of --pattern and --ignore, we ignore any changes to files and dirs starting with a dot."
107
+
108
+ end
109
+
110
+ if args.empty?
111
+ puts opts
112
+ nil
113
+ else
114
+ opts.parse! args
115
+ default_options[:cmd] = args.join(" ")
116
+
117
+ options = default_options.merge(options)
118
+
119
+ options
120
+ end
121
+ end
122
+
123
+ end
124
+ end
@@ -0,0 +1,339 @@
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 'p'
37
+ toggle_pause if watcher_running?
38
+ when 'x', 'q'
39
+ die
40
+ break # the break will stop this thread, in case the 'die' doesn't
41
+ else
42
+ puts "\n#{c.inspect} pressed inside rerun"
43
+ puts [["c", "clear screen"],
44
+ ["r", "restart"],
45
+ ["p", "toggle pause"],
46
+ ["x or q", "stop and exit"]
47
+ ].map { |key, description| " #{key} -- #{description}" }.join("\n")
48
+ puts
49
+ end
50
+ end
51
+ sleep 1 # todo: use select instead of polling somehow?
52
+ end
53
+ end
54
+ @keypress_thread.run
55
+ end
56
+
57
+ def stop_keypress_thread
58
+ @keypress_thread.kill if @keypress_thread
59
+ @keypress_thread = nil
60
+ end
61
+
62
+ def restart
63
+ @restarting = true
64
+ if @options[:restart]
65
+ restart_with_signal(@options[:signal])
66
+ else
67
+ stop
68
+ start
69
+ end
70
+ @restarting = false
71
+ end
72
+
73
+ def watcher_running?
74
+ @watcher && @watcher.running?
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 exit?
114
+ @options[:exit]
115
+ end
116
+
117
+ def app_name
118
+ @options[:name]
119
+ end
120
+
121
+ def restart_with_signal(restart_signal)
122
+ if @pid && (@pid != 0)
123
+ notify "restarting", "We will be with you shortly."
124
+ signal(restart_signal)
125
+ end
126
+ end
127
+
128
+ def force_polling
129
+ @options[:force_polling]
130
+ end
131
+
132
+ def start
133
+ if @already_running
134
+ taglines = [
135
+ "Here we go again!",
136
+ "Keep on trucking.",
137
+ "Once more unto the breach, dear friends, once more!",
138
+ "The road goes ever on and on, down from the door where it began.",
139
+ ]
140
+ notify "restarted", taglines[rand(taglines.size)]
141
+ else
142
+ taglines = [
143
+ "To infinity... and beyond!",
144
+ "Charge!",
145
+ ]
146
+ notify "launched", taglines[rand(taglines.size)]
147
+ @already_running = true
148
+ end
149
+
150
+ clear_screen if clear?
151
+ start_keypress_thread unless @keypress_thread
152
+
153
+ begin
154
+ @pid = run @run_command
155
+ rescue => e
156
+ puts "#{e.class}: #{e.message}"
157
+ exit
158
+ end
159
+
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, :force_polling => force_polling) 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
+ (watcher.adapter.nil? ? "" : " with #{watcher.adapter_name} adapter")
205
+ end
206
+ end
207
+
208
+ def run command
209
+ Kernel.spawn command
210
+ end
211
+
212
+ def change_message(changes)
213
+ message = [:modified, :added, :removed].map do |change|
214
+ count = changes[change] ? changes[change].size : 0
215
+ if count > 0
216
+ "#{count} #{change}"
217
+ end
218
+ end.compact.join(", ")
219
+
220
+ changed_files = changes.values.flatten
221
+ if changed_files.count > 0
222
+ message += ": "
223
+ message += changes.values.flatten[0..3].map { |path| path.split('/').last }.join(', ')
224
+ if changed_files.count > 3
225
+ message += ", ..."
226
+ end
227
+ end
228
+ message
229
+ end
230
+
231
+ def die
232
+ #stop_keypress_thread # don't do this since we're probably *in* the keypress thread
233
+ stop # stop the child process if it exists
234
+ exit 0 # todo: status code param
235
+ end
236
+
237
+ def join
238
+ @watcher.join
239
+ end
240
+
241
+ def running?
242
+ signal(0)
243
+ end
244
+
245
+ def signal(signal)
246
+ say "Sending signal #{signal} to #{@pid}" unless signal == 0
247
+ Process.kill(signal, @pid)
248
+ true
249
+ rescue
250
+ false
251
+ end
252
+
253
+ # todo: test escalation
254
+ def stop
255
+ default_signal = @options[:signal] || "TERM"
256
+ if @pid && (@pid != 0)
257
+ notify "stopping", "All good things must come to an end." unless @restarting
258
+ begin
259
+ timeout(5) do # todo: escalation timeout setting
260
+ # start with a polite SIGTERM
261
+ signal(default_signal) && Process.wait(@pid)
262
+ end
263
+ rescue Timeout::Error
264
+ begin
265
+ timeout(5) do
266
+ # escalate to SIGINT aka control-C since some foolish process may be ignoring SIGTERM
267
+ signal("INT") && Process.wait(@pid)
268
+ end
269
+ rescue Timeout::Error
270
+ # escalate to SIGKILL aka "kill -9" which cannot be ignored
271
+ signal("KILL") && Process.wait(@pid)
272
+ end
273
+ end
274
+ end
275
+ rescue => e
276
+ false
277
+ end
278
+
279
+ def git_head_changed?
280
+ old_git_head = @git_head
281
+ read_git_head
282
+ @git_head and old_git_head and @git_head != old_git_head
283
+ end
284
+
285
+ def read_git_head
286
+ git_head_file = File.join(dir, '.git', 'HEAD')
287
+ @git_head = File.exists?(git_head_file) && File.read(git_head_file)
288
+ end
289
+
290
+ def notify(title, body, background = true)
291
+ Notification.new(title, body, @options).send(background) if @options[:notify]
292
+ puts
293
+ say "#{app_name} #{title}"
294
+ end
295
+
296
+ def say msg
297
+ puts "#{Time.now.strftime("%T")} [rerun] #{msg}"
298
+ end
299
+
300
+ # non-blocking stdin reader.
301
+ # returns a 1-char string if a key was pressed; otherwise nil
302
+ #
303
+ def key_pressed
304
+ begin
305
+ # this "raw input" nonsense is because unix likes waiting for linefeeds before sending stdin
306
+
307
+ # 'raw' means turn raw input on
308
+
309
+ # restore proper output newline handling -- see stty.rb and "man stty" and /usr/include/sys/termios.h
310
+ # looks like "raw" flips off the OPOST bit 0x00000001 /* enable following output processing */
311
+ # which disables #define ONLCR 0x00000002 /* map NL to CR-NL (ala CRMOD) */
312
+ # so this sets it back on again since all we care about is raw input, not raw output
313
+ system("stty raw opost")
314
+
315
+ c = nil
316
+ if $stdin.ready?
317
+ c = $stdin.getc
318
+ end
319
+ c.chr if c
320
+ ensure
321
+ system "stty -raw" # turn raw input off
322
+ end
323
+
324
+ # note: according to 'man tty' the proper way restore the settings is
325
+ # tty_state=`stty -g`
326
+ # ensure
327
+ # system 'stty "#{tty_state}'
328
+ # end
329
+ # but this way seems fine and less confusing
330
+
331
+ end
332
+
333
+ def clear_screen
334
+ # see http://ascii-table.com/ansi-escape-sequences-vt-100.php
335
+ $stdout.print "\033[H\033[2J"
336
+ end
337
+
338
+ end
339
+ end
@@ -0,0 +1,22 @@
1
+ module Rerun
2
+ module System
3
+
4
+ def mac?
5
+ RUBY_PLATFORM =~ /darwin/i
6
+ end
7
+
8
+ def windows?
9
+ RUBY_PLATFORM =~ /mswin/i
10
+ end
11
+
12
+ def linux?
13
+ RUBY_PLATFORM =~ /linux/i
14
+ end
15
+
16
+ def rails?
17
+ rails_sig_file = File.expand_path(".")+"/config/boot.rb"
18
+ File.exists? rails_sig_file
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,130 @@
1
+ require 'listen'
2
+
3
+ Thread.abort_on_exception = true
4
+
5
+ # This class will watch a directory and alert you of
6
+ # new files, modified files, deleted files.
7
+ #
8
+ # Now uses the Listen gem, but spawns its own thread on top.
9
+ # We should probably be accessing the Listen thread directly.
10
+ #
11
+ # Author: Alex Chaffee
12
+ #
13
+ module Rerun
14
+ class Watcher
15
+ InvalidDirectoryError = Class.new(RuntimeError)
16
+
17
+ #def self.default_ignore
18
+ # Listen::Silencer.new(Listen::Listener.new).send :_default_ignore_patterns
19
+ #end
20
+
21
+ attr_reader :directory, :pattern, :priority
22
+
23
+ # Create a file system watcher. Start it by calling #start.
24
+ #
25
+ # @param options[:directory] the directory to watch (default ".")
26
+ # @param options[:pattern] the glob pattern to search under the watched directory (default "**/*")
27
+ # @param options[:priority] the priority of the watcher thread (default 0)
28
+ #
29
+ def initialize(options = {}, &client_callback)
30
+ @client_callback = client_callback
31
+
32
+ options = {
33
+ :directory => ".",
34
+ :pattern => "**/*",
35
+ :priority => 0,
36
+ }.merge(options)
37
+
38
+ @pattern = options[:pattern]
39
+ @directories = options[:directory]
40
+ @directories = sanitize_dirs(@directories)
41
+ @priority = options[:priority]
42
+ @force_polling = options[:force_polling]
43
+ @ignore = [options[:ignore]].flatten.compact
44
+ @thread = nil
45
+ end
46
+
47
+ def sanitize_dirs(dirs)
48
+ dirs = [*dirs]
49
+ dirs.map do |d|
50
+ d.chomp!("/")
51
+ unless FileTest.exists?(d) && FileTest.readable?(d) && FileTest.directory?(d)
52
+ raise InvalidDirectoryError, "Directory '#{d}' either doesnt exist or isn't readable"
53
+ end
54
+ File.expand_path(d)
55
+ end
56
+ end
57
+
58
+ def start
59
+ if @thread then
60
+ raise RuntimeError, "already started"
61
+ end
62
+
63
+ @thread = Thread.new do
64
+ @listener = Listen.to(*@directories, only: watching, ignore: ignoring, wait_for_delay: 1, force_polling: @force_polling) do |modified, added, removed|
65
+ if((modified.size + added.size + removed.size) > 0)
66
+ @client_callback.call(:modified => modified, :added => added, :removed => removed)
67
+ end
68
+ end
69
+ @listener.start
70
+ end
71
+
72
+ @thread.priority = @priority
73
+
74
+ sleep 0.1 until @listener
75
+
76
+ at_exit { stop } # try really hard to clean up after ourselves
77
+ end
78
+
79
+ def watching
80
+ Rerun::Glob.new(@pattern).to_regexp
81
+ end
82
+
83
+ def ignoring
84
+ # todo: --no-ignore-dotfiles
85
+ dotfiles = /^\.[^.]/ # at beginning of string, a real dot followed by any other character
86
+ [dotfiles] + @ignore.map { |x| Rerun::Glob.new(x).to_regexp }
87
+ end
88
+
89
+ # kill the file watcher thread
90
+ def stop
91
+ @thread.wakeup rescue ThreadError
92
+ begin
93
+ @listener.stop
94
+ rescue Exception => e
95
+ puts "#{e.class}: #{e.message} stopping listener"
96
+ end
97
+ @thread.kill rescue ThreadError
98
+ end
99
+
100
+ # wait for the file watcher to finish
101
+ def join
102
+ @thread.join if @thread
103
+ rescue Interrupt => e
104
+ # don't care
105
+ end
106
+
107
+ def pause
108
+ @listener.pause if @listener
109
+ end
110
+
111
+ def unpause
112
+ @listener.start if @listener
113
+ end
114
+
115
+ def running?
116
+ @listener && @listener.processing?
117
+ end
118
+
119
+ def adapter
120
+ @listener &&
121
+ (backend = @listener.instance_variable_get(:@backend)) &&
122
+ backend.instance_variable_get(:@adapter)
123
+ end
124
+
125
+ def adapter_name
126
+ adapter && adapter.class.name.split('::').last
127
+ end
128
+
129
+ end
130
+ end
data/lib/rerun.rb ADDED
@@ -0,0 +1,15 @@
1
+ here = File.expand_path(File.dirname(__FILE__))
2
+ $: << here unless $:.include?(here)
3
+
4
+ require "listen" # pull in the Listen gem
5
+ require "rerun/options"
6
+ require "rerun/system"
7
+ require "rerun/notification"
8
+ require "rerun/runner"
9
+ require "rerun/watcher"
10
+ require "rerun/glob"
11
+
12
+ module Rerun
13
+
14
+ end
15
+
@@ -0,0 +1,34 @@
1
+ $spec = Gem::Specification.new do |s|
2
+ s.specification_version = 2 if s.respond_to? :specification_version=
3
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4
+
5
+ s.name = 'poll-rerun'
6
+ s.version = '0.11.1'
7
+
8
+ s.description = "Fork of the original rerun gem but with a 'released' --force-polling flag available. Restarts your app when a file changes. A no-frills, command-line alternative to Guard, Shotgun, Autotest, etc."
9
+ s.summary = "Fork of original rerun, but with 'released' --force-polling flag. Launches an app, and restarts it whenever the filesystem changes. A no-frills, command-line alternative to Guard, Shotgun, Autotest, etc."
10
+
11
+ s.authors = ["Alex Chaffee"]
12
+ s.email = "alex@stinky.com"
13
+
14
+ s.files = %w[
15
+ README.md
16
+ LICENSE
17
+ Rakefile
18
+ poll-rerun.gemspec
19
+ bin/rerun
20
+ icons/rails_grn_sml.png
21
+ icons/rails_red_sml.png] +
22
+ Dir['lib/**/*.rb']
23
+ s.executables = ['rerun']
24
+ s.test_files = s.files.select {|path| path =~ /^spec\/.*_spec.rb/}
25
+
26
+ s.extra_rdoc_files = %w[README.md]
27
+
28
+ s.add_runtime_dependency 'listen', '~> 3.0'
29
+
30
+ s.homepage = "http://github.com/hawry/rerun"
31
+ s.require_paths = %w[lib]
32
+
33
+ s.license = 'MIT'
34
+ end