Module: Rubino::UpdateCheck
- Defined in:
- lib/rubino/update_check.rb
Overview
Boot-time “new version available” notice + the ‘rubino update` mechanics.
Two decoupled concerns, mirroring how ‘gh`/update-notifier do it:
* SHOW (sync, zero network): `notice_from_cache` reads
<RUBINO_HOME>/update_check.json and returns a one-line notice only when
the cached `latest` is a valid Gem::Version strictly greater than the
running VERSION. Pure local read — cannot slow boot, works offline.
* REFRESH (out-of-band): `refresh_async_if_stale` spawns a detached,
fully-rescued Thread (≈1.5s timeout) that GETs RubyGems and rewrites the
cache for the NEXT boot. It is never joined, so this boot never blocks.
Gated to once/24h, TTY-only, not-in-CI, and skipped entirely when
RUBINO_NO_UPDATE_CHECK is set.
The whole feature no-ops until rubino-agent is actually published: RubyGems currently returns “version”:“unknown”, and “unknown” / non-semver / nil / any network error are all treated as “no info” → no notice.
Constant Summary collapse
- LATEST_URL =
"https://rubygems.org/api/v1/versions/rubino-agent/latest.json"- GEM_NAME =
"rubino-agent"- CACHE_FILE =
"update_check.json"- CHECK_INTERVAL =
24h, like gh/Homebrew
24 * 60 * 60
- NET_TIMEOUT =
1.5
Class Method Summary collapse
-
.cache_path ⇒ Object
—- cache ————————————————————.
- .cached_latest ⇒ Object
-
.checks_enabled? ⇒ Boolean
All must hold (mirrors gh): no opt-out env, interactive TTY, not CI.
- .clear_cache! ⇒ Object
-
.fetch_latest ⇒ Object
The latest published version string, or nil on failure / “unknown” / non-semver.
-
.gem_update_command ⇒ Object
Argv form (no shell) + active interpreter via Gem.ruby → updates the right install on a multi-Ruby machine and is injection-safe.
-
.install_method ⇒ Object
How rubino was installed: :gem when a matching RubyGems spec is loaded, else :source (dev checkout / built from source / vendored).
- .installed_gem_version(name) ⇒ Object
-
.newer?(latest) ⇒ Boolean
latest is a valid version strictly greater than the running VERSION.
-
.notice_from_cache ⇒ Object
One-line dim notice when a newer version is cached, else nil.
-
.opted_out? ⇒ Boolean
The user’s full opt-out: RUBINO_NO_UPDATE_CHECK set (to anything non-blank) disables refresh AND the cached boot notice.
-
.refresh_async_if_stale ⇒ Object
Refresh the cache in a detached thread iff enabled and stale.
-
.semver?(str) ⇒ Boolean
X.Y / X.Y.Z — strict enough to reject “unknown” and other garbage.
-
.stale? ⇒ Boolean
True when the cache is missing or its checked_at is older than 24h.
-
.write_cache(latest) ⇒ Object
Atomic write (temp + rename) so a crashed refresh never leaves a torn file.
Class Method Details
.cache_path ⇒ Object
—- cache ————————————————————
97 98 99 |
# File 'lib/rubino/update_check.rb', line 97 def cache_path File.join(Rubino::Config::Loader.default_home_path, CACHE_FILE) end |
.cached_latest ⇒ Object
101 102 103 104 105 106 107 |
# File 'lib/rubino/update_check.rb', line 101 def cached_latest return nil unless File.exist?(cache_path) JSON.parse(File.read(cache_path))["latest"] rescue StandardError nil end |
.checks_enabled? ⇒ Boolean
All must hold (mirrors gh): no opt-out env, interactive TTY, not CI.
135 136 137 138 139 |
# File 'lib/rubino/update_check.rb', line 135 def checks_enabled? !opted_out? && $stdout.tty? && ENV["CI"].to_s.strip.empty? end |
.clear_cache! ⇒ Object
120 121 122 123 124 |
# File 'lib/rubino/update_check.rb', line 120 def clear_cache! File.delete(cache_path) if File.exist?(cache_path) rescue StandardError nil end |
.fetch_latest ⇒ Object
The latest published version string, or nil on failure / “unknown” / non-semver. Synchronous + short-timeout; callers that must not block run it on a detached thread (refresh_async_if_stale).
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/rubino/update_check.rb', line 76 def fetch_latest uri = URI(LATEST_URL) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = (uri.scheme == "https") http.open_timeout = NET_TIMEOUT http.read_timeout = NET_TIMEOUT req = Net::HTTP::Get.new(uri) req["User-Agent"] = "Rubino/#{Rubino::VERSION}" res = http.request(req) return nil unless res.is_a?(Net::HTTPSuccess) version = JSON.parse(res.body)["version"].to_s semver?(version) ? version : nil rescue StandardError nil end |
.gem_update_command ⇒ Object
Argv form (no shell) + active interpreter via Gem.ruby → updates the right install on a multi-Ruby machine and is injection-safe.
167 168 169 |
# File 'lib/rubino/update_check.rb', line 167 def gem_update_command [Gem.ruby, "-S", "gem", "update", GEM_NAME] end |
.install_method ⇒ Object
How rubino was installed: :gem when a matching RubyGems spec is loaded, else :source (dev checkout / built from source / vendored).
155 156 157 |
# File 'lib/rubino/update_check.rb', line 155 def install_method installed_gem_version(GEM_NAME) ? :gem : :source end |
.installed_gem_version(name) ⇒ Object
159 160 161 162 163 |
# File 'lib/rubino/update_check.rb', line 159 def installed_gem_version(name) Gem::Specification.find_by_name(name).version.to_s rescue Gem::MissingSpecError, StandardError nil end |
.newer?(latest) ⇒ Boolean
latest is a valid version strictly greater than the running VERSION.
179 180 181 182 183 184 185 |
# File 'lib/rubino/update_check.rb', line 179 def newer?(latest) return false unless semver?(latest) Gem::Version.new(latest) > Gem::Version.new(Rubino::VERSION) rescue ArgumentError false end |
.notice_from_cache ⇒ Object
One-line dim notice when a newer version is cached, else nil. The RUBINO_NO_UPDATE_CHECK opt-out disables the feature ENTIRELY — “no network, no notice” per docs/commands.md — so a previously-cached notice must not leak through either (#66).
43 44 45 46 47 48 49 50 |
# File 'lib/rubino/update_check.rb', line 43 def notice_from_cache return nil if opted_out? latest = cached_latest return nil unless newer?(latest) "▸ rubino v#{latest} available — run `rubino update`" end |
.opted_out? ⇒ Boolean
The user’s full opt-out: RUBINO_NO_UPDATE_CHECK set (to anything non-blank) disables refresh AND the cached boot notice.
130 131 132 |
# File 'lib/rubino/update_check.rb', line 130 def opted_out? !ENV["RUBINO_NO_UPDATE_CHECK"].to_s.strip.empty? end |
.refresh_async_if_stale ⇒ Object
Refresh the cache in a detached thread iff enabled and stale. Returns the spawned Thread (tests can join it) or nil when gated out. The caller never joins it on the boot path, so this boot is never slowed.
57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/rubino/update_check.rb', line 57 def refresh_async_if_stale return nil unless checks_enabled? return nil unless stale? Thread.new do latest = fetch_latest write_cache(latest) if latest rescue StandardError # Offline, DNS, TLS, JSON garbage, FS — silent. The cache is left as-is, # so a transient failure simply shows no notice. nil end end |
.semver?(str) ⇒ Boolean
X.Y / X.Y.Z — strict enough to reject “unknown” and other garbage.
174 175 176 |
# File 'lib/rubino/update_check.rb', line 174 def semver?(str) !!(str.to_s =~ /\A\d+\.\d+(\.\d+)?([-.][0-9A-Za-z.-]+)?\z/) end |
.stale? ⇒ Boolean
True when the cache is missing or its checked_at is older than 24h.
142 143 144 145 146 147 148 149 |
# File 'lib/rubino/update_check.rb', line 142 def stale? return true unless File.exist?(cache_path) checked_at = JSON.parse(File.read(cache_path))["checked_at"] Time.now.utc - Time.parse(checked_at) >= CHECK_INTERVAL rescue StandardError true end |
.write_cache(latest) ⇒ Object
Atomic write (temp + rename) so a crashed refresh never leaves a torn file.
110 111 112 113 114 115 116 117 118 |
# File 'lib/rubino/update_check.rb', line 110 def write_cache(latest) dir = File.dirname(cache_path) FileUtils.mkdir_p(dir) tmp = "#{cache_path}.#{Process.pid}.tmp" File.write(tmp, JSON.generate("checked_at" => Time.now.utc.iso8601, "latest" => latest)) File.rename(tmp, cache_path) rescue StandardError nil end |