Skip to content

Commit

Permalink
feat(updater): Visualize download progress
Browse files Browse the repository at this point in the history
  • Loading branch information
Hypfer committed Mar 19, 2023
1 parent b4c9e32 commit 992c38f
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 10 deletions.
7 changes: 6 additions & 1 deletion backend/lib/updater/Updater.js
Expand Up @@ -122,15 +122,20 @@ class Updater {
releaseTimestamp: this.state.releaseTimestamp
});

this.state = new States.ValetudoUpdaterDownloadingState({
const state = new States.ValetudoUpdaterDownloadingState({
downloadUrl: this.state.downloadUrl,
downloadPath: this.state.downloadPath,
expectedHash: this.state.expectedHash,
version: this.state.version,
releaseTimestamp: this.state.releaseTimestamp
});
this.state = state;
this.state.busy = true;

step.onProgressUpdate = (progressPercent) => {
state.metaData.progress = progressPercent;
};

step.execute().then((state) => {
this.state = state;

Expand Down
20 changes: 20 additions & 0 deletions backend/lib/updater/lib/steps/ValetudoUpdaterDownloadStep.js
@@ -1,6 +1,7 @@
const crypto = require("crypto");
const fs = require("fs");
const Logger = require("../../../Logger");
const PipelineThroughputTracker = require("../../../utils/PipelineThroughputTracker");
const States = require("../../../entities/core/updater");
const ValetudoUpdaterError = require("../ValetudoUpdaterError");
const ValetudoUpdaterStep = require("./ValetudoUpdaterStep");
Expand All @@ -25,14 +26,33 @@ class ValetudoUpdaterDownloadStep extends ValetudoUpdaterStep {
this.expectedHash = options.expectedHash;
this.version = options.version;
this.releaseTimestamp = options.releaseTimestamp;

/** @type {(number) => void} **/
this.onProgressUpdate = () => {};
}


async execute() {
try {
const downloadResponse = await get(this.downloadUrl, {responseType: "stream"});
const expectedDownloadSize = parseInt(downloadResponse.headers?.["content-length"]);

const progressTracker = new PipelineThroughputTracker((totalBytes) => {
if (expectedDownloadSize > 0) {
let downloadPercentage = totalBytes / expectedDownloadSize;

downloadPercentage = downloadPercentage * 100;
downloadPercentage = Math.round(downloadPercentage);
downloadPercentage = Math.max(downloadPercentage, 0);
downloadPercentage = Math.min(downloadPercentage, 100);

this.onProgressUpdate(downloadPercentage);
}
});

await pipeline(
downloadResponse.data,
progressTracker,
fs.createWriteStream(this.downloadPath)
);
} catch (e) {
Expand Down
28 changes: 28 additions & 0 deletions backend/lib/utils/PipelineThroughputTracker.js
@@ -0,0 +1,28 @@
const { Transform } = require("stream");

class PipelineThroughputTracker extends Transform {
/**
* @param {(number) => void} reportCallback
*/
constructor(reportCallback) {
super();

this.totalBytes = 0;
this.reportCallback = reportCallback;
}

_transform(chunk, encoding, callback) {
this.totalBytes += chunk.length;
this.reportCallback(this.totalBytes);

callback(null, chunk);
}

_flush(callback) {
this.reportCallback(this.totalBytes);

callback();
}
}

module.exports = PipelineThroughputTracker;
5 changes: 5 additions & 0 deletions frontend/src/api/types.ts
Expand Up @@ -465,9 +465,14 @@ export interface UpdaterConfiguration {
updateProvider: "github" | "github_nightly";
}

export interface UpdaterStateMetaData {
progress: number | undefined;
}

export interface UpdaterState {
__class: "ValetudoUpdaterIdleState" | "ValetudoUpdaterErrorState" | "ValetudoUpdaterApprovalPendingState" | "ValetudoUpdaterDownloadingState" | "ValetudoUpdaterApplyPendingState" | "ValetudoUpdaterDisabledState" | "ValetudoUpdaterNoUpdateRequiredState";
timestamp: string;
metaData: UpdaterStateMetaData;
busy: boolean;
type?: "unknown" | "not_embedded" | "not_docked" | "not_writable" | "not_enough_space" | "download_failed" | "no_matching_binary" | "missing_manifest" | "invalid_manifest" | "invalid_checksum";
message?: string;
Expand Down
32 changes: 23 additions & 9 deletions frontend/src/valetudo/Updater.tsx
Expand Up @@ -22,6 +22,7 @@ import {
Box,
Divider,
Grid,
LinearProgress,
Typography
} from "@mui/material";
import React from "react";
Expand Down Expand Up @@ -89,7 +90,7 @@ const UpdaterStateComponent : React.FunctionComponent<{ state: UpdaterState | un
}

const getIconForState = () : JSX.Element => {
if (state.busy) {
if (state.busy && state.__class !== "ValetudoUpdaterDownloadingState") {
return <BusyIcon sx={{ fontSize: "3rem" }}/>;
} else {
switch (state.__class) {
Expand All @@ -112,22 +113,37 @@ const UpdaterStateComponent : React.FunctionComponent<{ state: UpdaterState | un
};

const getContentForState = () : JSX.Element | undefined => {
if (state.busy) {
if (state.busy && state.__class !== "ValetudoUpdaterDownloadingState") {
return (
<Typography>The Updater is currently busy</Typography>
);
} else {
switch (state.__class) {
case "ValetudoUpdaterErrorState":
return (
<Typography color="red"> {state.message}</Typography>
<Typography color="red">{state.message}</Typography>
);
case "ValetudoUpdaterDownloadingState":
return (
<>
<Typography>Valetudo is currently downloading release {state.version}</Typography>
<Typography>
The Updater is currently downloading version
<br/>
<span
style={{
fontFamily: "\"JetBrains Mono\",monospace",
fontWeight: 200,
marginTop: "1rem"
}}
>
{state.version}
</span>
</Typography>
<br/>
<Typography>Please be patient...</Typography>
<LinearProgress
variant={state.metaData?.progress !== undefined ? "determinate" : "indeterminate"}
value={state.metaData?.progress}
/>
</>
);
case "ValetudoUpdaterApprovalPendingState":
Expand Down Expand Up @@ -220,10 +236,8 @@ const UpdaterStateComponent : React.FunctionComponent<{ state: UpdaterState | un
{
state.__class === "ValetudoUpdaterApplyPendingState" && !state.busy &&
<Typography color="red" style={{marginTop:"1rem", width: "80%"}}>
Please keep in mind that updating can be a dangerous operation.<br/>
Make sure that you&apos;ve thoroughly read the changelog to be aware of possible breaking changes.<br/><br/>
Also, during updates, you should always be prepared for some troubleshooting so please do not click apply if you currently don&apos;t have time for that.<br/><br/>
Also also remember that it might take a few moments until the map reappears after an update.
Please keep in mind that each update can require troubleshooting post-update.<br/>
Make sure that you&apos;ve thoroughly read the changelog to be aware of possible breaking changes.
</Typography>
}
</Grid>
Expand Down

0 comments on commit 992c38f

Please sign in to comment.