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:
parent
bd2626b41e
commit
713b4ef564
@ -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';
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
|
1055
static/js/webadb.js
1055
static/js/webadb.js
File diff suppressed because it is too large
Load Diff
@ -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">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user