again 1.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .DS_Store
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in again.gemspec
4
+ gemspec
@@ -0,0 +1,201 @@
1
+ # again.rb #
2
+
3
+ ## DESCRIPTION ##
4
+
5
+ again is the best way to iteratively develop with Ruby.
6
+
7
+ It monitors your application's code or any required library
8
+ for changes. In the likely event of a change, the code is
9
+ automatically loaded again.
10
+
11
+ ## Why? ##
12
+
13
+ After seeing a [live stream](http://www.youtube.com/watch?v=zdLDYUNRErQ&feature=player_detailpage#t=532s)
14
+ of Markus Persson [hyper-incrementally developing a game for a 48h game development competition in Java](
15
+ http://news.ycombinator.com/item?id=2911696), where he debugged
16
+ his 3d rendering code by just changing expressions, hitting save
17
+ and instantly seeing the effect, I wanted to have support for
18
+ hyper-iterative development in Ruby.
19
+
20
+ Change a color, save, switch to your application and it's changed.
21
+
22
+ again has been tested with Ruby 1.8, Ruby 1.9 and JRuby 1.6.2.
23
+ I use it for my personal development, including [Sinatra](
24
+ http://www.sinatrarb.com/) web development as well as
25
+ [Ruby/Gosu](http://www.libgosu.org/) game development.
26
+
27
+ (If anybody has a better link for the video of Markus Persson working incrementally,
28
+ [please drop me a mail](mailto:florian.s.gross@web.de)!)
29
+
30
+
31
+ ## INSTALL ##
32
+
33
+ gem install again
34
+
35
+
36
+ ## SYNOPSIS ##
37
+
38
+ ### Getting started with again ###
39
+
40
+ All you need to do to setup automatic reloading of your
41
+ code is to require again:
42
+
43
+ ```ruby
44
+ require 'again' # ...and again and again
45
+ ```
46
+
47
+ You should do this after all other libraries of your application
48
+ have already been loaded: Make it your last `require`.
49
+
50
+
51
+ ### Controlling what to execute again ###
52
+
53
+ #### Not running start-up logic again ####
54
+
55
+ When again detects a change to a file, it will reexecute all code
56
+ in that file. This also applies to the main file of your application,
57
+ the one you actually run from the command line.
58
+
59
+ For method and class definitions, this is no problem and things will
60
+ work exactly as you expect.
61
+
62
+ You should however wrap the actual start-up logic of your application
63
+ like this:
64
+
65
+ ```ruby
66
+ # Current file being executed from command line?
67
+ if __FILE__ == $PROGRAM_NAME then
68
+ # Show them windows
69
+ window = GameWindow.new
70
+ window.show
71
+ end
72
+ ```
73
+
74
+ This is something that is good style anyway (in order to allow your
75
+ code to be used as a library): The above would only be executed on
76
+ direct execution from the command line; and not when the file is
77
+ loaded as a library. [More detailed explanation of `__FILE__ == $PROGRAM_NAME`](http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/184607)
78
+
79
+ again actually invests some extra effort to make sure that
80
+ `__FILE__ == $PROGRAM_NAME` blocks will not be run again
81
+ after reloading your main application file.
82
+
83
+
84
+ #### Running additional code on reloads ####
85
+
86
+ You can also have logic that is only run on reloads:
87
+
88
+ ```ruby
89
+ if Again.reloaded? then
90
+ puts "Hello again"
91
+ end
92
+ ```
93
+
94
+ This is useful when you want to reset your application into a
95
+ predictable state or when your frameworks need some special logic
96
+ to make sure that your changes will be picked up. (Such as clearing
97
+ routes in Sinatra, [see below](#sinatra-again).)
98
+
99
+
100
+ ### gosu again ###
101
+
102
+ I'm using again for game development with Ruby, together with
103
+ [Ruby/Gosu](http://www.libgosu.org/). My code typically looks like this:
104
+
105
+ ```ruby
106
+ require 'rubygems'
107
+ require 'gosu'
108
+ # ...
109
+ require 'again'
110
+
111
+ include Gosu
112
+
113
+ class GameWindow < Gosu::Window
114
+ def initialize()
115
+ @objects = ...
116
+ end
117
+
118
+ def update()
119
+ @objects.each(&:update)
120
+ rescue Exception => err
121
+ handle_error(err)
122
+ end
123
+
124
+ def draw()
125
+ @objects.each(&:draw)
126
+ rescue Exception => err
127
+ handle_error(err)
128
+ end
129
+
130
+ def handle_error(err)
131
+ # Simple logic, but it does the trick
132
+ STDERR.puts err, err.backtrace
133
+ sleep 3
134
+ end
135
+ end
136
+
137
+ # Current file being executed from command line?
138
+ if __FILE__ == $PROGRAM_NAME then
139
+ # Let's play
140
+ window = GameWindow.new
141
+ window.show
142
+ end
143
+ ```
144
+
145
+ Rescuing exceptions in `update()` and `draw()` and
146
+ handling them in a central place is a very good idea.
147
+ If you have uncaught exceptions in there, Gosu will
148
+ shutdown your application. This would kill our nice
149
+ incremental development loop in the case of an error.
150
+
151
+
152
+ ### sinatra again ###
153
+
154
+ I also use again to incrementalize my [Sinatra](http://www.libgosu.org/) development.
155
+ This is very similar to the above Gosu example, but with
156
+ one special twist:
157
+
158
+ ```ruby
159
+ require 'rubygems'
160
+ require 'sinatra'
161
+ require 'json'
162
+ # ...
163
+ require 'again'
164
+
165
+ if Again.reloaded? then
166
+ # Here's the twist
167
+ Sinatra::Application.routes.clear
168
+ end
169
+
170
+ get '/' do
171
+ return "Awesome!"
172
+ end
173
+ ```
174
+
175
+ We need to clear the Application routes first. Otherwise
176
+ sinatra won't let us redefine our routes. That's all!
177
+
178
+
179
+ ## LICENSE ##
180
+
181
+ (The MIT License)
182
+
183
+ Copyright (c) 2012, Florian Gross <florian.s.gross@web.de>
184
+
185
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
186
+ this software and associated documentation files (the "Software"), to deal in
187
+ the Software without restriction, including without limitation the rights to
188
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
189
+ of the Software, and to permit persons to whom the Software is furnished to do
190
+ so, subject to the following conditions:
191
+
192
+ The above copyright notice and this permission notice shall be included in all
193
+ copies or substantial portions of the Software.
194
+
195
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
196
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
197
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
198
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
199
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
200
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
201
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "again/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "again"
7
+ s.version = Again::VERSION
8
+
9
+ s.authors = ["Florian Gross"]
10
+ s.email = ["Florian.S.Gross@web.de"]
11
+ s.homepage = "https://github.com/flgr/again"
12
+ s.summary = %q{The best way to iteratively develop with Ruby}
13
+ s.description = %q{again automatically reloads your application's code when it changes}
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = [] # `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ # specify any dependencies here; for example:
21
+ # s.add_development_dependency "rspec"
22
+ s.add_runtime_dependency "listen"
23
+ end
@@ -0,0 +1,9 @@
1
+ lib_dir = File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
2
+ $LOAD_PATH << lib_dir
3
+
4
+ require 'again'
5
+
6
+ if __FILE__ == $0 then
7
+ puts "Again loaded"
8
+ Again.join
9
+ end
@@ -0,0 +1,198 @@
1
+ require 'again/version'
2
+ require 'again/extras'
3
+ require 'again/patches'
4
+
5
+ require 'set'
6
+ require 'pathname'
7
+ require 'tmpdir'
8
+ require 'singleton'
9
+ require 'fileutils'
10
+
11
+ require 'rubygems'
12
+ require 'listen'
13
+
14
+ class Again
15
+ include Singleton
16
+
17
+ def self.start() self.instance end
18
+ def self.join() instance.join end
19
+ def self.reloaded?() instance.reloaded? end
20
+
21
+ def reloaded?() @reloaded end
22
+
23
+ def join()
24
+ # Can't simply join @watch_thread here as a refresh() can stop it.
25
+ # We want to survive refreshes so we instead join a thread which
26
+ # keeps checking for new watch_threads...
27
+ Thread.new do
28
+ loop do
29
+ if @watch_thread then
30
+ @watch_thread.alive?() ? @watch_thread.join : sleep(0.5)
31
+ end
32
+ end
33
+ end.join
34
+ end
35
+
36
+ private
37
+
38
+ def with_reloaded()
39
+ old_reloaded, @reloaded = @reloaded, true
40
+ yield
41
+ ensure
42
+ @reloaded = old_reloaded
43
+ end
44
+
45
+ def initialize()
46
+ @reloaded, @handler = false, nil
47
+ @watch_threads = ThreadGroup.new
48
+ @full_prog_name = File.expand_path($PROGRAM_NAME)
49
+ refresh()
50
+ end
51
+
52
+ def refresh()
53
+ refresh_libraries
54
+ refresh_handler
55
+ end
56
+
57
+ def refresh_libraries()
58
+ @base_paths = find_base_paths()
59
+ @libraries = find_libraries()
60
+ end
61
+
62
+ def find_features()
63
+ features = $LOADED_FEATURES.dup
64
+
65
+ # Allow realoding of main file (which is not a loaded library
66
+ # and thus does not appear in $LOADED_FEATURES)
67
+ features << @full_prog_name
68
+
69
+ # features doesn't include this file by default since
70
+ # technically it is still in the process of being loaded
71
+ # ...so manually include it
72
+ features << File.basename(__FILE__)
73
+
74
+ return features
75
+ end
76
+
77
+ # Avoid warnings on self-reload
78
+ remove_const(:JRUBY_PSEUDO_LIBS) if defined?(JRUBY_PSEUDO_LIBS)
79
+ remove_const(:PSEUDO_LIBS) if defined?(PSEUDO_LIBS)
80
+
81
+ # Probably incomplete...
82
+ JRUBY_PSEUDO_LIBS = %w(enumerable.jar enumerator.jar thread.rb tempfile.rb
83
+ java.rb jruby.rb socket.jar strscan.jar iconv.jar stringio.jar etc.jar
84
+ fcntl.rb timeout.rb digest/md5.jar jsignal_internal.rb rbconfig.rb
85
+ jruby/util.rb digest/sha1.jar digest.jar jruby/path_helper.rb)
86
+
87
+ PSEUDO_LIBS = %w(enumerable.so enumerator.so)
88
+
89
+ PSEUDO_LIBS.push(*JRUBY_PSEUDO_LIBS) if defined? JRUBY_VERSION
90
+
91
+ def pseudo_lib?(lib) PSEUDO_LIBS.include?(lib) end
92
+
93
+ RELOAD_MARKER = "--reload-temp--" unless defined?(RELOAD_MARKER)
94
+
95
+ def find_base_paths()
96
+ base_paths = *$LOAD_PATH.uniq
97
+ base_paths << File.dirname(@full_prog_name)
98
+
99
+ base_paths.map! { |path| File.expand_path(path) }
100
+ base_paths.reject! { |path| not File.exist?(path) or path.include?(RELOAD_MARKER) }
101
+ base_paths.uniq!
102
+
103
+ return base_paths
104
+ end
105
+
106
+ def find_libraries()
107
+ libraries, features = Set[], find_features
108
+
109
+ features.each do |rel_lib|
110
+ next if rel_lib.include?(RELOAD_MARKER)
111
+
112
+ res_lib = resolve_library(rel_lib)
113
+
114
+ if res_lib then
115
+ libraries << res_lib
116
+ elsif not pseudo_lib?(rel_lib) then
117
+ warn "Failed to resolve library %s to full path" % rel_lib
118
+ end
119
+ end
120
+
121
+ return libraries
122
+ end
123
+
124
+ def resolve_library(rel_lib)
125
+ return rel_lib if Pathname.new(rel_lib).absolute?
126
+
127
+ @base_paths.each do |path|
128
+ full_lib = File.join(path, rel_lib)
129
+ return full_lib if File.exist?(full_lib)
130
+ end
131
+
132
+ return nil
133
+ end
134
+
135
+ def refresh_handler()
136
+ # Our old @watch_thread will finish running after this
137
+ @handler.stop if @handler
138
+
139
+ @handler = Listen.to(*@base_paths)
140
+ @handler.change(&method(:on_change))
141
+
142
+ @watch_thread = Thread.new { @handler.start(true) }
143
+ end
144
+
145
+ def on_change(modified, added, removed)
146
+ (modified + added).each do |path|
147
+ reload(path) if @libraries.include?(path)
148
+ end
149
+ end
150
+
151
+ def reload(path)
152
+ STDERR.puts "Reloading %s" % path
153
+
154
+ # We don't want __FILE__ == $0 conditions to evaluate to true on reloads
155
+ # to avoid duplicate startup logic execution issues...
156
+ #
157
+ # So we intentionally create a copy of the file in that case to make
158
+ # those checks evaluate to false.
159
+
160
+ load_path = path
161
+ tmp_dir = nil
162
+
163
+ if @full_prog_name == path then
164
+ time = Time.now.to_i.to_s
165
+ tmp_dir = File.join(Dir.tmpdir, RELOAD_MARKER + "-dir-" + time)
166
+ Dir.mkdir(tmp_dir)
167
+
168
+ load_path = File.join(tmp_dir, RELOAD_MARKER + File.basename(load_path))
169
+
170
+ begin
171
+ # Try to hardlink first...
172
+ FileUtils.ln(path, load_path)
173
+ rescue Exception
174
+ # ...if that fails, create a copy.
175
+ FileUtils.copy(path, load_path)
176
+ end
177
+ end
178
+
179
+ begin
180
+ with_reloaded { load(load_path) }
181
+ rescue Exception => e
182
+ STDERR.puts e, "", e.backtrace.join("\n")
183
+ end
184
+
185
+ # need to do this from separate thread...
186
+ # we can not call handler.stop() from the thread
187
+ # that is getting the handler's callback
188
+ Thread.new { refresh }
189
+ ensure
190
+ FileUtils.rm_rf(tmp_dir) if tmp_dir
191
+ end
192
+ end
193
+
194
+ Again.start
195
+
196
+ if Again.reloaded? then
197
+ puts "Again reloaded"
198
+ end
@@ -0,0 +1,17 @@
1
+ class Again
2
+ def self.open_cmd()
3
+ (`which mate` rescue nil || "open").chomp
4
+ end
5
+
6
+ def self.open_lib(lib, cmd=open_cmd)
7
+ if lib = instance.resolve_library(lib) then
8
+ system(cmd, lib)
9
+ end
10
+ end
11
+
12
+ def self.open_libdir(lib, cmd=open_cmd)
13
+ if lib = instance.resolve_library(lib) then
14
+ system(cmd, File.dirname(lib))
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,2 @@
1
+ require 'again/patches/singleton'
2
+ require 'again/patches/listen'
@@ -0,0 +1,25 @@
1
+ require 'rubygems'
2
+ require 'listen'
3
+
4
+ class Listen::Adapter
5
+ def self.usable_and_works?(directories, options = {})
6
+ usable? #&& Array(directories).all? { |d| !writable?(d) || works?(d, options) }
7
+ end
8
+
9
+ def self.writable?(directory)
10
+ test_file = "#{directory}/.listen_test"
11
+ FileUtils.touch(test_file)
12
+ return true
13
+ rescue SystemCallError
14
+ return false
15
+ ensure
16
+ FileUtils.rm(test_file) if File.exists?(test_file)
17
+ end
18
+ end
19
+
20
+ # class Listen::DirectoryRecord
21
+ # # Don't use content-based modification at all (performance optimization)
22
+ # def content_modified?(path)
23
+ # return true
24
+ # end
25
+ # end
@@ -0,0 +1,17 @@
1
+ # Fix a bug in singleton: When the Singleton module is included multiple times
2
+ # in the same class, it will reset the instance variable on each inclusion.
3
+ #
4
+ # Together with again.rb this can easily result in lost state.
5
+ # So this version only sets the instance variable to nil if it was undefined.
6
+
7
+ require 'singleton'
8
+
9
+ class << Singleton
10
+ alias :included_without_guard :included
11
+
12
+ def included(klass)
13
+ return if klass.instance_variable_get(:@singleton_included)
14
+ included_without_guard(klass)
15
+ klass.instance_variable_set(:@singleton_included, true)
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ class Again
2
+ VERSION = "1.1.4"
3
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: again
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 1
9
+ - 4
10
+ version: 1.1.4
11
+ platform: ruby
12
+ authors:
13
+ - Florian Gross
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-07-02 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: listen
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ description: again automatically reloads your application's code when it changes
35
+ email:
36
+ - Florian.S.Gross@web.de
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - .gitignore
45
+ - Gemfile
46
+ - README.md
47
+ - Rakefile
48
+ - again.gemspec
49
+ - bin/again_test.rb
50
+ - lib/again.rb
51
+ - lib/again/extras.rb
52
+ - lib/again/patches.rb
53
+ - lib/again/patches/listen.rb
54
+ - lib/again/patches/singleton.rb
55
+ - lib/again/version.rb
56
+ homepage: https://github.com/flgr/again
57
+ licenses: []
58
+
59
+ post_install_message:
60
+ rdoc_options: []
61
+
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ hash: 3
70
+ segments:
71
+ - 0
72
+ version: "0"
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ hash: 3
79
+ segments:
80
+ - 0
81
+ version: "0"
82
+ requirements: []
83
+
84
+ rubyforge_project:
85
+ rubygems_version: 1.8.11
86
+ signing_key:
87
+ specification_version: 3
88
+ summary: The best way to iteratively develop with Ruby
89
+ test_files: []
90
+