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

@ -150,6 +150,11 @@ footer a, footer a:visited {
list-style-type: none;
}
.error-text {
/* Baseline Material error color */
color: #b00020;
}
/* latin */
@font-face {
font-family: 'Roboto';

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

View File

@ -26,8 +26,12 @@
<link rel="stylesheet" href="/grapheneos.css?29"/>
<link rel="manifest" href="/manifest.webmanifest"/>
<link rel="license" href="/LICENSE.txt"/>
<script defer="defer" src="/js/webadb.js?0"></script>
<script defer="defer" src="/js/web-install.js?1"></script>
<script defer="defer" src="/js/fastboot/libs/zip.min.js?0"></script>
<script type="module" src="/js/fastboot/common.js?0"></script>
<script type="module" src="/js/fastboot/factory.js?0"></script>
<script type="module" src="/js/fastboot/sparse.js?0"></script>
<script type="module" src="/js/fastboot/fastboot.js?0"></script>
<script type="module" src="/js/web-install.js?2"></script>
</head>
<body>
<header>
@ -129,11 +133,13 @@
<p>Unlock the bootloader to allow flashing the OS and firmware:</p>
<button id="unlock-bootloader" disabled="disabled">Unlock bootloader</button>
<button id="unlock-bootloader-button" disabled="disabled">Unlock bootloader</button>
<p>The command needs to be confirmed on the device and will wipe all data. Use one
of the volume keys to switch the selection to accepting it and the power button to
confirm.</p>
<p><strong id="unlock-bootloader-status"></strong></p>
</section>
<section id="obtaining-factory-images">
@ -144,9 +150,9 @@
<p>Press the button below to start the download:</p>
<button id="download-release" disabled="disabled">Download release</button>
<button id="download-release-button" disabled="disabled">Download release</button>
<p><strong>Download not implemented yet.</strong></p>
<p><strong id="download-release-status"></strong></p>
</section>
<section id="flashing-factory-images">
@ -155,14 +161,13 @@
<p>The initial install will be performed by flashing the factory images. This will
replace the existing OS installation and wipe all the existing data.</p>
<p><strong>Needs to be implemented based on extracting and flashing images from
the downloaded zip (or a split approach to downloading files).</strong></p>
<button id="flash-release" disabled="disabled">Flash release</button>
<button id="flash-release-button" disabled="disabled">Flash release</button>
<p>Wait for the flashing process to complete and proceed to
<a href="#locking-the-bootloader">locking the bootloader</a> before using the
device as locking wipes the data again.</p>
<p><strong id="flash-release-status"></strong></p>
</section>
<section id="locking-the-bootloader">
@ -177,11 +182,13 @@
<p>In the bootloader interface, set it to locked:</p>
<button id="lock-bootloader" disabled="disabled">Lock bootloader</button>
<button id="lock-bootloader-button" disabled="disabled">Lock bootloader</button>
<p>The command needs to be confirmed on the device and will wipe all data. Use one
of the volume buttons to switch the selection to accepting it and the power button
to confirm.</p>
<p><strong id="lock-bootloader-status"></strong></p>
</section>
<section id="post-installation">