samagotchi 0.111.1 → 0.113.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 +53 -1
- data/bin/chi +8 -0
- data/docs/architecture.md +2 -2
- data/docs/cli.md +4 -2
- data/docs/configuration.md +41 -21
- data/docs/internals/web-push.md +132 -42
- data/docs/sessions.md +3 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/push_command.rb +188 -0
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/session.rb +4 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +49 -3
- data/lib/samagotchi/web/public/chains.js +9 -6
- data/lib/samagotchi/web/public/format.js +20 -5
- data/lib/samagotchi/web/public/index.html +47 -5
- data/lib/samagotchi/web/public/lists_view.js +3 -2
- data/lib/samagotchi/web/public/notifications.js +91 -16
- data/lib/samagotchi/web/public/notify.js +3 -3
- data/lib/samagotchi/web/public/participant.html +4 -0
- data/lib/samagotchi/web/public/push.js +16 -17
- data/lib/samagotchi/web/public/push_devices.js +187 -0
- data/lib/samagotchi/web/public/session_state.js +4 -3
- data/lib/samagotchi/web/public/sw.js +6 -5
- data/lib/samagotchi/web/push/attention.rb +80 -35
- data/lib/samagotchi/web/push/client.rb +23 -6
- data/lib/samagotchi/web/push/notifier.rb +152 -20
- data/lib/samagotchi/web/push/presence.rb +60 -0
- data/lib/samagotchi/web/push/store.rb +86 -4
- data/lib/samagotchi/web/push.rb +13 -4
- data/lib/samagotchi/web/push_routes.rb +58 -8
- data/lib/samagotchi/web/server.rb +48 -15
- metadata +4 -1
|
@@ -8,6 +8,7 @@ require "time"
|
|
|
8
8
|
require "uri"
|
|
9
9
|
require_relative "crypto"
|
|
10
10
|
require_relative "../private_files"
|
|
11
|
+
require_relative "../../log"
|
|
11
12
|
require_relative "../../atomic_file"
|
|
12
13
|
require_relative "../../session"
|
|
13
14
|
|
|
@@ -16,8 +17,10 @@ module Samagotchi
|
|
|
16
17
|
module Push
|
|
17
18
|
# This machine's Web Push state, in $XDG_STATE_HOME/samagotchi/web-push/
|
|
18
19
|
# (next to web-token and web-grants/; a 0700 folder, 0600 files, never
|
|
19
|
-
# in config.yml): vapid.pem, chi web's one VAPID key,
|
|
20
|
-
# subscriptions.json, the devices that turned the bell on
|
|
20
|
+
# in config.yml): vapid.pem, chi web's one VAPID key,
|
|
21
|
+
# subscriptions.json, the devices that turned the bell on, and
|
|
22
|
+
# sent.json, the attention keys already handled (a restart pushes
|
|
23
|
+
# none of them again). A
|
|
21
24
|
# subscription holds no personal data (an opaque endpoint and two
|
|
22
25
|
# keys); the endpoint and keys never leave this store but for the push
|
|
23
26
|
# itself: what the page and the log show is Sub#public_h (the short
|
|
@@ -29,7 +32,11 @@ module Samagotchi
|
|
|
29
32
|
DIR = "web-push"
|
|
30
33
|
KEY_FILE = "vapid.pem"
|
|
31
34
|
SUBS_FILE = "subscriptions.json"
|
|
35
|
+
SENT_FILE = "sent.json"
|
|
32
36
|
LOCK = "store.lock"
|
|
37
|
+
# A handled key is kept this long (a restart catches up questions
|
|
38
|
+
# under an hour old).
|
|
39
|
+
SENT_KEEP = 24 * 3600
|
|
33
40
|
|
|
34
41
|
# The push services a subscription's endpoint may name: the
|
|
35
42
|
# browsers' (Apple, Google, Mozilla, Microsoft). chi web POSTs to
|
|
@@ -87,9 +94,12 @@ module Samagotchi
|
|
|
87
94
|
# Never the endpoint or the keys: what the page, the CLI and the log show.
|
|
88
95
|
def public_h
|
|
89
96
|
{ id: id, label: label, origin: origin, created_at: created_at, last_ok_at: last_ok_at,
|
|
90
|
-
last_error: last_error }
|
|
97
|
+
last_error: last_error, prefs: { done: done? } }
|
|
91
98
|
end
|
|
92
99
|
|
|
100
|
+
# The device wants a push for a long turn done (off by default).
|
|
101
|
+
def done? = prefs["done"] == true
|
|
102
|
+
|
|
93
103
|
def inspect = "#<Sub #{id} #{label.inspect}>"
|
|
94
104
|
alias_method :to_s, :inspect
|
|
95
105
|
end
|
|
@@ -101,6 +111,7 @@ module Samagotchi
|
|
|
101
111
|
end
|
|
102
112
|
|
|
103
113
|
def self.short_id(endpoint) = Digest::SHA256.hexdigest(endpoint.to_s)[0, 8]
|
|
114
|
+
def self.short_id?(text) = text.is_a?(String) && text.match?(/\A[0-9a-f]{8}\z/)
|
|
104
115
|
|
|
105
116
|
attr_reader :dir
|
|
106
117
|
|
|
@@ -113,6 +124,7 @@ module Samagotchi
|
|
|
113
124
|
@clock = clock
|
|
114
125
|
@key_mutex = Mutex.new
|
|
115
126
|
@key = nil
|
|
127
|
+
@sent_corrupt_logged = false
|
|
116
128
|
end
|
|
117
129
|
|
|
118
130
|
# Never the key.
|
|
@@ -121,11 +133,12 @@ module Samagotchi
|
|
|
121
133
|
|
|
122
134
|
def key_path = File.join(@dir, KEY_FILE)
|
|
123
135
|
def subs_path = File.join(@dir, SUBS_FILE)
|
|
136
|
+
def sent_path = File.join(@dir, SENT_FILE)
|
|
124
137
|
|
|
125
138
|
# Refuses a store others could read (PrivateFiles).
|
|
126
139
|
# @raise [Insecure]
|
|
127
140
|
def check_permissions!
|
|
128
|
-
PrivateFiles.check!(@dir, [key_path, subs_path], holds: "chi's push key and devices", error: Insecure)
|
|
141
|
+
PrivateFiles.check!(@dir, [key_path, subs_path, sent_path], holds: "chi's push key and devices", error: Insecure)
|
|
129
142
|
end
|
|
130
143
|
|
|
131
144
|
# chi web's VAPID key: vapid.pem, made (0600) the first time.
|
|
@@ -150,6 +163,10 @@ module Samagotchi
|
|
|
150
163
|
[]
|
|
151
164
|
end
|
|
152
165
|
|
|
166
|
+
# As #subscriptions, but a corrupt file raises (chi web push says so).
|
|
167
|
+
# @raise [Corrupt]
|
|
168
|
+
def subscriptions! = read
|
|
169
|
+
|
|
153
170
|
# The subscriptions for +origin+ (the ones chi web sends to now).
|
|
154
171
|
def for_origin(origin) = subscriptions.select { |sub| sub.origin == origin }
|
|
155
172
|
|
|
@@ -193,6 +210,31 @@ module Samagotchi
|
|
|
193
210
|
end
|
|
194
211
|
end
|
|
195
212
|
|
|
213
|
+
# Removes the device with this short id, only one subscribed on
|
|
214
|
+
# +origin+ (nil: any; chi web push rm).
|
|
215
|
+
# @return [Sub, nil] the one removed
|
|
216
|
+
def remove_device(id, origin: nil)
|
|
217
|
+
update do |subs|
|
|
218
|
+
gone = subs.find { |sub| sub.id == id && (origin.nil? || sub.origin == origin) }
|
|
219
|
+
[subs - [gone], gone]
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Sets the device's prefs (+done+: a push for a long turn done).
|
|
224
|
+
# @param origin [String, nil] only a device subscribed there
|
|
225
|
+
# @return [Sub, nil] the device as it is now, nil when there is none
|
|
226
|
+
def set_prefs(id, done:, origin: nil)
|
|
227
|
+
update do |subs|
|
|
228
|
+
found = nil
|
|
229
|
+
subs = subs.map do |sub|
|
|
230
|
+
next sub unless sub.id == id && (origin.nil? || sub.origin == origin)
|
|
231
|
+
|
|
232
|
+
found = sub.with(prefs: sub.prefs.merge("done" => done == true))
|
|
233
|
+
end
|
|
234
|
+
[subs, found]
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
196
238
|
# Records a delivery: +ok+ true stamps last_ok_at (and clears the
|
|
197
239
|
# error), else +error+ (a short word: "410", "timeout") is kept.
|
|
198
240
|
def mark(id, ok: false, error: nil)
|
|
@@ -206,10 +248,50 @@ module Samagotchi
|
|
|
206
248
|
end
|
|
207
249
|
end
|
|
208
250
|
|
|
251
|
+
# The attention keys already handled (pushed to a device, or
|
|
252
|
+
# skipped while the owner looked, or over the cap): key → iso8601
|
|
253
|
+
# time. None when sent.json is missing; one that doesn't read is
|
|
254
|
+
# logged once and read as none (the next #mark_sent rewrites it:
|
|
255
|
+
# it only spares a restart's repeat).
|
|
256
|
+
# @return [Hash{String => String}]
|
|
257
|
+
def sent_keys = read_sent
|
|
258
|
+
|
|
259
|
+
# Records +key+ as handled now; keys older than SENT_KEEP go.
|
|
260
|
+
def mark_sent(key)
|
|
261
|
+
locked do
|
|
262
|
+
at = now
|
|
263
|
+
keys = read_sent.reject { |_, time| stale_sent?(time, at) }
|
|
264
|
+
keys[key.to_s] = stamp(at)
|
|
265
|
+
body = JSON.pretty_generate({ "version" => 1, "keys" => keys })
|
|
266
|
+
AtomicFile.write(sent_path, "#{body}\n", perm: 0o600)
|
|
267
|
+
end
|
|
268
|
+
nil
|
|
269
|
+
end
|
|
270
|
+
|
|
209
271
|
private
|
|
210
272
|
|
|
211
273
|
def now = @clock.call
|
|
212
274
|
|
|
275
|
+
def read_sent
|
|
276
|
+
data = JSON.parse(File.read(sent_path))
|
|
277
|
+
keys = data["keys"] if data.is_a?(Hash)
|
|
278
|
+
raise JSON::ParserError, "no keys" unless keys.is_a?(Hash) && keys.all? { |k, v| k.is_a?(String) && v.is_a?(String) }
|
|
279
|
+
|
|
280
|
+
keys
|
|
281
|
+
rescue Errno::ENOENT
|
|
282
|
+
{}
|
|
283
|
+
rescue JSON::ParserError
|
|
284
|
+
Log.warn(:web, "push_sent_corrupt", path: sent_path) unless @sent_corrupt_logged
|
|
285
|
+
@sent_corrupt_logged = true
|
|
286
|
+
{}
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def stale_sent?(time, at)
|
|
290
|
+
at - Time.iso8601(time) > SENT_KEEP
|
|
291
|
+
rescue ArgumentError
|
|
292
|
+
true
|
|
293
|
+
end
|
|
294
|
+
|
|
213
295
|
def stamp(time) = time.utc.iso8601
|
|
214
296
|
|
|
215
297
|
# @return [Array(String, String, String)] endpoint, p256dh, auth
|
data/lib/samagotchi/web/push.rb
CHANGED
|
@@ -4,13 +4,14 @@ require_relative "push/crypto"
|
|
|
4
4
|
require_relative "push/store"
|
|
5
5
|
require_relative "push/client"
|
|
6
6
|
require_relative "push/attention"
|
|
7
|
+
require_relative "push/presence"
|
|
7
8
|
require_relative "push/notifier"
|
|
8
9
|
require_relative "../config"
|
|
9
10
|
|
|
10
11
|
module Samagotchi
|
|
11
12
|
module Web
|
|
12
13
|
# Web Push: chi web reaches the owner's phone (a Home Screen app that
|
|
13
|
-
# turned the bell on) when a session
|
|
14
|
+
# turned the bell on) when a session needs the user or a turn failed.
|
|
14
15
|
# It needs an https public origin (web.public_url or chi web --tunnel
|
|
15
16
|
# tailscale) and web.push.enabled (on by default); without a subscribed
|
|
16
17
|
# device it does nothing. See docs/configuration.md (Phone
|
|
@@ -21,9 +22,17 @@ module Samagotchi
|
|
|
21
22
|
class Refused < StandardError; end
|
|
22
23
|
|
|
23
24
|
# What chi web's push routes and its Notifier share for one run: the
|
|
24
|
-
# store, the client that signs with its key,
|
|
25
|
-
#
|
|
26
|
-
|
|
25
|
+
# store, the client that signs with its key, the public origin the
|
|
26
|
+
# devices subscribe on, and the pages' presence (the route writes it,
|
|
27
|
+
# the Notifier reads it).
|
|
28
|
+
Runtime = Data.define(:store, :client, :origin, :presence) do
|
|
29
|
+
def initialize(presence: Presence.new, **) = super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The test push (POST /api/push/test, chi web push test): no badge
|
|
33
|
+
# (it would clear the app's) and no open list (no cleanup).
|
|
34
|
+
TEST_PAYLOAD = { v: 1, kind: "show", tag: "chi:test", title: "chi", body: "test notification", url: "/" }.freeze
|
|
35
|
+
TEST_TTL = 600
|
|
27
36
|
|
|
28
37
|
module_function
|
|
29
38
|
|
|
@@ -7,7 +7,8 @@ module Samagotchi
|
|
|
7
7
|
module Web
|
|
8
8
|
# The owner's Web Push routes (App::ROUTES, all :owner rows: a
|
|
9
9
|
# participant gets no pushes): what the page's bell asks (GET /api/push),
|
|
10
|
-
# subscribing this device and unsubscribing it,
|
|
10
|
+
# subscribing this device and unsubscribing it, a test push, and the
|
|
11
|
+
# pages' presence (no push while the owner looks at one).
|
|
11
12
|
# Included in App: its helpers (json_response, error_response,
|
|
12
13
|
# parse_json, request_body) and @push (a Push::Runtime, nil when push is
|
|
13
14
|
# off: no https public_url, or web.push.enabled false) answer. See
|
|
@@ -17,16 +18,22 @@ module Samagotchi
|
|
|
17
18
|
|
|
18
19
|
private
|
|
19
20
|
|
|
20
|
-
# {enabled, public_key, origin, this}: +this+ is the
|
|
21
|
-
# endpoint the page sends as ?endpoint= (its
|
|
22
|
-
# this store doesn't have it for this
|
|
21
|
+
# {enabled, public_key, origin, this, devices, stale}: +this+ is the
|
|
22
|
+
# device whose endpoint the page sends as ?endpoint= (its
|
|
23
|
+
# pushManager's), nil when this store doesn't have it for this
|
|
24
|
+
# origin; +devices+ this origin's (Sub#public_h, +this+ marked),
|
|
25
|
+
# +stale+ how many were made on another origin (never sent to).
|
|
23
26
|
def handle_push_info(req)
|
|
24
27
|
return json_response(200, { enabled: false, public_key: nil, origin: nil, this: nil }) unless @push
|
|
25
28
|
|
|
26
29
|
endpoint = req.params["endpoint"].to_s
|
|
27
|
-
|
|
30
|
+
subs = @push.store.subscriptions
|
|
31
|
+
mine = subs.select { |sub| sub.origin == @push.origin }
|
|
32
|
+
this = endpoint.empty? ? nil : mine.find { |sub| sub.endpoint == endpoint }
|
|
28
33
|
json_response(200, { enabled: true, public_key: Push::Crypto.public_key_b64(@push.store.vapid_key),
|
|
29
|
-
origin: @push.origin, this: this && { id: this.id, label: this.label }
|
|
34
|
+
origin: @push.origin, this: this && { id: this.id, label: this.label },
|
|
35
|
+
devices: mine.map { |sub| sub.public_h.merge(this: sub.equal?(this)) },
|
|
36
|
+
stale: subs.size - mine.size })
|
|
30
37
|
end
|
|
31
38
|
|
|
32
39
|
# {subscription: {endpoint, keys: {p256dh, auth}}, replaces?: id} →
|
|
@@ -48,6 +55,37 @@ module Samagotchi
|
|
|
48
55
|
error_response(400, "invalid_subscription", e.message)
|
|
49
56
|
end
|
|
50
57
|
|
|
58
|
+
# PATCH …/subscriptions/ID {prefs: {done: true|false}} → 200 the
|
|
59
|
+
# device (Sub#public_h): whether it gets a push for a long turn done.
|
|
60
|
+
def handle_push_prefs(req, id)
|
|
61
|
+
return push_off unless @push
|
|
62
|
+
|
|
63
|
+
body = push_body(req)
|
|
64
|
+
prefs = body["prefs"] if body.is_a?(Hash)
|
|
65
|
+
unless prefs.is_a?(Hash) && prefs.keys == ["done"] && [true, false].include?(prefs["done"])
|
|
66
|
+
return error_response(400, "invalid_prefs", "give {prefs: {done: true|false}}")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sub = Push::Store.short_id?(id) && @push.store.set_prefs(id, done: prefs["done"], origin: @push.origin)
|
|
70
|
+
return unknown_device(id) unless sub
|
|
71
|
+
|
|
72
|
+
Log.info(:web, "push_prefs", device: sub.id, done: sub.done?)
|
|
73
|
+
json_response(200, sub.public_h)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# DELETE …/subscriptions/ID → 204: the devices list's Remove for
|
|
77
|
+
# another device (this device's row turns the bell off instead, so
|
|
78
|
+
# the page and the store agree).
|
|
79
|
+
def handle_push_remove(_req, id)
|
|
80
|
+
return push_off unless @push
|
|
81
|
+
|
|
82
|
+
sub = Push::Store.short_id?(id) && @push.store.remove_device(id, origin: @push.origin)
|
|
83
|
+
return unknown_device(id) unless sub
|
|
84
|
+
|
|
85
|
+
Log.info(:web, "push_removed", device: sub.id)
|
|
86
|
+
[204, { "Cache-Control" => "no-store" }, []]
|
|
87
|
+
end
|
|
88
|
+
|
|
51
89
|
# {endpoint} → 204: this device's bell turned off.
|
|
52
90
|
def handle_push_unsubscribe(req)
|
|
53
91
|
return push_off unless @push
|
|
@@ -70,9 +108,8 @@ module Samagotchi
|
|
|
70
108
|
endpoint = body["endpoint"] if body.is_a?(Hash)
|
|
71
109
|
subs = @push.store.for_origin(@push.origin)
|
|
72
110
|
subs = subs.select { |sub| sub.endpoint == endpoint } if endpoint.is_a?(String) && !endpoint.empty?
|
|
73
|
-
payload = { v: 1, kind: "show", tag: "chi:test", title: "chi", body: "test notification", url: "/" }
|
|
74
111
|
results = subs.map do |sub|
|
|
75
|
-
result = @push.client.deliver(sub,
|
|
112
|
+
result = @push.client.deliver(sub, Push::TEST_PAYLOAD, ttl: Push::TEST_TTL, urgency: "high")
|
|
76
113
|
Log.info(:web, "push_test", device: sub.id, code: result.code, ms: result.ms)
|
|
77
114
|
@push.store.remove(sub.id) if result.status == :gone
|
|
78
115
|
{ id: sub.id, label: sub.label, status: result.status.to_s, code: result.code, ms: result.ms }
|
|
@@ -80,6 +117,19 @@ module Samagotchi
|
|
|
80
117
|
json_response(200, { devices: results })
|
|
81
118
|
end
|
|
82
119
|
|
|
120
|
+
# {page, front} → 204: whether the user is at this page (visible,
|
|
121
|
+
# focused, touched lately; notifications.js reports it). Also with
|
|
122
|
+
# push off: a desk tab has nothing to be told.
|
|
123
|
+
def handle_push_presence(req)
|
|
124
|
+
page, front = Push::Presence.parse(push_body(req))
|
|
125
|
+
return error_response(400, "invalid_presence", "give {page: id, front: true|false}") unless page
|
|
126
|
+
|
|
127
|
+
@push&.presence&.report(page, front)
|
|
128
|
+
[204, { "Cache-Control" => "no-store" }, []]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def unknown_device(id) = error_response(404, "unknown_device", "no device #{id[0, 16]} pushes from this origin")
|
|
132
|
+
|
|
83
133
|
def push_off
|
|
84
134
|
error_response(409, "push_off", "push needs an https public URL (chi web --tunnel tailscale, or web.public_url) " \
|
|
85
135
|
"and web.push.enabled")
|
|
@@ -130,7 +130,9 @@ module Samagotchi
|
|
|
130
130
|
annotate_presets: annotate_presets, editor: editor, lan: lan, working_set: working_set,
|
|
131
131
|
public_url: public_url }
|
|
132
132
|
started = if plan
|
|
133
|
-
with_tunnel(provider, plan)
|
|
133
|
+
with_tunnel(provider, plan) do |up|
|
|
134
|
+
start(**options, public_notes: provider.start_lines(plan), on_bound: up)
|
|
135
|
+
end
|
|
134
136
|
else
|
|
135
137
|
start(**options)
|
|
136
138
|
end
|
|
@@ -147,25 +149,43 @@ module Samagotchi
|
|
|
147
149
|
end
|
|
148
150
|
|
|
149
151
|
# The tunnel on while the block runs (the server), off after it
|
|
150
|
-
# whatever ends it (Ctrl-C, SIGTERM, SIGHUP,
|
|
151
|
-
#
|
|
152
|
-
#
|
|
152
|
+
# whatever ends it (Ctrl-C, SIGTERM, SIGHUP), once #up may have taken
|
|
153
|
+
# effect: a refusal before it turns off nothing (a live tunnel of
|
|
154
|
+
# another chi web stays). The block gets #up to call once the port is
|
|
155
|
+
# bound (start's on_bound), so Serve never forwards the tailnet to a
|
|
156
|
+
# listener that took the port first. A signal during #up waits for it
|
|
157
|
+
# (its command runs in its own process group, and Serve keeps what it
|
|
158
|
+
# set even if chi stopped waiting), so it can't land between the serve
|
|
159
|
+
# going on and the teardown being due; #up is bounded by its timeout.
|
|
153
160
|
def self.with_tunnel(provider, plan)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
161
|
+
attempted = false
|
|
162
|
+
yield(lambda do
|
|
163
|
+
Thread.handle_interrupt(SignalException => :never) do
|
|
164
|
+
attempted = true
|
|
165
|
+
provider.up(plan)
|
|
166
|
+
end
|
|
167
|
+
end)
|
|
157
168
|
ensure
|
|
158
|
-
tunnel_down(provider, plan) if
|
|
169
|
+
tunnel_down(provider, plan) if attempted
|
|
159
170
|
end
|
|
160
171
|
|
|
161
172
|
# The tunnel's teardown, before any line (a hung-up terminal can't
|
|
162
173
|
# write): a failure is one warning line, never a crash or another exit
|
|
163
|
-
# status
|
|
174
|
+
# status. A second Ctrl-C (SIGTERM, SIGHUP) during it waits for it
|
|
175
|
+
# (bounded by its commands' timeouts) and is then dropped: the run
|
|
176
|
+
# already ends the way the first signal ends it.
|
|
164
177
|
def self.tunnel_down(provider, plan)
|
|
165
|
-
line =
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
178
|
+
line = nil
|
|
179
|
+
begin
|
|
180
|
+
Thread.handle_interrupt(SignalException => :never) do
|
|
181
|
+
line = begin
|
|
182
|
+
provider.down_line(plan, provider.down(plan))
|
|
183
|
+
rescue Tunnel::Refused => e
|
|
184
|
+
provider.down_warning(plan, e)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
rescue SignalException
|
|
188
|
+
nil
|
|
169
189
|
end
|
|
170
190
|
say(line) if line
|
|
171
191
|
end
|
|
@@ -187,11 +207,14 @@ module Samagotchi
|
|
|
187
207
|
# (web.public_url, or --tunnel's): every request needs the access token
|
|
188
208
|
# @param public_notes [Array<String>] lines after the proxied link's
|
|
189
209
|
# (the tunnel's)
|
|
210
|
+
# @param on_bound [#call, nil] run once the port is bound, before any
|
|
211
|
+
# line (the tunnel's #up); what it raises ends the run
|
|
190
212
|
# @return [Boolean] false when the port was taken (said so on stderr)
|
|
191
213
|
def self.start(port: nil, host: nil, url: nil, open_browser: false, state_dir: nil, manager: nil, markdown: false,
|
|
192
214
|
view: Config::BY_KEY["web.view"].default, annotate_presets: Config::BY_KEY["web.annotate_presets"].default, hub: nil, lan: nil,
|
|
193
215
|
editor: Config::BY_KEY["web.editor"].default, token_path: Token.path,
|
|
194
|
-
working_set: Config::BY_KEY["web.working_set"].default, public_url: nil, public_notes: []
|
|
216
|
+
working_set: Config::BY_KEY["web.working_set"].default, public_url: nil, public_notes: [],
|
|
217
|
+
on_bound: nil)
|
|
195
218
|
config_restart_unsubscribe = ConfigFile.watch_restart_settings!(process: :web) do |message|
|
|
196
219
|
Samagotchi::Log.warn(:config, "restart_required", echo: "Warning: config: #{message}")
|
|
197
220
|
end
|
|
@@ -219,14 +242,19 @@ module Samagotchi
|
|
|
219
242
|
# The Notifier seeds from the hub's snapshot, which is empty before
|
|
220
243
|
# a scan: one now, so what is already open never pushes.
|
|
221
244
|
hub.scan
|
|
222
|
-
notifier = Push::Notifier.new(store: push.store, client: push.client, origin: push.origin
|
|
245
|
+
notifier = Push::Notifier.new(store: push.store, client: push.client, origin: push.origin,
|
|
246
|
+
presence: push.presence).start(hub)
|
|
223
247
|
Samagotchi::Log.info(:web, "push", devices: push.store.for_origin(push.origin).size)
|
|
224
248
|
end
|
|
225
249
|
hub.start
|
|
226
250
|
# Said once the port is bound: a second chi web racing for it gets
|
|
227
251
|
# the in-use line instead.
|
|
228
252
|
running_path = Running.path(state_dir: state_dir)
|
|
253
|
+
webrick = nil
|
|
254
|
+
callback_unfinished = false
|
|
229
255
|
started = lambda do
|
|
256
|
+
callback_unfinished = true
|
|
257
|
+
on_bound&.call
|
|
230
258
|
record_running(running_path, port, public_url)
|
|
231
259
|
puts "Chi Web on #{url} (public: #{File.expand_path("public", __dir__)})"
|
|
232
260
|
puts lan_lines(ip: lan.ip, port: port, token: Token.read(token_path), others: lan.others, public: lan.public) if lan
|
|
@@ -243,12 +271,14 @@ module Samagotchi
|
|
|
243
271
|
open_url(open_target)
|
|
244
272
|
end
|
|
245
273
|
end
|
|
274
|
+
callback_unfinished = false
|
|
246
275
|
end
|
|
247
276
|
|
|
248
277
|
# The event loops (GET /api/events) end once WEBrick leaves
|
|
249
278
|
# :Running: it joins every request thread before run returns.
|
|
250
279
|
Rackup::Handler::WEBrick.run(app, Host: host, Port: port, AccessLog: [], Logger: Log.new($stderr, WEBrick::Log::WARN),
|
|
251
280
|
StartCallback: started) do |server|
|
|
281
|
+
webrick = server
|
|
252
282
|
app.server_running = -> { server.status == :Running }
|
|
253
283
|
listen_lan(server, lan, port) if lan
|
|
254
284
|
end
|
|
@@ -260,6 +290,9 @@ module Samagotchi
|
|
|
260
290
|
warn "Error: can't listen on #{lan.ip}:#{port} (#{e.message}); did the address change? Run chi web again"
|
|
261
291
|
false
|
|
262
292
|
ensure
|
|
293
|
+
# A start callback that raised (on_bound's refusal, a Ctrl-C) leaves
|
|
294
|
+
# WEBrick :Running with its sockets open.
|
|
295
|
+
webrick&.listeners&.each { |socket| socket.close unless socket.closed? } if callback_unfinished
|
|
263
296
|
Running.clear(path: running_path) if running_path
|
|
264
297
|
config_restart_unsubscribe&.call
|
|
265
298
|
notifier&.stop
|
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.113.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -503,6 +503,7 @@ files:
|
|
|
503
503
|
- lib/samagotchi/prompt_note.rb
|
|
504
504
|
- lib/samagotchi/prompt_warmup.rb
|
|
505
505
|
- lib/samagotchi/proxy_trace.rb
|
|
506
|
+
- lib/samagotchi/push_command.rb
|
|
506
507
|
- lib/samagotchi/question_answer.rb
|
|
507
508
|
- lib/samagotchi/question_asked.rb
|
|
508
509
|
- lib/samagotchi/question_desk.rb
|
|
@@ -780,6 +781,7 @@ files:
|
|
|
780
781
|
- lib/samagotchi/web/public/plain_line.js
|
|
781
782
|
- lib/samagotchi/web/public/prompt_history.js
|
|
782
783
|
- lib/samagotchi/web/public/push.js
|
|
784
|
+
- lib/samagotchi/web/public/push_devices.js
|
|
783
785
|
- lib/samagotchi/web/public/question_card.js
|
|
784
786
|
- lib/samagotchi/web/public/question_cards.js
|
|
785
787
|
- lib/samagotchi/web/public/read_view.js
|
|
@@ -843,6 +845,7 @@ files:
|
|
|
843
845
|
- lib/samagotchi/web/push/client.rb
|
|
844
846
|
- lib/samagotchi/web/push/crypto.rb
|
|
845
847
|
- lib/samagotchi/web/push/notifier.rb
|
|
848
|
+
- lib/samagotchi/web/push/presence.rb
|
|
846
849
|
- lib/samagotchi/web/push/store.rb
|
|
847
850
|
- lib/samagotchi/web/push_routes.rb
|
|
848
851
|
- lib/samagotchi/web/qr.rb
|