An app's own log: a devlog contract, and a Runtime tab for an APK component
Android forbids one app reading another's logcat, so an APK this server delivers has had no way to say what it did to somebody holding the phone and nothing else. It can now expose its own bounded log through a ContentProvider at `<applicationId>.devlog`, guarded by a permission declared here; README.md's "An app's own log" is the whole contract, and any project this server delivers can implement it. The phone reads that provider while the Runtime tab is open and forwards what is new into the component's runtime log on this machine, so the tab renders from the same store a service's does and the history outlives the phone. `LogKind::Runtime` stays one kind with two sources rather than growing a third, and this server parses nothing -- what arrives is one line of text each, appended, exactly as a service's stdout is. The log button is now unconditional, like the gear beside it: with the tab able to say which of several reasons there is nothing to read, its absence was the one thing that could not say anything at all. Supersedes ai-app posting its ring to ai-server over the tunnel, which put a phone's lines under the wrong component and only ever worked for that one project. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
3727c7c67b
commit
013d7116d7
11 files changed
+772
-44
No files matched your search
@@ -574,22 +574,44 @@ mutable at runtime from the phone.
|
||||
process marked its own errors -- and sequences with no meaning on a
|
||||
phone are consumed rather than printed, so a cursor movement cannot
|
||||
arrive looking like a corrupted log.
|
||||
- **A runtime log can carry a *phone app's* own lines, and nothing here
|
||||
had to change for it.** ai-app (`~/repos/ai-app-2`, the `rustify`
|
||||
branch) has a phone this project delivers APKs to whose owner has no
|
||||
`adb`; Android forbids one app reading another's `logcat`, so the app
|
||||
keeps a bounded in-memory copy of its own log and posts it to
|
||||
`ai-server` over the tunnel it is already enrolled against.
|
||||
`ai-server` re-emits each line into its own `tracing` output, which is
|
||||
this server's `Managed` service redirecting stdout to
|
||||
`$XDG_DATA_HOME/dev-updater/services/<key>-<component>/<same>.log` --
|
||||
so the lines arrive in the *server* component's Runtime tab, tagged
|
||||
`ai_server::client_log` and carrying the app's own clock. Worth knowing
|
||||
before adding a device-log store here: this route was chosen over one
|
||||
(a new authenticated write endpoint on the TLS surface, a per-app log
|
||||
store, `hasRuntimeLogs` for an APK component, and a second enrollment
|
||||
for that app) precisely because the existing runtime log already
|
||||
reaches the reader. Their reasoning is in ai-app's
|
||||
- **An APK's runtime log comes from the phone, through a contract any
|
||||
managed app can implement.** A `Server` runs here and its script says
|
||||
where it writes; an `Apk` runs on the phone, where Android forbids one
|
||||
app reading another's `logcat` -- so the app keeps a bounded copy of its
|
||||
own log and exposes it through a `ContentProvider` at
|
||||
`<applicationId>.devlog`, guarded by `dev.updater.permission.READ_DEVLOG`
|
||||
(declared here, `protectionLevel="normal"`). README.md has the whole
|
||||
contract; what matters here is the shape of it.
|
||||
**One kind, two sources**: `LogKind::Runtime` stays one tab and one
|
||||
route, and `AppEntry::component_logs` picks the source from the
|
||||
component's variant. That is deliberately not a third kind -- the reader
|
||||
is asking the same question either way, and a `hasDevLog` beside
|
||||
`hasRuntimeLogs` would have been the same axis said twice.
|
||||
The phone forwards rather than rendering the provider directly, into
|
||||
`POST /apps/{key}/components/{name}/runtime-log`, so the history
|
||||
outlives the phone and both kinds render from one store. **This server
|
||||
parses nothing**: what arrives is one line of text each, formatted on
|
||||
the phone, appended by `logs::append_devlog` -- the same posture it
|
||||
takes towards a service's stdout, and what keeps every managed app's log
|
||||
format out of here. Refused for a `Server`, because a file with two
|
||||
writers is two versions of what a service printed. The store is
|
||||
`$XDG_DATA_HOME/dev-updater/devlogs/<key>-<component>.log` with one
|
||||
rotation at 4 MiB (`.1`, the same two generations a build log has, and
|
||||
the only bound available -- a phone forwards a stream with no run
|
||||
boundary to rotate on).
|
||||
**`normal` rather than `signature` is a real trade and is written down
|
||||
in three places** (the manifest, README.md, and ai-app's
|
||||
`docs/DECISIONS.md` 2026-09-07): the two apps are signed with different
|
||||
locally generated keys, so a signature permission would be held by
|
||||
nothing, and what `normal` costs is that any app requesting it by name
|
||||
can read another's dev log on that phone.
|
||||
**What this replaced**: ai-app's app used to post its ring to
|
||||
`ai-server` over the tunnel, which re-emitted it into its own `tracing`
|
||||
output and so into the *server* component's runtime log. Nothing here
|
||||
had to change for that, which was its whole appeal -- but it put a
|
||||
phone's lines under a different component, needed the app to be enrolled
|
||||
against a server with a baked-in token, and only ever worked for that
|
||||
one project. It is deleted; their reasoning is in ai-app's
|
||||
`docs/DECISIONS.md`, 2026-09-07.
|
||||
- **A project's row has three buttons -- Pull, Update, Remove -- and the
|
||||
first two are the same act at two lengths.** Pull takes the commits and
|
||||
|
||||
Reference in new issue
Block a user