Skip to content

Commit

Permalink
Flash: merge the download and unpack steps
Browse files Browse the repository at this point in the history
  • Loading branch information
incognitojam committed Feb 20, 2025
1 parent 2c7cfac commit b50a787
Show file tree
Hide file tree
Showing 3 changed files with 23 additions and 104 deletions.
5 changes: 0 additions & 5 deletions src/app/Flash.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,6 @@ const steps = {
bgColor: 'bg-blue-500',
icon: cloudDownload,
},
[Step.UNPACKING]: {
status: 'Unpacking...',
bgColor: 'bg-blue-500',
icon: cloudDownload,
},
[Step.FLASHING]: {
status: 'Flashing device...',
description: 'Do not unplug your device until the process is complete.',
Expand Down
33 changes: 4 additions & 29 deletions src/utils/qdl.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import { qdlDevice } from '@commaai/qdl'
import { usbClass } from '@commaai/qdl/usblib'
import * as Comlink from 'comlink'

import { getManifest } from '../utils/manifest'
import { withProgress } from '../utils/progress'
import { getManifest } from './manifest'
import { withProgress } from './progress'

export const Step = {
INITIALIZING: 0,
READY: 1,
CONNECTING: 2,
DOWNLOADING: 3,
UNPACKING: 4,
FLASHING: 6,
ERASING: 7,
DONE: 8,
Expand All @@ -22,7 +21,6 @@ export const Error = {
UNRECOGNIZED_DEVICE: 1,
LOST_CONNECTION: 2,
DOWNLOAD_FAILED: 3,
UNPACK_FAILED: 4,
CHECKSUM_MISMATCH: 5,
FLASH_FAILED: 6,
ERASE_FAILED: 7,
Expand Down Expand Up @@ -246,34 +244,13 @@ export class QdlManager {
}

console.debug('[QDL] Downloaded all images')
this.setStep(Step.UNPACKING)
} catch (err) {
console.error('[QDL] Download error', err)
this.setError(Error.DOWNLOAD_FAILED)
}
}

/**
* @returns {Promise<void>}
* @private
*/
async unpackImages() {
this.setProgress(0)

try {
for await (const [image, onProgress] of withProgress(this.manifest, this.setProgress.bind(this))) {
this.setMessage(`Unpacking ${image.name}`)
await this.imageWorker.unpackImage(image, Comlink.proxy(onProgress))
}

console.debug('[QDL] Unpacked all images')
this.setStep(Step.FLASHING)
} catch (err) {
console.error('[QDL] Unpack error', err)
console.error('[QDL] Download error', err)
if (err.startsWith('Checksum mismatch')) {
this.setError(Error.CHECKSUM_MISMATCH)
} else {
this.setError(Error.UNPACK_FAILED)
this.setError(Error.DOWNLOAD_FAILED)
}
}
}
Expand Down Expand Up @@ -361,8 +338,6 @@ export class QdlManager {
if (this.error !== Error.NONE) return
await this.downloadImages()
if (this.error !== Error.NONE) return
await this.unpackImages()
if (this.error !== Error.NONE) return
await this.flashDevice()
if (this.error !== Error.NONE) return
await this.eraseDevice()
Expand Down
89 changes: 19 additions & 70 deletions src/workers/image.worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ async function readChunks(reader, total, { onChunk, onProgress = undefined }) {
}
}

/** @type {FileSystemDirectoryHandle} */
let root

/**
Expand All @@ -58,122 +59,70 @@ const imageWorker = {
},

/**
* Download an image to persistent storage.
* Download and unpack an image, saving it to persistent storage. Verifies that the image matches the expected
* SHA-256 checksum.
*
* @param {ManifestImage} image
* @param {progressCallback} [onProgress]
* @param {progressCallback} onProgress
* @returns {Promise<void>}
*/
async downloadImage(image, onProgress = undefined) {
const { archiveFileName, archiveUrl } = image
async downloadImage(image, onProgress) {
const { archiveUrl, checksum: expectedChecksum, fileName, size } = image

let writable
try {
const fileHandle = await root.getFileHandle(archiveFileName, { create: true })
const fileHandle = await root.getFileHandle(fileName, { create: true })
writable = await fileHandle.createWritable()
} catch (e) {
throw `Error opening file handle: ${e}`
}

console.debug('[ImageWorker] Downloading', archiveUrl)
console.debug('[ImageWorker] Downloading and unpacking', archiveUrl)
const response = await fetch(archiveUrl, { mode: 'cors' })
if (!response.ok) {
throw `Fetch failed: ${response.status} ${response.statusText}`
}

try {
const contentLength = +response.headers.get('Content-Length')
const reader = response.body.getReader()
await readChunks(reader, contentLength, {
onChunk: async (chunk) => await writable.write(chunk),
onProgress,
})
onProgress?.(1)
} catch (e) {
throw `Could not read response body: ${e}`
}

try {
await writable.close()
} catch (e) {
throw `Error closing file handle: ${e}`
}
},

/**
* Unpack and verify a downloaded image archive.
*
* Throws an error if the checksum does not match.
*
* @param {ManifestImage} image
* @param {progressCallback} [onProgress]
* @returns {Promise<void>}
*/
async unpackImage(image, onProgress = undefined) {
const { archiveFileName, checksum: expectedChecksum, fileName, size: imageSize } = image

/** @type {File} */
let archiveFile
try {
const archiveFileHandle = await root.getFileHandle(archiveFileName, { create: false })
archiveFile = await archiveFileHandle.getFile()
} catch (e) {
throw `Error opening archive file handle: ${e}`
}

// We don't need to write out the image if it isn't compressed
/** @type {FileSystemWritableFileStream|undefined} */
let writable = undefined
if (archiveFileName !== fileName) {
try {
const fileHandle = await root.getFileHandle(fileName, { create: true })
writable = await fileHandle.createWritable()
} catch (e) {
throw `Error opening output file handle: ${e}`
}
}

const shaObj = await createSHA256()
let complete
try {
let stream = archiveFile.stream()
let stream = response.body
if (image.compressed) {
stream = new XzReadableStream(stream)
}

const reader = stream.getReader()
await readChunks(reader, imageSize, {
await readChunks(stream.getReader(), size, {
onChunk: async (chunk) => {
await writable?.write(chunk)
await writable.write(chunk)
shaObj.update(chunk)
},
onProgress,
})

complete = true
onProgress?.(1)
onProgress(1)
} catch (e) {
throw `Error unpacking archive: ${e}`
}

if (!complete) {
throw 'Decompression error: unexpected end of stream'
}

try {
await writable?.close()
await writable.close()
} catch (e) {
throw `Error closing file handle: ${e}`
}

const checksum = shaObj.digest()
if (checksum !== expectedChecksum) {
try {
await root.removeEntry(fileName)
} catch {
// ignored
}
throw `Checksum mismatch: got ${checksum}, expected ${expectedChecksum}`
}
},

/**
* Get a file handle for an image.
*
* @param {ManifestImage} image
* @returns {Promise<FileSystemHandle>}
*/
Expand Down

0 comments on commit b50a787

Please sign in to comment.