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; list-style-type: none;
} }
.error-text {
/* Baseline Material error color */
color: #b00020;
}
/* latin */ /* latin */
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Roboto';

View File

@ -1,77 +1,102 @@
// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT
async function unlockBootloader() { import { FastbootDevice } from "./fastboot/fastboot.js";
const webusb = await Adb.open("WebUSB"); import * as Factory from "./fastboot/factory.js";
if (!webusb.isFastboot()) { const RELEASES_URL = "https://releases.grapheneos.org";
console.log("error: not in fastboot mode");
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"); async function unlockBootloader(setProgress) {
await ensureConnected(setProgress);
const fastboot = await webusb.connectFastboot(); setProgress("Unlocking bootloader...");
await fastboot.send("flashing unlock"); await device.runCommand("flashing unlock");
await fastboot.receive(); return "Bootloader unlocked.";
} }
async function downloadRelease() { async function downloadRelease(setProgress) {
const webusb = await Adb.open("WebUSB"); await ensureConnected(setProgress);
if (!webusb.isFastboot()) { setProgress("Getting device model...");
console.log("error: not in fastboot mode"); 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];
// 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}.`;
} }
console.log("connecting with fastboot"); async function flashRelease(setProgress) {
await ensureConnected(setProgress);
const fastboot = await webusb.connectFastboot(); setProgress("Flashing release...");
await fastboot.send("getvar:product"); await Factory.flashZip(device, lastReleaseZip, (action, partition) => {
const response = await fastboot.receive(); let userPartition = partition == "avb_custom_key" ? "verified boot key" : partition;
if (fastboot.get_cmd(response) == "FAIL") { if (action == "unpack") {
throw new Error("getvar product failed"); setProgress(`Unpacking image: ${userPartition}`);
} else {
setProgress(`Flashing image: ${userPartition}`);
} }
const decoder = new TextDecoder(); });
const product = decoder.decode(fastboot.get_payload(response)); return `Flashed ${lastReleaseZip} to device.`;
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.
} }
async function lockBootloader() { async function lockBootloader(setProgress) {
const webusb = await Adb.open("WebUSB"); await ensureConnected(setProgress);
if (!webusb.isFastboot()) { setProgress("Locking bootloader...");
console.log("error: not in fastboot mode"); await device.runCommand("flashing lock");
return "Bootloader locked.";
} }
console.log("connecting with fastboot"); function addButtonHook(id, callback) {
let statusField = document.getElementById(`${id}-status`);
let statusCallback = (status) => {
statusField.textContent = status;
statusField.className = "";
};
const fastboot = await webusb.connectFastboot(); let button = document.getElementById(`${id}-button`);
await fastboot.send("flashing lock"); button.disabled = false;
await fastboot.receive(); 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) { if ("usb" in navigator) {
console.log("WebUSB available"); console.log("WebUSB available");
const unlockBootloaderButton = document.getElementById("unlock-bootloader"); addButtonHook("unlock-bootloader", unlockBootloader);
unlockBootloaderButton.disabled = false; addButtonHook("download-release", downloadRelease);
unlockBootloaderButton.onclick = unlockBootloader; addButtonHook("flash-release", flashRelease);
addButtonHook("lock-bootloader", lockBootloader);
const downloadReleaseButton = document.getElementById("download-release");
downloadReleaseButton.disabled = false;
downloadReleaseButton.onclick = downloadRelease;
const lockBootloaderButton = document.getElementById("lock-bootloader");
lockBootloaderButton.disabled = false;
lockBootloaderButton.onclick = lockBootloader;
} else { } else {
console.log("WebUSB unavailable"); 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="stylesheet" href="/grapheneos.css?29"/>
<link rel="manifest" href="/manifest.webmanifest"/> <link rel="manifest" href="/manifest.webmanifest"/>
<link rel="license" href="/LICENSE.txt"/> <link rel="license" href="/LICENSE.txt"/>
<script defer="defer" src="/js/webadb.js?0"></script> <script defer="defer" src="/js/fastboot/libs/zip.min.js?0"></script>
<script defer="defer" src="/js/web-install.js?1"></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> </head>
<body> <body>
<header> <header>
@ -129,11 +133,13 @@
<p>Unlock the bootloader to allow flashing the OS and firmware:</p> <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 <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 of the volume keys to switch the selection to accepting it and the power button to
confirm.</p> confirm.</p>
<p><strong id="unlock-bootloader-status"></strong></p>
</section> </section>
<section id="obtaining-factory-images"> <section id="obtaining-factory-images">
@ -144,9 +150,9 @@
<p>Press the button below to start the download:</p> <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>
<section id="flashing-factory-images"> <section id="flashing-factory-images">
@ -155,14 +161,13 @@
<p>The initial install will be performed by flashing the factory images. This will <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> 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 <button id="flash-release-button" disabled="disabled">Flash release</button>
the downloaded zip (or a split approach to downloading files).</strong></p>
<button id="flash-release" disabled="disabled">Flash release</button>
<p>Wait for the flashing process to complete and proceed to <p>Wait for the flashing process to complete and proceed to
<a href="#locking-the-bootloader">locking the bootloader</a> before using the <a href="#locking-the-bootloader">locking the bootloader</a> before using the
device as locking wipes the data again.</p> device as locking wipes the data again.</p>
<p><strong id="flash-release-status"></strong></p>
</section> </section>
<section id="locking-the-bootloader"> <section id="locking-the-bootloader">
@ -177,11 +182,13 @@
<p>In the bootloader interface, set it to locked:</p> <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 <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 of the volume buttons to switch the selection to accepting it and the power button
to confirm.</p> to confirm.</p>
<p><strong id="lock-bootloader-status"></strong></p>
</section> </section>
<section id="post-installation"> <section id="post-installation">