rerun 0.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.
data/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ rerun
2
+ Copyright (c) 2009 Alex Chaffee <tomayko.com/about>
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 uses code from Rspactor
24
+ [insert license]
25
+
26
+ rerun uses code from FileSystemWatcher
27
+ http://paulhorman.com/filesystemwatcher/
28
+ [insert license]
29
+
data/README ADDED
@@ -0,0 +1,21 @@
1
+ Rerun
2
+
3
+ # Usage
4
+
5
+
6
+
7
+ # Other projects that do similar things
8
+
9
+ Restartomatic
10
+ http://github.com/adammck/restartomatic
11
+
12
+ Shotgun
13
+ http://github.com/rtomayko/shotgun
14
+
15
+
16
+ # Todo
17
+
18
+ .rerun config file
19
+ include/exclude dirs
20
+ include/exclude patterns
21
+
data/Rakefile ADDED
@@ -0,0 +1,64 @@
1
+ require 'rake'
2
+ require 'rake/clean'
3
+ require 'rake/testtask'
4
+ require 'spec/rake/spectask'
5
+
6
+ task :default => [:spec]
7
+ task :test => :spec
8
+
9
+ desc "Run all specs"
10
+ Spec::Rake::SpecTask.new('spec') do |t|
11
+ ENV['ENV'] = "test"
12
+ t.spec_files = FileList['spec/**/*_spec.rb']
13
+ t.ruby_opts = ['-rubygems'] if defined? Gem
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
25
+ end
26
+
27
+ def package(ext='')
28
+ "pkg/#{$spec.name}-#{$spec.version}" + ext
29
+ end
30
+
31
+ desc 'Build packages'
32
+ task :package => %w[.gem .tar.gz].map { |e| package(e) }
33
+
34
+ desc 'Build and install as local gem'
35
+ task :install => package('.gem') do
36
+ sh "gem install #{package('.gem')}"
37
+ end
38
+
39
+ directory 'pkg/'
40
+ CLOBBER.include('pkg')
41
+
42
+ file package('.gem') => %W[pkg/ #{$spec.name}.gemspec] + $spec.files do |f|
43
+ sh "gem build #{$spec.name}.gemspec"
44
+ mv File.basename(f.name), f.name
45
+ end
46
+
47
+ file package('.tar.gz') => %w[pkg/] + $spec.files do |f|
48
+ cmd = <<-SH
49
+ git archive \
50
+ --prefix=#{$spec.name}-#{$spec.version}/ \
51
+ --format=tar \
52
+ HEAD | gzip > #{f.name}
53
+ SH
54
+ sh cmd.gsub(/ +/, ' ')
55
+ end
56
+
57
+ desc 'Publish gem and tarball to rubyforge'
58
+ task 'release' => [package('.gem'), package('.tar.gz')] do |t|
59
+ sh <<-end
60
+ rubyforge add_release #{$rubyforge_project} #{$spec.name} #{$spec.version} #{package('.gem')} &&
61
+ rubyforge add_file #{$rubyforge_project} #{$spec.name} #{$spec.version} #{package('.tar.gz')}
62
+ end
63
+ end
64
+ 5
data/bin/rerun ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+
5
+ libdir = "#{File.expand_path(File.dirname(File.dirname(__FILE__)))}/lib"
6
+ puts "adding #{libdir}"
7
+ $LOAD_PATH.unshift libdir unless $LOAD_PATH.include?(libdir)
8
+
9
+ load 'rerun.gemspec' # defines "$spec" variable
10
+
11
+ require 'optparse'
12
+
13
+ options = {}
14
+
15
+ opts = OptionParser.new("", 24, ' ') { |opts|
16
+ opts.banner = "Usage: rerun cmd"
17
+
18
+ opts.separator ""
19
+ opts.separator "Options:"
20
+
21
+ opts.on_tail("-h", "--help", "--usage", "show this message") do
22
+ puts opts
23
+ exit
24
+ end
25
+
26
+ opts.on_tail("--version", "show version") do
27
+ puts $spec.version
28
+ exit
29
+ end
30
+
31
+ opts.parse! ARGV
32
+ }
33
+
34
+ #config = ARGV[0] || "config.ru"
35
+ #abort "configuration #{config} not found" unless File.exist? config
36
+ #
37
+ #if config =~ /\.ru$/ && File.read(config)[/^#\\(.*)/]
38
+ # opts.parse! $1.split(/\s+/)
39
+ #end
40
+
41
+ if ARGV.empty?
42
+ puts opts
43
+ exit
44
+ end
45
+
46
+ require 'rerun'
47
+ cmd = ARGV.join(" ")
48
+ Rerun::Runner.new(cmd, options).start
data/lib/fswatcher.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'watcher'
2
+
3
+ module Rerun
4
+ class FSWatcher < Watcher
5
+ end
6
+ end
data/lib/osxwatcher.rb ADDED
@@ -0,0 +1,105 @@
1
+ require "system"
2
+ require "watcher"
3
+
4
+ begin
5
+ require 'osx/foundation'
6
+ OSX.require_framework '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework'
7
+ rescue MissingSourceFile
8
+ # this is to not fail when running on a non-Mac
9
+ end
10
+
11
+ # stolen from RSpactor, http://github.com/mislav/rspactor
12
+ # based on http://rails.aizatto.com/2007/11/28/taming-the-autotest-beast-with-fsevents/
13
+
14
+ #TODO: make it notice deleted files
15
+ require "watcher"
16
+ module Rerun
17
+ class OSXWatcher < Rerun::Watcher
18
+ attr_reader :last_check, :valid_extensions
19
+ attr_reader :stream
20
+
21
+ def start
22
+ prime
23
+ timestamp_checked
24
+
25
+ dirs = Array(directories.map{|d| d.dir})
26
+
27
+ mac_callback = lambda do |stream, ctx, num_events, paths, marks, event_ids|
28
+ examine
29
+ # changed_files = extract_changed_files_from_paths(split_paths(paths, num_events))
30
+ # timestamp_checked
31
+ # puts "changed files:"
32
+ # p changed_files
33
+ # yield changed_files unless changed_files.empty?
34
+ end
35
+
36
+ @stream = OSX::FSEventStreamCreate(OSX::KCFAllocatorDefault, mac_callback, nil, dirs, OSX::KFSEventStreamEventIdSinceNow, @sleep_time, 0)
37
+ raise "Failed to create stream" unless stream
38
+
39
+ OSX::FSEventStreamScheduleWithRunLoop(stream, OSX::CFRunLoopGetCurrent(), OSX::KCFRunLoopDefaultMode)
40
+ unless OSX::FSEventStreamStart(stream)
41
+ raise "Failed to start stream"
42
+ end
43
+
44
+ @thread = Thread.new do
45
+ begin
46
+ OSX::CFRunLoopRun()
47
+ rescue Interrupt
48
+ OSX::FSEventStreamStop(stream)
49
+ OSX::FSEventStreamInvalidate(stream)
50
+ OSX::FSEventStreamRelease(stream)
51
+ @stream = nil
52
+ end
53
+ end
54
+
55
+ @thread.priority = @priority
56
+ end
57
+
58
+ def stop
59
+ @thread.kill
60
+ end
61
+
62
+ def timestamp_checked
63
+ @last_check = Time.now
64
+ end
65
+
66
+ def split_paths(paths, num_events)
67
+ paths.regard_as('*')
68
+ rpaths = []
69
+ num_events.times { |i| rpaths << paths[i] }
70
+ rpaths
71
+ end
72
+
73
+ def extract_changed_files_from_paths(paths)
74
+ changed_files = []
75
+ paths.each do |path|
76
+ next if ignore_path?(path)
77
+ Dir.glob(path + "*").each do |file|
78
+ next if ignore_file?(file)
79
+ changed_files << file if file_changed?(file)
80
+ end
81
+ end
82
+ changed_files
83
+ end
84
+
85
+ def file_changed?(file)
86
+ File.stat(file).mtime > last_check
87
+ end
88
+
89
+ def ignore_path?(path)
90
+ path =~ /(?:^|\/)\.(git|svn)/
91
+ end
92
+
93
+ def ignore_file?(file)
94
+ File.basename(file).index('.') == 0 or not valid_extension?(file)
95
+ end
96
+
97
+ def file_extension(file)
98
+ file =~ /\.(\w+)$/ and $1
99
+ end
100
+
101
+ def valid_extension?(file)
102
+ valid_extensions.nil? or valid_extensions.include?(file_extension(file))
103
+ end
104
+ end
105
+ end
data/lib/rerun.rb ADDED
@@ -0,0 +1,111 @@
1
+ require "system"
2
+ require "watcher"
3
+ require "osxwatcher"
4
+ require "fswatcher"
5
+
6
+ # todo: make this work in non-Mac and non-Unix environments (also Macs without growlnotify)
7
+ module Rerun
8
+ class Runner
9
+
10
+ include System
11
+
12
+ def initialize(run_command, options = {})
13
+ @run_command, @options = run_command, options
14
+ end
15
+
16
+ def restart
17
+ stop
18
+ start
19
+ end
20
+
21
+ def start
22
+ if (!@already_running)
23
+ taglines = [
24
+ "To infinity... and beyond!",
25
+ "Charge!",
26
+ ]
27
+ notify "Launching", taglines[rand(taglines.size)]
28
+ @already_running = true
29
+ else
30
+ taglines = [
31
+ "Here we go again!",
32
+ "Once more unto the breach, dear friends, once more!",
33
+ ]
34
+ notify "Restarting", taglines[rand(taglines.size)]
35
+ end
36
+
37
+ @pid = Kernel.fork do
38
+ Signal.trap("HUP") { stop; exit }
39
+ exec(@run_command)
40
+ end
41
+
42
+ Process.detach(@pid)
43
+
44
+ begin
45
+ sleep 2
46
+ rescue Interrupt => e
47
+ # in case someone hits control-C immediately
48
+ stop
49
+ exit
50
+ end
51
+
52
+ unless running?
53
+ notify "Launch Failed", "See console for error output"
54
+ @already_running = false
55
+ end
56
+
57
+ watcher_class = osx? ? OSXWatcher : FSWatcher
58
+ # watcher_class = FSWatcher
59
+
60
+ watcher = watcher_class.new do
61
+ restart
62
+ end
63
+ watcher.add_directory(".", "**/*.rb")
64
+ watcher.sleep_time = 1
65
+ watcher.start
66
+ watcher.join
67
+
68
+ end
69
+
70
+ def running?
71
+ signal(0)
72
+ end
73
+
74
+ def signal(signal)
75
+ Process.kill(signal, @pid)
76
+ true
77
+ rescue
78
+ false
79
+ end
80
+
81
+ def stop
82
+ if @pid && @pid != 0
83
+ notify "Stopping"
84
+ signal("KILL") && Process.wait(@pid)
85
+ end
86
+ rescue
87
+ false
88
+ end
89
+
90
+ def git_head_changed?
91
+ old_git_head = @git_head
92
+ read_git_head
93
+ @git_head and old_git_head and @git_head != old_git_head
94
+ end
95
+
96
+ def read_git_head
97
+ git_head_file = File.join(dir, '.git', 'HEAD')
98
+ @git_head = File.exists?(git_head_file) && File.read(git_head_file)
99
+ end
100
+
101
+ def notify(title, body)
102
+ growl title, body if has_growl?
103
+ puts
104
+ puts "#{Time.now.strftime("%T")} - #{app_name} #{title}: #{body}"
105
+ puts
106
+ end
107
+
108
+ end
109
+
110
+ end
111
+
data/lib/system.rb ADDED
@@ -0,0 +1,36 @@
1
+
2
+ # are we on OSX or not?
3
+ begin
4
+ require 'osx/foundation'
5
+ OSX.require_framework '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework'
6
+ $osx = true
7
+ rescue MissingSourceFile
8
+ # this is to not fail when running on a non-Mac
9
+ end
10
+
11
+ module Rerun
12
+ module System
13
+ def osx?
14
+ $osx
15
+ end
16
+
17
+ # do we have growl or not?
18
+ def has_growl?
19
+ growlcmd != ""
20
+ end
21
+
22
+ def growlcmd
23
+ `which growlnotify`.chomp
24
+ end
25
+
26
+ def app_name
27
+ # todo: make sure this works in non-Mac and non-Unix environments
28
+ File.expand_path(".").gsub(/^.*\//, '').capitalize
29
+ end
30
+
31
+ def growl(title, body)
32
+ `#{growlcmd} -n \"#{app_name}\" -m \"#{body}\" \"#{app_name} #{title}\" &`
33
+ end
34
+
35
+ end
36
+ end
data/lib/watcher.rb ADDED
@@ -0,0 +1,203 @@
1
+
2
+ Thread.abort_on_exception = true
3
+
4
+ # This class will watch a directory or a set of directories and alert you of
5
+ # new files, modified files, deleted files.
6
+ #
7
+ # Author: Paul Horman, http://paulhorman.com/filesystemwatcher/
8
+ # Author: Alex Chaffee
9
+ module Rerun
10
+ class Watcher
11
+ CREATED = 0
12
+ MODIFIED = 1
13
+ DELETED = 2
14
+
15
+ attr_accessor :sleep_time, :priority
16
+ attr_reader :directories
17
+
18
+ def initialize(&client_callback)
19
+ @client_callback = client_callback
20
+
21
+ @sleep_time = 1
22
+ @priority = 0
23
+
24
+ @directories = []
25
+ @files = []
26
+
27
+ @found = nil
28
+ @first_time = true
29
+ @thread = nil
30
+
31
+ end
32
+
33
+ # add a directory to be watched
34
+ # @param dir the directory to watch
35
+ # @param expression the glob pattern to search under the watched directory
36
+ def add_directory(dir, expression="**/*")
37
+ if FileTest.exists?(dir) && FileTest.readable?(dir) then
38
+ @directories << Directory.new(dir, expression)
39
+ else
40
+ raise InvalidDirectoryError, "Dir '#{dir}' either doesnt exist or isnt readable"
41
+ end
42
+ end
43
+
44
+ def remove_directory(dir)
45
+ @directories.delete(dir)
46
+ end
47
+
48
+ # add a specific file to the watch list
49
+ # @param file the file to watch
50
+ def add_file(file)
51
+ if FileTest.exists?(file) && FileTest.readable?(file) then
52
+ @files << file
53
+ else
54
+ raise InvalidFileError, "File '#{file}' either doesnt exist or isnt readable"
55
+ end
56
+ end
57
+
58
+ def remove_file(file)
59
+ @files.delete(file)
60
+ end
61
+
62
+ def prime
63
+ @first_time = true
64
+ @found = Hash.new()
65
+ examine
66
+ @first_time = false
67
+ end
68
+
69
+ def start
70
+ if @thread then
71
+ raise RuntimeError, "already started"
72
+ end
73
+
74
+ prime
75
+
76
+ @thread = Thread.new do
77
+ while true do
78
+ examine
79
+ sleep(@sleep_time)
80
+ end
81
+ end
82
+
83
+ @thread.priority = @priority
84
+
85
+ at_exit { stop } #?
86
+
87
+ end
88
+
89
+ # kill the filewatcher thread
90
+ def stop
91
+ begin
92
+ @thread.wakeup
93
+ rescue ThreadError => e
94
+ # ignore
95
+ end
96
+ begin
97
+ @thread.kill
98
+ rescue ThreadError => e
99
+ # ignore
100
+ end
101
+ end
102
+
103
+ # wait for the filewatcher to finish
104
+ def join
105
+ @thread.join() if @thread
106
+ rescue Interrupt => e
107
+ # don't care
108
+ end
109
+
110
+ private
111
+
112
+ def examine
113
+ already_examined = Hash.new()
114
+
115
+ @directories.each do |directory|
116
+ examine_files(directory.files(), already_examined)
117
+ end
118
+
119
+ examine_files(@files, already_examined) if not @files.empty?
120
+
121
+ # now diff the found files and the examined files to see if
122
+ # something has been deleted
123
+ all_found_files = @found.keys()
124
+ all_examined_files = already_examined.keys()
125
+ intersection = all_found_files - all_examined_files
126
+ intersection.each do |file_name|
127
+ @client_callback.call(DELETED, file_name)
128
+ @found.delete(file_name)
129
+ end
130
+
131
+ end
132
+
133
+ # loops over the file list check for new or modified files
134
+ def examine_files(files, already_examined)
135
+ files.each do |file_name|
136
+ # expand the file name to the fully qual path
137
+ full_file_name = File.expand_path(file_name)
138
+
139
+ # we cant do much if the file isnt readable anyway
140
+ if File.readable?(full_file_name) then
141
+ already_examined[full_file_name] = true
142
+ stat = File.stat(full_file_name)
143
+ mod_time = stat.mtime
144
+ size = stat.size
145
+
146
+ # on the first iteration just load all of the files into the foundList
147
+ if @first_time then
148
+ @found[full_file_name] = FoundFile.new(full_file_name, mod_time, size)
149
+ else
150
+ # see if we have found this file already
151
+ found_file = @found[full_file_name]
152
+
153
+ if found_file
154
+ if mod_time > found_file.mod_time || size != found_file.size then
155
+ @client_callback.call(MODIFIED, full_file_name)
156
+ end
157
+ else
158
+ @client_callback.call(CREATED, full_file_name)
159
+ end
160
+ @found[full_file_name] = FoundFile.new(full_file_name, mod_time, size)
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ class Directory
167
+ attr_reader :dir, :expression
168
+
169
+ def initialize(dir, expression)
170
+ @dir, @expression = dir, expression
171
+ @dir.chop! if @dir =~ %r{/$}
172
+ end
173
+
174
+ def files()
175
+ return Dir[@dir + "/" + @expression]
176
+ end
177
+ end
178
+
179
+ class FoundFile
180
+ attr_reader :status, :file_name, :mod_time, :size
181
+
182
+ def initialize(file_name, mod_time, size)
183
+ @file_name, @mod_time, @size = file_name, mod_time, size
184
+ end
185
+
186
+ def modified(mod_time)
187
+ @mod_time = mod_time
188
+ end
189
+
190
+ def to_s
191
+ "FoundFile[file_name=#{file_name}, mod_time=#{mod_time.to_i}, size=#{size}]"
192
+ end
193
+ end
194
+
195
+ # if the directory you want to watch doesnt exist or isn't readable this is thrown
196
+ class InvalidDirectoryError < StandardError;
197
+ end
198
+
199
+ # if the file you want to watch doesnt exist or isn't readable this is thrown
200
+ class InvalidFileError < StandardError;
201
+ end
202
+ end
203
+ end
data/rerun.gemspec ADDED
@@ -0,0 +1,38 @@
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'
6
+ s.version = '0.1'
7
+ s.date = '2009-06-14'
8
+
9
+ s.description = "Restarts your app when a file changes"
10
+ s.summary = s.description + ", man."
11
+
12
+ s.authors = ["Alex Chaffee"]
13
+ s.email = "alex@stinky.com"
14
+
15
+ s.files = %w[
16
+ README
17
+ LICENSE
18
+ Rakefile
19
+ rerun.gemspec
20
+ bin/rerun
21
+ lib/rerun.rb
22
+ lib/fswatcher.rb
23
+ lib/osxwatcher.rb
24
+ lib/system.rb
25
+ lib/watcher.rb
26
+ ]
27
+ s.executables = ['rerun']
28
+ s.test_files = s.files.select {|path| path =~ /^spec\/.*_spec.rb/}
29
+
30
+ s.extra_rdoc_files = %w[README]
31
+ #s.add_dependency 'rack', '>= 0.9.1'
32
+ #s.add_dependency 'launchy', '>= 0.3.3', '< 1.0'
33
+
34
+ s.homepage = "http://github.com/alexch/rerun/"
35
+ s.require_paths = %w[lib]
36
+ #s.rubyforge_project = ''
37
+ s.rubygems_version = '1.1.1'
38
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rerun
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.1"
5
+ platform: ruby
6
+ authors:
7
+ - Alex Chaffee
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-06-14 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Restarts your app when a file changes
17
+ email: alex@stinky.com
18
+ executables:
19
+ - rerun
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README
24
+ files:
25
+ - README
26
+ - LICENSE
27
+ - Rakefile
28
+ - rerun.gemspec
29
+ - bin/rerun
30
+ - lib/rerun.rb
31
+ - lib/fswatcher.rb
32
+ - lib/osxwatcher.rb
33
+ - lib/system.rb
34
+ - lib/watcher.rb
35
+ has_rdoc: true
36
+ homepage: http://github.com/alexch/rerun/
37
+ licenses: []
38
+
39
+ post_install_message:
40
+ rdoc_options: []
41
+
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ version:
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ requirements: []
57
+
58
+ rubyforge_project:
59
+ rubygems_version: 1.3.3
60
+ signing_key:
61
+ specification_version: 2
62
+ summary: Restarts your app when a file changes, man.
63
+ test_files: []
64
+