samagotchi 0.123.0 → 0.124.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -1
- data/bin/chi +13 -2
- data/docs/architecture.md +19 -1
- data/docs/cli.md +20 -5
- data/docs/desktop.md +51 -3
- data/docs/testing.md +2 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/desktop/macos/App.swift +9 -1
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +129 -17
- data/lib/samagotchi/desktop/macos/Images.swift +4 -3
- data/lib/samagotchi/desktop/macos/Panel.swift +315 -80
- data/lib/samagotchi/desktop/macos/Sessions.swift +272 -0
- data/lib/samagotchi/run_options.rb +43 -2
- data/lib/samagotchi/session_manager.rb +11 -0
- data/lib/samagotchi/update_command.rb +40 -13
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +198 -2
- data/lib/samagotchi/web/public/app.js +40 -1
- data/lib/samagotchi/web/public/data.js +17 -0
- data/lib/samagotchi/web/public/index.html +21 -2
- data/lib/samagotchi/web/public/toasts.js +13 -5
- data/lib/samagotchi/web/public/update_notice.js +6 -6
- data/lib/samagotchi/web/public/update_sheet.js +158 -0
- data/lib/samagotchi/web/server.rb +116 -7
- data/lib/samagotchi/web/session_hub.rb +28 -0
- data/lib/samagotchi/web/tunnel/tailscale.rb +8 -2
- data/lib/samagotchi/web/update_check.rb +14 -7
- metadata +3 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "rack"
|
|
5
5
|
require "rackup/handler/webrick"
|
|
6
|
+
require "rbconfig"
|
|
6
7
|
require "uri"
|
|
7
8
|
require "webrick"
|
|
8
9
|
|
|
@@ -21,9 +22,12 @@ require_relative "update_check"
|
|
|
21
22
|
require_relative "token"
|
|
22
23
|
require_relative "tunnel"
|
|
23
24
|
require_relative "../config"
|
|
25
|
+
require_relative "../child_env"
|
|
26
|
+
require_relative "../installed_gem"
|
|
24
27
|
require_relative "../paths"
|
|
25
28
|
require_relative "../project_scope"
|
|
26
29
|
require_relative "../log"
|
|
30
|
+
require_relative "../session_manager"
|
|
27
31
|
require_relative "../version"
|
|
28
32
|
require_relative "../cli/exit"
|
|
29
33
|
|
|
@@ -65,6 +69,8 @@ module Samagotchi
|
|
|
65
69
|
DEFAULT_HOST = "127.0.0.1"
|
|
66
70
|
# How long `chi web` waits for a server already on its port to answer.
|
|
67
71
|
PROBE_TIMEOUT = 0.3
|
|
72
|
+
RESTARTED_ENV = "SAMAGOTCHI_WEB_RESTARTED"
|
|
73
|
+
ADOPT_PIDS_ENV = "SAMAGOTCHI_WEB_ADOPT_PIDS"
|
|
68
74
|
|
|
69
75
|
# The raw header names next to Rack's env (ProxyGate's identity check):
|
|
70
76
|
# chi web is the only WEBrick server chi runs.
|
|
@@ -87,7 +93,9 @@ module Samagotchi
|
|
|
87
93
|
def self.launch(port: nil, host: nil, scope: "project", start_dir: nil, open_browser: false, markdown: false,
|
|
88
94
|
view: Config::BY_KEY["web.view"].default, annotate_presets: Config::BY_KEY["web.annotate_presets"].default,
|
|
89
95
|
editor: Config::BY_KEY["web.editor"].default, new_token: false,
|
|
90
|
-
working_set: Config::BY_KEY["web.working_set"].default, tunnel: nil, start_dir_from: :flag
|
|
96
|
+
working_set: Config::BY_KEY["web.working_set"].default, tunnel: nil, start_dir_from: :flag,
|
|
97
|
+
restarted: false, restart_argv: ["web"], exec: nil)
|
|
98
|
+
handed_on = false # a :restart leaves Serve to the next process, a run turns it off itself
|
|
91
99
|
start_given = !start_dir.to_s.strip.empty?
|
|
92
100
|
start_dir = StartDir.resolve(start_dir, from: start_dir_from)
|
|
93
101
|
rotate_token if new_token
|
|
@@ -120,6 +128,8 @@ module Samagotchi
|
|
|
120
128
|
warn in_use_message(port)
|
|
121
129
|
1
|
|
122
130
|
else
|
|
131
|
+
provider = nil
|
|
132
|
+
plan = nil
|
|
123
133
|
if tunnel
|
|
124
134
|
provider = Tunnel.for(tunnel)
|
|
125
135
|
plan = provider.prepare(port: port, config_owner: Samagotchi::Config.get("web.proxy.owner"),
|
|
@@ -142,11 +152,14 @@ module Samagotchi
|
|
|
142
152
|
public_url: public_url, start_dir: start_dir }
|
|
143
153
|
started = if plan
|
|
144
154
|
with_tunnel(provider, plan) do |up|
|
|
145
|
-
start(**options, public_notes: provider.start_lines(plan), on_bound: up)
|
|
155
|
+
start(**options, public_notes: provider.start_lines(plan, restarted: restarted), on_bound: up)
|
|
146
156
|
end
|
|
147
157
|
else
|
|
148
158
|
start(**options)
|
|
149
159
|
end
|
|
160
|
+
handed_on = !!started
|
|
161
|
+
return reexec(restart_argv, provider: provider, plan: plan, exec: exec) if started == :restart
|
|
162
|
+
|
|
150
163
|
started ? 0 : 1
|
|
151
164
|
end
|
|
152
165
|
rescue StartDir::Invalid, Lan::Error, Grants::Insecure, PublicUrl::Invalid, Tunnel::Refused, Push::Refused, Push::Store::Insecure => e
|
|
@@ -157,6 +170,11 @@ module Samagotchi
|
|
|
157
170
|
# status a shell gives a command it interrupted.
|
|
158
171
|
puts "#{"\n" if $stdout.tty?}Chi Web stopped."
|
|
159
172
|
CLI::Exit::INTERRUPTED
|
|
173
|
+
ensure
|
|
174
|
+
# A restarted chi web that didn't get to serve (a refusal, the port
|
|
175
|
+
# race, a bad start folder): the old process's Serve handler it was
|
|
176
|
+
# to adopt goes off, never left forwarding to nothing.
|
|
177
|
+
stop_restarted_tunnel(tunnel, resolve_port(port)) if restarted && tunnel && !handed_on
|
|
160
178
|
end
|
|
161
179
|
|
|
162
180
|
# A second chi web given a start folder (--start-dir, web.start_dir)
|
|
@@ -177,16 +195,87 @@ module Samagotchi
|
|
|
177
195
|
# (its command runs in its own process group, and Serve keeps what it
|
|
178
196
|
# set even if chi stopped waiting), so it can't land between the serve
|
|
179
197
|
# going on and the teardown being due; #up is bounded by its timeout.
|
|
198
|
+
# A :restart result leaves Serve up for the exec'd chi web to adopt.
|
|
180
199
|
def self.with_tunnel(provider, plan)
|
|
181
200
|
attempted = false
|
|
182
|
-
yield(lambda do
|
|
201
|
+
result = yield(lambda do
|
|
183
202
|
Thread.handle_interrupt(SignalException => :never) do
|
|
184
203
|
attempted = true
|
|
185
204
|
provider.up(plan)
|
|
186
205
|
end
|
|
187
206
|
end)
|
|
207
|
+
result
|
|
188
208
|
ensure
|
|
189
|
-
tunnel_down(provider, plan) if attempted
|
|
209
|
+
tunnel_down(provider, plan) if attempted && result != :restart
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Replace this process only after its listener and hub have stopped.
|
|
213
|
+
# Starter PIDs are children of this process; workers are detached and
|
|
214
|
+
# must not be adopted or signaled.
|
|
215
|
+
# No line before the exec: one on a hung-up terminal raises EIO, and
|
|
216
|
+
# the restart would end in a dead chi web; the log says it.
|
|
217
|
+
def self.reexec(argv, provider: nil, plan: nil, exec: nil)
|
|
218
|
+
program = reexec_program
|
|
219
|
+
raise ArgumentError, "RubyGems chi wrapper not found" if program.to_s.empty?
|
|
220
|
+
|
|
221
|
+
env = ChildEnv.unbundled.merge(
|
|
222
|
+
RESTARTED_ENV => "1",
|
|
223
|
+
ADOPT_PIDS_ENV => SessionManager.live_spawn_pids.join(",")
|
|
224
|
+
)
|
|
225
|
+
Samagotchi::Log.info(:web, "restart", program: program)
|
|
226
|
+
runner = exec || ->(environment, *args) { Kernel.exec(environment, *args) }
|
|
227
|
+
runner.call(env, RbConfig.ruby, program, *argv)
|
|
228
|
+
rescue SystemCallError, ArgumentError => e
|
|
229
|
+
tunnel_down(provider, plan) if provider && plan
|
|
230
|
+
warn "Error: couldn't restart chi web: #{e.message}"
|
|
231
|
+
1
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# What a restart execs: the installed gem's wrapper (it activates the
|
|
235
|
+
# newest installed chi), this bin/chi in a checkout; nil for a gem
|
|
236
|
+
# installed without one (--bindir).
|
|
237
|
+
def self.reexec_program
|
|
238
|
+
gem_spec = InstalledGem.spec
|
|
239
|
+
gem_spec ? InstalledGem.wrapper(gem_spec) : $PROGRAM_NAME
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# bin/chi's first step: the restart marker and the starters to adopt
|
|
243
|
+
# out of +env+, before anything (a worker, chi update) is spawned with
|
|
244
|
+
# it; the starters are adopted only under the marker.
|
|
245
|
+
# @return [Boolean] whether this chi web is a restarted one
|
|
246
|
+
def self.take_restart_env!(env = ENV, detach: Process.method(:detach))
|
|
247
|
+
restarted = env.delete(RESTARTED_ENV) == "1"
|
|
248
|
+
adopt_reexec_children!(env: env, detach: detach) if restarted
|
|
249
|
+
env.delete(ADOPT_PIDS_ENV)
|
|
250
|
+
restarted
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Adopt only the recorded child starters. They may have exited in the
|
|
254
|
+
# interval between collecting their PIDs and the new process starting.
|
|
255
|
+
def self.adopt_reexec_children!(env: ENV, detach: Process.method(:detach))
|
|
256
|
+
pids = env.delete(ADOPT_PIDS_ENV).to_s
|
|
257
|
+
pids.split(",").reject(&:empty?).each do |pid|
|
|
258
|
+
child_pid = Integer(pid, 10)
|
|
259
|
+
begin
|
|
260
|
+
detach.call(child_pid)
|
|
261
|
+
rescue Errno::ECHILD
|
|
262
|
+
nil
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Turns off the Serve handler a restarted chi web was to adopt, only
|
|
268
|
+
# when it is still exactly this port's (prepare says adopted); a
|
|
269
|
+
# foreign one, or none, stays as it is. Never raises: the start's own
|
|
270
|
+
# error is the one to say.
|
|
271
|
+
def self.stop_restarted_tunnel(tunnel, port)
|
|
272
|
+
provider = Tunnel.for(tunnel)
|
|
273
|
+
plan = provider.prepare(port: port, config_owner: Samagotchi::Config.get("web.proxy.owner"),
|
|
274
|
+
config_public_url: Samagotchi::Config.get("web.public_url"),
|
|
275
|
+
config_identity_header: Samagotchi::Config.get("web.proxy.identity_header"))
|
|
276
|
+
tunnel_down(provider, plan) if plan.adopted
|
|
277
|
+
rescue StandardError => e
|
|
278
|
+
Samagotchi::Log.warn(:web, "restart_tunnel_left", error: e.class.name, msg: e.message)
|
|
190
279
|
end
|
|
191
280
|
|
|
192
281
|
# The tunnel's teardown, before any line (a hung-up terminal can't
|
|
@@ -230,7 +319,8 @@ module Samagotchi
|
|
|
230
319
|
# @param on_bound [#call, nil] run once the port is bound, before any
|
|
231
320
|
# line (the tunnel's #up); what it raises ends the run
|
|
232
321
|
# @param start_dir [String] the folder chi web starts in (App's)
|
|
233
|
-
# @return [Boolean] false when the port was taken (said so on stderr)
|
|
322
|
+
# @return [Boolean, :restart] false when the port was taken (said so on stderr);
|
|
323
|
+
# :restart when the owner requested an in-place process replacement
|
|
234
324
|
def self.start(port: nil, host: nil, url: nil, open_browser: false, state_dir: nil, manager: nil, markdown: false,
|
|
235
325
|
view: Config::BY_KEY["web.view"].default, annotate_presets: Config::BY_KEY["web.annotate_presets"].default, hub: nil, lan: nil,
|
|
236
326
|
editor: Config::BY_KEY["web.editor"].default, token_path: Token.path,
|
|
@@ -252,6 +342,25 @@ module Samagotchi
|
|
|
252
342
|
app = App.new(manager: manager, state_dir: state_dir, markdown: markdown, view: view,
|
|
253
343
|
annotate_presets: annotate_presets, editor: editor, hub: hub, lan: lan_option, working_set: working_set,
|
|
254
344
|
public_url: public_url, public_token: public_token, push: push, start_dir: start_dir)
|
|
345
|
+
webrick = nil
|
|
346
|
+
restart_mutex = Mutex.new
|
|
347
|
+
restart_requested = false
|
|
348
|
+
restart_thread = nil
|
|
349
|
+
if app.respond_to?(:restart_web=)
|
|
350
|
+
app.restart_web = lambda do
|
|
351
|
+
restart_mutex.synchronize do
|
|
352
|
+
next false if restart_requested
|
|
353
|
+
next :no_wrapper if reexec_program.to_s.empty?
|
|
354
|
+
|
|
355
|
+
restart_requested = true
|
|
356
|
+
restart_thread = Thread.new do
|
|
357
|
+
sleep 0.2
|
|
358
|
+
webrick&.shutdown
|
|
359
|
+
end
|
|
360
|
+
true
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
255
364
|
Samagotchi::Log.info(:web, "start", url: "http://#{host}:#{port}", version: Samagotchi::VERSION)
|
|
256
365
|
Samagotchi::Log.info(:web, "lan", ip: lan.ip, interface: lan.interface) if lan
|
|
257
366
|
if public_url
|
|
@@ -273,7 +382,6 @@ module Samagotchi
|
|
|
273
382
|
# Said once the port is bound: a second chi web racing for it gets
|
|
274
383
|
# the in-use line instead.
|
|
275
384
|
running_path = Running.path(state_dir: state_dir)
|
|
276
|
-
webrick = nil
|
|
277
385
|
callback_unfinished = false
|
|
278
386
|
started = lambda do
|
|
279
387
|
callback_unfinished = true
|
|
@@ -305,7 +413,7 @@ module Samagotchi
|
|
|
305
413
|
app.server_running = -> { server.status == :Running }
|
|
306
414
|
listen_lan(server, lan, port) if lan
|
|
307
415
|
end
|
|
308
|
-
true
|
|
416
|
+
restart_requested ? :restart : true
|
|
309
417
|
rescue Errno::EADDRINUSE
|
|
310
418
|
warn in_use_message(port)
|
|
311
419
|
false
|
|
@@ -322,6 +430,7 @@ module Samagotchi
|
|
|
322
430
|
update_check&.stop
|
|
323
431
|
hub&.stop
|
|
324
432
|
Samagotchi::Log.info(:web, "stop") if app
|
|
433
|
+
restart_thread&.join
|
|
325
434
|
end
|
|
326
435
|
|
|
327
436
|
class LanListenError < StandardError; end
|
|
@@ -80,6 +80,7 @@ module Samagotchi
|
|
|
80
80
|
@installed = nil
|
|
81
81
|
@available = nil
|
|
82
82
|
@update_blocked = nil
|
|
83
|
+
@update_state = nil
|
|
83
84
|
@manager = manager
|
|
84
85
|
@now = now || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
85
86
|
@scan_interval = scan_interval
|
|
@@ -142,6 +143,16 @@ module Samagotchi
|
|
|
142
143
|
@monitor.synchronize { @installed }
|
|
143
144
|
end
|
|
144
145
|
|
|
146
|
+
# Look at what is installed now, not at the next full probe (after an
|
|
147
|
+
# install): a change is the usual `chi` event.
|
|
148
|
+
# @return [String, nil] the newest installed chi
|
|
149
|
+
def refresh_installed
|
|
150
|
+
@monitor.synchronize do
|
|
151
|
+
check_installed
|
|
152
|
+
@installed
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
145
156
|
# The complete chi version state used by event snapshots and info routes.
|
|
146
157
|
def chi_state
|
|
147
158
|
@monitor.synchronize do
|
|
@@ -161,6 +172,23 @@ module Samagotchi
|
|
|
161
172
|
nil
|
|
162
173
|
end
|
|
163
174
|
|
|
175
|
+
# Publish the owner-triggered update's progress or result for current
|
|
176
|
+
# tabs, and retain the latest state for a reconnect's snapshot.
|
|
177
|
+
def publish_update(state:, rows:, progress: nil, operation: nil, version: nil)
|
|
178
|
+
@monitor.synchronize do
|
|
179
|
+
@update_state = { state: state, rows: rows }
|
|
180
|
+
@update_state[:operation] = operation unless operation.nil?
|
|
181
|
+
@update_state[:version] = version unless version.nil?
|
|
182
|
+
@update_state[:progress] = progress unless progress.nil?
|
|
183
|
+
emit("chi_update", @update_state)
|
|
184
|
+
end
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def update_state
|
|
189
|
+
@monitor.synchronize { @update_state&.dup }
|
|
190
|
+
end
|
|
191
|
+
|
|
164
192
|
# Rescan one session now (its file, its folder, its owner), for the
|
|
165
193
|
# page's own actions: no waiting for the next tick.
|
|
166
194
|
def touch(id)
|
|
@@ -162,10 +162,16 @@ module Samagotchi
|
|
|
162
162
|
|
|
163
163
|
# What the start lines add: how it is reached and whose login is the
|
|
164
164
|
# owner, and the Tailnet Lock warning.
|
|
165
|
-
def start_lines(plan)
|
|
165
|
+
def start_lines(plan, restarted: false)
|
|
166
166
|
source = plan.owner_source == :config ? "web.proxy.owner" : "this machine's Tailscale login"
|
|
167
167
|
via = " via Tailscale Serve (tailnet only), owner #{plan.public_url.owner} (#{source})"
|
|
168
|
-
|
|
168
|
+
if plan.adopted
|
|
169
|
+
via += if restarted
|
|
170
|
+
"; adopted by the restarted chi web; it turns Serve off on exit"
|
|
171
|
+
else
|
|
172
|
+
"; Serve already forwarded port 443 here, chi web turns it off on exit"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
169
175
|
lines = [via]
|
|
170
176
|
if plan.lock_off
|
|
171
177
|
lines << "Tailnet Lock is off: a device Tailscale's servers add to your tailnet could reach this page; " \
|
|
@@ -93,6 +93,19 @@ module Samagotchi
|
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
# Why chi web can't install a gem here (the notice's and POST /api/update's
|
|
97
|
+
# one answer), or nil: a source checkout, a Bundler run, a gem dir it
|
|
98
|
+
# can't write.
|
|
99
|
+
# @return [String, nil]
|
|
100
|
+
def self.blocked_reason(gem_spec: InstalledGem.spec, bundler: nil, gem_dir: Gem.dir)
|
|
101
|
+
bundler = defined?(::Bundler) || !ENV["BUNDLE_GEMFILE"].to_s.empty? if bundler.nil?
|
|
102
|
+
return "checkout" unless gem_spec
|
|
103
|
+
return "bundler" if bundler
|
|
104
|
+
return "gem_dir_not_writable" unless File.writable?(gem_dir)
|
|
105
|
+
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
96
109
|
private
|
|
97
110
|
|
|
98
111
|
def run_loop
|
|
@@ -141,13 +154,7 @@ module Samagotchi
|
|
|
141
154
|
version if version && Gem::Version.correct?(version) && Gem::Version.new(version) > version_floor
|
|
142
155
|
end
|
|
143
156
|
|
|
144
|
-
def blocked_reason
|
|
145
|
-
return "checkout" unless @gem_spec
|
|
146
|
-
return "bundler" if @bundler
|
|
147
|
-
return "gem_dir_not_writable" unless File.writable?(@gem_dir)
|
|
148
|
-
|
|
149
|
-
nil
|
|
150
|
-
end
|
|
157
|
+
def blocked_reason = self.class.blocked_reason(gem_spec: @gem_spec, bundler: @bundler, gem_dir: @gem_dir)
|
|
151
158
|
|
|
152
159
|
def load_state
|
|
153
160
|
return { checked_at: nil, available: nil, notified: [] } unless File.file?(@state_file)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.124.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -321,6 +321,7 @@ files:
|
|
|
321
321
|
- lib/samagotchi/desktop/macos/Models.swift
|
|
322
322
|
- lib/samagotchi/desktop/macos/Panel.swift
|
|
323
323
|
- lib/samagotchi/desktop/macos/ProcessRunner.swift
|
|
324
|
+
- lib/samagotchi/desktop/macos/Sessions.swift
|
|
324
325
|
- lib/samagotchi/desktop/macos/Tmux.swift
|
|
325
326
|
- lib/samagotchi/desktop_command.rb
|
|
326
327
|
- lib/samagotchi/edit_preview.rb
|
|
@@ -864,6 +865,7 @@ files:
|
|
|
864
865
|
- lib/samagotchi/web/public/turn_view.js
|
|
865
866
|
- lib/samagotchi/web/public/unqueue.js
|
|
866
867
|
- lib/samagotchi/web/public/update_notice.js
|
|
868
|
+
- lib/samagotchi/web/public/update_sheet.js
|
|
867
869
|
- lib/samagotchi/web/public/vendor/directive-helpers.js
|
|
868
870
|
- lib/samagotchi/web/public/vendor/directive.js
|
|
869
871
|
- lib/samagotchi/web/public/vendor/directives/repeat.js
|