siding 0.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.
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+ require_relative "protocol"
5
+ require_relative "life_cycle"
6
+ require_relative "staleness"
7
+ require_relative "invocation"
8
+
9
+ module Siding
10
+ class Worker
11
+ PRESERVED_KEYS = %w[
12
+ SIDING_SERVER
13
+ ].freeze
14
+
15
+ RESOLUTION_KEY = Invocation::RESOLUTION_KEY
16
+ REVISION_KEY = Invocation::REVISION_KEY
17
+ BOOT_SECONDS_KEY = Invocation::BOOT_SECONDS_KEY
18
+
19
+ # A backtrace line belongs to the tool itself, not the application, if it is either a frame
20
+ # inside this file's own directory or the `-e:` frame `Server::BOOTSTRAP` runs as (server.rb
21
+ # spawns the server with `ruby -e`, so that frame has no path of its own to match). Shared
22
+ # between the uncaught-exception path and the rspec path so the two cannot drift apart.
23
+ HARNESS_FRAME_PATTERN = /\A(?:#{Regexp.escape(__dir__)}\/|-e:)/
24
+
25
+ attr_reader :connection, :message, :project_key, :manifest, :verdict, :revision_label, :boot_seconds
26
+
27
+ def initialize(connection:, message:, project_key:, manifest:, verdict:, boot_seconds: nil)
28
+ @connection = connection
29
+ @message = message
30
+ @project_key = project_key
31
+ @manifest = manifest
32
+ @verdict = verdict
33
+ @revision_label = verdict.revision_label
34
+ @boot_seconds = boot_seconds
35
+ end
36
+
37
+ def run
38
+ detach_from_server
39
+ streams = Protocol.receive_streams(connection)
40
+ install_streams(streams)
41
+ apply_environment
42
+ apply_working_directory
43
+ LifeCycle.repair_after_fork
44
+ refresh_application
45
+ announce_state
46
+ watch_the_client
47
+ execute
48
+ rescue Protocol::TruncatedMessage
49
+ exit! 1
50
+ rescue SystemExit => e
51
+ exit e.status
52
+ rescue SignalException => e
53
+ report_uncaught(e)
54
+ die_of(e)
55
+ rescue Exception => e
56
+ report_uncaught(e)
57
+ exit! 1
58
+ end
59
+
60
+ private
61
+
62
+ def detach_from_server
63
+ Process.setpgrp
64
+ rescue SystemCallError
65
+ nil
66
+ end
67
+
68
+ def install_streams(streams)
69
+ $stdin.reopen(streams[:stdin])
70
+ $stdout.reopen(streams[:stdout])
71
+ $stderr.reopen(streams[:stderr])
72
+
73
+ $stdout.sync = true
74
+ $stderr.sync = true
75
+
76
+ close_inherited_console
77
+ end
78
+
79
+ def close_inherited_console
80
+ IO.console(:close) if IO.respond_to?(:console)
81
+ rescue StandardError
82
+ nil
83
+ end
84
+
85
+ def apply_environment
86
+ incoming = message["env"]
87
+ return unless incoming.is_a?(Hash)
88
+
89
+ preserved = PRESERVED_KEYS.to_h { |key| [key, ENV.fetch(key, nil)] }
90
+ ENV.replace(incoming)
91
+ preserved.each { |key, value| value.nil? ? ENV.delete(key) : ENV[key] = value }
92
+ end
93
+
94
+ def apply_working_directory
95
+ cwd = message["cwd"]
96
+ Dir.chdir(cwd) if cwd && File.directory?(cwd)
97
+ end
98
+
99
+ def refresh_application
100
+ return unless verdict.reloadable?
101
+
102
+ reloader = application_reloader
103
+ return if reloader.nil?
104
+
105
+ reloader.reload!
106
+ @revision_label = Staleness.validate(manifest).revision_label
107
+ end
108
+
109
+ def application_reloader
110
+ return nil unless defined?(::Rails) && ::Rails.respond_to?(:application)
111
+
112
+ application = ::Rails.application
113
+ return nil unless application.respond_to?(:reloader)
114
+
115
+ reloader = application.reloader
116
+ reloader.respond_to?(:reload!) ? reloader : nil
117
+ end
118
+
119
+ def announce_state
120
+ ENV[RESOLUTION_KEY] = resolution
121
+ ENV[REVISION_KEY] = revision_label
122
+ ENV[BOOT_SECONDS_KEY] = format("%.3f", boot_seconds) if boot_seconds
123
+ end
124
+
125
+ def resolution
126
+ message["restarted"] ? "rebuild" : verdict.resolution
127
+ end
128
+
129
+ def watch_the_client
130
+ Thread.new do
131
+ loop do
132
+ message = Protocol.read_message(connection)
133
+ break if message.nil?
134
+
135
+ relay(message["name"]) if message.type == Protocol::SIGNAL
136
+ end
137
+ rescue StandardError
138
+ nil
139
+ ensure
140
+ terminate_process_group
141
+ end
142
+ end
143
+
144
+ def relay(name)
145
+ return if name.nil?
146
+
147
+ Process.kill(name, 0)
148
+ rescue ArgumentError, SystemCallError
149
+ nil
150
+ end
151
+
152
+ def terminate_process_group
153
+ Process.kill("TERM", 0)
154
+ rescue StandardError
155
+ nil
156
+ end
157
+
158
+ def die_of(error)
159
+ number = error.signo
160
+ exit!(1) if number.nil?
161
+
162
+ Signal.trap(number, "SYSTEM_DEFAULT")
163
+ Process.kill(number, Process.pid)
164
+ sleep 0.05
165
+ exit! 128 + number
166
+ rescue ArgumentError, SystemCallError, NoMethodError
167
+ exit! 1
168
+ end
169
+
170
+ def execute
171
+ argv = Array(message["argv"])
172
+ executable = argv.first
173
+ args = argv[1..] || []
174
+
175
+ $PROGRAM_NAME = executable
176
+
177
+ case executable
178
+ when "rails" then run_rails(args)
179
+ when "rake" then run_rake(args)
180
+ when "rspec" then run_rspec(args)
181
+ when "test" then run_rails(["test", *args])
182
+ else
183
+ raise Error, "no in-process runner for #{executable.inspect}"
184
+ end
185
+
186
+ exit 0
187
+ end
188
+
189
+ def run_rails(args)
190
+ require "rails/command"
191
+
192
+ aliases = {
193
+ "g" => "generate",
194
+ "d" => "destroy",
195
+ "c" => "console",
196
+ "s" => "server",
197
+ "db" => "dbconsole",
198
+ "r" => "runner",
199
+ "t" => "test"
200
+ }
201
+
202
+ ARGV.replace(args)
203
+
204
+ command = args.shift
205
+ command = aliases[command] || command
206
+ define_app_path if command == "server"
207
+
208
+ ::Rails::Command.invoke(command, args)
209
+ end
210
+
211
+ def define_app_path
212
+ return if defined?(::APP_PATH)
213
+
214
+ Object.const_set(:APP_PATH, File.join(File.realpath(project_key.app_root), "config", "application"))
215
+ end
216
+
217
+ def run_rake(args)
218
+ require "rake"
219
+ ARGV.replace(args)
220
+ app = ::Rake.application
221
+ app.standard_exception_handling do
222
+ app.init("rake", args)
223
+ suppress_worker_frames(app)
224
+ app.load_rakefile
225
+ app.top_level
226
+ end
227
+ end
228
+
229
+ def suppress_worker_frames(app)
230
+ existing = app.options.suppress_backtrace_pattern || ::Rake::Backtrace::SUPPRESS_PATTERN
231
+ app.options.suppress_backtrace_pattern = Regexp.union(existing, /\A#{Regexp.escape(__dir__)}/)
232
+ rescue StandardError
233
+ nil
234
+ end
235
+
236
+ def run_rspec(args)
237
+ require "rspec/core"
238
+ ::RSpec::Core::Runner.disable_autorun! if ::RSpec::Core::Runner.respond_to?(:disable_autorun!)
239
+ suppress_worker_frames_in_rspec
240
+ exit ::RSpec::Core::Runner.run(args, $stderr, $stdout)
241
+ end
242
+
243
+ def suppress_worker_frames_in_rspec
244
+ ::RSpec.configure { |config| config.backtrace_exclusion_patterns << HARNESS_FRAME_PATTERN }
245
+ rescue StandardError
246
+ nil
247
+ end
248
+
249
+ def report_uncaught(error)
250
+ error.set_backtrace(application_frames(error.backtrace)) if error.backtrace
251
+ $stderr.write(error.full_message)
252
+ rescue StandardError
253
+ nil
254
+ end
255
+
256
+ def application_frames(backtrace)
257
+ frames = backtrace.dup
258
+ frames.pop while frames.last && harness_frame?(frames.last)
259
+ frames
260
+ end
261
+
262
+ def harness_frame?(frame)
263
+ HARNESS_FRAME_PATTERN.match?(frame)
264
+ end
265
+ end
266
+ end
data/lib/siding.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "siding/error"
4
+ require_relative "siding/version"
5
+ require_relative "siding/platform"
6
+ require_relative "siding/project_key"
7
+ require_relative "siding/runtime"
8
+ require_relative "siding/logger"
9
+ require_relative "siding/protocol"
10
+ require_relative "siding/life_cycle"
11
+ require_relative "siding/invocation"
12
+ require_relative "siding/boot_component"
13
+
14
+ module Siding
15
+ class << self
16
+ def accelerated? = Invocation.accelerated?
17
+ def resolution = Invocation.resolution
18
+ def revision = Invocation.revision
19
+ def boot_seconds = Invocation.boot_seconds
20
+ def invocation = Invocation.to_h
21
+
22
+ def before_fork(name = nil, &block) = LifeCycle.before_fork(name, &block)
23
+ def after_fork(name = nil, &block) = LifeCycle.after_fork(name, &block)
24
+
25
+ def boot_component(*paths) = BootComponent.declare(*paths)
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: siding
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Yuji Yaginuma
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '8.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: watchcat
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.6'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.6'
40
+ email:
41
+ - yuuji.yaginuma@gmail.com
42
+ executables:
43
+ - siding
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - CLAUDE.md
48
+ - CODE_OF_CONDUCT.md
49
+ - LICENSE.txt
50
+ - README.md
51
+ - Rakefile
52
+ - exe/siding
53
+ - lib/siding.rb
54
+ - lib/siding/boot_component.rb
55
+ - lib/siding/cli.rb
56
+ - lib/siding/client.rb
57
+ - lib/siding/error.rb
58
+ - lib/siding/invocation.rb
59
+ - lib/siding/life_cycle.rb
60
+ - lib/siding/load_manifest.rb
61
+ - lib/siding/logger.rb
62
+ - lib/siding/platform.rb
63
+ - lib/siding/project_key.rb
64
+ - lib/siding/protocol.rb
65
+ - lib/siding/restarter.rb
66
+ - lib/siding/runtime.rb
67
+ - lib/siding/server.rb
68
+ - lib/siding/staleness.rb
69
+ - lib/siding/version.rb
70
+ - lib/siding/watch.rb
71
+ - lib/siding/worker.rb
72
+ homepage: https://github.com/y-yagi/siding
73
+ licenses:
74
+ - MIT
75
+ metadata:
76
+ allowed_push_host: https://rubygems.org
77
+ homepage_uri: https://github.com/y-yagi/siding
78
+ source_code_uri: https://github.com/y-yagi/siding
79
+ rubygems_mfa_required: 'true'
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: 3.2.0
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 4.0.17
95
+ specification_version: 4
96
+ summary: A Rails application preloader that never serves stale code.
97
+ test_files: []