body: lf()

in webapp/src/cmds.ts [280:549]


                        body: lf("Flashing your {0} took too long. Please disconnect your {0} from your computer and try reconnecting it.", pxt.appTarget.appTheme.boardName || lf("device")),
                        disagreeLbl: lf("Ok"),
                        hideAgree: true
                    });
                })
                .then(() => {
                    return pxt.commands.saveOnlyAsync(r);
                });
        });
}

function localhostDeployCoreAsync(resp: pxtc.CompileResult): Promise<void> {
    log('local deploy');
    core.infoNotification(lf("Uploading..."));
    let deploy = () => pxt.Util.requestAsync({
        url: "/api/deploy",
        headers: { "Authorization": Cloud.localToken },
        method: "POST",
        data: resp,
        allowHttpErrors: true // To prevent "Network request failed" warning in case of error. We're not actually doing network requests in localhost scenarios
    }).then(r => {
        if (r.statusCode !== 200) {
            core.errorNotification(lf("There was a problem, please try again"));
        } else if (r.json["boardCount"] === 0) {
            core.warningNotification(lf("Please connect your {0} to your computer and try again", pxt.appTarget.appTheme.boardName));
        }
    });

    return deploy()
}

function winrtSaveAsync(resp: pxtc.CompileResult) {
    return pxt.winrt.saveOnlyAsync(resp)
        .then((saved) => {
            if (saved) {
                core.infoNotification(lf("file saved!"));
            }
        })
        .catch((e) => core.errorNotification(lf("saving file failed...")));
}

export function setExtensionResult(res: pxt.editor.ExtensionResult) {
    extensionResult = res;
    applyExtensionResult();
}

function applyExtensionResult() {
    const res = extensionResult;
    if (!res) return;

    if (res.mkPacketIOWrapper) {
        log(`extension mkPacketIOWrapper`)
        pxt.packetio.mkPacketIOWrapper = res.mkPacketIOWrapper;
    }
    if (res.deployAsync) {
        log(`extension deploy core async`);
        pxt.commands.deployCoreAsync = res.deployAsync;
    }
    if (res.saveOnlyAsync) {
        log(`extension save only async`);
        pxt.commands.saveOnlyAsync = res.saveOnlyAsync;
    }
    if (res.saveProjectAsync) {
        log(`extension save project async`);
        pxt.commands.saveProjectAsync = res.saveProjectAsync;
    }
    if (res.renderBrowserDownloadInstructions) {
        log(`extension upload renderBrowserDownloadInstructions`);
        pxt.commands.renderBrowserDownloadInstructions = res.renderBrowserDownloadInstructions;
    }
    if (res.renderUsbPairDialog) {
        log(`extension renderUsbPairDialog`)
        pxt.commands.renderUsbPairDialog = res.renderUsbPairDialog;
    }
    if (res.renderIncompatibleHardwareDialog) {
        log(`extension renderIncompatibleHardwareDialog`)
        pxt.commands.renderIncompatibleHardwareDialog = res.renderIncompatibleHardwareDialog;
    }
    if (res.showUploadInstructionsAsync) {
        log(`extension upload instructions async`);
        pxt.commands.showUploadInstructionsAsync = res.showUploadInstructionsAsync;
    }
    if (res.patchCompileResultAsync) {
        log(`extension build patch`);
        pxt.commands.patchCompileResultAsync = res.patchCompileResultAsync;
    }
    if (res.blocklyPatch) {
        log(`extension blockly patch`);
        pxt.blocks.extensionBlocklyPatch = res.blocklyPatch;
    }
    if (res.webUsbPairDialogAsync) {
        log(`extension webusb pair dialog`);
        pxt.commands.webUsbPairDialogAsync = res.webUsbPairDialogAsync;
    }
    if (res.onTutorialCompleted) {
        log(`extension tutorial completed`);
        pxt.commands.onTutorialCompleted = res.onTutorialCompleted;
    }
}

export async function initAsync() {
    log(`cmds init`);
    pxt.onAppTargetChanged = () => {
        log('app target changed')
        initAsync()
    }

    // check webusb
    await pxt.usb.checkAvailableAsync()

    // unplug any existing packetio
    await pxt.packetio.disconnectAsync()

    // reset commands to browser
    pxt.packetio.mkPacketIOWrapper = undefined;
    pxt.commands.renderDisconnectDialog = undefined;
    pxt.commands.deployCoreAsync = browserDownloadDeployCoreAsync;
    pxt.commands.browserDownloadAsync = browserDownloadAsync;
    pxt.commands.saveOnlyAsync = browserDownloadDeployCoreAsync;
    pxt.commands.webUsbPairDialogAsync = webusb.webUsbPairDialogAsync;
    pxt.commands.showUploadInstructionsAsync = showUploadInstructionsAsync;
    pxt.packetio.mkPacketIOAsync = undefined;

    // uf2/hf2 support
    if (pxt.appTarget?.compile?.useUF2) {
        log(`hf2 wrapper`)
        pxt.packetio.mkPacketIOWrapper = pxt.HF2.mkHF2PacketIOWrapper;
    }

    // check if webUSB is available and usable
    if ((pxt.appTarget?.compile?.isNative || pxt.appTarget?.compile?.hasHex) && !pxt.BrowserUtils.isPxtElectron()) {
        // TODO: web USB is currently disabled in electron app, but should be supported.
        if (pxt.usb.isAvailable() && pxt.appTarget?.compile?.webUSB) {
            log(`enabled webusb`);
            pxt.usb.setEnabled(true);
            pxt.packetio.mkPacketIOAsync = pxt.usb.mkWebUSBHIDPacketIOAsync;
        } else if (!pxt.appTarget?.compile?.disableHIDBridge) {
            log(`enabled hid bridge (webusb disabled)`);
            pxt.usb.setEnabled(false);
            pxt.packetio.mkPacketIOAsync = hidbridge.mkHIDBridgePacketIOAsync;
        }
    }

    const forceBrowserDownload = /force(Hex)?(Browser)?Download/i.test(window.location.href);
    const webUSBSupported = pxt.usb.isEnabled && pxt.appTarget?.compile?.webUSB;
    if (forceBrowserDownload || pxt.appTarget?.serial?.noDeploy) {
        log(`deploy: force browser download`);
        // commands are ready
    } else if (isNativeHost()) {
        log(`deploy: webkit deploy/save`);
        pxt.commands.deployCoreAsync = nativeHostDeployCoreAsync;
        pxt.commands.saveOnlyAsync = nativeHostSaveCoreAsync;
        pxt.commands.workspaceLoadedAsync = nativeHostWorkspaceLoadedCoreAsync;
    } else if (pxt.winrt.isWinRT()) { // windows app
        log(`deploy: winrt`)
        pxt.packetio.mkPacketIOAsync = pxt.winrt.mkWinRTPacketIOAsync;
        pxt.commands.browserDownloadAsync = pxt.winrt.browserDownloadAsync;
        pxt.commands.deployCoreAsync = winrtDeployCoreAsync;
        pxt.commands.saveOnlyAsync = winrtSaveAsync;
    } else if (pxt.BrowserUtils.isPxtElectron()) {
        log(`deploy: electron`);
        pxt.commands.deployCoreAsync = electron.driveDeployAsync;
        pxt.commands.electronDeployAsync = electron.driveDeployAsync;
    } else if (webUSBSupported) {
        log(`deploy: webusb`);
        pxt.commands.deployCoreAsync = hidDeployCoreAsync;
        pxt.commands.renderDisconnectDialog = webusb.renderUnpairDialog;
    } else if (hidbridge.shouldUse()) {
        log(`deploy: hid`);
        pxt.commands.deployCoreAsync = hidDeployCoreAsync;
    } else if (pxt.BrowserUtils.isLocalHost() && Cloud.localToken) { // local node.js
        log(`deploy: localhost`);
        pxt.commands.deployCoreAsync = localhostDeployCoreAsync;
    } else { // in browser
        log(`deploy: browser only`);
        // commands are ready
    }

    applyExtensionResult();

    // don't initialize until extension has hookup as well
    if (pxt.winrt.isWinRT()) {
        log(`deploy: init winrt`)
        pxt.winrt.initWinrtHid(
            () => pxt.packetio.initAsync(true).then(wrap => wrap?.reconnectAsync()),
            () => pxt.packetio.disconnectAsync()
        );
    }
}

export function maybeReconnectAsync(pairIfDeviceNotFound = false, skipIfConnected = false) {
    log("[CLIENT]: starting reconnect")

    if (skipIfConnected && pxt.packetio.isConnected() && !disconnectingPacketIO) return Promise.resolve();

    if (reconnectPromise) return reconnectPromise;
    reconnectPromise = requestPacketIOLockAsync()
        .then(() => pxt.packetio.initAsync())
        .then(wrapper => {
            if (!wrapper) return Promise.resolve();
            return wrapper.reconnectAsync()
                .catch(e => {
                    if (e.type == "devicenotfound")
                        return pairIfDeviceNotFound && pairAsync();
                    throw e;
                })
        })
        .finally(() => {
            reconnectPromise = undefined;
        })
    return reconnectPromise;
}

export function pairAsync(): Promise<void> {
    pxt.tickEvent("cmds.pair")
    return pxt.commands.webUsbPairDialogAsync(pxt.usb.pairAsync, core.confirmAsync)
        .then(res => {
            if (res) return maybeReconnectAsync();
            else return core.infoNotification("Oops, no device was paired.")
        });
}

export function showDisconnectAsync(): Promise<void> {
    if (pxt.commands.renderDisconnectDialog) {
        const { header, jsx, helpUrl } = pxt.commands.renderDisconnectDialog();
        return core.dialogAsync({ header, jsx, helpUrl, hasCloseIcon: true });
    }
    return Promise.resolve();
}

export function disconnectAsync(): Promise<void> {
    log("[CLIENT]: starting disconnect")
    disconnectingPacketIO = true;
    return pxt.packetio.disconnectAsync()
        .then(() => {
            log("[CLIENT]: sending confirmed disconnect " + lockRef)
            hasLock = false;
            sendServiceWorkerMessage({
                type: "serviceworkerclient",
                action: "release-packet-io-lock",
                lock: lockRef
            });
            disconnectingPacketIO = false;
        })
}


// Generate a unique id for communicating with the service worker
const lockRef = pxtc.Util.guidGen();
let pendingPacketIOLockResolver: () => void;
let pendingPacketIOLockRejecter: () => void;
let serviceWorkerSupportedResolver: () => void;
let reconnectPromise: Promise<void>;
let hasLock = false;
let deployingPacketIO = false;
let disconnectingPacketIO = false;
let serviceWorkerSupported: boolean | undefined = undefined;

async function requestPacketIOLockAsync() {
    if (hasLock) return;
    if (pendingPacketIOLockResolver) return Promise.reject("Already waiting for packet lock");
    const supported = await checkIfServiceWorkerSupportedAsync();
    if (!supported) return;

    if (navigator?.serviceWorker?.controller) {
        return new Promise<void>((resolve, reject) => {
            pendingPacketIOLockResolver = resolve;
            pendingPacketIOLockRejecter = reject;
            log("[CLIENT]: requesting lock " + lockRef)
            sendServiceWorkerMessage({