Class: Lich::DependencyRecovery
- Inherits:
-
Object
- Object
- Lich::DependencyRecovery
- Defined in:
- documented/dependency_recovery.rb
Overview
Restores approved runtime gems from the Ruby4Lich5 manifest when an installed runtime is incomplete or a native extension cannot load. The manifest is deliberately an allow-list: a gem name is never turned into a URL, shell command, or RubyGems request by this class.
A manifest is a release asset controlled by the same human-reviewed promotion as the gem bundle. Its SHA-256 values protect against accidental corruption and mismatched assets; they are not a substitute for signing, because the manifest and assets currently share a publisher.
Defined Under Namespace
Constant Summary collapse
- DEFAULT_MANIFEST_URL =
Default HTTPS URL to the Ruby4Lich5 gem bundle manifest.
'https://github.com/Lich5/Ruby4Lich5/releases/download/' \ 'R4L5-gem-bundle-x64-mingw-ucrt/R4L5-gem-manifest.json'
- LOCK_FILENAME =
'.lich-dependency-recovery.lock'- HTTP_OPEN_TIMEOUT_SECONDS =
10- HTTP_READ_TIMEOUT_SECONDS =
60- SHA256_PATTERN =
Regexp matching a SHA-256 digest in the format required by manifest entries.
/\Asha256:[0-9a-f]{64}\z/- SAFE_FILENAME =
Regexp matching a safe archive member filename.
Allows any non-empty sequence of characters except backslash and forward slash, but rejects pure dot sequences (.", ..", etc.) to prevent directory traversal during ZIP extraction.
/\A(?!\.+\z)[^\\\/]+\z/- SAFE_NAME =
Regexp matching a safe gem or unit name in the manifest.
Restricts names to alphanumeric characters, underscore, period, and hyphen. This prevents manifest names from being misinterpreted as shell commands or path components.
/\A[a-zA-Z0-9_.-]+\z/
Class Method Summary collapse
- .log_helper_failure(payload_path, error) ⇒ void
-
.run_windows_replacement(payload_path) ⇒ Boolean
Runs in a detached Ruby process after the original Lich process exits.
Instance Method Summary collapse
-
#initialize(manifest_url: ENV.fetch('LICH_GEM_MANIFEST_URL', DEFAULT_MANIFEST_URL), gem_home: Gem.dir, temp_dir: (TEMP_DIR) ? TEMP_DIR : Dir.tmpdir), http_get: nil, install_gem: nil, extract_zip: nil, powershell_runner: nil, helper_launcher: nil) ⇒ DependencyRecovery
constructor
A new instance of DependencyRecovery.
-
#recover(gem_names, force: false, plan: nil) ⇒ Result
Downloads, verifies, and installs a previously approved manifest plan.
-
#recovery_plan(gem_names) ⇒ Plan
Loads and validates the manifest units covering
gem_names, without downloading gem artifacts.
Constructor Details
#initialize(manifest_url: ENV.fetch('LICH_GEM_MANIFEST_URL', DEFAULT_MANIFEST_URL), gem_home: Gem.dir, temp_dir: (TEMP_DIR) ? TEMP_DIR : Dir.tmpdir), http_get: nil, install_gem: nil, extract_zip: nil, powershell_runner: nil, helper_launcher: nil) ⇒ DependencyRecovery
Returns a new instance of DependencyRecovery.
100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'documented/dependency_recovery.rb', line 100 def initialize(manifest_url: ENV.fetch('LICH_GEM_MANIFEST_URL', DEFAULT_MANIFEST_URL), gem_home: Gem.dir, temp_dir: (defined?(TEMP_DIR) ? TEMP_DIR : Dir.tmpdir), http_get: nil, install_gem: nil, extract_zip: nil, powershell_runner: nil, helper_launcher: nil) @manifest_url = manifest_url @gem_home = gem_home @temp_dir = temp_dir @http_get = http_get @install_gem = install_gem || method(:install_gem_file) @extract_zip = extract_zip || method(:extract_zip_file) @powershell_runner = powershell_runner || Open3.method(:capture3) @helper_launcher = helper_launcher || method(:launch_windows_helper) end |
Class Method Details
.log_helper_failure(payload_path, error) ⇒ void
This method returns an undefined value.
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'documented/dependency_recovery.rb', line 191 def self.log_helper_failure(payload_path, error) payload = JSON.parse(File.read(payload_path)) rescue {} log_path = File.join(payload.fetch('temp_dir', Dir.tmpdir), 'lich5-missing-gems.log') File.open(log_path, 'a') do |file| file.puts "[#{Time.now}] Lich5 native gem replacement failure" file.puts " #{error.class}: #{error.}" file.puts end return unless Gem.win_platform? require 'win32ole' WIN32OLE.new('WScript.Shell').Popup( "Lich could not update the GTK runtime. See #{log_path} for details.", 0, 'Lich5: Ruby Gem Recovery', 16 ) rescue StandardError nil end |
.run_windows_replacement(payload_path) ⇒ Boolean
Runs in a detached Ruby process after the original Lich process exits. The payload contains only already-hash-verified package paths and the original Lich invocation; artifacts are re-verified before replacement.
178 179 180 181 182 183 184 185 186 |
# File 'documented/dependency_recovery.rb', line 178 def self.run_windows_replacement(payload_path) payload = JSON.parse(File.read(payload_path)) recovery = new(gem_home: payload.fetch('gem_home'), temp_dir: payload.fetch('temp_dir')) recovery.send(:run_windows_replacement!, payload) true rescue StandardError => e log_helper_failure(payload_path, e) false end |
Instance Method Details
#recover(gem_names, force: false, plan: nil) ⇒ Result
Downloads, verifies, and installs a previously approved manifest plan.
Installation always targets Gem.dir (or the injected runtime directory),
never a user-controlled GEM_HOME.
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'documented/dependency_recovery.rb', line 138 def recover(gem_names, force: false, plan: nil) planned = plan || recovery_plan(gem_names) return Result.new(installed_gems: [], error: planned.error) unless planned.success? result = with_recovery_workspace do |work_dir| artifact_cache = {} # No runtime lock or Gem.dir write occurs until every requested unit is # downloaded, extracted, and hash-verified. An unreachable release # endpoint therefore fails closed without changing the runtime. staged_units = planned.units.map { |unit| stage_unit(unit, work_dir, artifact_cache) } with_install_lock do replacement, direct = staged_units.partition { |staged| native_runtime_unit?(staged.fetch(:unit)) } installed = direct.flat_map { |staged| install_staged_unit(staged, force: force) } if replacement.empty? Result.new(installed_gems: installed) elsif replacement.length == 1 schedule_runtime_replacement(replacement.first, work_dir) Result.new(installed_gems: installed, restart_required: true) else raise Error, 'multiple native runtime replacement units are not supported in one recovery' end end end refresh_rubygems! unless result.restart_required result rescue Error, JSON::ParserError, OpenURI::HTTPError, SocketError, SystemCallError => e Result.new(installed_gems: [], error: e.) rescue StandardError => e # Recovery runs during boot. Keep an unexpected implementation failure # from turning into an unhelpful backtrace before GemCheck can report it. Result.new(installed_gems: [], error: "#{e.class}: #{e.}") end |
#recovery_plan(gem_names) ⇒ Plan
Loads and validates the manifest units covering gem_names, without
downloading gem artifacts. Callers use this to request consent first.
119 120 121 122 123 124 125 126 127 128 |
# File 'documented/dependency_recovery.rb', line 119 def recovery_plan(gem_names) requested = Array(gem_names).map(&:to_s).reject(&:empty?).uniq return Plan.new(units: []) if requested.empty? Plan.new(units: units_for(load_manifest, requested)) rescue Error, JSON::ParserError, OpenURI::HTTPError, SocketError, SystemCallError => e Plan.new(units: [], error: e.) rescue StandardError => e Plan.new(units: [], error: "#{e.class}: #{e.}") end |