"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NsisUpdater = void 0;
const builder_util_runtime_1 = require("builder-util-runtime");
const child_process_1 = require("child_process");
const path = require("path");
const BaseUpdater_1 = require("./BaseUpdater");
const FileWithEmbeddedBlockMapDifferentialDownloader_1 = require("./differentialDownloader/FileWithEmbeddedBlockMapDifferentialDownloader");
const GenericDifferentialDownloader_1 = require("./differentialDownloader/GenericDifferentialDownloader");
const main_1 = require("./main");
const util_1 = require("./util");
const Provider_1 = require("./providers/Provider");
const fs_extra_1 = require("fs-extra");
const windowsExecutableCodeSignatureVerifier_1 = require("./windowsExecutableCodeSignatureVerifier");
const url_1 = require("url");
const zlib_1 = require("zlib");
class NsisUpdater extends BaseUpdater_1.BaseUpdater {
    constructor(options, app) {
        super(options, app);
    }
    /*** @private */
    doDownloadUpdate(downloadUpdateOptions) {
        const provider = downloadUpdateOptions.updateInfoAndProvider.provider;
        const fileInfo = Provider_1.findFile(provider.resolveFiles(downloadUpdateOptions.updateInfoAndProvider.info), "exe");
        return this.executeDownload({
            fileExtension: "exe",
            downloadUpdateOptions,
            fileInfo,
            task: async (destinationFile, downloadOptions, packageFile, removeTempDirIfAny) => {
                const packageInfo = fileInfo.packageInfo;
                const isWebInstaller = packageInfo != null && packageFile != null;
                if (isWebInstaller && downloadUpdateOptions.disableWebInstaller) {
                    throw builder_util_runtime_1.newError(`Unable to download new version ${downloadUpdateOptions.updateInfoAndProvider.info.version}. Web Installers are disabled`, "ERR_UPDATER_WEB_INSTALLER_DISABLED");
                }
                if (!isWebInstaller && !downloadUpdateOptions.disableWebInstaller) {
                    this._logger.warn("disableWebInstaller is set to false, you should set it to true if you do not plan on using a web installer. This will default to true in a future version.");
                }
                if (isWebInstaller || (await this.differentialDownloadInstaller(fileInfo, downloadUpdateOptions, destinationFile, provider))) {
                    await this.httpExecutor.download(fileInfo.url, destinationFile, downloadOptions);
                }
                const signatureVerificationStatus = await this.verifySignature(destinationFile);
                if (signatureVerificationStatus != null) {
                    await removeTempDirIfAny();
                    // noinspection ThrowInsideFinallyBlockJS
                    throw builder_util_runtime_1.newError(`New version ${downloadUpdateOptions.updateInfoAndProvider.info.version} is not signed by the application owner: ${signatureVerificationStatus}`, "ERR_UPDATER_INVALID_SIGNATURE");
                }
                if (isWebInstaller) {
                    if (await this.differentialDownloadWebPackage(downloadUpdateOptions, packageInfo, packageFile, provider)) {
                        try {
                            await this.httpExecutor.download(new url_1.URL(packageInfo.path), packageFile, {
                                headers: downloadUpdateOptions.requestHeaders,
                                cancellationToken: downloadUpdateOptions.cancellationToken,
                                sha512: packageInfo.sha512,
                            });
                        }
                        catch (e) {
                            try {
                                await fs_extra_1.unlink(packageFile);
                            }
                            catch (ignored) {
                                // ignore
                            }
                            throw e;
                        }
                    }
                }
            },
        });
    }
    // $certificateInfo = (Get-AuthenticodeSignature 'xxx\yyy.exe'
    // | where {$_.Status.Equals([System.Management.Automation.SignatureStatus]::Valid) -and $_.SignerCertificate.Subject.Contains("CN=siemens.com")})
    // | Out-String ; if ($certificateInfo) { exit 0 } else { exit 1 }
    async verifySignature(tempUpdateFile) {
        let publisherName;
        try {
            publisherName = (await this.configOnDisk.value).publisherName;
            if (publisherName == null) {
                return null;
            }
        }
        catch (e) {
            if (e.code === "ENOENT") {
                // no app-update.yml
                return null;
            }
            throw e;
        }
        return await windowsExecutableCodeSignatureVerifier_1.verifySignature(Array.isArray(publisherName) ? publisherName : [publisherName], tempUpdateFile, this._logger);
    }
    doInstall(options) {
        const args = ["--updated"];
        if (options.isSilent) {
            args.push("/S");
        }
        if (options.isForceRunAfter) {
            args.push("--force-run");
        }
        const packagePath = this.downloadedUpdateHelper == null ? null : this.downloadedUpdateHelper.packageFile;
        if (packagePath != null) {
            // only = form is supported
            args.push(`--package-file=${packagePath}`);
        }
        const callUsingElevation = () => {
            _spawn(path.join(process.resourcesPath, "elevate.exe"), [options.installerPath].concat(args)).catch(e => this.dispatchError(e));
        };
        if (options.isAdminRightsRequired) {
            this._logger.info("isAdminRightsRequired is set to true, run installer using elevate.exe");
            callUsingElevation();
            return true;
        }
        _spawn(options.installerPath, args).catch((e) => {
            // https://github.com/electron-userland/electron-builder/issues/1129
            // Node 8 sends errors: https://nodejs.org/dist/latest-v8.x/docs/api/errors.html#errors_common_system_errors
            const errorCode = e.code;
            this._logger.info(`Cannot run installer: error code: ${errorCode}, error message: "${e.message}", will be executed again using elevate if EACCES"`);
            if (errorCode === "UNKNOWN" || errorCode === "EACCES") {
                callUsingElevation();
            }
            else {
                this.dispatchError(e);
            }
        });
        return true;
    }
    async differentialDownloadInstaller(fileInfo, downloadUpdateOptions, installerPath, provider) {
        try {
            if (this._testOnlyOptions != null && !this._testOnlyOptions.isUseDifferentialDownload) {
                return true;
            }
            const blockmapFileUrls = util_1.blockmapFiles(fileInfo.url, this.app.version, downloadUpdateOptions.updateInfoAndProvider.info.version);
            this._logger.info(`Download block maps (old: "${blockmapFileUrls[0]}", new: ${blockmapFileUrls[1]})`);
            const downloadBlockMap = async (url) => {
                const data = await this.httpExecutor.downloadToBuffer(url, {
                    headers: downloadUpdateOptions.requestHeaders,
                    cancellationToken: downloadUpdateOptions.cancellationToken,
                });
                if (data == null || data.length === 0) {
                    throw new Error(`Blockmap "${url.href}" is empty`);
                }
                try {
                    return JSON.parse(zlib_1.gunzipSync(data).toString());
                }
                catch (e) {
                    throw new Error(`Cannot parse blockmap "${url.href}", error: ${e}`);
                }
            };
            const downloadOptions = {
                newUrl: fileInfo.url,
                oldFile: path.join(this.downloadedUpdateHelper.cacheDir, builder_util_runtime_1.CURRENT_APP_INSTALLER_FILE_NAME),
                logger: this._logger,
                newFile: installerPath,
                isUseMultipleRangeRequest: provider.isUseMultipleRangeRequest,
                requestHeaders: downloadUpdateOptions.requestHeaders,
                cancellationToken: downloadUpdateOptions.cancellationToken,
            };
            if (this.listenerCount(main_1.DOWNLOAD_PROGRESS) > 0) {
                downloadOptions.onProgress = it => this.emit(main_1.DOWNLOAD_PROGRESS, it);
            }
            const blockMapDataList = await Promise.all(blockmapFileUrls.map(u => downloadBlockMap(u)));
            await new GenericDifferentialDownloader_1.GenericDifferentialDownloader(fileInfo.info, this.httpExecutor, downloadOptions).download(blockMapDataList[0], blockMapDataList[1]);
            return false;
        }
        catch (e) {
            this._logger.error(`Cannot download differentially, fallback to full download: ${e.stack || e}`);
            if (this._testOnlyOptions != null) {
                // test mode
                throw e;
            }
            return true;
        }
    }
    async differentialDownloadWebPackage(downloadUpdateOptions, packageInfo, packagePath, provider) {
        if (packageInfo.blockMapSize == null) {
            return true;
        }
        try {
            const downloadOptions = {
                newUrl: new url_1.URL(packageInfo.path),
                oldFile: path.join(this.downloadedUpdateHelper.cacheDir, builder_util_runtime_1.CURRENT_APP_PACKAGE_FILE_NAME),
                logger: this._logger,
                newFile: packagePath,
                requestHeaders: this.requestHeaders,
                isUseMultipleRangeRequest: provider.isUseMultipleRangeRequest,
                cancellationToken: downloadUpdateOptions.cancellationToken,
            };
            if (this.listenerCount(main_1.DOWNLOAD_PROGRESS) > 0) {
                downloadOptions.onProgress = it => this.emit(main_1.DOWNLOAD_PROGRESS, it);
            }
            await new FileWithEmbeddedBlockMapDifferentialDownloader_1.FileWithEmbeddedBlockMapDifferentialDownloader(packageInfo, this.httpExecutor, downloadOptions).download();
        }
        catch (e) {
            this._logger.error(`Cannot download differentially, fallback to full download: ${e.stack || e}`);
            // during test (developer machine mac or linux) we must throw error
            return process.platform === "win32";
        }
        return false;
    }
}
exports.NsisUpdater = NsisUpdater;
/**
 * This handles both node 8 and node 10 way of emitting error when spawning a process
 *   - node 8: Throws the error
 *   - node 10: Emit the error(Need to listen with on)
 */
async function _spawn(exe, args) {
    return new Promise((resolve, reject) => {
        try {
            const process = child_process_1.spawn(exe, args, {
                detached: true,
                stdio: "ignore",
            });
            process.on("error", error => {
                reject(error);
            });
            process.unref();
            if (process.pid !== undefined) {
                resolve(true);
            }
        }
        catch (error) {
            reject(error);
        }
    });
}
//# sourceMappingURL=NsisUpdater.js.map