rerun-cj 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +34 -0
- data/README.md +330 -0
- data/Rakefile +82 -0
- data/bin/rerun +12 -0
- data/icons/rails_grn_sml.png +0 -0
- data/icons/rails_red_sml.png +0 -0
- data/lib/rerun/glob.rb +85 -0
- data/lib/rerun/options.rb +99 -0
- data/lib/rerun/runner.rb +324 -0
- data/lib/rerun/system.rb +44 -0
- data/lib/rerun/watcher.rb +127 -0
- data/lib/rerun.rb +14 -0
- data/rerun.gemspec +34 -0
- metadata +80 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 27c6fa79f508fe873f0c236c273ea18346eadd9e
|
4
|
+
data.tar.gz: d79d590d3158eb9182b7790d040cb5598a58a6b4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4dddd98b58c3b26fcf6a3aebd2e7b965b2f827ad847f22ed5505e779355e08af7a4e5eb5ab79521fc7ff9a3dc1af7532ef6b8785eca95dd2c0bc38e10970f197
|
7
|
+
data.tar.gz: d437f2c29d6cba96f88d41aad99004b964b4e7f8c14b3c880cb77785ff39c4ed20410631cf464d1a9d607b6e843b2d37514514f762f60b0c479515c5a5dbe641
|
data/LICENSE
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
Rerun
|
2
|
+
Copyright (c) 2009 Alex Chaffee <alex@stinky.com>
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
of this software and associated documentation files (the "Software"), to
|
6
|
+
deal in the Software without restriction, including without limitation the
|
7
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
8
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in
|
12
|
+
all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
17
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
|
21
|
+
---
|
22
|
+
|
23
|
+
rerun partially based on code from Rspactor
|
24
|
+
Copyright (c) 2009 Mislav Marohnić
|
25
|
+
License as above (MIT open source).
|
26
|
+
|
27
|
+
rerun partially based on code from FileSystemWatcher
|
28
|
+
http://paulhorman.com/filesystemwatcher/
|
29
|
+
No license provided; assumed public domain.
|
30
|
+
|
31
|
+
rerun partially based on code from Shotgun
|
32
|
+
Copyright (c) 2009 Ryan Tomayko <tomayko.com/about>
|
33
|
+
License as above (MIT open source).
|
34
|
+
|
data/README.md
ADDED
@@ -0,0 +1,330 @@
|
|
1
|
+
# Rerun
|
2
|
+
|
3
|
+
<http://github.com/alexch/rerun>
|
4
|
+
|
5
|
+
Rerun launches your program, then watches the filesystem. If a relevant file
|
6
|
+
changes, then it restarts your program.
|
7
|
+
|
8
|
+
Rerun works for both long-running processes (e.g. apps) and short-running ones
|
9
|
+
(e.g. tests). It's basically a no-frills command-line alternative to Guard,
|
10
|
+
Shotgun, Autotest, etc. that doesn't require config files and works on any
|
11
|
+
command, not just Ruby programs.
|
12
|
+
|
13
|
+
Rerun's advantage is its simple design. Since it uses `exec` and the standard
|
14
|
+
Unix `SIGINT` and `SIGKILL` signals, you're sure the restarted app is really
|
15
|
+
acting just like it was when you ran it from the command line the first time.
|
16
|
+
|
17
|
+
By default only `*.{rb,js,css,coffee,scss,sass,erb,html,haml,ru,slim,md}` files are watched.
|
18
|
+
Use the `--pattern` option if you want to change this.
|
19
|
+
|
20
|
+
As of version 0.7.0, we use the Listen gem, which tries to use your OS's
|
21
|
+
built-in facilities for monitoring the filesystem, so CPU use is very light.
|
22
|
+
|
23
|
+
Rerun does not work on Windows. Sorry, but you can't do much relaunching
|
24
|
+
without "fork".
|
25
|
+
|
26
|
+
# Installation:
|
27
|
+
|
28
|
+
gem install rerun
|
29
|
+
|
30
|
+
("sudo" may be required on older systems, but try it without sudo first.)
|
31
|
+
|
32
|
+
If you are using RVM you might want to put this in your global gemset so it's
|
33
|
+
available to all your apps. (There really should be a better way to distinguish
|
34
|
+
gems-as-libraries from gems-as-tools.)
|
35
|
+
|
36
|
+
rvm @global do gem install rerun
|
37
|
+
|
38
|
+
The Listen gem looks for certain platform-dependent gems, and will complain if
|
39
|
+
they're not available. Unfortunately, Rubygems doesn't understand optional
|
40
|
+
dependencies very well, so you may have to install extra gems (and/or put them
|
41
|
+
in your Gemfile) to make Rerun work more smoothly on your system.
|
42
|
+
(Learn more at <https://github.com/guard/listen#listen-adapters>.)
|
43
|
+
For example, on Mac OS X, use
|
44
|
+
|
45
|
+
gem install rb-fsevent
|
46
|
+
|
47
|
+
# Usage:
|
48
|
+
|
49
|
+
rerun [options] [--] cmd
|
50
|
+
|
51
|
+
For example, if you're running a Sinatra app whose main file is `app.rb`:
|
52
|
+
|
53
|
+
rerun ruby app.rb
|
54
|
+
|
55
|
+
If the first part of the command is a `.rb` filename, then `ruby` is
|
56
|
+
optional, so the above can also be accomplished like this:
|
57
|
+
|
58
|
+
rerun app.rb
|
59
|
+
|
60
|
+
Rails doesn't automatically notice all config file changes, so you can force it
|
61
|
+
to restart when you change a config file like this:
|
62
|
+
|
63
|
+
rerun --dir config rails s
|
64
|
+
|
65
|
+
Or if you're using Thin to run a Rack app that's configured in config.ru
|
66
|
+
but you want it on port 4000 and in debug mode, and only want to watch
|
67
|
+
the `app` and `web` subdirectories:
|
68
|
+
|
69
|
+
rerun --dir app,web -- thin start --debug --port=4000 -R config.ru
|
70
|
+
|
71
|
+
The `--` is to separate rerun options from cmd options. You can also
|
72
|
+
use a quoted string for the command, e.g.
|
73
|
+
|
74
|
+
rerun --dir app "thin start --debug --port=4000 -R config.ru"
|
75
|
+
|
76
|
+
Rackup can also be used to launch a Rack server, so let's try that:
|
77
|
+
|
78
|
+
rerun -- rackup --port 4000 config.ru
|
79
|
+
|
80
|
+
Want to mimic [autotest](https://github.com/grosser/autotest)? Try
|
81
|
+
|
82
|
+
rerun -x rake
|
83
|
+
|
84
|
+
or
|
85
|
+
|
86
|
+
rerun -cx rspec
|
87
|
+
|
88
|
+
And if you're using [Spork](https://github.com/sporkrb/spork) with Rails, you
|
89
|
+
need to [restart your spork server](https://github.com/sporkrb/spork/issues/201)
|
90
|
+
whenever certain Rails environment files change, so why not put this in your
|
91
|
+
Rakefile...
|
92
|
+
|
93
|
+
desc "run spork (via rerun)"
|
94
|
+
task :spork do
|
95
|
+
sh "rerun --pattern '{Gemfile,Gemfile.lock,spec/spec_helper.rb,.rspec,spec/factories/**,config/environment.rb,config/environments/test.rb,config/initializers/*.rb,lib/**/*.rb}' -- spork"
|
96
|
+
end
|
97
|
+
|
98
|
+
and start using `rake spork` to launch your spork server?
|
99
|
+
|
100
|
+
(If you're using Guard instead of Rerun, check out
|
101
|
+
[guard-spork](https://github.com/guard/guard-spork)
|
102
|
+
for a similar solution.)
|
103
|
+
|
104
|
+
How about regenerating your HTML files after every change to your
|
105
|
+
[Erector](http://erector.rubyforge.org) widgets?
|
106
|
+
|
107
|
+
rerun -x erector --to-html my_site.rb
|
108
|
+
|
109
|
+
Use Heroku Cedar? `rerun` is now compatible with `foreman`. Run all your
|
110
|
+
Procfile processes locally and restart them all when necessary.
|
111
|
+
|
112
|
+
rerun foreman start
|
113
|
+
|
114
|
+
# Options:
|
115
|
+
|
116
|
+
`--dir` directory (or directories) to watch (default = "."). Separate multiple paths with ',' and/or use multiple `-d` options.
|
117
|
+
|
118
|
+
`--pattern` glob to match inside directory. This uses the Ruby Dir glob style -- see <http://www.ruby-doc.org/core/classes/Dir.html#M002322> for details.
|
119
|
+
By default it watches files ending in: `rb,js,css,coffee,scss,sass,erb,html,haml,ru,slim,md`.
|
120
|
+
On top of this, it also ignores dotfiles, `.tmp` files, and some other files and directories (like `.git` and `log`).
|
121
|
+
Run `rerun --help` to see the actual list.
|
122
|
+
|
123
|
+
`--ignore pattern` file glob to ignore (can be set many times). To ignore a directory, you must append '/*' e.g.
|
124
|
+
`--ignore 'coverage/*'`.
|
125
|
+
|
126
|
+
*On top of --pattern and --ignore, we ignore any changes to files and dirs starting with a dot.*
|
127
|
+
|
128
|
+
`--signal` (or -s) use specified signal (instead of the default SIGTERM) to terminate the previous process.
|
129
|
+
This may be useful for forcing the respective process to terminate as quickly as possible.
|
130
|
+
(`--signal KILL` is the equivalent of `kill -9`)
|
131
|
+
|
132
|
+
`--clear` (or -c) clear the screen before each run
|
133
|
+
|
134
|
+
`--exit` (or -x) expect the program to exit. With this option, rerun checks the return value; without it, rerun checks that the launched process is still running.
|
135
|
+
|
136
|
+
`--background` (or -b) disable on-the-fly commands, allowing the process to be backgrounded
|
137
|
+
|
138
|
+
`--no-growl` don't use growl
|
139
|
+
|
140
|
+
`--name` set the app name (for display)
|
141
|
+
|
142
|
+
Also `--version` and `--help`, naturally.
|
143
|
+
|
144
|
+
# Growl Notifications
|
145
|
+
|
146
|
+
If you have `growlnotify` available on the `PATH`, it sends notifications to
|
147
|
+
growl in addition to the console. If you have growl but don't want rerun to use it,
|
148
|
+
set the `--no-growl` option.
|
149
|
+
|
150
|
+
Download [growlnotify here](http://growl.info/downloads.php#generaldownloads)
|
151
|
+
now that Growl has moved to the App Store.
|
152
|
+
|
153
|
+
# On-The-Fly Commands
|
154
|
+
|
155
|
+
While the app is (re)running, you can make things happen by pressing keys:
|
156
|
+
|
157
|
+
* **r** -- restart (as if a file had changed)
|
158
|
+
* **c** -- clear the screen
|
159
|
+
* **x** or **q** -- exit (just like control-C)
|
160
|
+
* **p** -- pause/unpause filesystem watching
|
161
|
+
|
162
|
+
If you're backgrounding or using Pry or a debugger, you might not want these
|
163
|
+
keys to be trapped, so use the `--background` option.
|
164
|
+
|
165
|
+
# Signals
|
166
|
+
|
167
|
+
The current algorithm for killing the process is:
|
168
|
+
|
169
|
+
* send [SIGTERM](http://en.wikipedia.org/wiki/SIGTERM) (or the value of the `--signal` option)
|
170
|
+
* if that doesn't work after 4 seconds, send SIGINT (aka control-C)
|
171
|
+
* if that doesn't work after 2 more seconds, send SIGKILL (aka kill -9)
|
172
|
+
|
173
|
+
This seems like the most gentle and unixy way of doing things, but it does
|
174
|
+
mean that if your program ignores SIGTERM, it takes an extra 4 to 6 seconds to
|
175
|
+
restart.
|
176
|
+
|
177
|
+
# To Do:
|
178
|
+
|
179
|
+
## Must have for v1.0
|
180
|
+
* ".rerun" file to specify options per project or in $HOME.
|
181
|
+
* Make sure to pass through quoted options correctly to target process [bug]
|
182
|
+
* Optionally do "bundle install" before and "bundle exec" during launch
|
183
|
+
|
184
|
+
## Nice to have
|
185
|
+
* Smarter --signal option (specifying signal to try and a timeout to wait, repeated)
|
186
|
+
* If the last element of the command is a `.ru` file and there's no other command then use `rackup`
|
187
|
+
* Figure out an algorithm so "-x" is not needed (if possible) -- maybe by accepting a "--port" option or reading `config.ru`
|
188
|
+
* Specify (or deduce) port to listen for to determine success of a web server launch
|
189
|
+
|
190
|
+
## Wacky Ideas
|
191
|
+
* Make it work on Windows, like Guard now does. See
|
192
|
+
* https://github.com/guard/guard/issues/59
|
193
|
+
* https://github.com/guard/guard/issues/27
|
194
|
+
* On OS X:
|
195
|
+
* use a C library using growl's developer API <http://growl.info/developer/>
|
196
|
+
* Use growl's AppleScript or SDK instead of relying on growlnotify
|
197
|
+
* Use OS X notifications
|
198
|
+
* "Failed" icon for notifications
|
199
|
+
* On Linux:
|
200
|
+
* Test on Linux.
|
201
|
+
* Use libnotify or notify-send http://www.linuxjournal.com/content/tech-tip-get-notifications-your-scripts-notify-send
|
202
|
+
|
203
|
+
# Other projects that do similar things
|
204
|
+
|
205
|
+
* Restartomatic: <http://github.com/adammck/restartomatic>
|
206
|
+
* Shotgun: <http://github.com/rtomayko/shotgun>
|
207
|
+
* Rack::Reloader middleware: <http://github.com/rack/rack/blob/5ca8f82fb59f0bf0e8fd438e8e91c5acf3d98e44/lib/rack/reloader.rb>
|
208
|
+
* The Sinatra FAQ has a discussion at <http://www.sinatrarb.com/faq.html#reloading>
|
209
|
+
* Kicker: <http://github.com/alloy/kicker/>
|
210
|
+
* Watchr: <https://github.com/mynyml/watchr>
|
211
|
+
* Guard: <http://github.com/guard/guard>
|
212
|
+
* Autotest: <https://github.com/grosser/autotest>
|
213
|
+
|
214
|
+
# Why would I use this instead of Shotgun?
|
215
|
+
|
216
|
+
Shotgun does a "fork" after the web framework has loaded but before
|
217
|
+
your application is loaded. It then loads your app, processes a
|
218
|
+
single request in the child process, then exits the child process.
|
219
|
+
|
220
|
+
Rerun launches the whole app, then when it's time to restart, uses
|
221
|
+
"kill" to shut it down and starts the whole thing up again from
|
222
|
+
scratch.
|
223
|
+
|
224
|
+
So rerun takes somewhat longer than Shotgun to restart the app, but
|
225
|
+
does it much less frequently. And once it's running it behaves more
|
226
|
+
normally and consistently with your production app.
|
227
|
+
|
228
|
+
Also, Shotgun reloads the app on every request, even if it doesn't
|
229
|
+
need to. This is fine if you're loading a single file, but if your web
|
230
|
+
pages all load other files (CSS, JS, media) then that adds up quickly.
|
231
|
+
(I can only assume that the developers of shotgun are using caching or a
|
232
|
+
front web server so this isn't a pain point for them.)
|
233
|
+
|
234
|
+
And hey, does Shotgun reload your Worker processes if you're using Foreman and
|
235
|
+
a Procfile? I'm pretty sure it doesn't.
|
236
|
+
|
237
|
+
YMMV!
|
238
|
+
|
239
|
+
# Why would I use this instead of Rack::Reloader?
|
240
|
+
|
241
|
+
Rack::Reloader is certifiably beautiful code, and is a very elegant use
|
242
|
+
of Rack's middleware architecture. But because it relies on the
|
243
|
+
LOADED_FEATURES variable, it only reloads .rb files that were 'require'd,
|
244
|
+
not 'load'ed. That leaves out (non-Erector) template files, and also,
|
245
|
+
at least the way I was doing it, sub-actions (see
|
246
|
+
[this thread](http://groups.google.com/group/sinatrarb/browse_thread/thread/7329727a9296e96a#
|
247
|
+
)).
|
248
|
+
|
249
|
+
Rack::Reloader also doesn't reload configuration changes or redo other
|
250
|
+
things that happen during app startup. Rerun takes the attitude that if
|
251
|
+
you want to restart an app, you should just restart the whole app. You know?
|
252
|
+
|
253
|
+
# Why would I use this instead of Guard?
|
254
|
+
|
255
|
+
Guard is very powerful but requires some up-front configuration.
|
256
|
+
Rerun is meant as a no-frills command-line alternative requiring no knowledge
|
257
|
+
of Ruby nor config file syntax.
|
258
|
+
|
259
|
+
# Why did you write this?
|
260
|
+
|
261
|
+
I've been using [Sinatra](http://sinatrarb.com) and loving it. In order
|
262
|
+
to simplify their system, the Rat Pack removed auto-reloading from
|
263
|
+
Sinatra proper. I approve of this: a web application framework should be
|
264
|
+
focused on serving requests, not on munging Ruby ObjectSpace for
|
265
|
+
dev-time convenience. But I still wanted automatic reloading during
|
266
|
+
development. Shotgun wasn't working for me (see above) so I spliced
|
267
|
+
Rerun together out of code from Rspactor, FileSystemWatcher, and Shotgun
|
268
|
+
-- with a heavy amount of refactoring and rewriting. In late 2012 I
|
269
|
+
migrated the backend to the Listen gem, which was extracted from Guard,
|
270
|
+
so it should be more reliable and performant on multiple platforms.
|
271
|
+
|
272
|
+
# Credits
|
273
|
+
|
274
|
+
Rerun: [Alex Chaffee](http://alexchaffee.com), <mailto:alex@stinky.com>, <http://github.com/alexch/>
|
275
|
+
|
276
|
+
Based upon and/or inspired by:
|
277
|
+
|
278
|
+
* Shotgun: <http://github.com/rtomayko/shotgun>
|
279
|
+
* Rspactor: <http://github.com/mislav/rspactor>
|
280
|
+
* (In turn based on http://rails.aizatto.com/2007/11/28/taming-the-autotest-beast-with-fsevents/ )
|
281
|
+
* FileSystemWatcher: <http://paulhorman.com/filesystemwatcher/>
|
282
|
+
|
283
|
+
## Patches by:
|
284
|
+
|
285
|
+
* David Billskog <billskog@gmail.com>
|
286
|
+
* Jens B <https://github.com/dpree>
|
287
|
+
* Andrés Botero <https://github.com/anbotero>
|
288
|
+
* Dreamcat4
|
289
|
+
* <https://github.com/FND>
|
290
|
+
* Barry Sia <https://github.com/bsia>
|
291
|
+
* Paul Rangel <https://github.com/ismell>
|
292
|
+
|
293
|
+
# Version History
|
294
|
+
|
295
|
+
* ?
|
296
|
+
* better 'changed' message
|
297
|
+
|
298
|
+
* v0.10.0 4 May 2014
|
299
|
+
* add '.coffee,.slim,.md' to default pattern (thanks @xylinq)
|
300
|
+
* --ignore option
|
301
|
+
|
302
|
+
* v0.9.0 6 March 2014
|
303
|
+
* --dir (or -d) can be specified more than once, for multiple directories (thanks again Barry!)
|
304
|
+
* --name option
|
305
|
+
* press 'p' to pause/unpause filesystem watching (Barry is the man!)
|
306
|
+
* works with Listen 2 (note: needs 2.3 or higher)
|
307
|
+
* cooldown works, thanks to patches to underlying Listen gem
|
308
|
+
* ignore all dotfiles, and add actual list of ignored dirs and files
|
309
|
+
|
310
|
+
* v0.8.2
|
311
|
+
* bugfix, forcing Rerun to use Listen v1.0.3 while we work out the troubles we're having with Listen 1.3 and 2.1
|
312
|
+
|
313
|
+
* v0.8.1
|
314
|
+
* bugfix release (#30 and #34)
|
315
|
+
|
316
|
+
* v0.8.0
|
317
|
+
* --background option (thanks FND!) to disable the keyboard listener
|
318
|
+
* --signal option (thanks FND!)
|
319
|
+
* --no-growl option
|
320
|
+
* --dir supports multiple directories (thanks Barry!)
|
321
|
+
|
322
|
+
* v0.7.1
|
323
|
+
* bugfix: make rails icon work again
|
324
|
+
|
325
|
+
* v0.7.0
|
326
|
+
* uses Listen gem (which uses rb-fsevent for lightweight filesystem snooping)
|
327
|
+
|
328
|
+
# License
|
329
|
+
|
330
|
+
Open Source MIT License. See "LICENSE" file.
|
data/Rakefile
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
task :default => [:spec]
|
7
|
+
task :test => :spec
|
8
|
+
|
9
|
+
desc "Run all specs"
|
10
|
+
RSpec::Core::RakeTask.new('spec') do |t|
|
11
|
+
ENV['ENV'] = "test"
|
12
|
+
t.pattern = 'spec/**/*_spec.rb'
|
13
|
+
t.rspec_opts = ['--color']
|
14
|
+
end
|
15
|
+
|
16
|
+
$rubyforge_project = 'pivotalrb'
|
17
|
+
|
18
|
+
$spec =
|
19
|
+
begin
|
20
|
+
require 'rubygems/specification'
|
21
|
+
data = File.read('rerun.gemspec')
|
22
|
+
spec = nil
|
23
|
+
#Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join
|
24
|
+
spec = eval data
|
25
|
+
spec
|
26
|
+
end
|
27
|
+
|
28
|
+
def package(ext='')
|
29
|
+
"pkg/#{$spec.name}-#{$spec.version}" + ext
|
30
|
+
end
|
31
|
+
|
32
|
+
desc 'Exit if git is dirty'
|
33
|
+
task :check_git do
|
34
|
+
state = `git status 2> /dev/null | tail -n1`
|
35
|
+
clean = (state =~ /working directory clean/)
|
36
|
+
unless clean
|
37
|
+
warn "can't do that on an unclean git dir"
|
38
|
+
exit 1
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
desc 'Build packages'
|
43
|
+
task :package => %w[.gem .tar.gz].map { |e| package(e) }
|
44
|
+
|
45
|
+
desc 'Build and install as local gem'
|
46
|
+
task :install => package('.gem') do
|
47
|
+
sh "gem install #{package('.gem')}"
|
48
|
+
end
|
49
|
+
|
50
|
+
directory 'pkg/'
|
51
|
+
CLOBBER.include('pkg')
|
52
|
+
|
53
|
+
file package('.gem') => %W[pkg/ #{$spec.name}.gemspec] + $spec.files do |f|
|
54
|
+
sh "gem build #{$spec.name}.gemspec"
|
55
|
+
mv File.basename(f.name), f.name
|
56
|
+
end
|
57
|
+
|
58
|
+
file package('.tar.gz') => %w[pkg/] + $spec.files do |f|
|
59
|
+
cmd = <<-SH
|
60
|
+
git archive \
|
61
|
+
--prefix=#{$spec.name}-#{$spec.version}/ \
|
62
|
+
--format=tar \
|
63
|
+
HEAD | gzip > #{f.name}
|
64
|
+
SH
|
65
|
+
sh cmd.gsub(/ +/, ' ')
|
66
|
+
end
|
67
|
+
|
68
|
+
desc 'Publish gem and tarball to rubyforge'
|
69
|
+
task 'release' => [:check_git, package('.gem'), package('.tar.gz')] do |t|
|
70
|
+
puts "Releasing #{$spec.version}"
|
71
|
+
sh "gem push #{package('.gem')}"
|
72
|
+
puts "Tagging and pushing"
|
73
|
+
sh "git tag v#{$spec.version}"
|
74
|
+
sh "git push && git push --tags"
|
75
|
+
end
|
76
|
+
|
77
|
+
desc 'download github issues and pull requests'
|
78
|
+
task 'github' do
|
79
|
+
%w(issues pulls).each do |type|
|
80
|
+
sh "curl -o #{type}.json https://api.github.com/repos/alexch/rerun/#{type}"
|
81
|
+
end
|
82
|
+
end
|
data/bin/rerun
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
libdir = "#{File.expand_path(File.dirname(File.dirname(__FILE__)))}/lib"
|
5
|
+
$LOAD_PATH.unshift libdir unless $LOAD_PATH.include?(libdir)
|
6
|
+
|
7
|
+
require 'rerun'
|
8
|
+
require 'optparse'
|
9
|
+
|
10
|
+
options = Rerun::Options.parse
|
11
|
+
exit if options.nil?
|
12
|
+
runner = Rerun::Runner.keep_running(options[:cmd], options)
|
Binary file
|
Binary file
|
data/lib/rerun/glob.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# based on http://cpan.uwinnipeg.ca/htdocs/Text-Glob/Text/Glob.pm.html#glob_to_regex_string-
|
2
|
+
|
3
|
+
# todo: release as separate gem
|
4
|
+
#
|
5
|
+
module Rerun
|
6
|
+
class Glob
|
7
|
+
NO_LEADING_DOT = '(?=[^\.])' # todo
|
8
|
+
START_OF_FILENAME = '(\A|\/)' # beginning of string or a slash
|
9
|
+
END_OF_STRING = '\z'
|
10
|
+
|
11
|
+
def initialize glob_string
|
12
|
+
@glob_string = glob_string
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_regexp_string
|
16
|
+
chars = @glob_string.split('')
|
17
|
+
|
18
|
+
chars = smoosh(chars)
|
19
|
+
|
20
|
+
curlies = 0
|
21
|
+
escaping = false
|
22
|
+
string = chars.map do |char|
|
23
|
+
if escaping
|
24
|
+
escaping = false
|
25
|
+
char
|
26
|
+
else
|
27
|
+
case char
|
28
|
+
when '**'
|
29
|
+
"([^/]+/)*"
|
30
|
+
when '*'
|
31
|
+
".*"
|
32
|
+
when "?"
|
33
|
+
"."
|
34
|
+
when "."
|
35
|
+
"\\."
|
36
|
+
|
37
|
+
when "{"
|
38
|
+
curlies += 1
|
39
|
+
"("
|
40
|
+
when "}"
|
41
|
+
if curlies > 0
|
42
|
+
curlies -= 1
|
43
|
+
")"
|
44
|
+
else
|
45
|
+
char
|
46
|
+
end
|
47
|
+
when ","
|
48
|
+
if curlies > 0
|
49
|
+
"|"
|
50
|
+
else
|
51
|
+
char
|
52
|
+
end
|
53
|
+
when "\\"
|
54
|
+
escaping = true
|
55
|
+
"\\"
|
56
|
+
|
57
|
+
else
|
58
|
+
char
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end.join
|
63
|
+
START_OF_FILENAME + string + END_OF_STRING
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_regexp
|
67
|
+
Regexp.new(to_regexp_string)
|
68
|
+
end
|
69
|
+
|
70
|
+
def smoosh chars
|
71
|
+
out = []
|
72
|
+
until chars.empty?
|
73
|
+
char = chars.shift
|
74
|
+
if char == "*" and chars.first == "*"
|
75
|
+
chars.shift
|
76
|
+
chars.shift if chars.first == "/"
|
77
|
+
out.push("**")
|
78
|
+
else
|
79
|
+
out.push(char)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
out
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,99 @@
|
|
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}"
|
12
|
+
DEFAULT_DIRS = ["."]
|
13
|
+
|
14
|
+
DEFAULTS = {
|
15
|
+
:pattern => DEFAULT_PATTERN,
|
16
|
+
:signal => "TERM",
|
17
|
+
:growl => true,
|
18
|
+
:name => Pathname.getwd.basename.to_s.capitalize,
|
19
|
+
:ignore => []
|
20
|
+
}
|
21
|
+
|
22
|
+
def self.parse args = ARGV
|
23
|
+
options = DEFAULTS.dup
|
24
|
+
opts = OptionParser.new("", 24, ' ') do |opts|
|
25
|
+
opts.banner = "Usage: rerun [options] [--] cmd"
|
26
|
+
|
27
|
+
opts.separator ""
|
28
|
+
opts.separator "Launches an app, and restarts it when the filesystem changes."
|
29
|
+
opts.separator "See http://github.com/alexch/rerun for more info."
|
30
|
+
opts.separator "Version: #{$spec.version}"
|
31
|
+
opts.separator ""
|
32
|
+
opts.separator "Options:"
|
33
|
+
|
34
|
+
opts.on("-d dir", "--dir dir", "directory to watch, default = \"#{DEFAULT_DIRS}\". Specify multiple paths with ',' or separate '-d dir' option pairs.") do |dir|
|
35
|
+
elements = dir.split(",")
|
36
|
+
options[:dir] = (options[:dir] || []) + elements
|
37
|
+
end
|
38
|
+
|
39
|
+
# todo: rename to "--watch"
|
40
|
+
opts.on("-p pattern", "--pattern pattern", "file glob to watch, default = \"#{DEFAULTS[:pattern]}\"") do |pattern|
|
41
|
+
options[:pattern] = pattern
|
42
|
+
end
|
43
|
+
|
44
|
+
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|
|
45
|
+
options[:ignore] += [pattern]
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.on("-s signal", "--signal signal", "terminate process using this signal, default = \"#{DEFAULTS[:signal]}\"") do |signal|
|
49
|
+
options[:signal] = signal
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.on("-c", "--clear", "clear screen before each run") do
|
53
|
+
options[:clear] = true
|
54
|
+
end
|
55
|
+
|
56
|
+
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|
|
57
|
+
options[:exit] = true
|
58
|
+
end
|
59
|
+
|
60
|
+
opts.on("-b", "--background", "disable on-the-fly commands, allowing the process to be backgrounded") do
|
61
|
+
options[:background] = true
|
62
|
+
end
|
63
|
+
|
64
|
+
opts.on("-n name", "--name name", "name of app used in logs and notifications, default = \"#{DEFAULTS[:name]}\"") do |name|
|
65
|
+
options[:name] = name
|
66
|
+
end
|
67
|
+
|
68
|
+
opts.on("--no-growl", "don't use growl") do
|
69
|
+
options[:growl] = false
|
70
|
+
end
|
71
|
+
|
72
|
+
opts.on_tail("-h", "--help", "--usage", "show this message") do
|
73
|
+
puts opts
|
74
|
+
return
|
75
|
+
end
|
76
|
+
|
77
|
+
opts.on_tail("--version", "show version") do
|
78
|
+
puts $spec.version
|
79
|
+
return
|
80
|
+
end
|
81
|
+
|
82
|
+
opts.on_tail ""
|
83
|
+
opts.on_tail "On top of --pattern and --ignore, we ignore any changes to files and dirs starting with a dot."
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
if args.empty?
|
88
|
+
puts opts
|
89
|
+
nil
|
90
|
+
else
|
91
|
+
opts.parse! args
|
92
|
+
options[:cmd] = args.join(" ")
|
93
|
+
options[:dir] ||= DEFAULT_DIRS
|
94
|
+
options
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
data/lib/rerun/runner.rb
ADDED
@@ -0,0 +1,324 @@
|
|
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
|
+
stop
|
64
|
+
start
|
65
|
+
@restarting = false
|
66
|
+
end
|
67
|
+
|
68
|
+
def watcher_running?
|
69
|
+
@watcher && @watcher.running?
|
70
|
+
end
|
71
|
+
|
72
|
+
def toggle_pause
|
73
|
+
unless @pausing
|
74
|
+
say "Pausing. Press 'p' again to resume."
|
75
|
+
@watcher.pause
|
76
|
+
@pausing = true
|
77
|
+
else
|
78
|
+
say "Resuming"
|
79
|
+
@watcher.unpause
|
80
|
+
@pausing = false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def unpause
|
85
|
+
@watcher.unpause
|
86
|
+
end
|
87
|
+
|
88
|
+
def dir
|
89
|
+
@options[:dir]
|
90
|
+
end
|
91
|
+
|
92
|
+
def dirs
|
93
|
+
@options[:dir] || "."
|
94
|
+
end
|
95
|
+
|
96
|
+
def pattern
|
97
|
+
@options[:pattern]
|
98
|
+
end
|
99
|
+
|
100
|
+
def ignore
|
101
|
+
@options[:ignore] || []
|
102
|
+
end
|
103
|
+
|
104
|
+
def clear?
|
105
|
+
@options[:clear]
|
106
|
+
end
|
107
|
+
|
108
|
+
def exit?
|
109
|
+
@options[:exit]
|
110
|
+
end
|
111
|
+
|
112
|
+
def app_name
|
113
|
+
@options[:name]
|
114
|
+
end
|
115
|
+
|
116
|
+
def start
|
117
|
+
if windows?
|
118
|
+
raise "Sorry, Rerun does not work on Windows."
|
119
|
+
end
|
120
|
+
|
121
|
+
if (!@already_running)
|
122
|
+
taglines = [
|
123
|
+
"To infinity... and beyond!",
|
124
|
+
"Charge!",
|
125
|
+
]
|
126
|
+
notify "launched", taglines[rand(taglines.size)]
|
127
|
+
@already_running = true
|
128
|
+
else
|
129
|
+
taglines = [
|
130
|
+
"Here we go again!",
|
131
|
+
"Keep on trucking.",
|
132
|
+
"Once more unto the breach, dear friends, once more!",
|
133
|
+
"The road goes ever on and on, down from the door where it began.",
|
134
|
+
]
|
135
|
+
notify "restarted", taglines[rand(taglines.size)]
|
136
|
+
end
|
137
|
+
|
138
|
+
clear_screen if clear?
|
139
|
+
start_keypress_thread unless @keypress_thread
|
140
|
+
|
141
|
+
@pid = Kernel.fork do
|
142
|
+
begin
|
143
|
+
exec(@run_command)
|
144
|
+
rescue => e
|
145
|
+
puts "#{e.class}: #{e.message}"
|
146
|
+
exit
|
147
|
+
end
|
148
|
+
end
|
149
|
+
status_thread = Process.detach(@pid) # so if the child exits, it dies
|
150
|
+
|
151
|
+
Signal.trap("INT") do # INT = control-C -- allows user to stop the top-level rerun process
|
152
|
+
die
|
153
|
+
end
|
154
|
+
|
155
|
+
Signal.trap("TERM") do # TERM is the polite way of terminating a process
|
156
|
+
die
|
157
|
+
end
|
158
|
+
|
159
|
+
begin
|
160
|
+
sleep 2
|
161
|
+
rescue Interrupt => e
|
162
|
+
# in case someone hits control-C immediately ("oops!")
|
163
|
+
die
|
164
|
+
end
|
165
|
+
|
166
|
+
if exit?
|
167
|
+
status = status_thread.value
|
168
|
+
if status.success?
|
169
|
+
notify "succeeded", ""
|
170
|
+
else
|
171
|
+
notify "failed", "Exit status #{status.exitstatus}"
|
172
|
+
end
|
173
|
+
else
|
174
|
+
if !running?
|
175
|
+
notify "Launch Failed", "See console for error output"
|
176
|
+
@already_running = false
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
unless @watcher
|
181
|
+
|
182
|
+
watcher = Watcher.new(:directory => dirs, :pattern => pattern, :ignore => ignore) do |changes|
|
183
|
+
|
184
|
+
message = change_message(changes)
|
185
|
+
|
186
|
+
say "Change detected: #{message}"
|
187
|
+
restart unless @restarting
|
188
|
+
end
|
189
|
+
watcher.start
|
190
|
+
@watcher = watcher
|
191
|
+
say "Watching #{dir.join(', ')} for #{pattern}" +
|
192
|
+
(ignore.empty? ? "" : " (ignoring #{ignore.join(',')})") +
|
193
|
+
" using #{watcher.adapter.class.name.split('::').last} adapter"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def change_message(changes)
|
198
|
+
message = [:modified, :added, :removed].map do |change|
|
199
|
+
count = changes[change] ? changes[change].size : 0
|
200
|
+
if count > 0
|
201
|
+
"#{count} #{change}"
|
202
|
+
end
|
203
|
+
end.compact.join(", ")
|
204
|
+
|
205
|
+
changed_files = changes.values.flatten
|
206
|
+
if changed_files.count > 0
|
207
|
+
message += ": "
|
208
|
+
message += changes.values.flatten[0..3].map { |path| path.split('/').last }.join(', ')
|
209
|
+
if changed_files.count > 3
|
210
|
+
message += ", ..."
|
211
|
+
end
|
212
|
+
end
|
213
|
+
message
|
214
|
+
end
|
215
|
+
|
216
|
+
def die
|
217
|
+
#stop_keypress_thread # don't do this since we're probably *in* the keypress thread
|
218
|
+
stop # stop the child process if it exists
|
219
|
+
exit 0 # todo: status code param
|
220
|
+
end
|
221
|
+
|
222
|
+
def join
|
223
|
+
@watcher.join
|
224
|
+
end
|
225
|
+
|
226
|
+
def running?
|
227
|
+
signal(0)
|
228
|
+
end
|
229
|
+
|
230
|
+
def signal(signal)
|
231
|
+
say "Sending signal #{signal} to #{@pid}" unless signal == 0
|
232
|
+
Process.kill(signal, @pid)
|
233
|
+
true
|
234
|
+
rescue
|
235
|
+
false
|
236
|
+
end
|
237
|
+
|
238
|
+
# todo: test escalation
|
239
|
+
def stop
|
240
|
+
default_signal = @options[:signal] || "TERM"
|
241
|
+
if @pid && (@pid != 0)
|
242
|
+
notify "stopping", "All good things must come to an end." unless @restarting
|
243
|
+
begin
|
244
|
+
timeout(5) do # todo: escalation timeout setting
|
245
|
+
# start with a polite SIGTERM
|
246
|
+
signal(default_signal) && Process.wait(@pid)
|
247
|
+
end
|
248
|
+
rescue Timeout::Error
|
249
|
+
begin
|
250
|
+
timeout(5) do
|
251
|
+
# escalate to SIGINT aka control-C since some foolish process may be ignoring SIGTERM
|
252
|
+
signal("INT") && Process.wait(@pid)
|
253
|
+
end
|
254
|
+
rescue Timeout::Error
|
255
|
+
# escalate to SIGKILL aka "kill -9" which cannot be ignored
|
256
|
+
signal("KILL") && Process.wait(@pid)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
rescue => e
|
261
|
+
false
|
262
|
+
end
|
263
|
+
|
264
|
+
def git_head_changed?
|
265
|
+
old_git_head = @git_head
|
266
|
+
read_git_head
|
267
|
+
@git_head and old_git_head and @git_head != old_git_head
|
268
|
+
end
|
269
|
+
|
270
|
+
def read_git_head
|
271
|
+
git_head_file = File.join(dir, '.git', 'HEAD')
|
272
|
+
@git_head = File.exists?(git_head_file) && File.read(git_head_file)
|
273
|
+
end
|
274
|
+
|
275
|
+
def notify(title, body)
|
276
|
+
growl title, body if @options[:growl]
|
277
|
+
puts
|
278
|
+
say "#{app_name} #{title}"
|
279
|
+
end
|
280
|
+
|
281
|
+
def say msg
|
282
|
+
puts "#{Time.now.strftime("%T")} [rerun] #{msg}"
|
283
|
+
end
|
284
|
+
|
285
|
+
# non-blocking stdin reader.
|
286
|
+
# returns a 1-char string if a key was pressed; otherwise nil
|
287
|
+
#
|
288
|
+
def key_pressed
|
289
|
+
begin
|
290
|
+
# this "raw input" nonsense is because unix likes waiting for linefeeds before sending stdin
|
291
|
+
|
292
|
+
# 'raw' means turn raw input on
|
293
|
+
|
294
|
+
# restore proper output newline handling -- see stty.rb and "man stty" and /usr/include/sys/termios.h
|
295
|
+
# looks like "raw" flips off the OPOST bit 0x00000001 /* enable following output processing */
|
296
|
+
# which disables #define ONLCR 0x00000002 /* map NL to CR-NL (ala CRMOD) */
|
297
|
+
# so this sets it back on again since all we care about is raw input, not raw output
|
298
|
+
system("stty raw opost")
|
299
|
+
|
300
|
+
c = nil
|
301
|
+
if $stdin.ready?
|
302
|
+
c = $stdin.getc
|
303
|
+
end
|
304
|
+
c.chr if c
|
305
|
+
ensure
|
306
|
+
system "stty -raw" # turn raw input off
|
307
|
+
end
|
308
|
+
|
309
|
+
# note: according to 'man tty' the proper way restore the settings is
|
310
|
+
# tty_state=`stty -g`
|
311
|
+
# ensure
|
312
|
+
# system 'stty "#{tty_state}'
|
313
|
+
# end
|
314
|
+
# but this way seems fine and less confusing
|
315
|
+
|
316
|
+
end
|
317
|
+
|
318
|
+
def clear_screen
|
319
|
+
# see http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
320
|
+
$stdout.print "\033[H\033[2J"
|
321
|
+
end
|
322
|
+
|
323
|
+
end
|
324
|
+
end
|
data/lib/rerun/system.rb
ADDED
@@ -0,0 +1,44 @@
|
|
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
|
+
# do we have growl or not?
|
17
|
+
def growl_available?
|
18
|
+
mac? && (growlcmd != "")
|
19
|
+
end
|
20
|
+
|
21
|
+
def growlcmd
|
22
|
+
growlnotify = `which growlnotify`.chomp
|
23
|
+
# todo: check version of growlnotify and warn if it's too old
|
24
|
+
growlnotify
|
25
|
+
end
|
26
|
+
|
27
|
+
def icon
|
28
|
+
here = File.expand_path(File.dirname(__FILE__))
|
29
|
+
icondir = File.expand_path("#{here}/../../icons")
|
30
|
+
rails_sig_file = File.expand_path(".")+"/config/boot.rb"
|
31
|
+
"#{icondir}/rails_red_sml.png" if File.exists? rails_sig_file
|
32
|
+
end
|
33
|
+
|
34
|
+
def growl(title, body, background = true)
|
35
|
+
if growl_available?
|
36
|
+
icon_str = ("--image \"#{icon}\"" if icon)
|
37
|
+
s = "#{growlcmd} -n \"#{app_name}\" -m \"#{body}\" \"#{app_name} #{title}\" #{icon_str}"
|
38
|
+
s += " &" if background
|
39
|
+
`#{s}`
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,127 @@
|
|
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
|
+
@ignore = [options[:ignore]].flatten.compact
|
43
|
+
@thread = nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def sanitize_dirs(dirs)
|
47
|
+
dirs = [*dirs]
|
48
|
+
dirs.map do |d|
|
49
|
+
d.chomp!("/")
|
50
|
+
unless FileTest.exists?(d) && FileTest.readable?(d) && FileTest.directory?(d)
|
51
|
+
raise InvalidDirectoryError, "Directory '#{d}' either doesnt exist or isn't readable"
|
52
|
+
end
|
53
|
+
File.expand_path(d)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def start
|
58
|
+
if @thread then
|
59
|
+
raise RuntimeError, "already started"
|
60
|
+
end
|
61
|
+
|
62
|
+
@thread = Thread.new do
|
63
|
+
@listener = Listen.to(*@directories, only: watching, ignore: ignoring, wait_for_delay: 1) do |modified, added, removed|
|
64
|
+
if((modified.size + added.size + removed.size) > 0)
|
65
|
+
@client_callback.call(:modified => modified, :added => added, :removed => removed)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
@listener.start
|
69
|
+
end
|
70
|
+
|
71
|
+
@thread.priority = @priority
|
72
|
+
|
73
|
+
sleep 0.1 until @listener
|
74
|
+
|
75
|
+
at_exit { stop } # try really hard to clean up after ourselves
|
76
|
+
end
|
77
|
+
|
78
|
+
def watching
|
79
|
+
Rerun::Glob.new(@pattern).to_regexp
|
80
|
+
end
|
81
|
+
|
82
|
+
def ignoring
|
83
|
+
# todo: --no-ignore-dotfiles
|
84
|
+
# dotfiles = /^\.[^.]/ # at beginning of string, a real dot followed by any other character
|
85
|
+
# [dotfiles] + @ignore.map { |x| Rerun::Glob.new(x).to_regexp }
|
86
|
+
@ignore.map { |x| Rerun::Glob.new(x).to_regexp }
|
87
|
+
end
|
88
|
+
|
89
|
+
def adapter
|
90
|
+
@listener.registry[:adapter] || (timeout(4) do
|
91
|
+
sleep 1 until adapter = @listener.registry[:adapter]
|
92
|
+
adapter
|
93
|
+
end)
|
94
|
+
end
|
95
|
+
|
96
|
+
# kill the file watcher thread
|
97
|
+
def stop
|
98
|
+
@thread.wakeup rescue ThreadError
|
99
|
+
begin
|
100
|
+
@listener.stop
|
101
|
+
rescue Exception => e
|
102
|
+
puts "#{e.class}: #{e.message} stopping listener"
|
103
|
+
end
|
104
|
+
@thread.kill rescue ThreadError
|
105
|
+
end
|
106
|
+
|
107
|
+
# wait for the file watcher to finish
|
108
|
+
def join
|
109
|
+
@thread.join if @thread
|
110
|
+
rescue Interrupt => e
|
111
|
+
# don't care
|
112
|
+
end
|
113
|
+
|
114
|
+
def pause
|
115
|
+
@listener.pause if @listener
|
116
|
+
end
|
117
|
+
|
118
|
+
def unpause
|
119
|
+
@listener.unpause if @listener
|
120
|
+
end
|
121
|
+
|
122
|
+
def running?
|
123
|
+
@listener && @listener.instance_variable_get(:@adapter)
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
data/lib/rerun.rb
ADDED
@@ -0,0 +1,14 @@
|
|
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/runner"
|
8
|
+
require "rerun/watcher"
|
9
|
+
require "rerun/glob"
|
10
|
+
|
11
|
+
module Rerun
|
12
|
+
|
13
|
+
end
|
14
|
+
|
data/rerun.gemspec
ADDED
@@ -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 = 'rerun-cj'
|
6
|
+
s.version = '0.10.0'
|
7
|
+
|
8
|
+
s.description = "Restarts your app when a file changes. A no-frills, command-line alternative to Guard, Shotgun, Autotest, etc."
|
9
|
+
s.summary = "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
|
+
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', '~> 2.7', '>= 2.7.3'
|
29
|
+
|
30
|
+
s.homepage = "http://github.com/alexch/rerun/"
|
31
|
+
s.require_paths = %w[lib]
|
32
|
+
|
33
|
+
s.license = 'MIT'
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rerun-cj
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.10.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alex Chaffee
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-09-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: listen
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.7'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 2.7.3
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '2.7'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 2.7.3
|
33
|
+
description: Restarts your app when a file changes. A no-frills, command-line alternative
|
34
|
+
to Guard, Shotgun, Autotest, etc.
|
35
|
+
email: alex@stinky.com
|
36
|
+
executables:
|
37
|
+
- rerun
|
38
|
+
extensions: []
|
39
|
+
extra_rdoc_files:
|
40
|
+
- README.md
|
41
|
+
files:
|
42
|
+
- LICENSE
|
43
|
+
- README.md
|
44
|
+
- Rakefile
|
45
|
+
- bin/rerun
|
46
|
+
- icons/rails_grn_sml.png
|
47
|
+
- icons/rails_red_sml.png
|
48
|
+
- lib/rerun.rb
|
49
|
+
- lib/rerun/glob.rb
|
50
|
+
- lib/rerun/options.rb
|
51
|
+
- lib/rerun/runner.rb
|
52
|
+
- lib/rerun/system.rb
|
53
|
+
- lib/rerun/watcher.rb
|
54
|
+
- rerun.gemspec
|
55
|
+
homepage: http://github.com/alexch/rerun/
|
56
|
+
licenses:
|
57
|
+
- MIT
|
58
|
+
metadata: {}
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubyforge_project:
|
75
|
+
rubygems_version: 2.2.2
|
76
|
+
signing_key:
|
77
|
+
specification_version: 2
|
78
|
+
summary: Launches an app, and restarts it whenever the filesystem changes. A no-frills,
|
79
|
+
command-line alternative to Guard, Shotgun, Autotest, etc.
|
80
|
+
test_files: []
|