again 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+