Implement web install using fastboot.js

This implements the WebUSB-based web installer using fastboot.js to
act as a fastboot client. A bare minimum UI with a plain-text
status/progress caption for each step is included, as well as a plain
button to trigger it and basic error handling.

WebADB has been removed now that we are only using fastboot.js.

Initial features:

- Unlocking and locking the bootloader
- Downloading the latest GrapheneOS release available for the device
- Flashing the factory images zip
- Reusing USB connections
This commit is contained in:
Danny Lin
2021-01-22 20:14:00 -08:00
committed by Daniel Micay
parent bd2626b41e
commit 713b4ef564
4 changed files with 105 additions and 1123 deletions

View File

@@ -1,77 +1,102 @@
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT
async function unlockBootloader() {
const webusb = await Adb.open("WebUSB");
import { FastbootDevice } from "./fastboot/fastboot.js";
import * as Factory from "./fastboot/factory.js";
if (!webusb.isFastboot()) {
console.log("error: not in fastboot mode");
const RELEASES_URL = "https://releases.grapheneos.org";
let device = new FastbootDevice();
let lastReleaseZip = null;
async function ensureConnected(setProgress) {
console.log(device.device);
if (!device.isConnected) {
setProgress("Connecting to device...");
await device.connect();
}
console.log("connecting with fastboot");
const fastboot = await webusb.connectFastboot();
await fastboot.send("flashing unlock");
await fastboot.receive();
}
async function downloadRelease() {
const webusb = await Adb.open("WebUSB");
async function unlockBootloader(setProgress) {
await ensureConnected(setProgress);
if (!webusb.isFastboot()) {
console.log("error: not in fastboot mode");
}
console.log("connecting with fastboot");
const fastboot = await webusb.connectFastboot();
await fastboot.send("getvar:product");
const response = await fastboot.receive();
if (fastboot.get_cmd(response) == "FAIL") {
throw new Error("getvar product failed");
}
const decoder = new TextDecoder();
const product = decoder.decode(fastboot.get_payload(response));
const baseUrl = "https://releases.grapheneos.org/";
const metadata = await (await fetch(baseUrl + product + "-stable")).text();
const buildNumber = metadata.split(" ")[0];
const downloadUrl = baseUrl + product + "-factory-" + buildNumber + ".zip";
console.log(downloadUrl);
// Download factory images
//
// Need to do this in a way that works well with huge files, particularly since the zip needs
// to be extracted. Could potentially split it up on the server if this works out badly.
setProgress("Unlocking bootloader...");
await device.runCommand("flashing unlock");
return "Bootloader unlocked.";
}
async function lockBootloader() {
const webusb = await Adb.open("WebUSB");
async function downloadRelease(setProgress) {
await ensureConnected(setProgress);
if (!webusb.isFastboot()) {
console.log("error: not in fastboot mode");
}
setProgress("Getting device model...");
let product = await device.getVariable("product");
setProgress("Finding latest release...");
let metadataResp = await fetch(`${RELEASES_URL}/${product}-stable`);
let metadata = await metadataResp.text();
let releaseId = metadata.split(" ")[0];
console.log("connecting with fastboot");
const fastboot = await webusb.connectFastboot();
await fastboot.send("flashing lock");
await fastboot.receive();
// Download and cache the zip as a blob
setProgress(`Downloading ${releaseId} release for ${product}...`);
lastReleaseZip = `${product}-factory-${releaseId}.zip`;
await Factory.downloadZip(`${RELEASES_URL}/${lastReleaseZip}`);
return `Downloaded ${releaseId} release for ${product}.`;
}
async function flashRelease(setProgress) {
await ensureConnected(setProgress);
setProgress("Flashing release...");
await Factory.flashZip(device, lastReleaseZip, (action, partition) => {
let userPartition = partition == "avb_custom_key" ? "verified boot key" : partition;
if (action == "unpack") {
setProgress(`Unpacking image: ${userPartition}`);
} else {
setProgress(`Flashing image: ${userPartition}`);
}
});
return `Flashed ${lastReleaseZip} to device.`;
}
async function lockBootloader(setProgress) {
await ensureConnected(setProgress);
setProgress("Locking bootloader...");
await device.runCommand("flashing lock");
return "Bootloader locked.";
}
function addButtonHook(id, callback) {
let statusField = document.getElementById(`${id}-status`);
let statusCallback = (status) => {
statusField.textContent = status;
statusField.className = "";
};
let button = document.getElementById(`${id}-button`);
button.disabled = false;
button.onclick = async () => {
try {
let finalStatus = await callback(statusCallback);
statusCallback(finalStatus);
} catch (error) {
statusCallback(`Error: ${error.message}`);
statusField.className = "error-text";
}
};
}
// zip.js is loaded separately.
// eslint-disable-next-line no-undef
zip.configure({
workerScriptsPath: "/js/fastboot/libs/",
});
if ("usb" in navigator) {
console.log("WebUSB available");
const unlockBootloaderButton = document.getElementById("unlock-bootloader");
unlockBootloaderButton.disabled = false;
unlockBootloaderButton.onclick = unlockBootloader;
const downloadReleaseButton = document.getElementById("download-release");
downloadReleaseButton.disabled = false;
downloadReleaseButton.onclick = downloadRelease;
const lockBootloaderButton = document.getElementById("lock-bootloader");
lockBootloaderButton.disabled = false;
lockBootloaderButton.onclick = lockBootloader;
addButtonHook("unlock-bootloader", unlockBootloader);
addButtonHook("download-release", downloadRelease);
addButtonHook("flash-release", flashRelease);
addButtonHook("lock-bootloader", lockBootloader);
} else {
console.log("WebUSB unavailable");
}

File diff suppressed because it is too large Load Diff