Class: Everywhere::Commands::Logs
- Inherits:
-
Dry::CLI::Command
- Object
- Dry::CLI::Command
- Everywhere::Commands::Logs
- Defined in:
- lib/everywhere/commands/logs.rb
Overview
Tail the native shell's logs. Two platforms, one shape: the iOS Simulator's unified log scoped to the shell process, or Android's logcat scoped to the shell's process id. Watches by default; --last replays recent history and exits.
Constant Summary collapse
- PREDICATE =
The template contract fixes the product name (App.app), so the shell's process on the Simulator is always "App" — one predicate covers every Everywhere app without knowing which one is installed. Scoping to the process alone still yields ~100k lines/half hour of Apple-framework chatter (WebKit resource loads, activities); the default predicate keeps what a shell developer logs — NSLog (Foundation sender, no subsystem), Hotwire Native's subsystem, UIKit warnings — and drops the rest. --verbose falls back to the whole process.
'process == "App" AND eventType == logEvent AND NOT subsystem BEGINSWITH "com.apple"'- VERBOSE_PREDICATE =
'process == "App"'- DEFAULT_LEVEL =
Android has no predicate language, so the same job is split in two:
--pid=fixes the process (see #android_logs) and a filterspec fixes the priority floor.Inside our own process the analogue of iOS's Apple-framework chatter is the platform's own: ART's GC bookkeeping, binder transactions, chromium's internal tracing. Those sit at VERBOSE, so the default floor is DEBUG — which keeps Hotwire Native's logging (Hotwire-Core and Hotwire-Navigation both log at debug), keeps every WebView console message (chromium reports them at INFO), and drops the runtime's housekeeping. --verbose lowers the floor to VERBOSE.
Deliberately NOT a tag list: filtering on Hotwire-Core/Hotwire-Navigation would be the obvious way to cut noise and would hide the two things a shell developer most wants — their own Log calls and their page's console output. Scoping by process keeps everything that is ours and nothing that isn't, which is the same trade the iOS predicate makes.
"*:D"- VERBOSE_LEVEL =
"*:V"- DEFAULT_FORMAT =
-v timeis the compact-style analogue: with the process already fixed, repeating its pid on every line is noise. --verbose widens to threadtime, which adds the thread id you need once you're reading runtime internals. "time"- VERBOSE_FORMAT =
"threadtime"- LOGCAT_TIME =
logcat's -t takes a line count or a timestamp; it has no notion of "5m". So a bare number passes straight through as a count, and a duration in the iOS --last spelling is converted to the timestamp logcat wants.
That timestamp is compared against the device's clock. An emulator inherits the host's clock and timezone so it lines up exactly; a physical device whose clock has drifted will return a little more or less than asked. A line count has no such ambiguity, which is why both spellings stay.
"%m-%d %H:%M:%S.000"- DURATION_UNITS =
{ "s" => 1, "m" => 60, "h" => 3600, "d" => 86_400 }.freeze
Instance Method Summary collapse
-
#android_argv(serial, pid, last: nil, verbose: false) ⇒ Object
-ddumps what's buffered and exits — thelog showhalf of the iOS split — and-tbounds that dump. -
#android_logs(serial: nil, app_id: nil, last: nil, verbose: false) ⇒ Object
logcat carries every process on the device, so the pid lookup is the whole filter — and it's also why "the app isn't running" is a distinct, extremely common state that has to be said plainly rather than shown as an empty stream.
-
#argv(udid, last: nil, verbose: false) ⇒ Object
simctl spawn runs the
logtool inside the Simulator runtime — the supported way to read a Simulator's unified log from the host. - #call(ios: false, android: false, last: nil, udid: nil, serial: nil, app_id: nil, verbose: false) ⇒ Object
-
#ios_logs(udid: nil, last: nil, verbose: false) ⇒ Object
--- iOS ------------------------------------------------------------------.
- #logcat_window(last) ⇒ Object
-
#window_label(last) ⇒ Object
"200" reads as a count, everything else as a duration.
Instance Method Details
#android_argv(serial, pid, last: nil, verbose: false) ⇒ Object
-d dumps what's buffered and exits — the log show half of the iOS
split — and -t bounds that dump. Without -d, logcat follows.
130 131 132 133 134 135 |
# File 'lib/everywhere/commands/logs.rb', line 130 def android_argv(serial, pid, last: nil, verbose: false) argv = [Everywhere::AndroidSdk.adb!, "-s", serial, "logcat", "--pid=#{pid}"] argv += ["-d", "-t", logcat_window(last)] if last argv + ["-v", verbose ? VERBOSE_FORMAT : DEFAULT_FORMAT, verbose ? VERBOSE_LEVEL : DEFAULT_LEVEL] end |
#android_logs(serial: nil, app_id: nil, last: nil, verbose: false) ⇒ Object
logcat carries every process on the device, so the pid lookup is the whole filter — and it's also why "the app isn't running" is a distinct, extremely common state that has to be said plainly rather than shown as an empty stream. Unlike iOS, the process name isn't fixed by the template (it's the applicationId), so the config supplies it.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/everywhere/commands/logs.rb', line 111 def android_logs(serial: nil, app_id: nil, last: nil, verbose: false) serial ||= Everywhere::Emulator.booted_serial UI.die!("no Android emulator or device — start one with `every dev --android`") unless serial application_id = app_id || Everywhere::Config.load(Dir.pwd).bundle_id(target: "android-arm64") pid = Everywhere::Emulator.pid_of(serial, application_id) unless pid UI.die!("#{application_id} isn't running on #{serial} — start it with `every dev --android` " \ "(or pass --app-id if that isn't this app's applicationId)") end UI.step(last ? "Android logs from the last #{UI.bold(window_label(last))}" : "watching Android logs for #{UI.bold(application_id)} #{UI.dim("(Ctrl-C to stop)")}") $stdout.flush # see ios_logs — exec discards a buffered header exec(*android_argv(serial, pid, last: last, verbose: verbose)) end |
#argv(udid, last: nil, verbose: false) ⇒ Object
simctl spawn runs the log tool inside the Simulator runtime — the
supported way to read a Simulator's unified log from the host. log show
spells verbosity --info/--debug where log stream takes --level.
94 95 96 97 98 99 100 101 102 |
# File 'lib/everywhere/commands/logs.rb', line 94 def argv(udid, last: nil, verbose: false) argv = ["xcrun", "simctl", "spawn", udid, "log"] argv += if last ["show", "--last", last, *(verbose ? ["--info", "--debug"] : [])] else ["stream", *(verbose ? ["--level", "debug"] : [])] end argv + ["--style", "compact", "--predicate", verbose ? VERBOSE_PREDICATE : PREDICATE] end |
#call(ios: false, android: false, last: nil, udid: nil, serial: nil, app_id: nil, verbose: false) ⇒ Object
64 65 66 67 68 69 70 71 72 73 |
# File 'lib/everywhere/commands/logs.rb', line 64 def call(ios: false, android: false, last: nil, udid: nil, serial: nil, app_id: nil, verbose: false, **) UI.die!("pick a platform — run: every logs --ios, or every logs --android") unless ios || android UI.die!("--ios and --android are two separate log streams — pick one") if ios && android if android android_logs(serial: serial, app_id: app_id, last: last, verbose: verbose) else ios_logs(udid: udid, last: last, verbose: verbose) end end |
#ios_logs(udid: nil, last: nil, verbose: false) ⇒ Object
--- iOS ------------------------------------------------------------------
77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/everywhere/commands/logs.rb', line 77 def ios_logs(udid: nil, last: nil, verbose: false) udid ||= Everywhere::Simulator.booted_udid UI.die!("no booted iOS Simulator — start one with `every dev --ios`") unless udid UI.step(last ? "iOS Simulator logs from the last #{UI.bold(last)}" : "watching iOS Simulator logs #{UI.dim("(Ctrl-C to stop)")}") # exec replaces this process, so anything still sitting in Ruby's stdout # buffer dies with it — and stdout IS buffered whenever it's a pipe, # which is exactly how `every dev`'s log toggle runs us. Without this # the header above silently vanishes in the one context that spawns us. $stdout.flush exec(*argv(udid, last: last, verbose: verbose)) end |
#logcat_window(last) ⇒ Object
149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/everywhere/commands/logs.rb', line 149 def logcat_window(last) text = last.to_s.strip return text if text.match?(/\A\d+\z/) if (match = text.match(/\A(\d+)([smhd])\z/i)) seconds = match[1].to_i * DURATION_UNITS.fetch(match[2].downcase) return (Time.now - seconds).strftime(LOGCAT_TIME) end UI.die!(%(--last takes a line count ("200") or a duration ("30s", "5m", "2h") — got #{last.inspect})) end |
#window_label(last) ⇒ Object
"200" reads as a count, everything else as a duration.
162 |
# File 'lib/everywhere/commands/logs.rb', line 162 def window_label(last) = last.to_s.match?(/\A\d+\z/) ? "#{last} lines" : last.to_s |