///|
/// Serializes update downloads in this process.
///
/// The native commit lock is intentionally much shorter lived. This mutex
/// prevents two windows in one application from downloading the same release;
/// the native lock protects the final decision against other processes.
let update_install_mutex : @async.Mutex = @async.Mutex::Mutex()
///|
pub enum InstallOutcome {
Installed
AlreadyInstalled
} derive(Debug, Eq)
///|
async fn coordinate_install(
target_revision : UInt64,
current_revision : () -> UInt64 raise UpdateError,
apply : async () -> InstallOutcome raise UpdateError,
) -> InstallOutcome raise UpdateError {
update_install_mutex.acquire() catch {
error => raise InstallInterrupted(detail=@debug.render(Repr(error)))
}
defer update_install_mutex.release()
let installed_revision = current_revision()
if target_revision < installed_revision {
raise RollbackRejected(target=target_revision, installed=installed_revision)
}
if target_revision == installed_revision {
return AlreadyInstalled
}
apply()
}
///|
/// Streams, authenticates and installs an available update.
///
/// The native stage owns a private archive from the first chunk through
/// expansion. `download_into` authenticates the same chunks written there, and
/// any transfer or authentication failure discards the stage before returning.
///
/// The native stage is created beside the running application bundle. This is
/// what keeps final replacement on one filesystem when the application is
/// installed on an external volume. Neither the archive path nor the expanded
/// bundle path crosses the FFI boundary, so verification cannot be separated
/// from use by replacing a path after it was checked.
///
/// The application is not restarted. When to do that is a question about the
/// user's unsaved work, not about the update, so it belongs to the caller —
/// see `relaunch`.
pub async fn UpdateChannel::download_and_install(
self : UpdateChannel,
fetch : ArtifactFetch,
update : AvailableUpdate,
on_progress? : async (Int64) -> Unit noraise = _ => (),
) -> InstallOutcome raise UpdateError {
coordinate_install(
update.revision,
fn() -> UInt64 raise UpdateError {
@native.update_current_revision() catch {
error =>
raise InstallFailed(
step="read the installed update revision",
detail=error.message(),
)
}
},
async fn() -> InstallOutcome raise UpdateError {
let stage = @native.update_stage_begin_for_current_app(
update.size,
update.revision,
) catch {
error =>
raise InstallFailed(
step="prepare a private artifact stage",
detail=error.message(),
)
}
let active = Ref(true)
defer (if active.val { stage.abort() catch { _ => () } })
self.download_into(
fetch,
update,
fn(chunk) -> Unit raise UpdateError {
stage.write(chunk) catch {
error =>
raise InstallFailed(
step="write the private artifact stage",
detail=error.message(),
)
}
},
on_progress,
)
let outcome = stage.install() catch {
error => {
if error.is_update_busy() {
raise InstallBusy
}
if error.is_update_rollback() {
let current = @native.update_current_revision() catch {
read_error =>
raise InstallFailed(
step="read the installed revision after a concurrent update",
detail=read_error.message(),
)
}
raise RollbackRejected(target=update.revision, installed=current)
}
if error.is_update_revision_mismatch() {
raise RevisionMismatch(detail=error.message())
}
raise InstallFailed(
step="validate and replace the application",
detail=error.message(),
)
}
}
active.val = false
match outcome {
@native.Installed => Installed
@native.AlreadyInstalled => AlreadyInstalled
}
},
)
}
///|
/// Starts the installed replacement.
///
/// The caller is expected to exit once this returns. Two copies of the same
/// application running against the same state is a worse outcome than a moment
/// with none, and the new process is already starting by the time this
/// returns.
pub fn relaunch() -> Unit raise UpdateError {
@native.update_relaunch() catch {
error =>
raise InstallFailed(
step="start the new application",
detail=error.message(),
)
}
}