Skip to content

Commit b1e44f2

Browse files
authored
Give the manager ways in again, and name builds by where they came from (#809)
Flash Vector, open it from the root manager, close it, and there is no way back in. The dialer code registered a filter with no action, which matches nothing, so that branch has been dead since #597; it works again and is rebound to 832867, VECTOR on the keypad. Parasitically the manager is not installed, so the launcher has nothing to show either: the pinned shortcut and the standalone install that #796 dropped return, in an "Opening Vector" section and a first-launch prompt, with `getManagerApk()` handing over the APK the host cannot read. Sixteen new strings, translated into all eighteen languages. The version string now names where a build came from rather than calling every canary "dirty", which it did only because the workflow writes signing credentials into the tracked `gradle.properties`. Repository and commit both come from the pull request's head, since GitHub's defaults describe the run and not the code. Closes #815.
1 parent 4455239 commit b1e44f2

35 files changed

Lines changed: 1590 additions & 33 deletions

File tree

.github/workflows/core.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ jobs:
1515
permissions:
1616
contents: write
1717
env:
18+
# Where this build's code came from, for the version string the manager and module.prop show.
19+
# Both are the head of the pull request rather than GitHub's defaults, which on a pull request
20+
# describe the run instead of the code: GITHUB_REPOSITORY is this repository even when the
21+
# branch came from a fork, and the checked-out HEAD is an ephemeral merge commit that exists
22+
# nowhere and cannot be looked up by anyone who reads it off a device. Both head.* values are
23+
# null outside a pull request, where the defaults are already right.
24+
VECTOR_BUILD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
25+
VECTOR_BUILD_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
1826
CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion"
1927
CCACHE_NOHASHDIR: "true"
2028
CCACHE_HARDLINK: "true"

build.gradle.kts

Lines changed: 121 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.android.build.gradle.api.AndroidBasePlugin
44
import com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask
55
import java.io.ByteArrayOutputStream
66
import javax.inject.Inject
7+
import org.gradle.api.provider.Property
78
import org.gradle.api.provider.ValueSource
89
import org.gradle.api.provider.ValueSourceParameters
910
import org.gradle.process.ExecOperations
@@ -60,46 +61,142 @@ abstract class GitLatestTagValueSource : ValueSource<String, ValueSourceParamete
6061
}
6162

6263
/**
63-
* The commit a build actually came from, short, with a marker for uncommitted changes.
64+
* Which build this is, in one string: where it was built and from what commit.
6465
*
6566
* The version code is the commit count on origin/master, so every branch build carries master's
6667
* number: a build flashed from a feature branch and one flashed from master both report "v2.0
6768
* (3052)" and cannot be told apart on the device. That is not hypothetical — it cost a real
6869
* investigation to establish which of the two was installed.
6970
*
70-
* The hash goes in module.prop's human-readable version only. BuildConfig.VERSION_NAME, which is
71-
* what the manager prints on Home, is deliberately left clean.
71+
* Two shapes, because "where did this binary come from" has two different answers:
72+
*
73+
* *On GitHub Actions*, `JingMatrix-Vector-93d66473` — the repository the code came from, then the
74+
* commit. Forks build the same code from the same commits, so the hash alone identifies a
75+
* *revision* and not a *build*, and a fork's artifact was indistinguishable from ours.
76+
*
77+
* Both halves are the *head* ones, and neither is read from the environment GitHub sets by default,
78+
* because on a pull request both defaults name something other than the code that was built:
79+
*
80+
* `GITHUB_REPOSITORY` is the repository that *ran* the workflow. A pull request from a fork is built
81+
* here, so it would stamp `JingMatrix/Vector` onto a branch that has never been near it — which is
82+
* exactly the artifact the name most needs to distinguish.
83+
*
84+
* `HEAD` is worse. For a pull request the runner checks out an ephemeral merge of the branch into
85+
* the base, so `git rev-parse HEAD` names a commit that exists in no repository, cannot be looked up
86+
* by anyone who reads it off a device, and is gone once the run is. The commit that was pushed is
87+
* the one that identifies the build.
88+
*
89+
* So the workflow passes both in: `VECTOR_BUILD_REPOSITORY` and `VECTOR_BUILD_COMMIT`, each taken
90+
* from `github.event.pull_request.head.*` when there is a pull request and from the ordinary default
91+
* when there is not — outside a pull request the two agree anyway. Absent either, this falls back to
92+
* what the environment says and to `HEAD`, which covers a workflow too old to set them.
93+
*
94+
* *Locally*, the bare `93d66473`, or `93d66473-thinkpad` when the tree has uncommitted changes.
95+
* That build corresponds to no commit at all, so naming the commit alone would name something the
96+
* binary does not match — and whoever is looking at it is far better served by the machine it was
97+
* built on than by the word "dirty", since a device that has a hand-built framework on it usually
98+
* has exactly one candidate author.
99+
*
100+
* The dirty check is deliberately not applied on CI. The workflow's own "Write key" step appends
101+
* the signing credentials to the tracked `gradle.properties` before Gradle starts, so every master
102+
* and tag build reports a modified tree — every published canary has said `-dirty` for that reason
103+
* and no other, which sent at least one user looking for a fault that was not there (#815).
104+
*
105+
* The result goes in module.prop's human-readable version and in BuildConfig.VERSION_HASH, which
106+
* the manager shows on the status page. BuildConfig.VERSION_NAME is deliberately left clean.
72107
*/
73-
abstract class GitCommitHashValueSource : ValueSource<String, ValueSourceParameters.None> {
108+
abstract class GitCommitHashValueSource : ValueSource<String, GitCommitHashValueSource.Parameters> {
109+
interface Parameters : ValueSourceParameters {
110+
/**
111+
* `owner/repo` when GitHub Actions is building this, empty otherwise.
112+
*
113+
* Threaded in as parameters rather than read with `System.getenv` inside [obtain], which the
114+
* configuration cache does not see and therefore does not invalidate on.
115+
*/
116+
val buildRepository: Property<String>
117+
118+
/** The full SHA that was pushed, when CI knows one and it is not what is checked out. */
119+
val buildCommit: Property<String>
120+
}
121+
74122
@get:Inject abstract val execOperations: ExecOperations
75123

124+
/**
125+
* Runs [command] and returns its trimmed output, or null if it failed or said nothing.
126+
*
127+
* `exec` *throws* when the executable cannot be started at all, which `isIgnoreExitValue` does
128+
* not cover — `hostname` is not on every machine — so the whole call is wrapped rather than just
129+
* its exit code inspected.
130+
*/
131+
private fun capture(vararg command: String): String? =
132+
runCatching {
133+
val output = ByteArrayOutputStream()
134+
val result = execOperations.exec {
135+
commandLine(command.toList())
136+
standardOutput = output
137+
errorOutput = ByteArrayOutputStream()
138+
isIgnoreExitValue = true
139+
}
140+
if (result.exitValue != 0) null else output.toString().trim().ifBlank { null }
141+
}
142+
.getOrNull()
143+
144+
/**
145+
* The machine's name, reduced to what is safe in a version string.
146+
*
147+
* A host name can carry anything the owner typed into it, and this value is interpolated into
148+
* module.prop and a generated Kotlin string literal. Unknown hosts fall back to "local", which
149+
* still reads correctly: not a CI build, and not a clean tree.
150+
*/
151+
private fun hostname(): String {
152+
val raw = capture("hostname") ?: System.getenv("HOSTNAME") ?: return "local"
153+
// The short name only. A fully qualified host name is mostly domain, and the domain is the
154+
// part that is identical across every machine that would ever build this.
155+
val short = raw.substringBefore('.')
156+
val safe = short.filter { it.isLetterOrDigit() || it == '-' || it == '_' }
157+
return safe.ifBlank { "local" }
158+
}
159+
76160
override fun obtain(): String {
77-
val hash = ByteArrayOutputStream()
78-
val hashResult = execOperations.exec {
79-
commandLine("git", "rev-parse", "--short", "HEAD")
80-
standardOutput = hash
81-
isIgnoreExitValue = true
82-
}
83-
if (hashResult.exitValue != 0 || hash.toString().isBlank()) return "unknown"
84-
85-
// A dirty tree is the case worth naming: that build corresponds to no commit at all, so
86-
// "which commit is this" has no answer and the version should say so rather than name a
87-
// commit the binary does not match.
88-
val dirty = ByteArrayOutputStream()
89-
val dirtyResult = execOperations.exec {
90-
commandLine("git", "status", "--porcelain", "--untracked-files=no")
91-
standardOutput = dirty
92-
isIgnoreExitValue = true
161+
// Abbreviated by git rather than by hand, so a CI build and a local build of the same commit
162+
// read identically however long this repository's abbreviation happens to be.
163+
val head = capture("git", "rev-parse", "--short", "HEAD") ?: return "unknown"
164+
165+
val repository = parameters.buildRepository.getOrElse("")
166+
if (repository.isBlank()) {
167+
val dirty = capture("git", "status", "--porcelain", "--untracked-files=no") != null
168+
return if (dirty) "$head-${hostname()}" else head
93169
}
94-
val suffix =
95-
if (dirtyResult.exitValue == 0 && dirty.toString().isNotBlank()) "-dirty" else ""
96-
return hash.toString().trim() + suffix
170+
171+
// The pushed commit is an ancestor of the merge that was checked out, so it is in the
172+
// repository and git will abbreviate it. The truncation is only reached if it somehow is
173+
// not, and a slightly odd length beats reporting the merge commit or nothing at all.
174+
val pushed = parameters.buildCommit.getOrElse("").takeIf { it.isNotBlank() }
175+
val short =
176+
pushed?.let { capture("git", "rev-parse", "--short", it) ?: it.take(head.length) } ?: head
177+
return repository.replace('/', '-') + "-" + short
97178
}
98179
}
99180

100181
// This defers the execution of the git commands and allows Gradle to cache the results.
101182
val versionCodeProvider by extra(providers.of(GitCommitCountValueSource::class.java) {})
102-
val versionHashProvider by extra(providers.of(GitCommitHashValueSource::class.java) {})
183+
val versionHashProvider by
184+
extra(
185+
providers.of(GitCommitHashValueSource::class.java) {
186+
// Set on every GitHub Actions runner and on nothing else, so the presence of either is
187+
// the test for "this is a CI build". The workflow's own variables win because they name
188+
// the branch that was pushed; GitHub's defaults name the run that built it.
189+
parameters.buildRepository.set(
190+
providers
191+
.environmentVariable("VECTOR_BUILD_REPOSITORY")
192+
.orElse(providers.environmentVariable("GITHUB_REPOSITORY"))
193+
.orElse("")
194+
)
195+
parameters.buildCommit.set(
196+
providers.environmentVariable("VECTOR_BUILD_COMMIT").orElse("")
197+
)
198+
}
199+
)
103200
val versionNameProvider by extra(providers.of(GitLatestTagValueSource::class.java) {})
104201

105202
val injectedPackageName by extra("com.android.shell")

daemon/src/main/kotlin/org/matrix/vector/daemon/VectorService.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ object VectorService : IDaemonService.Stub() {
3838
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) TelephonyManager.ACTION_SECRET_CODE
3939
else Telephony.Sms.Intents.SECRET_CODE_ACTION
4040

41+
/** Dial *#*#832867#*#* ("VECTOR" on the keypad) to open the manager. */
42+
private const val SECRET_CODE = "832867"
43+
4144
override fun dispatchSystemServerContext(
4245
appThread: IBinder?,
4346
activityToken: IBinder?,
@@ -148,9 +151,9 @@ object VectorService : IDaemonService.Stub() {
148151
IntentFilter(NotificationManager.moduleScopeAction).apply { addDataScheme("module") }
149152

150153
val secretCodeFilter =
151-
IntentFilter().apply {
154+
IntentFilter(ACTION_SECRET_CODE).apply {
152155
addDataScheme("android_secret_code")
153-
addDataAuthority("5776733", null)
156+
addDataAuthority(SECRET_CODE, null)
154157
}
155158

156159
// Define strict Android 14+ flags and the system-only BRICK permission

daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import org.matrix.vector.daemon.data.PreferenceStore
3737
import org.matrix.vector.daemon.env.Dex2OatServer
3838
import org.matrix.vector.daemon.env.LogcatMonitor
3939
import org.matrix.vector.daemon.system.*
40+
import org.matrix.vector.daemon.utils.InstallerVerifier
4041
import org.matrix.vector.daemon.utils.PackageOptimizer
4142
import org.matrix.vector.daemon.utils.RootImplementation
4243
import org.matrix.vector.daemon.utils.applyXspaceWorkaround
@@ -293,6 +294,22 @@ object ManagerService : ILSPManagerService.Stub() {
293294

294295
override fun softReboot() = VectorDaemon.softReboot()
295296

297+
/**
298+
* The flashed manager APK, verified, for the manager to install as an ordinary app.
299+
*
300+
* The same file and the same check as [ApplicationService.requestInjectedManagerBinder], which
301+
* serves it to the host process for injection — one APK, one signature gate, whichever way it
302+
* leaves the module directory.
303+
*/
304+
override fun getManagerApk(): ParcelFileDescriptor? =
305+
runCatching {
306+
InstallerVerifier.verifyInstallerSignature(FileSystem.managerApkPath.toString())
307+
ParcelFileDescriptor.open(
308+
FileSystem.managerApkPath.toFile(), ParcelFileDescriptor.MODE_READ_ONLY)
309+
}
310+
.onFailure { Log.e(TAG, "Failed to open or verify manager APK", it) }
311+
.getOrNull()
312+
296313
override fun reboot() {
297314
powerManager?.reboot(false, null, false)
298315
}

manager/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ android {
5151

5252
defaultConfig {
5353
applicationId = defaultManagerPackageName
54-
// The commit this manager was built from, short, with a marker when the tree was dirty.
54+
// Which build this manager is: the repository on CI, the machine when built locally from
55+
// a modified tree, and the short commit either way. See GitCommitHashValueSource.
5556
// The version code is `git rev-list --count origin/master`, so a branch build and the
5657
// official build of the same depth are indistinguishable by number — and the manager and
5758
// the daemon are flashed separately, so they can be different builds of the same number.

manager/src/debug/kotlin/org/matrix/vector/manager/demo/FakeManagerService.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,14 @@ class FakeManagerService(
216216
override fun getLogPart(verbose: Boolean, name: String?): ParcelFileDescriptor? =
217217
real?.getLogPart(verbose, name)
218218

219+
/**
220+
* Delegated, so the offer to install the manager is live or dead exactly as it really is.
221+
*
222+
* Null when there is no daemon behind the demo, which is the same answer the real one gives
223+
* when it cannot serve the APK — and the status page renders that case rather than crashing.
224+
*/
225+
override fun getManagerApk(): ParcelFileDescriptor? = real?.managerApk
226+
219227
override fun getXposedVersionName(): String? = real?.xposedVersionName
220228

221229
override fun clearLogs(verbose: Boolean): Boolean = real?.clearLogs(verbose) ?: false

0 commit comments

Comments
 (0)