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");
const fastboot = await webusb.connectFastboot();
await fastboot.send("flashing unlock");
await fastboot.receive();
} }
async function downloadRelease() { async function unlockBootloader(setProgress) {
const webusb = await Adb.open("WebUSB"); await ensureConnected(setProgress);
if (!webusb.isFastboot()) { setProgress("Unlocking bootloader...");
console.log("error: not in fastboot mode"); await device.runCommand("flashing unlock");
} return "Bootloader unlocked.";
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.
} }
async function lockBootloader() { 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];
console.log("connecting with fastboot"); // Download and cache the zip as a blob
setProgress(`Downloading ${releaseId} release for ${product}...`);
const fastboot = await webusb.connectFastboot(); lastReleaseZip = `${product}-factory-${releaseId}.zip`;
await fastboot.send("flashing lock"); await Factory.downloadZip(`${RELEASES_URL}/${lastReleaseZip}`);
await fastboot.receive(); 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) { 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">