poll-rerun 0.11.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.
@@ -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