From 6fd0bce4bbe0e8c0865de800d6b8a116342b312a Mon Sep 17 00:00:00 2001 From: Turtlepaw <81275769+Turtlepaw@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:22:42 -0500 Subject: [PATCH] fix: auto updater and installer permissions --- README.md | 6 + install/install.ps1 | 3 + package.json | 4 +- scripts/utils.ts | 331 ++++++++++++++++++-------------------------- 4 files changed, 149 insertions(+), 195 deletions(-) diff --git a/README.md b/README.md index 2a454b4..e51f9d8 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ curl -s https://raw.githubusercontent.com/Turtlepaw/clockwork/refs/heads/main/in ### Add a repository +Clockwork will run + ```shell clockwork add ``` @@ -157,6 +159,10 @@ The script automatically checks for updates of itself when running. For the auto-updater to work, all executables built **must retain the original name given** and be published as assets in a GitHub release. You must also set the `repoOwner` (and `repoName` if needed) for the auto updater to fetch assets from. +##### Testing the auto updater + +For testing, temporarily set the `version` in `package.json` to something lower and recompile the executables, the version set in `package.json` will automatically be injected into the compiled javascript. + ## Acknowledgements - [Google's Watch Face Format Sample repository](https://github.com/android/wear-os-samples/tree/main/WatchFaceFormat) diff --git a/install/install.ps1 b/install/install.ps1 index bb47a66..6f49b0d 100644 --- a/install/install.ps1 +++ b/install/install.ps1 @@ -13,6 +13,9 @@ if (-Not (Test-Path -Path $installDir)) { New-Item -ItemType Directory -Path $installDir } +# Set permissions for the installation directory +icacls $installDir /grant "Everyone:(OI)(CI)F" /T + # Download the latest release $latestReleaseUrl = "https://github.com/Turtlepaw/clockwork/releases/latest/download/clockwork-win.exe" $destinationPath = Join-Path -Path $installDir -ChildPath "clockwork.exe" diff --git a/package.json b/package.json index 320a616..14ef3b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wff-build-script", - "version": "1.1.2", + "version": "1.1.3", "main": "scripts/build.js", "repository": "https://github.com/gondwanasoft/wff-build-script.git", "author": "https://github.com/gondwanasoft", @@ -49,4 +49,4 @@ "node_modules/ora/**" ] } -} +} \ No newline at end of file diff --git a/scripts/utils.ts b/scripts/utils.ts index 786d95a..e386a4d 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -5,6 +5,7 @@ import { execSync } from "child_process"; import inquirer from "inquirer"; import https from "https"; import sudo from "@vscode/sudo-prompt"; +import { platform } from "os"; // GitHub API URL for the latest release const repoOwner = "Turtlepaw"; @@ -51,14 +52,15 @@ export async function progressIndicator(taskName: string) { * Determine the full path of the current binary. * * Linux and MacOS - "/usr/local/bin/clockwork" - * Windows - "C:\Program Files\Clockwork\clockwork.exe" + * Windows - "C:\\Users\\user\\Clockwork" */ function getBinaryPath() { const platform = process.platform; let binaryPath = ""; if (platform === "win32") { - binaryPath = path.join("C:", "Program Files", "Clockwork"); + const userHome = process.env.USERPROFILE || "C:\\Users\\user"; + binaryPath = path.join(userHome, "Clockwork"); } else { binaryPath = path.join("/", "usr", "local", "bin"); } @@ -66,171 +68,6 @@ function getBinaryPath() { return binaryPath; } -// export async function updater(debugMode: boolean, VERSION: string) { -// const binaryPath = getBinaryPath(); -// const _buildDownloadDirectory = ".wff-build-script/downloads"; -// const buildDownloadsDirectory = path.resolve( -// path.join(binaryPath, _buildDownloadDirectory) -// ); -// const pkgName = "clockwork"; - -// /** -// * Safely replace a binary file (cross-platform). -// * @param currentPath - Path to the current binary. -// * @param tempPath - Temporary path for the new binary. -// */ -// async function replaceBinary(currentPath: string, tempPath: string) { -// try { -// // Check if the current binary exists -// if (fs.existsSync(currentPath)) { -// const backupPath = `${currentPath}.bak`; - -// // Rename the current file to .bak (as a backup) -// if (debugMode) -// console.log( -// chalk.yellow(`Renaming current binary to: ${backupPath}`) -// ); -// fs.renameSync(currentPath, backupPath); - -// // Move the new binary to the target path -// if (debugMode) -// console.log( -// chalk.yellow(`Replacing with new binary: ${currentPath}`) -// ); -// fs.renameSync(tempPath, currentPath); - -// if (debugMode) -// console.log(chalk.green("Binary replaced successfully!")); -// } else { -// // If the binary doesn't exist, just move the new binary -// if (debugMode) -// console.log( -// chalk.yellow(`No existing binary found. Adding new binary.`) -// ); -// fs.renameSync(tempPath, currentPath); -// } -// } catch (error: any) { -// console.error(chalk.red("Failed to replace binary:"), error.message); -// throw error; -// } -// } - -// /** -// * Download a file using curl (cross-platform). -// * @param url - The URL to download the file from. -// * @param outputPath - The path to save the downloaded file. -// */ -// function downloadFile(url: string, outputPath: string) { -// try { -// if (debugMode) console.log(chalk.cyan(`Downloading file from: ${url}`)); -// execSync(`curl -L --create-dirs -o "${outputPath}" "${url}"`, { -// stdio: "inherit", -// }); -// if (debugMode) -// console.log(chalk.green(`File downloaded to: ${outputPath}`)); -// } catch (error: any) { -// console.error(chalk.red("Failed to download file:"), error.message); -// throw error; -// } -// } - -// async function fetchLatestReleaseWithCurl(release: any) { -// const updateSpinner = await progressIndicator( -// "Determining latest release asset..." -// ); -// try { -// if (release.assets && release.assets.length > 0) { -// const platform = process.platform; -// const asset = release.assets.find((a: any) => { -// if (platform === "win32" && a.name.includes(`${pkgName}-win`)) -// return true; -// if (platform === "linux" && a.name.includes(`${pkgName}-linux`)) -// return true; -// if (platform === "darwin" && a.name.includes(`${pkgName}-macos`)) -// return true; -// return false; -// }); - -// if (!asset) { -// updateSpinner.updateMessage( -// "No compatible asset found for the current platform." -// ); -// return; -// } - -// const downloadUrl = asset.browser_download_url; -// const outputPath = binaryPath; - -// if (!fs.existsSync(buildDownloadsDirectory)) { -// fs.mkdirSync(buildDownloadsDirectory, { recursive: true }); -// } - -// // Add temporary path to .gitignore -// const gitignorePath = path.resolve(".", ".gitignore"); - -// updateSpinner.updateMessage("Adding files to git ignore..."); -// const content = `# Temporary download files\n${_buildDownloadDirectory}\n`; -// if (!fs.existsSync(gitignorePath)) { -// fs.writeFileSync(gitignorePath, content); -// } else { -// const gitignoreContent = fs.readFileSync(gitignorePath, "utf8"); -// if (!gitignoreContent.includes(_buildDownloadDirectory)) { -// fs.appendFileSync(gitignorePath, `\n${content}`); -// } -// } - -// updateSpinner.updateMessage("Downloading latest release..."); -// const tempPath = path.resolve( -// buildDownloadsDirectory, -// `${asset.name}.tmp` -// ); // Temporary file path - -// downloadFile(downloadUrl, tempPath); -// replaceBinary(outputPath, tempPath); -// console.log( -// chalk.green(`Replaced ${outputPath} with the latest version.`) -// ); -// } else { -// console.error(chalk.red("No assets found in the latest release.")); -// } -// } catch (error: any) { -// console.error("Error fetching or downloading release:", error.message); -// } finally { -// updateSpinner.stop(true); -// } -// } - -// // Check if newer version is available -// const updateSpinner = await progressIndicator("Checking for updates..."); -// try { -// const latestVersion = await fetchLatestRelease(); -// if (latestVersion && latestVersion.version !== VERSION) { -// updateSpinner.stop(true); -// // Ask user if they want to download the latest release -// const answer = await inquirer.prompt([ -// { -// type: "confirm", -// name: "download-update", -// message: `A newer version (${latestVersion.version}) is available. Download the latest release?`, -// default: false, -// }, -// ]); - -// if (answer["download-update"]) { -// await fetchLatestReleaseWithCurl(latestVersion.data); -// console.log("Update complete."); -// process.exit(0); -// } -// } else { -// updateSpinner.updateMessage("No updates available."); -// updateSpinner.stop(true); -// } -// } catch { -// updateSpinner.stop(false); -// console.error("Failed to check for updates."); -// } -// } - /** * Cleanup old tmp download files in buildDownloadsDirectory. */ @@ -241,7 +78,9 @@ async function cleanup(buildDownloadsDirectory: string) { .filter((f) => f.endsWith(".tmp")); if (tmpFiles.length > 0) { const spinner = await progressIndicator("Cleaning downloaded files..."); - tmpFiles.forEach((f) => fs.unlinkSync(f)); + tmpFiles.forEach((f) => + fs.unlinkSync(path.join(buildDownloadsDirectory, f)) + ); spinner.stop(true); } } @@ -250,20 +89,33 @@ async function cleanup(buildDownloadsDirectory: string) { * Fetch the latest release info from the GitHub API. */ async function fetchLatestRelease() { - const response = await fetch(apiUrl, { - headers: { "User-Agent": "Node.js" }, // Required by GitHub API - }); + try { + const response = await fetch(apiUrl, { + headers: { "User-Agent": "Node.js" }, // Required by GitHub API + }); - if (!response.ok) { - console.error("Failed to fetch release info:", response.statusText); - return; - } + if (!response.ok) { + if ( + response.status === 403 && + response.headers.get("X-RateLimit-Remaining") === "0" + ) { + console.error( + "GitHub API rate limit exceeded. Please try again later." + ); + } else { + console.error("Failed to fetch release info:", response.statusText); + } + return; + } - const release = await response.json(); - return { - version: release.tag_name, - data: release, - }; + const release = await response.json(); + return { + version: release.tag_name, + data: release, + }; + } catch (error: any) { + console.error("Error fetching release info:", error.message); + } } /** @@ -271,14 +123,24 @@ async function fetchLatestRelease() { * @param url - The URL to download the file from. * @param outputPath - The path to save the downloaded file. */ -async function downloadFile(url: string, outputPath: string): Promise { - console.log(chalk.cyan(`Downloading file from: ${url}`)); +async function downloadFile( + url: string, + outputPath: string, + debugMode: boolean +): Promise { + if (debugMode) console.log(chalk.cyan(`Downloading file from: ${url}`)); return new Promise((resolve, reject) => { const file = fs.createWriteStream(outputPath); https .get(url, (response) => { - if (response.statusCode !== 200) { + if (response.statusCode === 302 && response.headers.location) { + // Follow redirect + downloadFile(response.headers.location, outputPath, debugMode) + .then(resolve) + .catch(reject); + return; + } else if (response.statusCode !== 200) { reject( new Error( `Failed to download file: HTTP Status ${response.statusCode}` @@ -291,7 +153,8 @@ async function downloadFile(url: string, outputPath: string): Promise { file.on("finish", () => { file.close(); - console.log(chalk.green(`File downloaded to: ${outputPath}`)); + if (debugMode) + console.log(chalk.green(`File downloaded to: ${outputPath}`)); resolve(); }); @@ -328,10 +191,14 @@ async function requestElevatedPermissions() { * @param url - The URL to download the file from. * @param outputPath - The path to save the downloaded file. */ -async function downloadLatestRelease(url: string, outputPath: string) { +async function downloadLatestRelease( + url: string, + outputPath: string, + debugMode: boolean +) { try { - await downloadFile(url, outputPath); - console.log(chalk.green("Download completed successfully.")); + await downloadFile(url, outputPath, debugMode); + if (debugMode) console.log(chalk.green("Download completed successfully.")); } catch (error: any) { console.error( chalk.red("Failed to download the latest release:"), @@ -341,12 +208,78 @@ async function downloadLatestRelease(url: string, outputPath: string) { } } +/** + * Replace the current binary with the downloaded one. + * @param oldPath - The path to the temporary backup of the current binary. + * @param downloadPath - The path to the downloaded file. + * @param _binaryPath - The directory containing the binary. + */ +function replaceBinary( + oldPath: string, + downloadPath: string, + _binaryPath: string, + debugMode: boolean +) { + const platform = process.platform; + const binaryPath = path.join( + _binaryPath, + `clockwork${platform === "win32" ? ".exe" : ""}` + ); + + try { + // Ensure the old binary is moved to a temporary location + if (fs.existsSync(binaryPath)) { + fs.renameSync(binaryPath, oldPath); // Move the current binary to a temp location + } + + // Move the downloaded binary to replace the old binary + fs.renameSync(downloadPath, binaryPath); + + // Ensure the new binary is executable + fs.chmodSync(binaryPath, 0o755); + + if (debugMode) { + console.log(chalk.green("Binary replaced successfully.")); + console.log( + chalk.yellow( + `The old binary has been moved to ${oldPath}. Cleanup will happen on the next run.` + ) + ); + } + } catch (error: any) { + console.error( + chalk.red("Failed to replace the binary. Rolling back changes...") + ); + + // Rollback: Try to restore the original binary if something went wrong + try { + if (fs.existsSync(oldPath)) { + fs.renameSync(oldPath, binaryPath); + } + } catch (rollbackError: any) { + console.error( + chalk.red("Rollback failed. Manual intervention may be required:"), + rollbackError.message + ); + } + + console.error(chalk.red("Error details:"), error.message); + } +} + export async function updater(debugMode: boolean, VERSION: string) { const binaryPath = getBinaryPath(); const _buildDownloadDirectory = ".wff-build-script/downloads"; const buildDownloadsDirectory = path.resolve( path.join(binaryPath, _buildDownloadDirectory) ); + + try { + await cleanup(buildDownloadsDirectory); + } catch (error: any) { + console.error(chalk.red("Failed to cleanup old files:"), error.message); + } + const updateSpinner = await progressIndicator("Checking for updates..."); try { const latestVersion = await fetchLatestRelease(); @@ -362,7 +295,7 @@ export async function updater(debugMode: boolean, VERSION: string) { ]); if (answer["download-update"]) { - await requestElevatedPermissions(); + //await requestElevatedPermissions(); const platform = process.platform; const asset = latestVersion.data.assets.find((a: any) => { if (platform === "win32" && a.name.includes("clockwork-win")) @@ -375,20 +308,32 @@ export async function updater(debugMode: boolean, VERSION: string) { }); if (!asset) { - console.error( - chalk.red("No compatible asset found for the current platform.") + updateSpinner.updateMessage( + "No compatible asset found for the current platform." ); + updateSpinner.stop(false); return; } const downloadUrl = asset.browser_download_url; const outputPath = path.resolve( buildDownloadsDirectory, - `${asset.name}.tmp` + `${asset.name}` ); - await downloadLatestRelease(downloadUrl, outputPath); - console.log(chalk.green("Update complete.")); + const oldPath = path.resolve( + buildDownloadsDirectory, + `${asset.name}.old.tmp` + ); + + if (!fs.existsSync(buildDownloadsDirectory)) { + fs.mkdirSync(buildDownloadsDirectory, { recursive: true }); + } + + await downloadLatestRelease(downloadUrl, outputPath, debugMode); + await replaceBinary(oldPath, outputPath, binaryPath, debugMode); + updateSpinner.updateMessage("Update completed successfully."); + updateSpinner.stop(true); process.exit(0); } } else {