API — ICoreBlocks/ICoreUpdater
The public contract of 21 header(s) under src/ICoreBlocks/ICoreUpdater — 24 class/struct definition(s), 162 declaration(s). Each section shows the header's banner and its public (and protected-virtual) surface exactly as the file writes it.
ICoreAppImageUpdate.h#
src/ICoreBlocks/ICoreUpdater/ICoreAppImageUpdate.h
ICoreAppImageUpdate#
ICoreAppImageUpdate.h:63 · class · nested Environment, Plan · 6 declaration(s)
ICoreAppImageUpdate — applying an update to a Linux AppImage (U5.3).
class ICoreAppImageUpdate {
public:
ICoreAppImageUpdate() = delete;
enum class Outcome {
Ready, // plan() only: nothing stands in the way
Applied, // apply() only: the new AppImage is in place
HandedOff, // handOff() only: the new AppImage started; QUIT NOW
// ---- refusals: NOTHING was touched ------------------------------
NotAnAppImage, // $APPIMAGE is unset -- this is not one
RefusedByInstallKind, // a package manager owns it (U5.4), or Unknown
StagedFileMissing,
DifferentFilesystem, // rename(2) would be EXDEV
// ---- failures ---------------------------------------------------
CouldNotMakeExecutable, // chmod on the STAGED file; nothing has moved
SwapFailed, // ICoreUpdateInstaller reverted; old one is back
RelaunchFailed // the swap SUCCEEDED and exec did not
};
// Everything plan() may look at. Taken as a value so a case can describe a
// Linux AppImage on a machine that has never seen one.
struct Environment {
// $APPIMAGE, as ICoreInstallKind::Environment already collects it.
// Empty means this process is not an AppImage.
std::string appImagePath;
// ICoreInstallKind's verdict. ⚠ Read as `selfUpdateAllowed`, never
// re-derived from `kind` -- SystemPrograms' answer changes the day
// U8.2 is decided, and a second derivation would not change with it.
bool selfUpdateAllowed = false;
// The argv this process was started with, for the relaunch. argv[0] is
// REPLACED with the AppImage path: the running argv[0] is whatever the
// runtime put there, which on some launchers is not a path at all.
std::vector<std::string> arguments;
};
struct Plan {
Outcome outcome = Outcome::NotAnAppImage;
std::filesystem::path target; // $APPIMAGE -- the file replaced
std::filesystem::path staged; // the verified replacement
std::filesystem::path keepPreviousAt; // where the old one goes (U5.5)
// What relaunch() will exec. argv[0] is `target`.
std::vector<std::string> relaunchArguments;
// For the log. Names paths, so never shown raw to the user.
std::string detail;
// Body in the .cpp: the header surface rule permits none here.
[[nodiscard]] bool ok() const;
};
// PURE. Answers every refusal before a single byte moves, so a caller can
// tell the user why without half-applying anything.
[[nodiscard]] static Plan plan(const Environment& environment,
const std::filesystem::path& staged,
const std::filesystem::path& keepPreviousAt);
// The file work, in the order the header argues for: make the staged file
// executable, then swap. Returns Applied with the new AppImage at
// `plan.target` and the old one at `plan.keepPreviousAt`.
//
// Does NOT relaunch. The caller arms ICoreUpdateFirstRun between the two,
// because after relaunch() this process is gone.
static Outcome apply(const Plan& plan, std::string* detail);
// execv(plan.target, plan.relaunchArguments). ⚠ DOES NOT RETURN ON
// SUCCESS. A return is always a failure, and the caller's next move is
// ICoreUpdateInstaller::revertTo() -- the swap worked and the build cannot
// be started, which is precisely the case U5.5 exists for.
static Outcome relaunch(const Plan& plan, std::string* detail);
// The relaunch the running APPLICATION uses (ICoreUpdateFlow, 2026-09-28):
// start the new AppImage detached, RETURN, and let the caller quit through
// the normal path -- the macOS `open -n` shape. relaunch()'s execv replaces
// this process on the spot, skipping every save and shutdown hook, which
// is right for a headless tool and wrong for a window somebody has work in.
static Outcome handOff(const Plan& plan, std::string* detail);
// One sentence, user-facing. Says what to DO where there is anything to do.
[[nodiscard]] static std::string describe(Outcome outcome);
static int selfTest(std::vector<std::string>* failures);
};
};
ICoreDeltaBundle.h#
src/ICoreBlocks/ICoreUpdater/ICoreDeltaBundle.h
ICoreDeltaBundle#
ICoreDeltaBundle.h:46 · class · nested Entry, Manifest · 3 declaration(s)
ICoreDeltaBundle — assembling an update from a delta instead of a full artifact (ACCOUNT_MANAGER U8.4).
class ICoreDeltaBundle {
public:
static constexpr const char* FORMAT = "icore-delta-1";
static constexpr const char* MANIFEST_NAME = "icore-delta.json";
static constexpr const char* PAYLOAD_DIR = "payload";
enum class Outcome {
Assembled, // `assembled` holds a complete, verified bundle
NotADelta, // no manifest, or a format this build does not know
WrongVersions, // the delta is not FROM what is installed
ExtractFailed, // ditto refused the zip
CopyFailed, // the installed bundle could not be copied
VerifyFailed, // assembled, and does not match the manifest
NotMacOs
};
// One entry in the resulting bundle. A symlink carries a target and no
// digest; hashing what it points AT would make an identical-looking bundle
// out of a link and a copy, and they are not the same to a code signature.
struct Entry {
bool isSymlink = false;
bool executable = false;
std::string sha256; // lowercase hex, empty for a symlink
std::string target; // symlink target, empty for a file
[[nodiscard]] bool operator==(const Entry& other) const;
[[nodiscard]] bool operator!=(const Entry& other) const;
};
struct Manifest {
std::string format;
std::string product;
std::string platform;
std::string fromVersion;
std::string toVersion;
std::vector<std::string> deletions;
std::map<std::string, Entry> entries;
[[nodiscard]] bool ok() const;
};
// PURE. Rejects anything it does not fully understand rather than applying
// the half it does.
[[nodiscard]] static bool parseManifest(const std::string& json,
Manifest* out,
std::string* error);
// Copy, delete, overlay, verify. `assembled` is only set on Assembled.
//
// ⚠ `runningVersion` is checked against the manifest's from_version before
// a byte is copied: a delta applied over the wrong base is the one failure
// that could produce a bundle that runs and is subtly wrong.
static Outcome assemble(const std::filesystem::path& installedBundle,
const std::filesystem::path& deltaZip,
const std::filesystem::path& workRoot,
const std::string& runningVersion,
std::filesystem::path* assembled,
std::string* detail);
// Every entry, and every entry only. Returns the complaints; empty is a
// pass. Public because a caller may want to re-assert it after its own
// step, and because a suite has to be able to ask it directly.
[[nodiscard]] static std::vector<std::string> verify(
const Manifest& manifest, const std::filesystem::path& bundle);
// ⚠ EVERY refusal means "take the full artifact", and this says so in code
// rather than in the header's prose. "…and then fall back" is exactly the
// sentence that gets dropped when somebody writes the flow six months from
// now, and a delta that fails without a fallback is an update that simply
// stops working for the users whose install drifted.
//
// The switch has no `default:`, on purpose: a new Outcome added later will
// not compile until somebody decides which side of this it falls on.
[[nodiscard]] static bool fallsBackToFullArtifact(Outcome outcome);
[[nodiscard]] static std::string describe(Outcome outcome);
static int selfTest(std::vector<std::string>* failures);
};
};
ICoreDownloadClient.h#
src/ICoreBlocks/ICoreUpdater/ICoreDownloadClient.h
ICoreDownloadClient#
ICoreDownloadClient.h:42 · class · nested Result · 4 declaration(s)
ICoreDownloadClient / ICoreHttpDownloadClient / ICoreFakeDownloadClient "May I download this release, and where from?" — POST /api/download, which the Worker answers by calling grant_download() and...
class ICoreDownloadClient {
public:
struct Result {
// The exchange completed and the body parsed. Says NOTHING about
// whether the download was authorised -- see grant.ok.
bool ok = false;
// 0 when no response line arrived at all.
int httpStatus = 0;
// Diagnostics. Names hosts and timeouts; never shown to a user.
std::string error;
ICoreDownloadGrant grant;
};
using Callback = std::function<void(const Result&)>;
virtual ~ICoreDownloadClient();
// Holds an in-flight exchange in its subclass; copying one is meaningless.
// PUBLIC rather than private, matching ICoreReleaseClient: a deleted
// operation is a public contract, not private data.
ICoreDownloadClient(const ICoreDownloadClient&) = delete;
ICoreDownloadClient& operator=(const ICoreDownloadClient&) = delete;
// `releaseId` is ICoreUpdateManifest::Release::id -- the uuid, not the
// version, because a version is not unique across platforms and kinds.
// `token` is the licence JWT; empty is refused without a round trip.
virtual void requestGrant(const std::string& releaseId,
const std::string& platform,
const std::string& token,
Callback done) = 0;
protected:
ICoreDownloadClient();
};
};
ICoreHttpDownloadClient#
ICoreDownloadClient.h:83 · class · bases public ICoreDownloadClient · pImpl · 4 declaration(s)
The real one.
class ICoreHttpDownloadClient : public ICoreDownloadClient {
public:
// ⚠ NO BASE URL ARGUMENT, unlike ICoreReleaseClient. That class takes one
// because the check endpoint is harmless public metadata; this one carries
// the licence token, and a peer that can be pointed elsewhere is a peer
// that can be pointed at a machine collecting them. Same call as
// ICoreLicenseClient's, and for the same reason.
ICoreHttpDownloadClient();
~ICoreHttpDownloadClient() override;
void requestGrant(const std::string& releaseId,
const std::string& platform,
const std::string& token,
Callback done) override;
// "https://systemsicore.com/api". A constant, exposed so a diagnostics
// line can print it -- not so anything can change it.
[[nodiscard]] static std::string baseUrl();
// ---- the part that can be wrong, exposed so it can be tested ----------
//
// Pure. The transport needs a network; the MAPPING is where the defects
// live, and it is the difference between an upsell and a bug report.
//
// `reachedServer` is false when no response line arrived at all.
[[nodiscard]] static Result mapReply(int httpStatus,
const std::string& body,
bool reachedServer);
void setTimeoutMsecs(int msecs);
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreFakeDownloadClient#
ICoreDownloadClient.h:124 · class · bases public ICoreDownloadClient · pImpl · 9 declaration(s)
The test one.
class ICoreFakeDownloadClient : public ICoreDownloadClient {
public:
ICoreFakeDownloadClient();
~ICoreFakeDownloadClient() override;
void requestGrant(const std::string& releaseId,
const std::string& platform,
const std::string& token,
Callback done) override;
// What the next call answers. Set once; it stands until replaced.
void setResult(const Result& result);
// A DIFFERENT answer for the second grant onwards, which is what U3.3
// needs: the first grant hands out a URL that expires mid-transfer, and
// the re-grant must hand out a fresh one. Unset means every call answers
// setResult()'s value.
void setResultForRegrant(const Result& result);
// Convenience for the three shapes that matter.
[[nodiscard]] static Result granted(const std::string& url,
const std::string& version,
const std::string& sha256,
std::int64_t sizeBytes);
[[nodiscard]] static Result denied(ICoreDownloadDenial::Reason reason);
[[nodiscard]] static Result unreachable();
[[nodiscard]] int grantCount() const;
[[nodiscard]] std::string lastReleaseId() const;
[[nodiscard]] std::string lastToken() const;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreDownloadDenial.h#
src/ICoreBlocks/ICoreUpdater/ICoreDownloadDenial.h
ICoreDownloadDenial#
ICoreDownloadDenial.h:40 · class · 6 declaration(s)
ICoreDownloadDenial grant_download() answers {"ok": false, "deny_reason": "..."} for eight distinct reasons, and this is the map from each one to what the user is told.
class ICoreDownloadDenial {
public:
ICoreDownloadDenial() = delete;
enum class Reason {
None, // ok == true; not a denial
UnknownRelease, // "unknown_release"
Yanked, // "yanked"
NoLicense, // "no_license"
InvalidLicense, // "invalid_license"
TierLacksFeature, // "tier_lacks_feature"
MaintenanceExpired, // "maintenance_expired"
VersionBeyondEntitlement, // "version_beyond_entitlement"
RateLimited, // "rate_limited"
Unverified, // "unverified"
Unrecognised // the server knows something this build does not
};
// What the caller should DO about it, which is not derivable from the
// reason without repeating this table at every call site.
enum class Disposition {
NotDenied,
Retryable, // rate_limited: try later, automatically, with backoff
Upsell, // maintenance_expired: a purchase would fix it
NeedsSignIn, // no_license / invalid_license / unverified
Blocked, // yanked / tier_lacks_feature: nothing the user can do now
Defect // version_beyond_entitlement / unknown_release: report it
};
[[nodiscard]] static Reason fromServerString(std::string_view denyReason);
[[nodiscard]] static std::string serverString(Reason reason);
[[nodiscard]] static Disposition dispositionOf(Reason reason);
// ⚠ ONE SENTENCE PER REASON, AND NO TWO ARE THE SAME. That is not a style
// preference: the suite asserts it, because collapsing two of these is
// the defect this class exists to prevent and it is invisible in review.
[[nodiscard]] static std::string userText(Reason reason);
// Every enumerator except None, for a loop that must not miss one when
// the server adds a reason.
[[nodiscard]] static std::vector<Reason> allDenials();
};
};
ICoreDownloadGrant.h#
src/ICoreBlocks/ICoreUpdater/ICoreDownloadGrant.h
ICoreDownloadGrant
The parsed answer to POST /api/download — one authorisation to fetch one release, or one refusal with a reason. This is grant_download()'s return value with the JSON gone (ICorePublicFace/AccountsDataBase/icoreblocks_licensing.sql).
⚠ A REFUSAL PARSES SUCCESSFULLY.
{"ok": false, "deny_reason": "..."}is a well-formed answer to a well-formed question, and parse() returns TRUE for it. Only a body that is not JSON, or is JSON that says neither yes nor no, is a parse failure. Collapsing the two is how "your maintenance lapsed" becomes "download failed" — see ICoreDownloadDenial, which exists entirely to stop that.
ICoreDownloadGrant#
ICoreDownloadGrant.h:47 · struct · 1 declaration(s)
A PARSED DTO, so its members are PUBLIC and there is no Impl — the same call as ICoreUpdateManifest's.
struct ICoreDownloadGrant {
public:
// True only when the server authorised the download.
bool ok = false;
// Meaningful when !ok. Reason::None when ok.
ICoreDownloadDenial::Reason denial = ICoreDownloadDenial::Reason::None;
// Exactly as the server spelled it, kept even when `denial` came back
// Unrecognised — a reason this build has never heard of is the one thing
// worth reporting verbatim in a support conversation.
std::string rawDenyReason;
// ⚠ SECRET, AND SHORT-LIVED. See the header note. Empty when !ok.
std::string url;
std::string version;
std::string sha256; // lowercase hex; what U4.1 compares against
std::int64_t sizeBytes = 0;
std::string signature; // detached Ed25519 over the sha256; may be
// empty until U0.4/U0.5 land
// ISO-8601, as the server spelled it. Diagnostics only: the client does
// NOT decide the URL has expired by comparing clocks — a machine with a
// wrong clock would then never download anything, or would keep using a
// dead URL. Expiry is learned from the fetch, which answers 403.
std::string expiresAt;
// Echoed by grant_download so the client and the server can be seen to
// agree. ICoreUpdateChecker applied these BEFORE the request; a
// disagreement here is a defect report, not a licence problem.
std::string versionCeiling;
bool inCeilingGrace = false;
// ---- parsing ---------------------------------------------------------
// Returns false and fills `error` only when the body is not JSON, is not
// an object, or carries neither an authorisation nor a refusal. A refusal
// returns TRUE with ok == false — see the header note.
[[nodiscard]] static bool parse(const std::string& json,
ICoreDownloadGrant* out,
std::string* error);
// ---- the request -----------------------------------------------------
// POST <baseUrl>/download — the body.
//
// `platform` is sent although the release id already implies it: it costs
// nothing and it is what lets the Worker refuse a macOS artifact to a
// Windows client rather than presign one it will never be able to run.
[[nodiscard]] static std::string requestBody(const std::string& releaseId,
const std::string& platform);
// The file name to stage the artifact under, taken from the URL's last
// path segment with the query string dropped.
//
// ⚠ THIS IS ATTACKER-INFLUENCED TEXT and is never used as a path on its
// own. ICoreUpdateStaging::artifactPath() sanitises it and joins it to a
// directory this process owns; a name with a slash, a "..", or nothing
// usable in it lands as the fallback below rather than escaping anywhere.
[[nodiscard]] std::string fileName() const;
};
};
ICoreInstallKind.h#
src/ICoreBlocks/ICoreUpdater/ICoreInstallKind.h
ICoreInstallKind#
ICoreInstallKind.h:58 · class · nested Environment, Result · 5 declaration(s)
ICoreInstallKind How this copy of the app got onto the machine, and therefore whether it is allowed to replace itself.
class ICoreInstallKind {
public:
ICoreInstallKind() = delete;
enum class Kind {
Unknown, // could not tell -- refuses, see the note above
Standalone, // a bundle/binary run from wherever it was unpacked
UserApplications, // ~/Applications, %LOCALAPPDATA%\Programs, ~/.local
SystemPrograms, // /Applications (self-updates when writable, U6.5), %ProgramFiles%
AppImage, // a Linux AppImage: one file, replaceable in place
PackageManaged // apt/dnf/Homebrew/Snap/Flatpak/MSIX owns it
};
// Everything detect() is allowed to look at. Collected by
// currentEnvironment(); fabricated wholesale by a test.
struct Environment {
// The running executable, symlinks NOT resolved -- what the process
// was launched by (ICoreStandardPaths::applicationFilePath()).
std::filesystem::path executablePath;
// The same file with symlinks resolved, or empty when it could not be
// resolved. BOTH are needed and neither substitutes for the other: a
// Homebrew formula puts a symlink in /opt/homebrew/bin pointing into
// the Cellar, so the launched path hides the manager and the resolved
// path reveals it. A cask does the opposite -- the resolved path is an
// ordinary /Applications bundle and the CASKROOM is upstream of it.
std::filesystem::path resolvedExecutablePath;
std::filesystem::path homePath; // $HOME / %USERPROFILE%
// Set by the launcher of the packaging system in question, and each
// one is conclusive on its own -- no path guessing needed.
std::string appImagePath; // $APPIMAGE
std::string snapName; // $SNAP_NAME
std::string flatpakId; // $FLATPAK_ID
std::filesystem::path programFiles; // %ProgramFiles%
std::filesystem::path programFilesX86; // %ProgramFiles(x86)%
std::filesystem::path localAppData; // %LOCALAPPDATA%
// Can THIS PROCESS move the .app bundle it runs from out of its folder
// and a new one in? Write + search on the folder, write on the bundle
// (moving a directory rewrites its ".."), and no sticky bit on the
// folder reserving the bundle for another owner. Measured with
// access(2) by currentEnvironment() on macOS, false everywhere else.
// Only a macOS /Applications copy reads it (U6.5).
bool bundleReplaceable = false;
};
struct Result {
Kind kind = Kind::Unknown;
// False for PackageManaged, Unknown, a Windows SystemPrograms install
// and a macOS /Applications copy this account cannot replace. This is
// the single question U5.4 asks; do not re-derive it from `kind` at a
// call site -- SystemPrograms' answer already changed once (U6.5), and
// a re-derivation elsewhere did not change with it.
bool selfUpdateAllowed = false;
// Self-update is refused, but the verified artifact can still be put
// in the user's hands to install by dragging it: a macOS
// /Applications copy this account cannot replace (U6.6). Never true
// for a package manager -- a hand-installed copy over Homebrew's is
// exactly the corruption U5.4 exists to prevent. Always false when
// selfUpdateAllowed is true.
bool manualInstallOffered = false;
// The package manager's name when there is one ("Homebrew", "APT or
// DNF", "Snap", "Flatpak", "MSIX"), otherwise empty.
std::string manager;
// What the user should do instead, in the manager's own words
// ("brew upgrade --cask icoreblocks"). Empty when self-update is
// allowed. U5.4 shows this; it is never a path, so it is safe to log.
std::string upgradeHint;
// Why self-update is refused, as a sentence. Empty when it is allowed.
std::string reason;
// U6.8: the https page where the owner of this install updates it --
// today only the Microsoft Store listing, for an MSIX. The dialog
// offers it as "Open in Microsoft Store"; empty for every other kind.
std::string storePageUrl;
// What an apply step would have to replace: the .app bundle on macOS,
// the AppImage file on Linux, the executable elsewhere. Empty when
// the kind is Unknown. Never send this anywhere -- it is a local path,
// and the telemetry rule forbids paths (U7.1).
std::filesystem::path installRoot;
};
// Pure. Everything it knows is in `env`.
[[nodiscard]] static Result detect(const Environment& env);
// The one impure function: reads the process's own paths and environment.
[[nodiscard]] static Environment currentEnvironment();
// Convenience for the boot path: detect(currentEnvironment()).
[[nodiscard]] static Result detect();
// For logs and the account/update panels. Stable, English, not localised.
[[nodiscard]] static std::string describe(Kind kind);
};
};
ICoreMacOsUpdate.h#
src/ICoreBlocks/ICoreUpdater/ICoreMacOsUpdate.h
ICoreMacOsUpdate#
ICoreMacOsUpdate.h:67 · class · nested Environment, Plan · 4 declaration(s)
ICoreMacOsUpdate — applying an update to a macOS .app bundle (U5.1).
class ICoreMacOsUpdate {
public:
ICoreMacOsUpdate() = delete;
enum class Outcome {
Ready, // plan() only
Extracted, // extract() only: the bundle is out and unmounted
Applied, // apply() only: the new bundle is installed
HandedOff, // relaunch() only: `open` accepted it; QUIT NOW
// ---- refusals: NOTHING was touched ------------------------------
NotMacOs,
RefusedByInstallKind, // SystemPrograms (U8.2), package-managed, Unknown
StagedFileMissing,
UnknownArtifactKind, // not a .dmg and not a .zip
// ---- failures ---------------------------------------------------
CouldNotMount,
NoBundleInArtifact, // it mounted/unzipped and held no .app
CouldNotCopyOut,
CouldNotClearQuarantine,
SwapFailed, // ICoreUpdateInstaller reverted; old one is back
RelaunchFailed // the swap SUCCEEDED and `open` did not
};
struct Environment {
// The .app being replaced — the BUNDLE ROOT, not Contents/MacOS/x.
std::filesystem::path currentBundle;
// ⚠ Read as `selfUpdateAllowed`, never re-derived from `kind`:
// SystemPrograms' answer changes the day U8.2 is decided and a second
// derivation would not change with it.
bool selfUpdateAllowed = false;
std::vector<std::string> arguments; // carried through the relaunch
};
struct Plan {
Outcome outcome = Outcome::NotMacOs;
std::filesystem::path target; // the installed .app
std::filesystem::path staged; // the VERIFIED .dmg or .zip
std::filesystem::path workRoot; // private; the mount and the copy
std::filesystem::path keepPreviousAt; // where the old bundle goes (U5.5)
std::vector<std::string> relaunchArguments;
std::string detail;
// Body in the .cpp: the header surface rule permits none here.
[[nodiscard]] bool ok() const;
};
// PURE. Every refusal is answered before a byte moves.
[[nodiscard]] static Plan plan(const Environment& environment,
const std::filesystem::path& staged,
const std::filesystem::path& workRoot,
const std::filesystem::path& keepPreviousAt);
// Mounts (or unzips), copies the .app out, and DETACHES — on every path
// out, including failure. Returns the extracted bundle's path in `bundle`.
//
// ⚠ The caller must have re-asserted ICoreUpdateVerifier::stillTheSameFile()
// on `plan.staged` immediately before calling this. What is verified is the
// ARTIFACT; what is installed is what comes out of it.
static Outcome extract(const Plan& plan,
std::filesystem::path* bundle,
std::string* detail);
// Clears com.apple.quarantine on `bundle`, then swaps it into place.
static Outcome apply(const Plan& plan,
const std::filesystem::path& bundle,
std::string* detail);
// U6.6 -- the install this copy may NOT do itself, put in the user's
// hands instead: a verified .dmg is opened (Finder mounts it and shows the
// drag-to-Applications window), a .zip is revealed in Finder. Only ever
// called for a user who pressed Download; the automatic path never opens a
// window. False, and nothing opened, for any other artifact or off macOS.
static bool openForManualInstall(const std::filesystem::path& verifiedArtifact,
std::string* detail);
// `/usr/bin/open -n <target>`, detached. ⚠ RETURNS on success, unlike the
// AppImage path's execv — see the header. HandedOff means the caller must
// now quit; anything else means the swap happened and the new build could
// not be started, which is what U5.5's revert is for.
static Outcome relaunch(const Plan& plan, std::string* detail);
[[nodiscard]] static std::string describe(Outcome outcome);
static int selfTest(std::vector<std::string>* failures);
};
};
ICoreReleaseClient.h#
src/ICoreBlocks/ICoreUpdater/ICoreReleaseClient.h
ICoreReleaseClient#
ICoreReleaseClient.h:36 · class · nested Result · 4 declaration(s)
ICoreReleaseClient / ICoreHttpReleaseClient "Ask the server what the newest build is." An interface, because every test in this module has to run with no network, and a real implementation over ICo...
class ICoreReleaseClient {
public:
struct Result {
bool ok = false; // false = ask again later, say nothing
// 0 when the exchange never got a response at all (no network, DNS
// failure, timeout). Distinguished from a 4xx/5xx because only the
// latter says anything about the server's opinion of the request.
int httpStatus = 0;
// Diagnostics. Never shown to a user: it names hosts and timeouts.
std::string error;
ICoreUpdateManifest manifest; // meaningful only when ok
};
using Callback = std::function<void(const Result&)>;
virtual ~ICoreReleaseClient();
// Holds an in-flight exchange in its subclass; copying one is meaningless.
// PUBLIC rather than private, deliberately: the header surface rule reads
// a deleted operation in the private section as private data, and a
// deleted member is a public contract -- "you may not do this" -- not an
// implementation detail.
ICoreReleaseClient(const ICoreReleaseClient&) = delete;
ICoreReleaseClient& operator=(const ICoreReleaseClient&) = delete;
// `currentVersion` is sent so the server can answer whether the RUNNING
// build was yanked — see ICoreUpdateManifest::requestUrl.
virtual void fetchLatest(const std::string& platform,
const std::string& channel,
const std::string& currentVersion,
Callback done) = 0;
protected:
ICoreReleaseClient();
};
};
ICoreHttpReleaseClient#
ICoreReleaseClient.h:79 · class · bases public ICoreReleaseClient · pImpl · 4 declaration(s)
The real one.
class ICoreHttpReleaseClient : public ICoreReleaseClient {
public:
// `baseUrl` is the origin only — "https://systemsicore.com". The path is
// this class's, so a caller cannot point it at a different endpoint by
// accident.
//
// ⚠ https ONLY, and it is checked. An http:// base would make the whole
// exchange readable and rewritable by anything on the path, and the answer
// it carries decides what the machine downloads next.
explicit ICoreHttpReleaseClient(std::string baseUrl);
~ICoreHttpReleaseClient() override;
void fetchLatest(const std::string& platform,
const std::string& channel,
const std::string& currentVersion,
Callback done) override;
// Total cap per attempt, and how many attempts. Defaults are 10 s and 3,
// set in the constructor: a version check is small, idempotent and
// completely uninteresting to the user, so it may retry freely and must
// never sit on a half-open socket.
void setTimeoutMsecs(int msecs);
void setRetryPolicy(int attempts, int initialBackoffMsecs);
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreSemVer.h#
src/ICoreBlocks/ICoreUpdater/ICoreSemVer.h
ICoreSemVer#
ICoreSemVer.h:55 · class · nested Key, Vector · 6 declaration(s)
ICoreSemVer "Is version A strictly newer than version B?" — the client half of a comparison the SERVER already makes, and the reason this class exists at all rather than a three-line helper in the ...
class ICoreSemVer {
public:
ICoreSemVer() = delete;
// The three-component sort key, exactly semver_key()'s int[3].
// `ok` is false when a component would overflow Postgres' int4 — see the
// divergence note above. The returned key is then meaningless.
struct Key {
int major = 0;
int minor = 0;
int patch = 0;
bool ok = true;
};
[[nodiscard]] static Key key(const std::string& version);
// semver_gt(a, b) — is `a` STRICTLY newer than `b`?
// False when either side is not comparable, so an unusable version is
// never treated as an upgrade.
[[nodiscard]] static bool greaterThan(const std::string& a, const std::string& b);
// True when key() can produce a usable answer for this string. Every
// string that is not overflowing is comparable — including nonsense,
// which reads as 0.0.0.
[[nodiscard]] static bool isComparable(const std::string& version);
// ---- The ported vectors -------------------------------------------
//
// The SQL ships no vector table, so these are transcribed from the
// function bodies clause by clause and each one names the clause it
// pins. tools/semver_vectors.sql runs the SAME table through Postgres,
// so the owner can prove the two agree on a live database rather than
// taking this file's word for it. If you add a vector, add it there too.
struct Vector {
const char* a;
const char* b;
bool expected; // semver_gt(a, b)
const char* pins; // the clause this case exists to pin
};
[[nodiscard]] static std::vector<Vector> vectors();
// Runs vectors() and appends a line per failure. Returns the failure
// count, so 0 is a pass. U7.7 registers this in ICorePreReleaseTests;
// until then it is callable from anywhere that wants the proof.
static int selfTest(std::vector<std::string>* failures);
};
};
ICoreUpdateChecker.h#
src/ICoreBlocks/ICoreUpdater/ICoreUpdateChecker.h
Named in decide()'s default argument, so the umbrella's declaration is not enough -- the type has to be complete here.
ICoreUpdateChecker#
ICoreUpdateChecker.h:49 · class · nested Decision · 2 declaration(s)
ICoreUpdateChecker The ONE place that turns a manifest, a licence and a machine id into "offer this build" or "do not, and here is why".
class ICoreUpdateChecker {
public:
ICoreUpdateChecker() = delete;
enum class Outcome {
// Nothing published, or nothing newer than what is running.
UpToDate,
// Newer, entitled, inside the rollout: offer it.
UpdateAvailable,
// Newer exists and the licence's version ceiling excludes it. NOT an
// error -- see the header note.
HeldByCeiling,
// Newer exists and this machine is not in the rollout percentage yet.
// Say nothing to the user; ask again tomorrow.
HeldByRollout,
// The RUNNING build was pulled. This outranks everything else,
// including "up to date", because latest_releases excludes yanked
// rows and the person running one is otherwise told they are current.
RunningYanked,
// The running build is BELOW the product's supported-version floor
// (`product_policy.min_supported_version`, U8.1). Stronger than a yank
// in what it says -- "this version is no longer supported" is about a
// whole range of builds rather than one bad one -- and it is the only
// outcome here driven by a value somebody types on the server rather
// than by a comparison of published facts.
//
// ⚠ IT DOES NOT STOP THE APPLICATION, and that is a decision (U8.1,
// answered 2026-08-21). The client learns it is unsupported and says so
// unmistakably; refusing to run on a server's say-so is a separate,
// larger question, because a wrong floor would brick every install
// below it including customers who cannot update today. A user who
// ignores the banner keeps working.
//
// ⚠ ONLY WHEN NOTHING CAN BE INSTALLED FROM HERE (2026-09-28). A build
// below the floor that CAN take the published release -- newer,
// entitled, installable on this OS, direct from what is running --
// gets UpdateAvailable with `supportedFloor` set instead, so the
// automatic path and the Install button reach exactly the users the
// floor was raised for. This outcome used to answer both cases, and
// offersUpdate() is false for it, so raising the floor switched in-app
// updating OFF for everybody below it. What is left here is a user who
// has to leave the app to move: held by the ceiling, the OS, an
// interim version, a paused rollout, or nothing newer published.
RunningUnsupported,
// The published build needs a newer OS than this machine is running
// (`releases.min_os_version`). NOT offered: an update that installs
// and then will not launch is worse than one never offered, because
// the user is left with a broken install and no obvious way back.
RequiresNewerOs,
// The published build cannot be installed DIRECTLY from what is
// running (`releases.min_upgradable_from`) -- a project-format or
// settings-layout change needs an intermediate version stepped
// through first. Also not an error: the update is real and the user
// can have it, in two steps.
NeedsIntermediateVersion,
// A version string on one side or the other cannot be compared (see
// ICoreSemVer's int4-overflow divergence). Nothing is offered.
NotComparable
};
struct Decision {
Outcome outcome = Outcome::UpToDate;
// The version the outcome is about: the newer build for
// UpdateAvailable/HeldByCeiling/HeldByRollout, the running one for
// RunningYanked, empty for UpToDate.
std::string version;
// ⚠ U8.4 -- WHICH ARTIFACT TO FETCH, AND THE ONE TO FETCH INSTEAD.
// `artifact` is the delta when the server offered one that applies to
// exactly what is running, and the full release otherwise; `fallback`
// is ALWAYS the full release. Two fields rather than a flag because
// every delta failure -- a refused download, a digest that does not
// match, a bundle that does not assemble -- has the same remedy, and a
// caller holding only the delta would have to go back to the checker to
// learn what it was. Both empty unless the outcome offers an update.
ICoreUpdateManifest::Release artifact;
ICoreUpdateManifest::Release fallback;
// True when `artifact` is a delta. Sugar, but it stops every call site
// spelling the same two-part test and getting it half right.
[[nodiscard]] bool usesDelta() const;
// The floor the server states, when the RUNNING build is below it, so
// the message can name what the user has to reach rather than telling
// them they are "too old" and leaving them to guess. Present for
// RunningUnsupported, and for an UpdateAvailable that is the way off an
// unsupported build -- see belowSupportedFloor().
std::string supportedFloor;
// Present only for HeldByCeiling.
std::string ceiling;
// Present only for RequiresNewerOs: the floor the release states, and
// what this machine actually reports. The second is included because
// the interesting failure is an EMPTY one -- see the note on
// `osVersionUnknown` below.
std::string requiredOsVersion;
std::string runningOsVersion;
// ⚠ TRUE WHEN THE OS FLOOR WAS NOT ENFORCED BECAUSE THE OS VERSION
// COULD NOT BE READ. The update is offered anyway, deliberately (see
// decide()'s note), and this is how a support conversation can tell
// "we checked and it passed" from "we could not check".
bool osVersionUnknown = false;
// Present only for NeedsIntermediateVersion: the oldest version that
// may upgrade straight to `version`.
std::string minUpgradableFrom;
// 0-99 for this machine and this release; -1 when no release was
// considered. Reported so a support conversation can ask for it
// instead of guessing why a user has not been offered a build.
int bucket = -1;
// The release's own rollout_percent, echoed for the same reason.
int rolloutPercent = 100;
// ⚠ ENGLISH, AND USER-VISIBLE. It is deliberately here and not in the
// panel: the ceiling wording is a licence statement, and it has been
// wrong in three different places in this design's history. One
// sentence, no exclamation, never an error tone for HeldByCeiling.
std::string message;
// True only for Outcome::UpdateAvailable. The single question a
// caller should ask; do not re-derive it from `outcome`.
[[nodiscard]] bool offersUpdate() const;
// True when the running build is below the supported-version floor,
// whatever the outcome. With offersUpdate() it is the urgent offer --
// the banner says "no longer supported" and names the fix; without
// it, it is RunningUnsupported.
[[nodiscard]] bool belowSupportedFloor() const;
};
// `runningVersion` is this build's own version (ICoreProductInfo::version()).
// `hashedMachineId` is ICoreMachineId::hashed(); an empty one is allowed
// and puts the machine in bucket 0, so a machine whose id could not be
// read is treated as an EARLY adopter rather than being excluded forever.
//
// `runningOsVersion` defaults to ICoreOsVersion::current(); a suite passes
// its own so the OS floor can be exercised on a machine that is not the
// one being described. An EMPTY one means "could not read", which does NOT
// withhold the update -- see decide().
[[nodiscard]] static Decision decide(const ICoreUpdateManifest& manifest,
const ICoreEntitlements& entitlements,
const std::string& runningVersion,
const std::string& hashedMachineId,
const std::string& runningOsVersion
= ICoreOsVersion::current());
// The rollout bucket, 0-99, for one machine and one release. Stable for a
// fixed pair, and independent between releases so a machine unlucky in
// one rollout is not systematically last in every one.
[[nodiscard]] static int bucketOf(const std::string& hashedMachineId,
const std::string& releaseId);
};
};
ICoreUpdateDownloader.h#
src/ICoreBlocks/ICoreUpdater/ICoreUpdateDownloader.h
ICoreUpdateDownloader#
ICoreUpdateDownloader.h:54 · class · pImpl · nested Attempt, Result · 10 declaration(s)
ICoreUpdateDownloader Authorises one download and puts the bytes on disk.
class ICoreUpdateDownloader {
public:
// ---- the pure half: when to ask for a new URL (U3.3) -----------------
//
// Separated out and made pure because it is the part that can be wrong in
// a way no local test would otherwise catch: the failure it handles only
// happens on a slow connection to a large file, which is the one
// configuration a developer machine never has.
// What one fetch attempt ended as, in this module's own words.
//
// ⚠ DELIBERATELY NOT ICoreHttpDownloadResult::Outcome, which is the same
// list. That type lives in a header that includes <QNetworkReply>, and
// Rule 2 forbids src/ICoreBlocks naming a Qt type or reaching a Qt include —
// including transitively through a header of its own. The mapping is one
// switch in the .cpp, which is the correct place for it to be.
enum class FetchOutcome {
Ok,
Refused, // the artifact host answered, and not with the bytes
Unreachable, // no answer at all
Interrupted, // it started and stopped short
WriteFailed,
Cancelled
};
struct Attempt {
FetchOutcome outcome = FetchOutcome::Unreachable;
int httpStatus = 0;
std::int64_t bytesBefore = 0; // on disk when THIS fetch began
std::int64_t bytesAfter = 0; // on disk now
int regrantsUsed = 0;
};
enum class Step {
Finish,
ReGrant // ask for a new URL and resume the same file
};
// `maxRegrants` bounds the loop. Two is the default and it is not
// arbitrary: three grants of five minutes each covers a fifteen-minute
// download, which is a 400 MB artifact on a 4 Mbit line.
[[nodiscard]] static Step nextStep(const Attempt& attempt, int maxRegrants);
// ---- the result ------------------------------------------------------
struct Result {
enum class Outcome {
Ready, // the bytes are on disk. NOT verified — see above
Denied, // grant_download() said no; ask `denial` which no
Unreachable, // no network, at either step
Refused, // the artifact host refused, and a fresh grant did not help
Interrupted, // the transfer stopped short and the re-grants ran out
StagingUnusable, // the staging directory is not ours — see ICoreUpdateStaging
WriteFailed, // the disk
Cancelled
};
Outcome outcome = Outcome::Unreachable;
// Empty unless the download got as far as choosing a destination.
// ⚠ A LOCAL PATH: never send it anywhere (U7.1 forbids paths in
// telemetry) and never show it as the whole of an error message.
std::filesystem::path artifactPath;
std::int64_t bytesOnDisk = 0;
std::int64_t expectedBytes = -1;
// Meaningful only for Outcome::Denied.
ICoreDownloadDenial::Reason denial = ICoreDownloadDenial::Reason::None;
std::string denyReason; // as the server spelled it
// Carried out of the grant so the verifier does not need a second
// round trip. ⚠ THESE ARE THE SERVER'S CLAIM about the artifact, and
// the signature is what turns a claim into evidence — see
// ICoreUpdateVerifier. May be empty until U0.4/U0.5 land.
std::string version;
std::string sha256;
std::string signature;
std::int64_t grantedSize = 0;
// How many grants this download spent, of the 20/hour. 1 is the happy
// path; more means the URL expired mid-transfer, which is normal on a
// slow line and is worth knowing when a support ticket says "it keeps
// failing".
int grants = 0;
// Diagnostics: names hosts and paths, so never shown to a user.
std::string error;
[[nodiscard]] bool ok() const;
// ONE SENTENCE, English, for the user. Routes Denied through
// ICoreDownloadDenial::userText() rather than restating it — that
// class exists so the four denials do not collapse, and restating
// them here is exactly how they would.
[[nodiscard]] std::string userText() const;
};
using Progress = std::function<void(std::int64_t bytesOnDisk, std::int64_t expectedBytes)>;
using Done = std::function<void(const Result&)>;
// The client is INJECTED and not owned: a test passes
// ICoreFakeDownloadClient and every denial path runs with no network. The
// caller's client must outlive this object.
explicit ICoreUpdateDownloader(ICoreDownloadClient& client);
~ICoreUpdateDownloader();
// Owns an in-flight transfer; copying one is meaningless.
ICoreUpdateDownloader(const ICoreUpdateDownloader&) = delete;
ICoreUpdateDownloader& operator=(const ICoreUpdateDownloader&) = delete;
// The licence JWT. Without it the grant is refused before any request is
// made — see ICoreDownloadClient.
void setToken(std::string token);
void setMaxRegrants(int regrants);
// Starts, and returns immediately; `done` arrives on the event loop.
// Calling it while a download is running is refused (the callback fires
// with Outcome::Refused) rather than starting a second one into the same
// file.
void start(const ICoreUpdateManifest::Release& release, Progress onProgress, Done done);
// Stops the transfer. `done` still fires, once, with Outcome::Cancelled.
// The partial file stays — see the header note.
void cancel();
[[nodiscard]] bool isRunning() const;
// ---- the escape hatch the verifier needs -----------------------------
// Removes the staged artifact for this release, so the next start() is a
// fresh download rather than a resume.
//
// ⚠ CALL THIS AFTER A DIGEST FAILURE, ALWAYS. Without it a re-cut release
// resumes a partial that can never digest correctly, and the retry loop
// is infinite. Safe when nothing is staged.
static bool discardStaged(const ICoreUpdateManifest::Release& release,
const std::string& fileName);
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreUpdateFirstRun.h#
src/ICoreBlocks/ICoreUpdater/ICoreUpdateFirstRun.h
ICoreUpdateFirstRun#
ICoreUpdateFirstRun.h:46 · class · nested Trial · 7 declaration(s)
ICoreUpdateFirstRun — an updated build is on trial until it starts once (U5.5 on the account board).
class ICoreUpdateFirstRun {
public:
ICoreUpdateFirstRun() = delete;
// How many launches a new build gets to reach confirm(). One: a build that
// crashes on start-up crashes deterministically far more often than not,
// and every extra attempt is another crash the user watches.
static constexpr int ATTEMPTS_ALLOWED = 1;
enum class Verdict {
NoTrial, // ordinary launch — nothing was updated, or it is confirmed
FirstAttempt, // a new build's first launch: count it, then carry on
RevertToPrevious // it did not survive its attempt; put the old one back
};
// What the marker file says. `present` false means there is no trial —
// which is also what an unreadable or incoherent file produces.
struct Trial {
bool present = false;
int attempts = 0;
std::filesystem::path previousVersionAt;
std::filesystem::path installPath;
// The version being tried, for a log line. Never sent anywhere — the
// telemetry rule forbids a version (U7.1).
std::string version;
};
// PURE.
[[nodiscard]] static Verdict decide(const Trial& trial);
// Written by the process that performed the swap, BEFORE it hands over to
// the new build. `previousVersionAt` is ICoreUpdateInstaller::Result's.
static bool arm(const std::filesystem::path& markerFile,
const std::filesystem::path& installPath,
const std::filesystem::path& previousVersionAt,
const std::string& version,
std::string* detail);
// Never throws and never reports a parse failure as a trial.
[[nodiscard]] static Trial read(const std::filesystem::path& markerFile);
// Increment and FLUSH. Called before the rest of start-up runs.
static bool noteAttempt(const std::filesystem::path& markerFile, std::string* detail);
// The build is up. Clears the marker, then removes the previous version.
// Returns false only if the marker could not be cleared — a previous
// version that could not be deleted is a leak, not a failure, and saying
// so would make a successful update look broken.
static bool confirm(const std::filesystem::path& markerFile, std::string* detail);
// Puts the previous version back and clears the marker. Uses
// ICoreUpdateInstaller::revertTo(), so it inherits its refusal to
// overwrite anything already at the install path.
static bool revert(const std::filesystem::path& markerFile,
const Trial& trial,
std::string* detail);
[[nodiscard]] static std::string describe(Verdict verdict);
static int selfTest(std::vector<std::string>* failures);
};
};
ICoreUpdateFlow.h#
src/ICoreBlocks/ICoreUpdater/ICoreUpdateFlow.h
ICoreUpdateFlow#
ICoreUpdateFlow.h:51 · class · pImpl · nested Result · 10 declaration(s)
ICoreUpdateFlow — the orchestrator the updater was missing (U5.7).
class ICoreUpdateFlow {
public:
struct Result {
enum class Outcome {
HandedOff, // applied, relaunch started — the caller must QUIT
Applied, // applied, and this platform does not relaunch
NothingToDo, // the decision offered no artifact
Denied, // the server refused the download (see denial)
Unreachable, // no network
DownloadFailed, // the transfer did not complete
VerifyFailed, // the bytes are not the build we published
AssembleFailed, // both the delta AND the full artifact failed to
// produce an installable bundle
ApplyFailed, // the swap failed; the old build is still there
RefusedByInstall, // /Applications, a package manager (U8.2)
Cancelled,
PlatformNotWired, // see the note on start()
OtherCopiesRunning // another copy of the app is running: nothing was
// downloaded or touched (ICoreRunningCopies)
};
Outcome outcome = Outcome::NothingToDo;
std::string userText; // ONE sentence, for a human
std::string detail; // diagnostics: paths, tool names, never shown raw
// What actually happened, for telemetry and for a support answer that
// does not require reading a log: whether the delta was used, and
// whether it was abandoned for the full artifact.
bool triedDelta = false;
bool fellBackToFull = false;
// RefusedByInstall only: the FULL artifact, downloaded and verified
// (digest and signature) and left in the private staging directory,
// so a copy that cannot replace itself can still be handed the exact
// bytes that were proven (U6.6). Empty on every other outcome. A local
// path: never logged, never sent.
std::filesystem::path verifiedArtifact;
[[nodiscard]] bool ok() const;
};
using Progress = std::function<void(int percent, std::int64_t received, std::int64_t total)>;
using Done = std::function<void(const Result&)>;
// The client is INJECTED and not owned, exactly as ICoreUpdateDownloader
// takes it: a suite passes the fake and every path here runs with no
// network. The caller's client must outlive this object.
explicit ICoreUpdateFlow(ICoreDownloadClient& client);
~ICoreUpdateFlow();
ICoreUpdateFlow(const ICoreUpdateFlow&) = delete;
ICoreUpdateFlow& operator=(const ICoreUpdateFlow&) = delete;
// The licence JWT, passed straight to the downloader's grant.
void setToken(std::string token);
// ⚠ WHETHER THE NEW BUILD IS STARTED WHEN THE SWAP SUCCEEDS. True is the
// Install button: the user asked, they are waiting, and Result::HandedOff
// tells the caller to quit so two copies are never live at once.
//
// FALSE IS THE AUTOMATIC RUN, and the difference is the reason automatic
// updating is allowed to be on by default. Nobody asked for this one, so
// nobody is waiting for it, and a relaunch would close a window somebody is
// working in -- the single cost that would make "on by default" the wrong
// call. With it false the swap lands, the session carries on unharmed in
// the build it started in, and the new version is what launches next time.
// The outcome is Applied rather than HandedOff, so no caller quits.
void setRelaunchWhenDone(bool relaunch);
// What is installed and how this process was launched. Defaults to the
// running process; a suite sets it to a directory it built.
void setEnvironment(const std::filesystem::path& currentBundle,
bool selfUpdateAllowed,
const std::vector<std::string>& launchArguments);
// Runs the whole sequence. `done` fires exactly once, on the event loop.
//
// ⚠ WIRED: macOS (a .app, from a .dmg or .zip) and, since 2026-09-28, a
// Linux AppImage (ICoreAppImageUpdate; the new file is started detached
// and this process quits, rather than execv'd over a live window). NOT
// wired, and saying so with PlatformNotWired rather than pretending:
// Windows, which ships through the Microsoft Store only -- the Store owns
// an MSIX install and updates it, the Worker refuses every Windows
// download (`windows_store_only`), and ICoreInstallKind reports the
// install as package-managed, so no Windows run ever reaches this point.
void start(const ICoreUpdateChecker::Decision& decision,
const std::string& runningVersion,
Progress onProgress,
Done done);
void cancel();
[[nodiscard]] bool isRunning() const;
[[nodiscard]] static std::string describe(Result::Outcome outcome);
static int selfTest(std::vector<std::string>* failures);
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreUpdateInstaller.h#
src/ICoreBlocks/ICoreUpdater/ICoreUpdateInstaller.h
ICoreUpdateInstaller#
ICoreUpdateInstaller.h:48 · class · nested Decision, Result, InstallerCommand · 6 declaration(s)
ICoreUpdateInstaller — the gate every apply passes through, and the file swap underneath it.
class ICoreUpdateInstaller {
public:
ICoreUpdateInstaller() = delete;
enum class Outcome {
Applied, // the new file is in place, the old is aside
// ⚠ NOT Applied, AND CONFUSING THE TWO IS THE WINDOWS BUG (U5.2). The
// installer is now running as a detached process and NOTHING HAS BEEN
// REPLACED YET -- it cannot be, because this process still has the
// .exe open. The only correct next action is to QUIT. A caller that
// reads this as success reports "updated" and goes on running the old
// build, and the installer then either waits forever or fails on a
// locked file, which reads as a permissions bug.
HandedOff,
// ---- refusals: NOTHING was touched ------------------------------
RefusedPackageManaged, // U5.4 — a package manager owns this install
RefusedSystemWide, // needs an authorised helper; see U8.2
RefusedUnknownInstall, // could not tell, so it does not try
// ---- failures: the OLD version is still runnable -----------------
StagedFileMissing, // never started
CouldNotMoveCurrentAside, // step 2 failed; nothing moved
CouldNotMoveNewIntoPlace, // step 3 failed and step 2 was reverted
CouldNotStartInstaller // U5.2: the hand-off never began; old build intact
};
struct Decision {
bool allowed = false;
Outcome outcome = Outcome::RefusedUnknownInstall;
// One sentence, already user-facing. Comes from ICoreInstallKind,
// which is where the wording for each manager lives — this class does
// not write a second copy of it.
std::string userText;
// The manager's own command ("brew upgrade --cask icoreblocks"), or
// empty. Safe to log and safe to show: it is never a path.
std::string upgradeHint;
};
// PURE. Takes the detection result rather than detecting, so a suite can
// ask the question for a Homebrew cask on a machine that has no Homebrew.
[[nodiscard]] static Decision decide(const ICoreInstallKind::Result& install);
struct Result {
Outcome outcome = Outcome::RefusedUnknownInstall;
// ⚠ THE INVARIANT, MEASURED RATHER THAN ASSUMED. True whenever a file
// is at the install path when this returns — the old one after any
// failure, the new one after success. A case asserts it for EVERY
// outcome; if it is ever false the machine has no application on it.
bool installPathOccupied = false;
// Where the previous version was moved to, when it was moved at all.
// U5.5 puts it back from here.
std::filesystem::path previousVersionAt;
// For the log. Never shown raw to the user, because it names paths.
std::string detail;
};
// Steps 1-4 above. `current` is the file or bundle being replaced,
// `staged` the verified replacement, `keepPreviousAt` where the old one is
// moved to (it must not exist).
//
// Every one of the three must be on the SAME filesystem, or the renames
// are not atomic and may not even be permitted. The staging directory that
// U3.2 creates is chosen for that.
static Result swapInPlace(const std::filesystem::path& current,
const std::filesystem::path& staged,
const std::filesystem::path& keepPreviousAt);
// Puts `keepPreviousAt` back at `current`. This is what runs when the new
// build fails to start once (U5.5), and it is what swapInPlace() itself
// calls when step 3 fails. Public because those are two different callers
// and one of them is minutes later, in a different process.
//
// Refuses rather than overwrites when something already occupies
// `current`: two versions racing to occupy one path is worse than one
// failed revert.
static bool revertTo(const std::filesystem::path& keepPreviousAt,
const std::filesystem::path& current,
std::string* detail);
// ---- U5.2: Windows replaces us while we are NOT running ---------------
//
// THE FACT THE WHOLE ROW EXISTS FOR: a running .exe cannot be overwritten
// on Windows. The file is held by the loader for as long as the process
// lives, so the swapInPlace() shape above -- which works on macOS and on
// an AppImage because a POSIX rename() does not care that the file is
// open -- cannot work there at all. ⚠ AND IT DOES NOT FAIL IN A WAY THAT
// SAYS SO: the rename returns a sharing violation, which surfaces as
// "access denied" and reads as a permissions problem, so the instinct is
// to run the updater as administrator. That makes it worse, not better.
//
// The shape Windows actually needs is the opposite of a swap: hand the
// signed installer the job, exit, and let it replace the files and
// relaunch. That means the apply step's success is a process that is
// STILL RUNNING when this one ends, which is why HandedOff exists as its
// own outcome and why startDetached() is the only correct spawn.
enum class Strategy {
Refuse, // decide() said no; nothing platform-specific applies
SwapInPlace, // rename the file/bundle aside and the new one in
HandOff // spawn an installer and quit (Windows)
};
// PURE. `platformTag` is ICoreProductInfo::platformTag() -- the same token
// `releases.platform` is keyed on ("windows-x64", "macos-arm64", ...).
//
// ⚠ THE PLATFORM DECIDES THE SHAPE, NOT THE INSTALL KIND. Both are asked,
// and in this order: a refusal outranks everything (a package-managed
// Windows install must not be handed to an installer either), and only
// then does the platform choose between the two shapes.
[[nodiscard]] static Strategy strategyFor(const ICoreInstallKind::Result& install,
std::string_view platformTag);
// What to spawn, and with what. PURE, because the argument list is the
// part that can be silently wrong.
struct InstallerCommand {
bool valid = false;
std::filesystem::path program; // the STAGED, verified installer
std::vector<std::string> arguments;
std::string why; // set only when !valid
};
// ⚠ `/S` AND NOTHING ELSE. That is NSIS's silent switch, and the artifact
// U0.1 produces on Windows is an NSIS installer.
//
// ⚠ `/D=` IS DELIBERATELY NEVER PASSED, and this is the trap worth naming:
// it overrides the installation DIRECTORY. Passing the current install
// path "to be safe" is how an update silently relocates an installation --
// NSIS requires /D to be last and unquoted, so a path with a space in it
// (C:\Program Files\...) truncates and the app is installed to C:\Program.
// The installer already knows where it lives, from its own registry key.
//
// A `.msi` is REFUSED rather than guessed at: it needs
// `msiexec /i <path> /qn`, whose restart and elevation semantics differ
// from NSIS's, and neither this tree nor U0.1 produces one. Guessing an
// install command that cannot be run here is worse than saying so.
[[nodiscard]] static InstallerCommand silentCommandFor(const std::filesystem::path& staged);
// Spawns it DETACHED and returns HandedOff. The caller must then quit.
//
// ⚠ DETACHED IS LOAD-BEARING, NOT TIDINESS. A child that dies with its
// parent is killed at exactly the moment it is needed -- this process
// exiting is the event that lets the installer proceed.
//
// Nothing on disk is touched here, so every failure leaves the old build
// installed and runnable, and `installPathOccupied` is true for BOTH
// outcomes this can return.
static Result handOffToInstaller(const std::filesystem::path& staged);
// For a log line and the update panel.
[[nodiscard]] static std::string describe(Outcome outcome);
static int selfTest(std::vector<std::string>* failures);
};
};
ICoreUpdateManifest.h#
src/ICoreBlocks/ICoreUpdater/ICoreUpdateManifest.h
ICoreUpdateManifest
The parsed answer to "is there anything newer, and is what I am running still good?" — one round trip, public metadata, and NO DOWNLOAD URL.
THE SERVER SHAPE THIS MIRRORS is release_status() in ICorePublicFace/AccountsDataBase/20260819_updater.sql, fronted by the Worker at GET /api/releases/latest. Its three top-level keys are current_status, update_available and latest; this class is those keys with the JSON gone.
⚠ THE SCHEMA THIS PARSES IS PROPOSED, NOT APPLIED. 20260819_updater.sql is a migration for the OWNER to run (board row U8.1), and until it is, /api/releases/latest answers the OLD shape — latest_releases without rollout_percent, is_mandatory, min_os_version or min_upgradable_from, and
ICoreUpdateManifest#
ICoreUpdateManifest.h:49 · struct · nested Release · 0 declaration(s)
A PARSED DTO, so its members are PUBLIC and there is no Impl.
struct ICoreUpdateManifest {
public:
// What the server says about the version the client is RUNNING.
//
// ⚠ Unknown IS NOT AN ERROR and must not stop anything. A developer
// build, a version whose row was deleted, or a server that predates the
// migration all land here, and the client carries on.
enum class RunningStatus {
Unknown,
Ok,
Yanked
};
// One release row from latest_releases.
struct Release {
std::string id; // uuid; also the rollout bucket's salt
std::string version;
std::string channel; // "stable" | "beta" | "nightly"
std::string platform; // "macos-arm64", ... -- the same token
// ICORE_PLATFORM_TAG names the artifact with
std::string kind; // "installer" | "sdk" | "addon"
std::string sha256; // lowercase hex, 64 chars
std::int64_t sizeBytes = 0;
std::string signature; // detached Ed25519 over the sha256; may
// be empty until U0.4/U0.5 land
std::string releaseNotesUrl;
std::string publishedAt; // ISO-8601, as the server spelled it
std::string requiredFeature; // empty = any valid licence
bool requiresMaintenance = true;
// From the proposed migration; absent on a server that predates it.
std::string minOsVersion;
std::string minUpgradableFrom;
// U8.4. Set ONLY on a delta row (`kind == "update_delta"`): the version
// this delta applies TO. Empty on every full artifact, which is what
// isDelta() below reads.
std::string fromVersion;
bool isMandatory = false;
// ⚠ DEFAULTS TO 100, i.e. everybody. An absent rollout_percent must
// mean "fully rolled out" -- defaulting it to 0 would silently stop
// offering updates to every user the moment the client is pointed at
// a server that does not carry the column.
int rolloutPercent = 100;
// A row with no version is not a row: everything downstream compares
// against it.
[[nodiscard]] bool isValid() const;
// A delta carries a from_version and says so in `kind`. Both, not
// either: a row with one and not the other is a malformed row, and
// acting on half of it is how a delta gets applied as a full artifact.
[[nodiscard]] bool isDelta() const;
};
RunningStatus status = RunningStatus::Unknown;
std::string yankReason = {};
// ⚠ THE SUPPORTED-VERSION FLOOR (U8.1), AND IT IS EMPTY UNTIL SOMEBODY
// DELIBERATELY SETS IT ON THE SERVER. A product-level fact, not a per-release
// one -- "the oldest version we still support" is one statement, and putting
// it on release rows would make it n statements that can disagree.
//
// ⚠ EMPTY MEANS NO FLOOR, AND SO DOES ANYTHING UNREADABLE. This is the only
// field the server can send that stops software already running on
// somebody's machine, so every uncertain reading resolves the same way:
// absent (a server predating the migration), null, "", or a string
// ICoreSemVer cannot parse all mean the same thing here -- carry on. A
// client that invented a floor from a typo would be a client that bricks a
// working install because someone fat-fingered a release form.
std::string minSupportedVersion = {};
// ⚠ U8.4 -- THE DELTA THE SERVER OFFERS, WHICH IS AN OPTIMISATION AND NEVER
// AN OBLIGATION. Present only when the request carried ?version= and a delta
// row exists FROM exactly that version; `isValid()` is false otherwise.
// Nothing downstream may treat an absent or unusable delta as a failure:
// `latest` is always the answer that works, and every refusal in
// ICoreDeltaBundle has the same remedy -- take the full artifact.
Release delta = {};
// Empty version means "nothing published for this platform and channel",
// which is an answer, not a failure.
Release latest = {};
// The server's own semver_gt() result. DIAGNOSTICS ONLY -- see the header
// note. ICoreUpdateChecker recomputes it locally.
bool serverUpdateAvailable = false;
// ---- parsing --------------------------------------------------------
// Returns false and fills `error` when the body is not JSON, is not an
// object, or carries a `latest` with no version. Everything else parses.
[[nodiscard]] static bool parse(const std::string& json,
ICoreUpdateManifest* out,
std::string* error);
// ---- the request ----------------------------------------------------
// GET <baseUrl>/api/releases/latest?platform=&channel=&version=&kind=
//
// `currentVersion` is what makes the YANK check possible: latest_releases
// excludes yanked rows, so a client that does not say what it is running
// is told it is up to date -- and it is exactly the person running a
// pulled build who most needs to hear otherwise. Send it always.
//
// Every value is percent-encoded. A version string reaches this from a
// VERSION file and a platform token from the build, but neither is a
// reason to paste unescaped text into a URL.
[[nodiscard]] static std::string requestUrl(const std::string& baseUrl,
const std::string& platform,
const std::string& channel,
const std::string& currentVersion,
const std::string& kind = "installer");
};
};
ICoreUpdatePolicy.h#
src/ICoreBlocks/ICoreUpdater/ICoreUpdatePolicy.h
ICoreUpdatePolicy#
ICoreUpdatePolicy.h:53 · class · 18 declaration(s)
ICoreUpdatePolicy — which builds this copy is willing to be offered, and how much it may do about one without asking.
class ICoreUpdatePolicy {
public:
ICoreUpdatePolicy() = delete;
enum class Channel { Stable, Beta, Nightly };
enum class Mode {
Ask, // tell me, and I will decide
AutoDownload, // fetch and verify it quietly; ask before replacing
AutoInstall // do the whole thing, and tell me afterwards
};
// ---- channels --------------------------------------------------------
// "stable" / "beta" / "nightly" -- the server's spelling, sent as
// ?channel= and stored in releases.channel.
[[nodiscard]] static std::string channelId(Channel channel);
// std::nullopt for anything this build does not know, which is NOT an
// error: it is a settings file with something else in it.
[[nodiscard]] static std::optional<Channel> channelFromId(std::string_view id);
// "Stable", "Beta (pre-release)", "Nightly (internal)".
[[nodiscard]] static std::string channelDisplayName(Channel channel);
// ⚠ THE U8.3 GATE, AND THE ONE PLACE IT IS SPELLED. Until the product
// decides otherwise this is `stable` alone; a picker offering more than
// this is offering something nobody has agreed to support.
[[nodiscard]] static std::vector<Channel> customerVisibleChannels();
[[nodiscard]] static bool isCustomerVisible(Channel channel);
// ---- modes -----------------------------------------------------------
[[nodiscard]] static std::string modeId(Mode mode);
[[nodiscard]] static std::optional<Mode> modeFromId(std::string_view id);
[[nodiscard]] static std::string modeDisplayName(Mode mode);
[[nodiscard]] static std::vector<Mode> allModes();
// ⚠ THE ONE PLACE "IS AUTOMATIC UPDATING ON" IS ANSWERED, and the reason
// it is a function rather than a comparison at each call site. Every
// caller that wants to know whether it may act without asking asks this;
// a second `mode == Mode::Ask` written out somewhere else is a second
// copy of the rule, and the requirement it enforces -- nothing is EVER
// installed unasked once the user has turned this off -- is only as good
// as the number of places it is written.
[[nodiscard]] static bool isAutomatic(Mode mode);
// AutoInstall alone: the mode that may replace the build with no click.
// AutoDownload fetches and verifies and then stops, which is the setting
// for someone who wants the wait gone but the decision kept.
[[nodiscard]] static bool installsWithoutAsking(Mode mode);
// ---- what a stored value actually resolves to ------------------------
[[nodiscard]] static Channel defaultChannel();
// ⚠ AutoInstall. See the note on the definition: the default is On, and
// Ask is what a user chooses when they want it off.
[[nodiscard]] static Mode defaultMode();
// Unknown, misspelled, empty, or a channel this build must not put a
// customer on: all four answer defaultChannel(). See the resolve() note.
[[nodiscard]] static Channel resolveChannel(std::string_view storedId);
[[nodiscard]] static Mode resolveMode(std::string_view storedId);
// What Shell hands ICoreUpdateService::configure(). Never the raw stored
// string -- that is the line the gate above exists to close.
[[nodiscard]] static std::string resolvedChannelId(std::string_view storedId);
static int selfTest(std::vector<std::string>* failures);
};
};
ICoreUpdateSchedule.h#
src/ICoreBlocks/ICoreUpdater/ICoreUpdateSchedule.h
ICoreUpdateSchedule#
ICoreUpdateSchedule.h:58 · class · pImpl · nested State, Vector · 17 declaration(s)
ICoreUpdateSchedule — WHEN the update check runs, and nothing else.
class ICoreUpdateSchedule {
public:
using TimePoint = std::chrono::system_clock::time_point;
// Once a day while running. The token's own lifetime and the licence
// refresh are on the same period deliberately: ICoreLicenseGate's
// REFRESH_EVERY_HOURS is 24 for the same reason, and a user who leaves the
// app open for a week should learn about a new build in a day, not never.
static constexpr int CHECK_EVERY_HOURS = 24;
// How long after the licence resolves the FIRST check may start. Long
// enough that startup has finished and short enough that a session which
// is only a few minutes long still checks once. The `--console` and
// `--check-updates` hooks settle for 1500 ms; this is two orders of
// magnitude more because those two ARE the run, and this one is a
// background errand that must never be part of it.
static constexpr int SETTLE_SECONDS = 30;
enum class Reason {
// Nothing to do at this instant.
NotDue,
// The licence has not resolved yet. Distinguished from NotDue so a
// diagnostics line can say which of the two it is: "not due for 19 h"
// and "still waiting for the licence" look identical otherwise, and
// the second one is a wiring bug when it lasts.
LicenseNotResolved,
// The once-per-launch check, after the settle window.
Launch,
// The 24 h one.
Periodic,
// "Check now". Bypasses the timer and the settle window; it does NOT
// bypass the endpoint's own rate limiting, which is the server's
// business and not modelled here.
UserRequested
};
// Everything decide() is allowed to know. A value, so a test writes a
// machine that has been running for three days in four lines.
struct State {
bool licenseResolved = false;
TimePoint licenseResolvedAt {};
// Whether a check has ever been STARTED in this process, and when.
// "Started", not "succeeded" — see the unreachable note above.
bool everChecked = false;
TimePoint lastCheckStartedAt{};
// One at a time. A second check started while one is in flight would
// race its own callback, and the answer would be the same answer.
bool checkInFlight = false;
// Set by requestCheckNow(), cleared when a check starts.
bool userRequested = false;
};
// ---- the decision ----------------------------------------------------
// Pure. No clock, no network, no filesystem, no state of its own.
[[nodiscard]] static Reason decide(const State& state, TimePoint now);
// When the next check becomes due, given a state. TimePoint{} (the epoch)
// when nothing is scheduled — the licence has not resolved, or a check is
// already in flight. For a diagnostics line and the account panel, never
// as an input to decide().
[[nodiscard]] static TimePoint nextDueAt(const State& state);
[[nodiscard]] static std::string describe(Reason reason);
// True for the three reasons that mean "start a check". The single
// question a caller should ask; do not re-derive it from the enumerator,
// which is how NotDue and LicenseNotResolved come to be handled
// differently by accident.
[[nodiscard]] static bool startsCheck(Reason reason);
// ---- the stateful half, for the shell --------------------------------
ICoreUpdateSchedule();
~ICoreUpdateSchedule();
ICoreUpdateSchedule(const ICoreUpdateSchedule&) = delete;
ICoreUpdateSchedule& operator=(const ICoreUpdateSchedule&) = delete;
// The licence resolved. Idempotent: calling it again does not restart the
// settle window, so a refresh that re-confirms the same licence cannot
// postpone the launch check indefinitely.
void noteLicenseResolved(TimePoint now);
// "Check now" from the account panel or the update dialog.
void requestCheckNow();
[[nodiscard]] Reason due(TimePoint now) const;
// A check is starting. Anchors the clock and consumes the user request.
void noteCheckStarted(TimePoint now);
// A check finished. `reachable` is false when the endpoint could not be
// asked at all — recorded for diagnostics, and deliberately NOT used to
// schedule a retry.
void noteCheckFinished(bool reachable);
[[nodiscard]] const State& state() const;
[[nodiscard]] bool lastCheckReachable() const;
// ---- the case table --------------------------------------------------
struct Vector {
const char* name;
bool licenseResolved;
long long licenseResolvedAtSec; // seconds from an arbitrary epoch
bool everChecked;
long long lastCheckStartedAtSec;
bool checkInFlight;
bool userRequested;
long long nowSec;
Reason expected;
const char* pins;
};
[[nodiscard]] static std::vector<Vector> vectors();
// Runs vectors() and appends a line per failure. 0 is a pass.
static int selfTest(std::vector<std::string>* failures);
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreUpdateService.h#
src/ICoreBlocks/ICoreUpdater/ICoreUpdateService.h
ICoreUpdateService#
ICoreUpdateService.h:49 · class · pImpl · 20 declaration(s)
ICoreUpdateService — the running application's update check.
class ICoreUpdateService {
public:
using TimePoint = ICoreUpdateSchedule::TimePoint;
// The process-wide instance. Never null, never replaced.
static ICoreUpdateService& instance();
ICoreUpdateService();
~ICoreUpdateService();
ICoreUpdateService(const ICoreUpdateService&) = delete;
ICoreUpdateService& operator=(const ICoreUpdateService&) = delete;
// ---- wiring ----------------------------------------------------------
// The shell installs the real ICoreHttpReleaseClient here; a case installs
// a fake. Passing nullptr disarms the service: poll() will decide as
// usual and then start nothing, which is what a build with no network
// stack configured should do.
void setClient(std::shared_ptr<ICoreReleaseClient> client);
// What the check asks for. The platform tag is the artifact token from
// ICoreProductInfo::platformTag(); an empty one disarms the service the
// same way a null client does, because a build with no tag cannot ask for
// the right artifact.
void configure(std::string platformTag, std::string channel,
std::string runningVersion, std::string hashedMachineId);
// ⚠ WHETHER THIS COPY MAY ACT ON A DECISION WITHOUT BEING ASKED (U6.3).
// The id is ICoreUpdatePolicy's -- "ask", "auto-download", "auto-install"
// -- and it is RESOLVED here, so a hand-edited preferences.ini carrying
// something else gets the default rather than being honoured or crashing.
//
// Shell sets it, exactly as it sets the channel, because the preference
// lives in ICoreUserPreferences at L9 and this object is L3: a service that
// read the setting itself would be an L3 module including L9, which is the
// include the layering guard exists to catch.
//
// ⚠ "ask" DISARMS EVERY AUTOMATIC PATH IN THIS CLASS, WITH NO EXCEPTION.
// Not a yanked build, not one below the supported floor, not one the server
// marked is_mandatory. Those change what the BANNER says and nothing else,
// which is precisely what they did before this setter existed. There is no
// state a server can put a client into that overrides a user who turned
// automatic updating off -- see ICoreUpdatePolicy::isAutomatic().
void setMode(std::string modeId);
// The entitlements every decision is made against. A3.1 calls this from
// the licence gate's stateChanged; until then it is never called and the
// defaults are never consulted, because nothing polls before
// noteLicenseResolved().
void setEntitlements(const ICoreEntitlements& entitlements);
// ---- the cadence -----------------------------------------------------
// The licence resolved: the once-per-launch check becomes due
// SETTLE_SECONDS later. Idempotent.
void noteLicenseResolved(TimePoint now);
// "Check now", from the account panel or the update dialog. Bypasses the
// timer and the settle window; it does not bypass the endpoint's own rate
// limiting. Takes effect on the next poll, so a user who clicks twice in a
// second still makes one request.
void requestCheckNow();
// Driven by the shell's timer. Starts a check if the schedule says one is
// due, and returns whether it did.
bool poll(TimePoint now);
// ---- what came back --------------------------------------------------
// Fires once per completed check that produced a decision. Receivers hold
// an ICoreSignalScope declared LAST.
ICoreSignal<ICoreUpdateChecker::Decision> decided;
// Fires when a check could not be MADE at all, with the transport's
// message. Separate from `decided` on purpose: an unreachable endpoint is
// not an update state, and a panel that rendered it as one would tell a
// user on a train that their licence had a problem.
ICoreSignal<std::string> checkFailed;
[[nodiscard]] const ICoreUpdateSchedule& schedule() const;
// The last decision, for a panel that opens after the check ran rather
// than during it. Outcome::UpToDate with an empty version until one has.
[[nodiscard]] ICoreUpdateChecker::Decision lastDecision() const;
// ---- installing what was decided (U5.7) --------------------------------
//
// ⚠ THE JOIN LIVES HERE BECAUSE THE SHELL ALREADY CONFIGURES THIS OBJECT
// and the window must not. ICoreUpdateDialog's header states the rule the
// other way round: the dialog reports and owns no mechanism, and the caller
// owns the downloader. Making the caller a Studio window would have put the
// update mechanism — and the bearer token — inside ICoreStudio.
// The licence JWT for the download grant, from the shell, which owns the
// gate. ⚠ NEVER LOGGED AND NEVER SHOWN: see ICoreLicenseGate::rawToken().
void setDownloadToken(std::string token);
// Runs the whole sequence for lastDecision(): grant, download, verify,
// assemble (delta) or extract, apply, relaunch — falling back from a delta
// to the full artifact exactly once. Does nothing when there is nothing to
// install or when a run is already in flight.
void installNow();
void cancelInstall();
[[nodiscard]] bool isInstalling() const;
// Whether the run in flight (or the last one) was started by the service
// itself rather than by the Install button. The dialog uses it to say
// "installing automatically" instead of offering a Cancel that reads as
// though the user had started something.
[[nodiscard]] bool isInstallingAutomatically() const;
// 0..100, or -1 for "started, length unknown" — the dialog renders that as
// a moving bar rather than one stuck at zero.
ICoreSignal<int, std::int64_t, std::int64_t> installProgress;
// Fires exactly once per installNow(). ⚠ A HandedOff outcome means the
// application MUST quit: the replacement has been started and two copies
// are now live.
ICoreSignal<ICoreUpdateFlow::Result> installFinished;
// A one-line status for a diagnostics command: whether the licence has
// resolved, what the poll would do now, and when the next check is due.
[[nodiscard]] std::string describe(TimePoint now) const;
static int selfTest(std::vector<std::string>* failures);
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreUpdateStaging.h#
src/ICoreBlocks/ICoreUpdater/ICoreUpdateStaging.h
ICoreUpdateStaging#
ICoreUpdateStaging.h:47 · class · 4 declaration(s)
ICoreUpdateStaging Where a downloaded build is written while it is being checked, and the reason that location is not an implementation detail.
class ICoreUpdateStaging {
public:
enum class Status {
Ready,
NotPrivate, // exists, and is a symlink / not ours / group- or world-accessible
CreateFailed // could not be created at all
};
// Creates or re-validates the staging directory. Safe to call repeatedly,
// and MEANT to be: the check is what makes the path trustworthy at the
// moment of use, so callers re-run it rather than caching a yes.
[[nodiscard]] static Status prepare();
// Empty until prepare() has returned Ready.
[[nodiscard]] static std::filesystem::path directory();
// Where an artifact of this release version and file name is staged. The
// version is part of the path so a half-finished download of 1.2.0 can
// never be mistaken for one of 1.3.0.
[[nodiscard]] static std::filesystem::path artifactPath(const std::string& version,
const std::string& fileName);
// Removes everything staged. Called before a download begins.
static bool clear();
[[nodiscard]] static std::string describe(Status status);
};
ICoreUpdateTelemetry.h#
src/ICoreBlocks/ICoreUpdater/ICoreUpdateTelemetry.h
ICoreUpdateTelemetry#
ICoreUpdateTelemetry.h:36 · class · 5 declaration(s)
ICoreUpdateTelemetry — the ONE translation from the updater's vocabularies into the four telemetry events of U7.1.
class ICoreUpdateTelemetry {
public:
ICoreUpdateTelemetry() = delete;
// What a completed check should report. Total over Outcome.
[[nodiscard]] static ICoreUpdateCheckResult resultOf(ICoreUpdateChecker::Outcome outcome);
// What a refused download grant should report. Total over Reason.
//
// ⚠ EVERY DENIAL MAPS TO ONE VALUE, AND THAT IS THE DESIGN, NOT AN
// OVERSIGHT. grant_download already writes the specific deny_reason into
// download_events server-side, so re-sending it here would duplicate a
// number the database already has, more coarsely and with more chance of
// drifting from it. What usage_events is for is the half the server CANNOT
// see: which client-side stage an update died at — the digest, the
// signature, the staging directory, the apply. Those are separate
// enumerators; the denials are one.
//
// The eleven-armed switch is kept rather than collapsed to a `default`
// precisely because it is the tripwire: a Reason added to the server's
// vocabulary stops this file compiling, and whoever adds it decides then
// whether it is still just a denial.
[[nodiscard]] static ICoreUpdateFailure failureOf(ICoreDownloadDenial::Reason reason);
// A check that never completed. Reported as an update_check with result
// "failed" AND an update_failed with reason "check_failed": the first
// keeps the check-rate denominator honest, the second is what a failure
// dashboard counts. Losing either makes one of the two numbers wrong.
[[nodiscard]] static ICoreUpdateCheckResult checkDidNotComplete();
static int selfTest(std::vector<std::string>* failures);
};
};
ICoreUpdateVerifier.h#
src/ICoreBlocks/ICoreUpdater/ICoreUpdateVerifier.h
ICoreUpdateVerifier#
ICoreUpdateVerifier.h:72 · class · nested Result, Verified · 7 declaration(s)
ICoreUpdateVerifier Decides whether a downloaded artifact is the build that was published.
class ICoreUpdateVerifier {
public:
enum class Outcome {
Ok,
FileMissing, // nothing at that path, or it will not open
SizeMismatch, // almost always a truncated or resumed download
DigestMismatch, // the bytes are not the build that was published
NoExpectedDigest, // the manifest carried none -- never a pass
MalformedExpectedDigest, // not 64 hex characters
ReadFailed // the file shrank, or the disk gave up, mid-read
};
struct Result {
Outcome outcome = Outcome::FileMissing;
std::string computedSha256; // lowercase hex; empty if never computed
std::int64_t bytesRead = 0;
// Body in the .cpp: the header surface rule permits no bodies here.
[[nodiscard]] bool ok() const;
};
// `expectedSizeBytes` <= 0 skips the size check, for a caller whose
// manifest genuinely lacks one. The digest check is never skippable.
[[nodiscard]] static Result verifyDigest(const std::string& artifactPath,
const std::string& expectedSha256Hex,
std::int64_t expectedSizeBytes = 0);
// -----------------------------------------------------------------
// U4.2 -- the second check. A digest says the bytes are what the manifest
// measured; only this says the measurement came from us.
// -----------------------------------------------------------------
enum class SignatureOutcome {
Ok,
NoSignature, // the manifest carried none -- NEVER a pass
NoPinnedKey, // this build pins no release key -- NEVER a pass
MalformedSignature, // not a 64-byte Ed25519 signature
MalformedPinnedKey, // our own key is wrong; still a refusal
BadSignature // it verified against no key we hold
};
// ⚠ WHAT IS SIGNED IS THE 64-CHARACTER LOWERCASE HEX DIGEST TEXT, as ASCII
// -- not the 32 raw bytes it decodes to, and not the artifact. That choice
// is this file's to make until the release procedure (U0.5) exists, and it
// is made here so U0.5 has one thing to match rather than a guess:
//
// * `releases.sha256` is a TEXT column constrained to ^[0-9a-f]{64}$, so
// the hex string is what the signing script has in its hand and what a
// human reviewing the release row can see. "Sign the text in the
// database" has one reading; "sign the bytes that text decodes to" has
// a decoding step in it, and a decoding step is a place to disagree.
// * Signing the artifact itself would mean streaming hundreds of
// megabytes through the signer on an offline machine, twice.
//
// `signatureText` is the manifest's `releases.signature`: unpadded
// base64url, or lowercase hex -- ICoreSignatureVerifier tells them apart by
// length, so the release procedure may emit either.
//
// `acceptedKeys` defaults to pinnedReleaseKeys() and the production path
// never passes anything else. It is a parameter so the suite can hold a
// key -- U7.2 must be able to assert that a signature by an UNTRUSTED key
// fails, and it cannot do that in a build whose pinned table is empty.
// Naming it here beats the alternative, which is a test that skips the
// case with a comment about how it would have been nice to check.
[[nodiscard]] static SignatureOutcome verifySignature(
const std::string& sha256Hex,
const std::string& signatureText,
const std::vector<std::string>& acceptedKeys = pinnedReleaseKeys());
// The public halves this build accepts, compiled in. EMPTY today: the
// offline release key pair is U0.4 and is procurement, so every signature
// check currently answers NoPinnedKey -- which is a refusal, not a pass.
//
// ⚠ NEVER a key fetched from the network. A key downloaded over the same
// channel as the artifact proves that whoever served the artifact also
// served the key. ⚠ NEVER the JWT key either -- ACCOUNT_MANAGER N5: one
// Worker compromise would then mint the licence AND the update.
//
// Set at build time from the ICORE_RELEASE_PUBLIC_KEYS CMake cache
// variable (semicolon-separated), so a release build pins and a developer
// build does not silently inherit one.
[[nodiscard]] static std::vector<std::string> pinnedReleaseKeys();
// -----------------------------------------------------------------
// U4.3 -- what was verified, and whether it still is.
// -----------------------------------------------------------------
// Produced ONLY by verify(). `ok` is true when the digest matched AND the
// signature verified -- never one of the two.
struct Verified {
bool ok = false;
std::string path;
Result digest;
SignatureOutcome signature = SignatureOutcome::NoSignature;
// WHICH FILE the digest was computed over. Invalid unless `ok`.
ICoreFileIdentity identity;
};
// Both checks, digest first. The order is not a preference: a truncated
// download must be reported as a truncated download, and running the
// signature check first would report it as an untrusted build.
[[nodiscard]] static Verified verify(
const std::string& artifactPath,
const std::string& expectedSha256Hex,
const std::string& signatureText,
std::int64_t expectedSizeBytes = 0,
const std::vector<std::string>& acceptedKeys = pinnedReleaseKeys());
// ⚠ CALL THIS IMMEDIATELY BEFORE THE HANDOFF, and treat false as a refusal
// to install -- never as a reason to re-verify, which would just start the
// window again. False for a Verified that was never ok, so a caller cannot
// skip the first check by asking only this one.
[[nodiscard]] static bool stillTheSameFile(const Verified& verified);
// For the message the user actually sees. Deliberately says what to DO:
// "the download did not finish" is actionable, "verification failed" is
// not, and the two are different diagnoses (see the size note above).
[[nodiscard]] static std::string describe(const Result& result);
// The same, for the signature half. "Not signed by us" and "we hold no key
// to check it with" are OUR bug and THEIR attack respectively, and the
// sentences say which.
[[nodiscard]] static std::string describe(const Verified& verified);
// Returns the failure count and appends a sentence per failure (U7.2).
static int selfTest(std::vector<std::string>* failures);
};