rerun 0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+