ruby_everywhere 0.7.0 → 0.8.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/exe/every +2 -13
- data/exe/rbe +2 -13
- data/lib/everywhere/boot.rb +9 -2
- data/lib/everywhere/builders/android.rb +16 -42
- data/lib/everywhere/builders/base.rb +53 -0
- data/lib/everywhere/builders/desktop.rb +7 -23
- data/lib/everywhere/builders/ios.rb +49 -52
- data/lib/everywhere/builders/native_sources.rb +38 -0
- data/lib/everywhere/child_processes.rb +4 -4
- data/lib/everywhere/child_supervision.rb +172 -0
- data/lib/everywhere/clock.rb +12 -0
- data/lib/everywhere/commands/build.rb +20 -9
- data/lib/everywhere/commands/dev.rb +59 -284
- data/lib/everywhere/commands/doctor.rb +6 -6
- data/lib/everywhere/commands/install.rb +1 -0
- data/lib/everywhere/commands/platform/auth_status.rb +1 -0
- data/lib/everywhere/commands/platform/build.rb +5 -6
- data/lib/everywhere/commands/platform/login.rb +4 -4
- data/lib/everywhere/commands/platform/logout.rb +1 -0
- data/lib/everywhere/commands/platform/runner.rb +62 -7
- data/lib/everywhere/commands/preview.rb +24 -97
- data/lib/everywhere/commands/publish.rb +2 -0
- data/lib/everywhere/commands/release.rb +14 -11
- data/lib/everywhere/commands/shell_dir.rb +2 -0
- data/lib/everywhere/commands/updates_keygen.rb +25 -1
- data/lib/everywhere/config/app.rb +126 -0
- data/lib/everywhere/config/auth.rb +108 -0
- data/lib/everywhere/config/data.rb +50 -0
- data/lib/everywhere/config/deep_linking.rb +107 -0
- data/lib/everywhere/config/desktop_ui.rb +153 -0
- data/lib/everywhere/config/mobile.rb +211 -0
- data/lib/everywhere/config/native_desktop.rb +168 -0
- data/lib/everywhere/config/native_mobile.rb +337 -0
- data/lib/everywhere/config/shell.rb +57 -0
- data/lib/everywhere/config/updates.rb +63 -0
- data/lib/everywhere/config.rb +30 -1423
- data/lib/everywhere/desktop_dev_app.rb +138 -0
- data/lib/everywhere/dock/state.rb +3 -1
- data/lib/everywhere/entrypoint.rb +24 -0
- data/lib/everywhere/error.rb +8 -0
- data/lib/everywhere/framework.rb +2 -2
- data/lib/everywhere/host.rb +11 -0
- data/lib/everywhere/ignore.rb +11 -5
- data/lib/everywhere/line_pump.rb +3 -1
- data/lib/everywhere/minisign.rb +1 -0
- data/lib/everywhere/native_platform.rb +44 -0
- data/lib/everywhere/paths.rb +23 -7
- data/lib/everywhere/platform/client.rb +19 -1
- data/lib/everywhere/platform/snapshot.rb +6 -4
- data/lib/everywhere/plist.rb +17 -0
- data/lib/everywhere/png.rb +1 -0
- data/lib/everywhere/raw_tty.rb +51 -0
- data/lib/everywhere/s3.rb +1 -0
- data/lib/everywhere/shell_pages.rb +109 -0
- data/lib/everywhere/tab_filter.rb +36 -0
- data/lib/everywhere/task_pool.rb +3 -4
- data/lib/everywhere/ui.rb +6 -1
- data/lib/everywhere/version.rb +6 -4
- data/lib/everywhere.rb +1 -2
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +0 -2
- data/support/release/macos/notarize.sh +3 -0
- metadata +24 -1
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "dry/cli"
|
|
4
|
-
require "socket"
|
|
5
4
|
require "shellwords"
|
|
6
5
|
require "fileutils"
|
|
7
6
|
require_relative "../shellout"
|
|
8
|
-
require_relative "../
|
|
9
|
-
require_relative "../
|
|
10
|
-
require_relative "../
|
|
7
|
+
require_relative "../host"
|
|
8
|
+
require_relative "../desktop_dev_app"
|
|
9
|
+
require_relative "../raw_tty"
|
|
11
10
|
require_relative "../builders/ios"
|
|
12
11
|
require_relative "../builders/android"
|
|
13
12
|
require_relative "../builders/desktop"
|
|
@@ -15,8 +14,8 @@ require_relative "../simulator"
|
|
|
15
14
|
require_relative "../emulator"
|
|
16
15
|
require_relative "../console"
|
|
17
16
|
require_relative "../child_processes"
|
|
17
|
+
require_relative "../child_supervision"
|
|
18
18
|
require_relative "../task_pool"
|
|
19
|
-
require_relative "../relay"
|
|
20
19
|
require_relative "../dock"
|
|
21
20
|
require_relative "../log_filter"
|
|
22
21
|
require_relative "logs"
|
|
@@ -37,6 +36,8 @@ module Everywhere
|
|
|
37
36
|
# Dock keeps the keymap and every target's state pinned to the bottom of the
|
|
38
37
|
# terminal while the logs scroll above it.
|
|
39
38
|
class Dev < Dry::CLI::Command
|
|
39
|
+
include ChildSupervision
|
|
40
|
+
|
|
40
41
|
desc "Run the dev server with native shells on demand (live reload, no packaging)"
|
|
41
42
|
|
|
42
43
|
IOS_TARGET = "ios-arm64"
|
|
@@ -60,10 +61,7 @@ module Everywhere
|
|
|
60
61
|
@shell_dir = shell_dir
|
|
61
62
|
port = @port = validate_port!(port)
|
|
62
63
|
@dev_url_override = dev_url
|
|
63
|
-
|
|
64
|
-
@pids = {}
|
|
65
|
-
@relays = {}
|
|
66
|
-
@children = Mutex.new
|
|
64
|
+
init_children!
|
|
67
65
|
targets = requested_targets(target, desktop: desktop, ios: ios, android: android,
|
|
68
66
|
mobile: mobile, browser: browser)
|
|
69
67
|
@framework, @config = detect_app
|
|
@@ -127,6 +125,12 @@ module Everywhere
|
|
|
127
125
|
UI.die!("no supported framework here (Rails, Hanami, Sinatra) and no remote.url in " \
|
|
128
126
|
"config/everywhere.yml — nothing to run")
|
|
129
127
|
end
|
|
128
|
+
|
|
129
|
+
# The bundle id keys every per-app dir under ~/.rubyeverywhere that the
|
|
130
|
+
# session stages into, so it's checked before anything is written.
|
|
131
|
+
errors = config.identity_errors
|
|
132
|
+
UI.die!("app: in config/everywhere.yml:\n#{errors.join("\n")}") unless errors.empty?
|
|
133
|
+
|
|
130
134
|
[framework, config]
|
|
131
135
|
end
|
|
132
136
|
|
|
@@ -186,6 +190,15 @@ module Everywhere
|
|
|
186
190
|
end
|
|
187
191
|
end
|
|
188
192
|
|
|
193
|
+
# Everything the running desktop shell is staged out of: the assets, the
|
|
194
|
+
# Dock icon, the throwaway .app it runs from. Memoized because a restart
|
|
195
|
+
# restages all of it against the same config.
|
|
196
|
+
def desktop_dev_app = @desktop_dev_app ||= DesktopDevApp.new(config: @config)
|
|
197
|
+
|
|
198
|
+
def dev_bundle_name = desktop_dev_app.dev_bundle_name
|
|
199
|
+
|
|
200
|
+
def dev_info_plist = desktop_dev_app.dev_info_plist
|
|
201
|
+
|
|
189
202
|
# `d` relaunches, the same way `i` and `a` do — quit the running window and
|
|
190
203
|
# open a fresh one. Refusing with "already running" made the desktop the
|
|
191
204
|
# odd one out, and left no way to restart a shell that had wedged or was
|
|
@@ -193,84 +206,6 @@ module Everywhere
|
|
|
193
206
|
#
|
|
194
207
|
# The rebuild is free either way: a warm `cargo run` re-checks the crate
|
|
195
208
|
# graph in well under a second before handing off.
|
|
196
|
-
# native/desktop/assets/** → the staged flat folder the running shell
|
|
197
|
-
# reads. Runs the same compiler `every build` does, so an asset that
|
|
198
|
-
# resolves in dev resolves in the packaged app too. Returns
|
|
199
|
-
# [dir, errors]; the dir is returned even when the app ships no assets,
|
|
200
|
-
# because the shell only has to cope with it not existing.
|
|
201
|
-
def stage_desktop_assets
|
|
202
|
-
dest = Everywhere::Paths.desktop_assets_dir(@config.bundle_id)
|
|
203
|
-
source = File.join(@config.root, "native", "desktop", "assets")
|
|
204
|
-
_names, errors = Everywhere::DesktopAssets.write!(source, dest)
|
|
205
|
-
[dest, errors]
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# A minimal .app around the dev binary, so macOS has a CFBundleName to
|
|
209
|
-
# read. Without it every menu that names the app says "example-shell" —
|
|
210
|
-
# the crate name — because an unbundled process has nothing else to go on.
|
|
211
|
-
# (Setting NSProcessInfo's processName at runtime does NOT fix this: the
|
|
212
|
-
# menu bar and the predefined About/Hide/Quit items are built from the
|
|
213
|
-
# bundle, and macOS has already decided by the time any Rust runs.)
|
|
214
|
-
#
|
|
215
|
-
# Only the plist matters here. No icon is stamped: the shell sets the Dock
|
|
216
|
-
# icon at runtime from NATIVE_ICON_PATH, because Tauri applies its own
|
|
217
|
-
# compiled-in icon after launch and would win over a bundle icns anyway.
|
|
218
|
-
# `every build` writes the real, complete plist — this is the dev subset.
|
|
219
|
-
def stage_dev_bundle
|
|
220
|
-
app = File.join(Everywhere::Paths.desktop_dev_app_dir(@config.bundle_id),
|
|
221
|
-
"#{@config.name}.app")
|
|
222
|
-
macos = File.join(app, "Contents", "MacOS")
|
|
223
|
-
FileUtils.mkdir_p(macos)
|
|
224
|
-
File.write(File.join(app, "Contents", "Info.plist"), dev_info_plist)
|
|
225
|
-
[app, File.join(macos, dev_exe_name)]
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# Spaces are legal in CFBundleExecutable but a nuisance in every shell
|
|
229
|
-
# command that names it, so the executable follows the packaged app's
|
|
230
|
-
# convention and drops them.
|
|
231
|
-
def dev_exe_name = @config.name.gsub(/\s+/, "")
|
|
232
|
-
|
|
233
|
-
def dev_info_plist
|
|
234
|
-
<<~PLIST
|
|
235
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
236
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
237
|
-
<plist version="1.0">
|
|
238
|
-
<dict>
|
|
239
|
-
<key>CFBundleName</key><string>#{@config.name}</string>
|
|
240
|
-
<key>CFBundleDisplayName</key><string>#{@config.name}</string>
|
|
241
|
-
<key>CFBundleExecutable</key><string>#{dev_exe_name}</string>
|
|
242
|
-
<key>CFBundleIdentifier</key><string>#{@config.bundle_id}</string>
|
|
243
|
-
<key>CFBundlePackageType</key><string>APPL</string>
|
|
244
|
-
<key>CFBundleShortVersionString</key><string>#{@config.version}</string>
|
|
245
|
-
<key>NSHighResolutionCapable</key><true/>
|
|
246
|
-
</dict>
|
|
247
|
-
</plist>
|
|
248
|
-
PLIST
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
# The Dock icon for the running shell. `cargo run` produces a bare binary
|
|
252
|
-
# with no .app around it, so macOS has nothing to read an icon from and
|
|
253
|
-
# Tauri's compiled-in placeholder wins — which is how you end up staring
|
|
254
|
-
# at the gem's icon while testing your own app.
|
|
255
|
-
#
|
|
256
|
-
# Shaped with the same Big Sur inset + squircle the packaged .icns gets
|
|
257
|
-
# (Icon.macos_master), so the Dock looks identical in dev and in a release
|
|
258
|
-
# build rather than subtly rounder in one of them. Returns "" when the app
|
|
259
|
-
# has no icon — the shell reads that as "leave it alone".
|
|
260
|
-
def stage_desktop_icon
|
|
261
|
-
source = @config.icon
|
|
262
|
-
return "" unless source && File.exist?(source)
|
|
263
|
-
|
|
264
|
-
dest = File.join(Everywhere::Paths.desktop_icon_dir(@config.bundle_id), "icon.png")
|
|
265
|
-
FileUtils.mkdir_p(File.dirname(dest))
|
|
266
|
-
FileUtils.cp(source, dest) unless Everywhere::Icon.macos_master(source, dest)
|
|
267
|
-
dest
|
|
268
|
-
rescue StandardError => e
|
|
269
|
-
# A Dock icon is never worth failing a dev session over.
|
|
270
|
-
UI.warn("couldn't prepare the Dock icon (#{e.class}) — using the default")
|
|
271
|
-
""
|
|
272
|
-
end
|
|
273
|
-
|
|
274
209
|
def launch_desktop
|
|
275
210
|
# Checked before anything is staged: without cargo the build dies deep
|
|
276
211
|
# in a relayed `sh: cargo: command not found`, after a dock entry and a
|
|
@@ -290,7 +225,7 @@ module Everywhere
|
|
|
290
225
|
return
|
|
291
226
|
end
|
|
292
227
|
|
|
293
|
-
assets_dir, asset_errors = stage_desktop_assets
|
|
228
|
+
assets_dir, asset_errors = desktop_dev_app.stage_desktop_assets
|
|
294
229
|
unless asset_errors.empty?
|
|
295
230
|
asset_errors.each { |error| UI.warn(error) }
|
|
296
231
|
@dock.set(:desktop, :failed, detail: "native/desktop/assets")
|
|
@@ -326,10 +261,10 @@ module Everywhere
|
|
|
326
261
|
supervise(:desktop,
|
|
327
262
|
{ "NATIVE_DEV_URL" => @dev_url, "NATIVE_CONFIG" => @config.to_shell_json,
|
|
328
263
|
"NATIVE_ASSETS_DIR" => assets_dir,
|
|
329
|
-
"NATIVE_ICON_PATH" => stage_desktop_icon,
|
|
264
|
+
"NATIVE_ICON_PATH" => desktop_dev_app.stage_desktop_icon,
|
|
330
265
|
"CARGO_TARGET_DIR" => prepared.target_dir,
|
|
331
266
|
"CARGO_TERM_COLOR" => "always" },
|
|
332
|
-
desktop_command(prepared), chdir: prepared.dir,
|
|
267
|
+
desktop_dev_app.desktop_command(prepared), chdir: prepared.dir,
|
|
333
268
|
filter: LogFilter.for(:cargo), log: dist_log("desktop-dev.log"),
|
|
334
269
|
# cargo colors its status lines, so the marker has to be
|
|
335
270
|
# matched past the escape codes. "Finished" is cargo's last
|
|
@@ -340,31 +275,6 @@ module Everywhere
|
|
|
340
275
|
})
|
|
341
276
|
end
|
|
342
277
|
|
|
343
|
-
# macOS runs the shell from inside a throwaway .app so it has a name;
|
|
344
|
-
# everywhere else `cargo run` is still the whole story.
|
|
345
|
-
#
|
|
346
|
-
# Three steps in one command rather than three Ruby calls, because the
|
|
347
|
-
# link has to happen between the compile and the launch and we want ONE
|
|
348
|
-
# supervised child: cargo's progress relays into the dock, and exec then
|
|
349
|
-
# replaces it with the app, so the pid we're tracking is the window.
|
|
350
|
-
#
|
|
351
|
-
# A hard link, not a copy: same volume (both under ~/.rubyeverywhere), so
|
|
352
|
-
# it's free and instant, where copying a debug Tauri binary is ~100MB on
|
|
353
|
-
# every launch. cargo replaces the file on rebuild rather than writing
|
|
354
|
-
# through it, so the link is remade each time — hence `ln -f`.
|
|
355
|
-
def desktop_command(prepared)
|
|
356
|
-
return "cargo run" unless RUBY_PLATFORM.include?("darwin")
|
|
357
|
-
|
|
358
|
-
_app, exe = stage_dev_bundle
|
|
359
|
-
built = File.join(prepared.target_dir, "debug", "example-shell")
|
|
360
|
-
[
|
|
361
|
-
"cargo build",
|
|
362
|
-
"{ ln -f #{built.shellescape} #{exe.shellescape} 2>/dev/null || " \
|
|
363
|
-
"cp -f #{built.shellescape} #{exe.shellescape}; }",
|
|
364
|
-
"exec #{exe.shellescape}"
|
|
365
|
-
].join(" && ")
|
|
366
|
-
end
|
|
367
|
-
|
|
368
278
|
# Build the shell (Debug, warm after the first run), put it on a booted
|
|
369
279
|
# Simulator and hand it the dev URL. The Simulator shares the Mac's
|
|
370
280
|
# loopback, so 127.0.0.1 works as-is; the template's ATS exception
|
|
@@ -374,7 +284,7 @@ module Everywhere
|
|
|
374
284
|
# xcodebuild and simctl are macOS-only, so `i` elsewhere can only ever
|
|
375
285
|
# produce a long build that fails at the first tool. Android stays
|
|
376
286
|
# available everywhere — its toolchain genuinely is cross-platform.
|
|
377
|
-
unless
|
|
287
|
+
unless Host.darwin?
|
|
378
288
|
UI.warn("iOS builds need macOS and Xcode — #{UI.dim("nothing to do on this platform")}")
|
|
379
289
|
@dock.set(:ios, :idle, detail: "needs macOS")
|
|
380
290
|
return
|
|
@@ -429,7 +339,7 @@ module Everywhere
|
|
|
429
339
|
return
|
|
430
340
|
end
|
|
431
341
|
|
|
432
|
-
opener =
|
|
342
|
+
opener = Host.darwin? ? "open" : "xdg-open"
|
|
433
343
|
# A headless Linux box (or WSL) has no xdg-open, and run? would just
|
|
434
344
|
# return nil — the key would look broken. Print the URL instead.
|
|
435
345
|
unless Shellout.tool?(opener)
|
|
@@ -461,124 +371,51 @@ module Everywhere
|
|
|
461
371
|
|
|
462
372
|
# --- child supervision ----------------------------------------------------
|
|
463
373
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
# believe they're on a terminal and keep their colors; PTY.spawn also
|
|
467
|
-
# setsids, so a Ctrl-C at our terminal can't reach them and teardown
|
|
468
|
-
# decides when they die.
|
|
469
|
-
def supervise(key, env, cmd, chdir:, filter: nil, log: nil, on_line: nil)
|
|
470
|
-
if Shellout.pty?
|
|
471
|
-
io, pid = Shellout.spawn_pty(env, cmd, chdir: chdir)
|
|
472
|
-
thread = relay_thread(key, io, pid, filter: filter, log: log, on_line: on_line)
|
|
473
|
-
else
|
|
474
|
-
# Windows, or any platform without a pty: fall back to inherited fds.
|
|
475
|
-
# No tags and no dock framing, but everything else works.
|
|
476
|
-
pid = Shellout.spawn(env, cmd, chdir: chdir)
|
|
477
|
-
end
|
|
478
|
-
Process.detach(pid)
|
|
479
|
-
@children.synchronize do
|
|
480
|
-
@pids[key] = pid
|
|
481
|
-
@relays[key] = thread if thread
|
|
482
|
-
end
|
|
483
|
-
pid
|
|
484
|
-
end
|
|
374
|
+
def spawn_child(env, cmd, chdir:)
|
|
375
|
+
return Shellout.spawn_pty(env, cmd, chdir: chdir) if Shellout.pty?
|
|
485
376
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
Thread.current.report_on_exception = false
|
|
490
|
-
begin
|
|
491
|
-
Relay.new(io, source: key, log: log, filter: filter, on_line: on_line).run
|
|
492
|
-
rescue StandardError => e
|
|
493
|
-
# Losing a relay costs us that child's output, nothing more. It must
|
|
494
|
-
# not surface later out of the join in teardown.
|
|
495
|
-
Kernel.warn("#{key} output relay stopped: #{e.class}: #{e.message}")
|
|
496
|
-
end
|
|
497
|
-
# Only report a death we didn't ask for. stop_child removes the pid
|
|
498
|
-
# BEFORE it signals, so a deliberate restart can never match here —
|
|
499
|
-
# what does match is a shell that quit or crashed on its own, which is
|
|
500
|
-
# exactly what the dock should show.
|
|
501
|
-
@dock.set(key, :stopped) if !@stopping && @children.synchronize { @pids[key] } == pid
|
|
502
|
-
end
|
|
377
|
+
# Windows, or any platform without a pty: fall back to inherited fds.
|
|
378
|
+
# No tags and no dock framing, but everything else works.
|
|
379
|
+
[nil, Shellout.spawn(env, cmd, chdir: chdir)]
|
|
503
380
|
end
|
|
504
381
|
|
|
505
|
-
#
|
|
506
|
-
#
|
|
507
|
-
#
|
|
508
|
-
def
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
reap([pid], grace: grace)
|
|
513
|
-
thread&.join(1)
|
|
382
|
+
# The extra advice an `every dev` failure carries: this is the app's own
|
|
383
|
+
# bin/dev, run from the app's own checkout, so the two usual explanations
|
|
384
|
+
# are worth naming.
|
|
385
|
+
def wait_for_port(port, pid: nil, timeout: 60,
|
|
386
|
+
dead_hint: " (did `bundle install` run? does bin/dev's foreman exist?)",
|
|
387
|
+
slow_hint: " — it may just be slow to boot; check its output above")
|
|
388
|
+
super
|
|
514
389
|
end
|
|
515
390
|
|
|
516
|
-
#
|
|
517
|
-
#
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
live.each { |pid| ChildProcesses.signal(pid, "TERM") }
|
|
523
|
-
deadline = monotonic + grace
|
|
524
|
-
sleep(0.1) while live.any? { |pid| ChildProcesses.alive?(pid) } && monotonic < deadline
|
|
525
|
-
live.select { |pid| ChildProcesses.alive?(pid) }.each { |pid| ChildProcesses.signal(pid, "KILL") }
|
|
391
|
+
# Only report a death we didn't ask for. stop_child removes the pid
|
|
392
|
+
# BEFORE it signals, so a deliberate restart can never match here —
|
|
393
|
+
# what does match is a shell that quit or crashed on its own, which is
|
|
394
|
+
# exactly what the dock should show.
|
|
395
|
+
def on_child_exit(key, pid)
|
|
396
|
+
@dock.set(key, :stopped) if !@stopping && @children.synchronize { @pids[key] } == pid
|
|
526
397
|
end
|
|
527
398
|
|
|
528
|
-
def child_pid(key) = @children.synchronize { @pids[key] }
|
|
529
|
-
|
|
530
399
|
# --- the key menu ---------------------------------------------------------
|
|
531
400
|
|
|
532
|
-
#
|
|
533
|
-
# and stair-steps every log line we relay; stty -icanon -echo disables only
|
|
534
|
-
# line buffering and echo, leaving output processing (and Ctrl-C as SIGINT)
|
|
535
|
-
# intact. Builds no longer run on this thread, so the menu stays responsive
|
|
401
|
+
# Builds no longer run on this thread, so the menu stays responsive
|
|
536
402
|
# while one is going — including `q`.
|
|
537
403
|
def interact
|
|
538
404
|
@dock.announce_keys
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
when "l" then toggle_logs
|
|
552
|
-
when "r" then background(:web) { restart_server }
|
|
553
|
-
when "?" then @dock.toggle_help
|
|
554
|
-
when "q", "\u0004", nil then break # q, Ctrl-D, EOF (Ctrl-C stays a real SIGINT)
|
|
405
|
+
RawTty.with do
|
|
406
|
+
loop do
|
|
407
|
+
case $stdin.getc
|
|
408
|
+
when "d" then background(:desktop) { launch_desktop }
|
|
409
|
+
when "i" then background(:ios) { launch_ios }
|
|
410
|
+
when "a" then background(:android) { launch_android }
|
|
411
|
+
when "b" then launch_browser
|
|
412
|
+
when "l" then toggle_logs
|
|
413
|
+
when "r" then background(:web) { restart_server }
|
|
414
|
+
when "?" then @dock.toggle_help
|
|
415
|
+
when "q", "\u0004", nil then break # q, Ctrl-D, EOF (Ctrl-C stays a real SIGINT)
|
|
416
|
+
end
|
|
555
417
|
end
|
|
556
418
|
end
|
|
557
|
-
ensure
|
|
558
|
-
system("stty", saved_tty) if saved_tty
|
|
559
|
-
end
|
|
560
|
-
|
|
561
|
-
# The tty settings to put back on the way out, or nil when there's no
|
|
562
|
-
# usable stty to read them with — the one signal both the raw-mode setup
|
|
563
|
-
# and its restore key off.
|
|
564
|
-
def saved_tty_state
|
|
565
|
-
return nil unless Shellout.tool?("stty")
|
|
566
|
-
|
|
567
|
-
state = `stty -g`.chomp
|
|
568
|
-
state.empty? ? nil : state
|
|
569
|
-
rescue StandardError
|
|
570
|
-
nil
|
|
571
|
-
end
|
|
572
|
-
|
|
573
|
-
# The dock hides the cursor and the key loop puts the tty in -icanon
|
|
574
|
-
# -echo; a SIGTERM that skipped our ensure blocks would leave the user's
|
|
575
|
-
# shell in that state. exit(1) from a trap unwinds normally, which is all
|
|
576
|
-
# this needs to do — a handler must stay this small, because anything that
|
|
577
|
-
# takes a lock (Console, the dock) raises ThreadError in trap context.
|
|
578
|
-
def trap_exit_signals
|
|
579
|
-
%w[TERM HUP].each { |sig| Signal.trap(sig) { exit(1) } }
|
|
580
|
-
rescue ArgumentError
|
|
581
|
-
nil
|
|
582
419
|
end
|
|
583
420
|
|
|
584
421
|
# --- headless -------------------------------------------------------------
|
|
@@ -624,26 +461,8 @@ module Everywhere
|
|
|
624
461
|
|
|
625
462
|
# --- teardown -------------------------------------------------------------
|
|
626
463
|
|
|
627
|
-
# Teardown must never raise — it runs from an ensure, where an exception
|
|
628
|
-
# would mask whatever actually ended the session — and one failed step
|
|
629
|
-
# must not skip the rest, or a child survives the CLI that spawned it.
|
|
630
464
|
def teardown
|
|
631
|
-
@
|
|
632
|
-
pids, relays = @children ? @children.synchronize { [@pids.values, @relays.values] } : [[], []]
|
|
633
|
-
[
|
|
634
|
-
-> { @dock&.close },
|
|
635
|
-
-> { @pool&.shutdown },
|
|
636
|
-
# TERM everything, then give it a moment and KILL whatever is left.
|
|
637
|
-
# The CLI exits the instant this returns, so anything still alive is
|
|
638
|
-
# orphaned — and a windowed shell is exactly the kind of child that
|
|
639
|
-
# doesn't drop dead on the first signal.
|
|
640
|
-
-> { reap(pids, grace: 2) },
|
|
641
|
-
-> { relays.each { |thread| thread.join(1) } }
|
|
642
|
-
].each do |step|
|
|
643
|
-
step.call
|
|
644
|
-
rescue StandardError => e
|
|
645
|
-
Kernel.warn("teardown: #{e.class}: #{e.message}")
|
|
646
|
-
end
|
|
465
|
+
teardown_children(before: [-> { @dock&.close }, -> { @pool&.shutdown }])
|
|
647
466
|
end
|
|
648
467
|
|
|
649
468
|
# --- helpers --------------------------------------------------------------
|
|
@@ -655,50 +474,6 @@ module Everywhere
|
|
|
655
474
|
rescue StandardError
|
|
656
475
|
nil
|
|
657
476
|
end
|
|
658
|
-
|
|
659
|
-
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
660
|
-
|
|
661
|
-
# Checked once, up front: Integer() would otherwise raise from inside
|
|
662
|
-
# port_open? — an ArgumentError backtrace after the dock has already
|
|
663
|
-
# taken over the terminal.
|
|
664
|
-
def validate_port!(port)
|
|
665
|
-
number = begin
|
|
666
|
-
Integer(port.to_s, 10)
|
|
667
|
-
rescue ArgumentError, TypeError
|
|
668
|
-
UI.die!("--port must be a number (got #{port.inspect})")
|
|
669
|
-
end
|
|
670
|
-
UI.die!("--port must be between 1 and 65535 (got #{number})") unless (1..65_535).cover?(number)
|
|
671
|
-
number.to_s
|
|
672
|
-
end
|
|
673
|
-
|
|
674
|
-
# Any connect error means "nothing listening yet" for our purposes —
|
|
675
|
-
# ECONNREFUSED is the common one, but a firewall or an exhausted fd table
|
|
676
|
-
# answers differently and must not abort the session.
|
|
677
|
-
def port_open?(port)
|
|
678
|
-
TCPSocket.new("127.0.0.1", Integer(port)).close
|
|
679
|
-
true
|
|
680
|
-
rescue SystemCallError
|
|
681
|
-
false
|
|
682
|
-
end
|
|
683
|
-
|
|
684
|
-
# Watches the child as well as the port: a dev server that dies on
|
|
685
|
-
# startup (no `bundle install`, a bin/dev whose foreman isn't installed)
|
|
686
|
-
# never opens the port, and waiting out the full timeout for it buries
|
|
687
|
-
# the error it already printed under a minute of silence.
|
|
688
|
-
def wait_for_port(port, pid: nil, timeout: 60)
|
|
689
|
-
deadline = monotonic + timeout
|
|
690
|
-
until port_open?(port)
|
|
691
|
-
if pid && !ChildProcesses.alive?(pid)
|
|
692
|
-
UI.die!("the dev server exited before it finished booting — scroll up for its output " \
|
|
693
|
-
"(did `bundle install` run? does bin/dev's foreman exist?)")
|
|
694
|
-
end
|
|
695
|
-
if monotonic > deadline
|
|
696
|
-
UI.die!("dev server never came up on port #{port} after #{timeout}s — it may just be slow " \
|
|
697
|
-
"to boot; check its output above")
|
|
698
|
-
end
|
|
699
|
-
sleep 0.25
|
|
700
|
-
end
|
|
701
|
-
end
|
|
702
477
|
end
|
|
703
478
|
end
|
|
704
479
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "dry/cli"
|
|
4
4
|
require_relative "../shellout"
|
|
5
|
+
require_relative "../host"
|
|
5
6
|
require_relative "../paths"
|
|
6
7
|
require_relative "../config"
|
|
7
8
|
require_relative "../android_sdk"
|
|
@@ -60,7 +61,6 @@ module Everywhere
|
|
|
60
61
|
required ? name : "#{name} #{UI.dim("(not in build.targets — informational)")}"
|
|
61
62
|
end
|
|
62
63
|
|
|
63
|
-
def macos? = RUBY_PLATFORM.include?("darwin")
|
|
64
64
|
|
|
65
65
|
# Desktop is the implicit default: an app with no build.targets presses
|
|
66
66
|
# for the host. Any non-mobile target (or --target) counts as desktop,
|
|
@@ -79,7 +79,7 @@ module Everywhere
|
|
|
79
79
|
UI.step(section_label("Desktop target", required))
|
|
80
80
|
# Mirrors the gate in `every build`: pressing is macOS-only today, so
|
|
81
81
|
# probing for its toolchain elsewhere would only mislead.
|
|
82
|
-
unless
|
|
82
|
+
unless Host.darwin?
|
|
83
83
|
return fix_check("macOS", "desktop builds are macOS-only for now") { false } if required
|
|
84
84
|
|
|
85
85
|
UI.warn("desktop builds are macOS-only for now — skipped")
|
|
@@ -101,14 +101,14 @@ module Everywhere
|
|
|
101
101
|
# it, since it stays off PATH (keg-only). Everywhere else the package
|
|
102
102
|
# manager installs a current bison normally.
|
|
103
103
|
def bison?
|
|
104
|
-
return Shellout.tool?("bison") unless
|
|
104
|
+
return Shellout.tool?("bison") unless Host.darwin?
|
|
105
105
|
|
|
106
106
|
out, status = Shellout.capture("brew", "--prefix", "bison")
|
|
107
107
|
!status.nil? && status.success? && !out.strip.empty?
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
def bison_label =
|
|
111
|
-
def bison_hint =
|
|
110
|
+
def bison_label = Host.darwin? ? "homebrew bison" : "bison"
|
|
111
|
+
def bison_hint = Host.darwin? ? "brew install bison" : "install bison with your package manager"
|
|
112
112
|
|
|
113
113
|
# Config.load raises on an unparseable everywhere.yml; doctor reports it
|
|
114
114
|
# as its first check and carries on with an empty config, so the machine
|
|
@@ -152,7 +152,7 @@ module Everywhere
|
|
|
152
152
|
UI.step(section_label("iOS target", required))
|
|
153
153
|
# Off macOS every tool below is guaranteed absent — one honest line
|
|
154
154
|
# beats four unfixable ✗s.
|
|
155
|
-
unless
|
|
155
|
+
unless Host.darwin?
|
|
156
156
|
return fix_check("macOS with Xcode", "iOS apps can only be built on a Mac") { false } if required
|
|
157
157
|
|
|
158
158
|
UI.warn("needs macOS and Xcode — skipped")
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "dry/cli"
|
|
4
4
|
require "tmpdir"
|
|
5
5
|
require "fileutils"
|
|
6
|
+
require_relative "../../clock"
|
|
6
7
|
require_relative "../../ui"
|
|
7
8
|
require_relative "../../config"
|
|
8
9
|
require_relative "../../framework"
|
|
@@ -172,7 +173,7 @@ module Everywhere
|
|
|
172
173
|
def poll_and_tail(client, build_id, build_url = nil)
|
|
173
174
|
printed = Hash.new("")
|
|
174
175
|
last = {}
|
|
175
|
-
started =
|
|
176
|
+
started = Clock.monotonic
|
|
176
177
|
status = UI::Status.new
|
|
177
178
|
errors = 0
|
|
178
179
|
|
|
@@ -220,10 +221,10 @@ module Everywhere
|
|
|
220
221
|
# faster than we poll so the line reads as "alive" during the multi-minute
|
|
221
222
|
# pre-runner wait rather than freezing between requests.
|
|
222
223
|
def spin(status, artifacts, started, interval)
|
|
223
|
-
deadline =
|
|
224
|
+
deadline = Clock.monotonic + interval
|
|
224
225
|
loop do
|
|
225
|
-
status.update("#{waiting_summary(artifacts)} · #{UI.elapsed(
|
|
226
|
-
remaining = deadline -
|
|
226
|
+
status.update("#{waiting_summary(artifacts)} · #{UI.elapsed(Clock.monotonic - started)} elapsed")
|
|
227
|
+
remaining = deadline - Clock.monotonic
|
|
227
228
|
break if remaining <= 0
|
|
228
229
|
|
|
229
230
|
sleep([remaining, SPINNER_TICK].min)
|
|
@@ -248,8 +249,6 @@ module Everywhere
|
|
|
248
249
|
label.empty? ? artifact["status"].to_s : label
|
|
249
250
|
end
|
|
250
251
|
|
|
251
|
-
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
252
|
-
|
|
253
252
|
def tail_log(artifact, printed)
|
|
254
253
|
full = artifact["log"].to_s
|
|
255
254
|
prev = printed[artifact["id"]]
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "dry/cli"
|
|
4
4
|
require "socket"
|
|
5
|
+
require_relative "../../clock"
|
|
6
|
+
require_relative "../../error"
|
|
5
7
|
require_relative "../../ui"
|
|
6
8
|
require_relative "../../platform/client"
|
|
7
9
|
require_relative "../../platform/credentials"
|
|
@@ -51,10 +53,10 @@ module Everywhere
|
|
|
51
53
|
private
|
|
52
54
|
|
|
53
55
|
def poll(client, device_code, interval, expires_in)
|
|
54
|
-
deadline = monotonic + (expires_in.positive? ? expires_in : 600)
|
|
56
|
+
deadline = Clock.monotonic + (expires_in.positive? ? expires_in : 600)
|
|
55
57
|
loop do
|
|
56
58
|
sleep(interval)
|
|
57
|
-
UI.die!("login timed out — run `every platform login` again") if monotonic > deadline
|
|
59
|
+
UI.die!("login timed out — run `every platform login` again") if Clock.monotonic > deadline
|
|
58
60
|
|
|
59
61
|
# The user is mid-approval in a browser; a dropped connection here must
|
|
60
62
|
# not kill the login. Keep polling until the deadline instead.
|
|
@@ -75,8 +77,6 @@ module Everywhere
|
|
|
75
77
|
end
|
|
76
78
|
end
|
|
77
79
|
|
|
78
|
-
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
79
|
-
|
|
80
80
|
# Shown on the approval page + used as the token's name, so a user can tell
|
|
81
81
|
# their machines apart when reviewing/revoking tokens.
|
|
82
82
|
def client_name
|