relaunch 0.0.1 → 0.15.0

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,376 @@
1
+ require 'timeout'
2
+ require 'io/wait'
3
+
4
+ module Rerun
5
+ class Runner
6
+
7
+ # The watcher instance that wait for changes
8
+ attr_reader :watcher
9
+
10
+ def self.keep_running(cmd, options)
11
+ runner = new(cmd, options)
12
+ runner.start
13
+ runner.join
14
+ # apparently runner doesn't keep running anymore (as of Listen 2) so we have to sleep forever :-(
15
+ sleep 10000 while true # :-(
16
+ end
17
+
18
+ include System
19
+ include ::Timeout
20
+
21
+ def initialize(run_command, options = {})
22
+ @run_command, @options = run_command, options
23
+ @run_command = "ruby #{@run_command}" if @run_command.split(' ').first =~ /\.rb$/
24
+ @options[:directory] ||= options.delete(:dir) || '.'
25
+ @options[:ignore] ||= []
26
+ end
27
+
28
+ def start_keypress_thread
29
+ return if @options[:background]
30
+
31
+ @keypress_thread = Thread.new do
32
+ while true
33
+ if c = key_pressed
34
+ case c.downcase
35
+ when 'c'
36
+ say "Clearing screen"
37
+ clear_screen
38
+ when 'r'
39
+ say "Restarting"
40
+ restart
41
+ when 'f'
42
+ say "Stopping and starting"
43
+ restart(false)
44
+ when 'p'
45
+ toggle_pause
46
+ when 'x', 'q'
47
+ die
48
+ break # the break will stop this thread, in case the 'die' doesn't
49
+ else
50
+ puts "\n#{c.inspect} pressed inside rerun"
51
+ puts [["c", "clear screen"],
52
+ ["r", "restart"],
53
+ ["f", "forced restart (stop and start)"],
54
+ ["p", "toggle pause"],
55
+ ["x or q", "stop and exit"]
56
+ ].map {|key, description| " #{key} -- #{description}"}.join("\n")
57
+ puts
58
+ end
59
+ end
60
+ sleep 1 # todo: use select instead of polling somehow?
61
+ end
62
+ end
63
+ @keypress_thread.run
64
+ end
65
+
66
+ def stop_keypress_thread
67
+ @keypress_thread.kill if @keypress_thread
68
+ @keypress_thread = nil
69
+ end
70
+
71
+ def restart(with_signal = true)
72
+ @restarting = true
73
+ if @options[:restart] && with_signal
74
+ restart_with_signal(@options[:signal])
75
+ else
76
+ stop
77
+ start
78
+ end
79
+ @restarting = false
80
+ end
81
+
82
+ def toggle_pause
83
+ unless @pausing
84
+ say "Pausing. Press 'p' again to resume."
85
+ @watcher.pause
86
+ @pausing = true
87
+ else
88
+ say "Resuming."
89
+ @watcher.unpause
90
+ @pausing = false
91
+ end
92
+ end
93
+
94
+ def unpause
95
+ @watcher.unpause
96
+ end
97
+
98
+ def dir
99
+ @options[:directory]
100
+ end
101
+
102
+ def pattern
103
+ @options[:pattern]
104
+ end
105
+
106
+ def clear?
107
+ @options[:clear]
108
+ end
109
+
110
+ def quiet?
111
+ @options[:quiet]
112
+ end
113
+
114
+ def verbose?
115
+ @options[:verbose]
116
+ end
117
+
118
+ def exit?
119
+ @options[:exit]
120
+ end
121
+
122
+ def app_name
123
+ @options[:name]
124
+ end
125
+
126
+ def restart_with_signal(restart_signal)
127
+ if @pid && (@pid != 0)
128
+ notify "restarting", "We will be with you shortly."
129
+ send_signal(restart_signal)
130
+ end
131
+ end
132
+
133
+ def force_polling
134
+ @options[:force_polling]
135
+ end
136
+
137
+ def start
138
+ if @already_running
139
+ taglines = [
140
+ "Here we go again!",
141
+ "Keep on trucking.",
142
+ "Once more unto the breach, dear friends, once more!",
143
+ "The road goes ever on and on, down from the door where it began.",
144
+ ]
145
+ notify "restarted", taglines[rand(taglines.size)]
146
+ else
147
+ taglines = [
148
+ "To infinity... and beyond!",
149
+ "Charge!",
150
+ ]
151
+ notify "launched", taglines[rand(taglines.size)]
152
+ @already_running = true
153
+ end
154
+
155
+ clear_screen if clear?
156
+ start_keypress_thread unless @keypress_thread
157
+
158
+ begin
159
+ @pid = run @run_command
160
+ say "Rerun (#{$PID}) running #{app_name} (#{@pid})"
161
+ rescue => e
162
+ puts "#{e.class}: #{e.message}"
163
+ raise ExitException
164
+ end
165
+
166
+ status_thread = Process.detach(@pid) # so if the child exits, it dies
167
+
168
+ Signal.trap("INT") do # INT = control-C -- allows user to stop the top-level rerun process
169
+ die
170
+ end
171
+
172
+ Signal.trap("TERM") do # TERM is the polite way of terminating a process
173
+ die
174
+ end
175
+
176
+ begin
177
+ sleep 2
178
+ rescue Interrupt => e
179
+ # in case someone hits control-C immediately ("oops!")
180
+ die
181
+ end
182
+
183
+ if exit?
184
+ status = status_thread.value
185
+ if status.success?
186
+ notify "succeeded", ""
187
+ else
188
+ notify "failed", "Exit status #{status.exitstatus}"
189
+ end
190
+ else
191
+ if !running?
192
+ notify "Launch Failed", "See console for error output"
193
+ @already_running = false
194
+ end
195
+ end
196
+
197
+ unless @watcher
198
+ watcher = Watcher.new(@options) do |changes|
199
+ message = change_message(changes)
200
+ say "Change detected: #{message}"
201
+ restart unless @restarting
202
+ end
203
+ watcher.start
204
+ @watcher = watcher
205
+ ignore = @options[:ignore]
206
+ say "Watching #{dir.join(', ')} for #{pattern}" +
207
+ (ignore.empty? ? "" : " (ignoring #{ignore.join(',')})") +
208
+ (watcher.adapter.nil? ? "" : " with #{watcher.adapter_name} adapter")
209
+ end
210
+ end
211
+
212
+ def run command
213
+ Kernel.spawn command
214
+ end
215
+
216
+ def change_message(changes)
217
+ message = [:modified, :added, :removed].map do |change|
218
+ count = changes[change] ? changes[change].size : 0
219
+ if count > 0
220
+ "#{count} #{change}"
221
+ end
222
+ end.compact.join(", ")
223
+
224
+ changed_files = changes.values.flatten
225
+ if changed_files.count > 0
226
+ message += ": "
227
+ message += changes.values.flatten[0..3].map {|path| path.split('/').last}.join(', ')
228
+ if changed_files.count > 3
229
+ message += ", ..."
230
+ end
231
+ end
232
+ message
233
+ end
234
+
235
+ def die
236
+ #stop_keypress_thread # don't do this since we're probably *in* the keypress thread
237
+ stop # stop the child process if it exists
238
+ raise ExitException # todo: status code param
239
+ end
240
+
241
+ def join
242
+ @watcher.join
243
+ end
244
+
245
+ def running?
246
+ send_signal(0)
247
+ end
248
+
249
+ # Send the signal to process @pid and wait for it to die.
250
+ # @returns true if the process dies
251
+ # @returns false if either sending the signal fails or the process fails to die
252
+ def signal_and_wait(signal)
253
+
254
+ signal_sent = if windows?
255
+ force_kill = (signal == 'KILL')
256
+ system("taskkill /T #{'/F' if force_kill} /PID #{@pid}")
257
+ else
258
+ send_signal(signal)
259
+ end
260
+
261
+ if signal_sent
262
+ # the signal was successfully sent, so wait for the process to die
263
+ begin
264
+ timeout(@options[:wait]) do
265
+ Process.wait(@pid)
266
+ end
267
+ process_status = $?
268
+ say "Process ended: #{process_status}" if verbose?
269
+ true
270
+ rescue Timeout::Error
271
+ false
272
+ end
273
+ else
274
+ false
275
+ end
276
+ end
277
+
278
+ # Send the signal to process @pid.
279
+ # @returns true if the signal is sent
280
+ # @returns false if sending the signal fails
281
+ # If sending the signal fails, the exception will be swallowed
282
+ # (and logged if verbose is true) and this method will return false.
283
+ #
284
+ def send_signal(signal)
285
+ say "Sending signal #{signal} to #{@pid}" unless signal == 0 if verbose?
286
+ Process.kill(signal, @pid)
287
+ true
288
+ rescue => e
289
+ say "Signal #{signal} failed: #{e.class}: #{e.message}" if verbose?
290
+ false
291
+ end
292
+
293
+ # todo: test escalation
294
+ def stop
295
+ if @pid && (@pid != 0)
296
+ notify "stopping", "All good things must come to an end." unless @restarting
297
+ @options[:signal].split(',').each do |signal|
298
+ success = signal_and_wait(signal)
299
+ return true if success
300
+ end
301
+ end
302
+ rescue
303
+ false
304
+ end
305
+
306
+ def git_head_changed?
307
+ old_git_head = @git_head
308
+ read_git_head
309
+ @git_head and old_git_head and @git_head != old_git_head
310
+ end
311
+
312
+ def read_git_head
313
+ git_head_file = File.join(dir, '.git', 'HEAD')
314
+ @git_head = File.exist?(git_head_file) && File.read(git_head_file)
315
+ end
316
+
317
+ def notify(title, body, background = true)
318
+ Notification.new(title, body, @options).send(background) if @options[:notify]
319
+ puts
320
+ say "#{app_name} #{title}"
321
+ end
322
+
323
+ def say msg
324
+ puts "#{Time.now.strftime("%T")} [rerun] #{msg}" unless quiet?
325
+ end
326
+
327
+ def stty(args)
328
+ system "stty #{args}"
329
+ end
330
+
331
+ # non-blocking stdin reader.
332
+ # returns a 1-char string if a key was pressed; otherwise nil
333
+ #
334
+ def key_pressed
335
+ return one_char if windows?
336
+ begin
337
+ # this "raw input" nonsense is because unix likes waiting for linefeeds before sending stdin
338
+
339
+ # 'raw' means turn raw input on
340
+
341
+ # restore proper output newline handling -- see "man stty" and /usr/include/sys/termios.h
342
+ # looks like "raw" flips off the OPOST bit 0x00000001 /* enable following output processing */
343
+ # which disables #define ONLCR 0x00000002 /* map NL to CR-NL (ala CRMOD) */
344
+ # so this sets it back on again since all we care about is raw input, not raw output
345
+ stty "raw opost"
346
+ one_char
347
+ ensure
348
+ stty "-raw" # turn raw input off
349
+ end
350
+
351
+ # note: according to 'man tty' the proper way restore the settings is
352
+ # tty_state=`stty -g`
353
+ # ensure
354
+ # system 'stty "#{tty_state}'
355
+ # end
356
+ # but this way seems fine and less confusing
357
+ end
358
+
359
+ def clear_screen
360
+ # see http://ascii-table.com/ansi-escape-sequences-vt-100.php
361
+ $stdout.print "\033[H\033[2J"
362
+ end
363
+
364
+ private
365
+ def one_char
366
+ c = nil
367
+ msg = $stdin.respond_to?(:wait_readable) ? :wait_readable : :ready?
368
+
369
+ if $stdin.send(msg)
370
+ c = $stdin.getc
371
+ end
372
+ c.chr if c
373
+ end
374
+
375
+ end
376
+ end
@@ -0,0 +1,22 @@
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|mingw32)/i
10
+ end
11
+
12
+ def linux?
13
+ RUBY_PLATFORM =~ /linux/i
14
+ end
15
+
16
+ def rails?
17
+ rails_sig_file = File.expand_path(".")+"/config/boot.rb"
18
+ File.exist? rails_sig_file
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,134 @@
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, :ignore_dotfiles
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
+ :ignore_dotfiles => true,
37
+ }.merge(options)
38
+
39
+ @pattern = options[:pattern]
40
+ @directories = options[:directory]
41
+ @directories = sanitize_dirs(@directories)
42
+ @priority = options[:priority]
43
+ @force_polling = options[:force_polling]
44
+ @ignore = [options[:ignore]].flatten.compact
45
+ @ignore_dotfiles = options[:ignore_dotfiles]
46
+ @thread = nil
47
+ end
48
+
49
+ def sanitize_dirs(dirs)
50
+ dirs = [*dirs]
51
+ dirs.map do |d|
52
+ d.chomp!("/")
53
+ unless FileTest.exist?(d) && FileTest.readable?(d) && FileTest.directory?(d)
54
+ raise InvalidDirectoryError, "Directory '#{d}' either doesnt exist or isn't readable"
55
+ end
56
+ File.expand_path(d)
57
+ end
58
+ end
59
+
60
+ def start
61
+ if @thread then
62
+ raise RuntimeError, "already started"
63
+ end
64
+
65
+ @thread = Thread.new do
66
+ @listener = Listen.to(*@directories, only: watching, ignore: ignoring, wait_for_delay: 1, force_polling: @force_polling) do |modified, added, removed|
67
+ count = modified.size + added.size + removed.size
68
+ if count > 0
69
+ @client_callback.call(:modified => modified, :added => added, :removed => removed)
70
+ end
71
+ end
72
+ @listener.start
73
+ end
74
+
75
+ @thread.priority = @priority
76
+
77
+ sleep 0.1 until @listener
78
+
79
+ at_exit { stop } # try really hard to clean up after ourselves
80
+ end
81
+
82
+ def watching
83
+ Rerun::Glob.new(@pattern).to_regexp
84
+ end
85
+
86
+ def ignoring
87
+ patterns = []
88
+ if ignore_dotfiles
89
+ patterns << /^\.[^.]/ # at beginning of string, a real dot followed by any other character
90
+ end
91
+ patterns + @ignore.map { |x| Rerun::Glob.new(x).to_regexp }
92
+ end
93
+
94
+ # kill the file watcher thread
95
+ def stop
96
+ @thread.wakeup rescue ThreadError
97
+ begin
98
+ @listener.stop
99
+ rescue Exception => e
100
+ puts "#{e.class}: #{e.message} stopping listener"
101
+ end
102
+ @thread.kill rescue ThreadError
103
+ end
104
+
105
+ # wait for the file watcher to finish
106
+ def join
107
+ @thread.join if @thread
108
+ rescue Interrupt
109
+ # don't care
110
+ end
111
+
112
+ def pause
113
+ @listener.pause if @listener
114
+ end
115
+
116
+ def unpause
117
+ @listener.start if @listener
118
+ end
119
+
120
+ def running?
121
+ @listener && @listener.processing?
122
+ end
123
+
124
+ def adapter
125
+ @listener &&
126
+ (backend = @listener.instance_variable_get(:@backend)) &&
127
+ backend.instance_variable_get(:@adapter)
128
+ end
129
+
130
+ def adapter_name
131
+ adapter && adapter.class.name.split('::').last
132
+ end
133
+ end
134
+ end
data/lib/rerun.rb ADDED
@@ -0,0 +1,17 @@
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/notification"
8
+ require "rerun/runner"
9
+ require "rerun/watcher"
10
+ require "rerun/glob"
11
+
12
+ module Rerun
13
+ # Raised when the runner wants to exit cleanly.
14
+ # This allows tests to catch the exit instead of terminating the process.
15
+ class ExitException < StandardError; end
16
+ end
17
+
data/relaunch.gemspec ADDED
@@ -0,0 +1,44 @@
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 = 'relaunch'
6
+ s.version = '0.15.0'
7
+ s.required_ruby_version = '>= 3.3'
8
+
9
+ s.description = "Restarts your app when a file changes. A no-frills, command-line alternative to Guard, Shotgun, Autotest, etc."
10
+ s.summary = "Launches an app, and restarts it whenever the filesystem changes. A no-frills, command-line alternative to Guard, Shotgun, Autotest, etc."
11
+
12
+ s.authors = ["Alex Chaffee", "Patrik Ragnarsson"]
13
+ s.email = ["alexch@gmail.com", "patrik@starkast.net"]
14
+
15
+ s.files = %w[
16
+ README.md
17
+ Changelog.md
18
+ Fork.md
19
+ LICENSE
20
+ Rakefile
21
+ relaunch.gemspec
22
+ bin/rerun
23
+ bin/rerun.bat
24
+ bin/relaunch
25
+ bin/relaunch.bat
26
+ icons/rails_grn_sml.png
27
+ icons/rails_red_sml.png] +
28
+ Dir['lib/**/*.rb']
29
+ s.executables = ['rerun', 'relaunch']
30
+ s.test_files = s.files.select {|path| path =~ /^spec\/.*_spec.rb/}
31
+
32
+ s.extra_rdoc_files = %w[README.md]
33
+
34
+ s.add_runtime_dependency 'listen', '~> 3.0'
35
+
36
+ s.homepage = "https://github.com/spinels/relaunch"
37
+ s.metadata["homepage_uri"] = s.homepage
38
+ s.metadata["changelog_uri"] = "#{s.homepage}/blob/main/Changelog.md"
39
+ s.metadata["source_code_uri"] = "#{s.homepage}/tree/main"
40
+ s.metadata["rubygems_mfa_required"] = "true"
41
+ s.require_paths = %w[lib]
42
+
43
+ s.license = 'MIT'
44
+ end
metadata CHANGED
@@ -1,24 +1,69 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relaunch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
- - Placeholder
7
+ - Alex Chaffee
8
+ - Patrik Ragnarsson
8
9
  bindir: bin
9
10
  cert_chain: []
10
11
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
12
- executables: []
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: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ description: Restarts your app when a file changes. A no-frills, command-line alternative
28
+ to Guard, Shotgun, Autotest, etc.
29
+ email:
30
+ - alexch@gmail.com
31
+ - patrik@starkast.net
32
+ executables:
33
+ - relaunch
34
+ - rerun
13
35
  extensions: []
14
- extra_rdoc_files: []
36
+ extra_rdoc_files:
37
+ - README.md
15
38
  files:
39
+ - Changelog.md
40
+ - Fork.md
41
+ - LICENSE
42
+ - README.md
43
+ - Rakefile
44
+ - bin/relaunch
45
+ - bin/relaunch.bat
46
+ - bin/rerun
47
+ - bin/rerun.bat
48
+ - icons/rails_grn_sml.png
49
+ - icons/rails_red_sml.png
16
50
  - lib/relaunch.rb
17
- homepage: https://github.com/spinels/elsewhere
51
+ - lib/rerun.rb
52
+ - lib/rerun/glob.rb
53
+ - lib/rerun/notification.rb
54
+ - lib/rerun/options.rb
55
+ - lib/rerun/runner.rb
56
+ - lib/rerun/system.rb
57
+ - lib/rerun/watcher.rb
58
+ - relaunch.gemspec
59
+ homepage: https://github.com/spinels/relaunch
18
60
  licenses:
19
61
  - MIT
20
- metadata: {}
21
- post_install_message: Coming soon.
62
+ metadata:
63
+ homepage_uri: https://github.com/spinels/relaunch
64
+ changelog_uri: https://github.com/spinels/relaunch/blob/main/Changelog.md
65
+ source_code_uri: https://github.com/spinels/relaunch/tree/main
66
+ rubygems_mfa_required: 'true'
22
67
  rdoc_options: []
23
68
  require_paths:
24
69
  - lib
@@ -26,14 +71,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
26
71
  requirements:
27
72
  - - ">="
28
73
  - !ruby/object:Gem::Version
29
- version: '3.0'
74
+ version: '3.3'
30
75
  required_rubygems_version: !ruby/object:Gem::Requirement
31
76
  requirements:
32
77
  - - ">="
33
78
  - !ruby/object:Gem::Version
34
79
  version: '0'
35
80
  requirements: []
36
- rubygems_version: 4.0.19
37
- specification_version: 4
38
- summary: Placeholder gem
81
+ rubygems_version: 4.0.16
82
+ specification_version: 2
83
+ summary: Launches an app, and restarts it whenever the filesystem changes. A no-frills,
84
+ command-line alternative to Guard, Shotgun, Autotest, etc.
39
85
  test_files: []