Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
96bb777
ADFA-4128: qb 07/12 core-provisioning — Core slice 3: proxy-app insta…
fryanpan Aug 21, 2026
36d4dda
ADFA-4128: qb 07 review fixes — parseDiagnostics no-throw contract + …
fryanpan Aug 22, 2026
f1c2818
ADFA-4128 (7/11): address CodeRabbit review
fryanpan Aug 27, 2026
e57b556
ADFA-4128: qb-07 review fixes - per-spawn daemon state, IO-confined i…
fryanpan Sep 1, 2026
7675f55
ADFA-4128: 0902 review round on quickbuild:core provisioning
fryanpan Sep 3, 2026
e1389ac
style: spotless reformat of DaemonProcessClient, no functional change
fryanpan Sep 3, 2026
91ae288
ADFA-4128: shutdown takes the start mutex, and the polite stop runs u…
fryanpan Sep 3, 2026
5e404a3
ADFA-4128: guard every installed-package read, and state why OTHER is…
fryanpan Sep 3, 2026
1dd4b97
ADFA-4128: drop blank classpath entries; deadline before the isRunnin…
fryanpan Sep 3, 2026
5da7153
ADFA-4128: pin the pump drain with a daemon that replies and exits in…
fryanpan Sep 3, 2026
a4cd973
ADFA-4128: let the start, not the watcher's timing, decide whether an…
fryanpan Sep 4, 2026
e8f19d6
ADFA-4128: carry a successful compile's warnings on CompileOutput
fryanpan Sep 4, 2026
969711e
ADFA-4128: read the daemon's own op duration into daemonMillis
fryanpan Sep 4, 2026
65059af
ADFA-4128: log the swallowed install-launch and stderr-drain exceptions
fryanpan Sep 4, 2026
35de2ad
ADFA-4128: keep a failed stamp read distinct from an absent package
fryanpan Sep 4, 2026
382afcd
ADFA-4128: make pendingUserActionSeen an AtomicBoolean
fryanpan Sep 4, 2026
4378374
ADFA-4128: expose quickbuild:protocol as an api dependency of core
fryanpan Sep 4, 2026
02b788e
ADFA-4128: suspend the generation store and scratch tree on an inject…
fryanpan Sep 4, 2026
12bd5b4
ADFA-4128: name the layout walkers and the scratch residue a failed p…
fryanpan Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions quickbuild/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ tasks.register<JacocoReport>("jacocoTestReport") {
dependencies {
implementation(projects.logger)
implementation(projects.eventbusEvents)
// Wire DTOs/constants shared with the daemon (single protocol definition).
implementation(projects.quickbuild.protocol)
// Wire DTOs/constants shared with the daemon (single protocol definition). api, not
// implementation: CompileOutput.stats, DexOutput.stats and DaemonReply.BuildFailed.stats
// put its types on this module's public surface.
api(projects.quickbuild.protocol)

implementation(libs.common.kotlin.coroutines.android)
implementation(libs.google.gson)
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package org.appdevforall.cotg.quickbuild.data

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.appdevforall.cotg.quickbuild.domain.reload.GenerationStore
import org.slf4j.LoggerFactory
import java.io.File
import java.io.IOException

/**
* Keeps the generation counter in `<project>/.androidide/quickbuild/generation`.
*
* Lives with the project rather than in the app-private [QuickBuildScratch] tree because
* scratch is deleted on session teardown while this counter must outlive sessions: an
* installed proxy app keys its payloads by generation, so only a surviving counter lets a
* later session stay strictly newer. A corrupt or unreadable file loads as null (fresh
* session), so a broken state file cannot take quick build down.
*
* Every read and write runs under [ioDispatcher]: the file sits under the project root on
* FUSE-backed storage, and the callers are on the session thread that concurrency.md says
* must not block.
*
* @property file the counter file; it need not exist yet, its parent directory is created on
* first [save], and a sibling `.tmp` is the write staging path.
* @property ioDispatcher where the file I/O runs; injectable so tests can pin the hop.
*/
class FileGenerationStore(
private val file: File,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) : GenerationStore {
/**
* Reads the persisted counter.
*
* @return the stored generation, or null when the file is missing, unreadable, or does not
* parse as a Long - all of which the caller treats as a fresh session.
*/
override suspend fun load(): Long? =
withContext(ioDispatcher) {
try {
if (file.isFile) file.readText().trim().toLongOrNull() else null
} catch (e: IOException) {
log.warn("Failed to read generation from {}; starting fresh", file, e)
null
}
}

/**
* Persists the counter atomically via temp file plus rename.
*
* @param generation the value to store; the caller guarantees it is strictly greater than
* any previously saved one, since the installed proxy app keys its payloads by it.
* @throws IOException when the value could not be persisted: the staged write failed
* before any rename was tried, or both renames AND the direct-write fallback failed.
* Unlike [load] this is never swallowed, since losing it would let a later session
* reuse a generation.
*/
override suspend fun save(generation: Long) =
withContext(ioDispatcher) {
file.parentFile?.mkdirs()
val tmp = File(file.parentFile, file.name + ".tmp")
tmp.writeText(generation.toString())
if (!tmp.renameTo(file)) {
// Windows-style rename-over-existing failure path; harmless on device but
// keeps the store correct wherever the JVM tests run.
file.delete()
if (!tmp.renameTo(file)) {
// The old value is already deleted, so a bare throw here would leave NO
// counter at all - the next load() would restart the sequence, the exact
// reuse the class exists to rule out. Non-atomic beats lost.
try {
file.writeText(generation.toString())
} catch (e: IOException) {
throw IOException("Unable to persist generation $generation to $file", e)
} finally {
tmp.delete()
}
}
}
}

companion object {
private val log = LoggerFactory.getLogger("QB-GenerationStore")

/**
* Builds a store at the canonical per-project location of the generation file.
*
* @param projectRoot the user project's root directory; the file lands at
* `.androidide/quickbuild/generation` beneath it, and neither need exist yet.
* @param ioDispatcher where the file I/O runs; see the class KDoc.
* @return a store for that path; no filesystem access happens until [load] or [save].
*/
fun forProject(
projectRoot: File,
ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
): FileGenerationStore = FileGenerationStore(File(projectRoot, ".androidide/quickbuild/generation"), ioDispatcher)
}
}
Loading
Loading