From 9c837c028df8ee947c08a3bab3762eb3073b2208 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 27 Sep 2025 11:42:34 -0400 Subject: [PATCH 1/7] unity-cli@v1.0.1 - export public api for use in other typescript projects --- package-lock.json | 16 ++++++++-------- package.json | 5 +++-- src/index.ts | 10 ++++++++++ 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 32de46f..b85865c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@electron/asar": "^4.0.1", @@ -1896,9 +1896,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", - "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.8.tgz", + "integrity": "sha512-be0PUaPsQX/gPWWgFsdD+GFzaoig5PXaUC1xLkQiYdDnANU8sMnHoQd8JhbJQuvTWrWLyeFN9Imb5Qtfvr4RrQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2310,9 +2310,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.223", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.223.tgz", - "integrity": "sha512-qKm55ic6nbEmagFlTFczML33rF90aU+WtrJ9MdTCThrcvDNdUHN4p6QfVN78U06ZmguqXIyMPyYhw2TrbDUwPQ==", + "version": "1.5.224", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.224.tgz", + "integrity": "sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg==", "dev": true, "license": "ISC" }, diff --git a/package.json b/package.json index 8d29ac4..d452ea9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.0.0", + "version": "1.0.1", "description": "A command line utility for the Unity Game Engine.", "author": "RageAgainstThePixel", "license": "MIT", @@ -23,6 +23,7 @@ "build": "tsc", "dev": "tsc --watch", "link": "npm link", + "unlink": "npm unlink @rage-against-the-pixel/unity-cli", "tests": "jest --roots tests" }, "dependencies": { @@ -44,4 +45,4 @@ "ts-node": "^10.9.2", "typescript": "^5.9.2" } -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index c00a107..c066b93 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,16 @@ import { UnityProject } from './unity-project'; import { CheckAndroidSdkInstalled } from './android-sdk'; import { UnityEditor } from './unity-editor'; +// export public API +export * from './license-client'; +export * from './utilities'; +export * from './unity-hub'; +export * from './logging'; +export * from './unity-version'; +export * from './unity-project'; +export * from './android-sdk'; +export * from './unity-editor'; + const pkgPath = join(__dirname, '..', 'package.json'); const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); const program = new Command(); From fc48b117d97c8a76c02a16987c51370a8b7c2b6c Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 27 Sep 2025 16:59:02 -0400 Subject: [PATCH 2/7] cleanup imports in index --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index c066b93..7488aea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ #!/usr/bin/env node import 'source-map-support/register'; +import * as fs from 'fs'; import * as os from 'os'; import { Command } from 'commander'; -import { readFileSync } from 'fs'; import path, { join } from 'path'; import { LicenseType, LicensingClient } from './license-client'; import { PromptForSecretInput } from './utilities'; @@ -25,7 +25,7 @@ export * from './android-sdk'; export * from './unity-editor'; const pkgPath = join(__dirname, '..', 'package.json'); -const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); +const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); const program = new Command(); program.name('unity-cli') From 33cd2bbebe56615442a0ac7ed8eeb7f4c1e5fa1a Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 27 Sep 2025 17:16:04 -0400 Subject: [PATCH 3/7] general cleanup and formatting added docs --- src/android-sdk.ts | 5 +---- src/license-client.ts | 27 +++++++++++++++++++++++++-- src/logging.ts | 6 ++---- src/unity-editor.ts | 35 ++++++++++++++++++++--------------- src/unity-hub.ts | 28 +++++++++++++++++----------- src/unity-project.ts | 19 ++++++++++++++++--- src/unity-version.ts | 10 +++++----- src/utilities.ts | 42 +++++++++++++++++++++++++++++++++++++----- 8 files changed, 123 insertions(+), 49 deletions(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index 99b739d..ac50386 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -2,15 +2,12 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import { spawn } from 'child_process'; +import { Logger } from './logging'; import { UnityEditor } from './unity-editor'; import { ReadFileContents, ResolveGlobToPath } from './utilities'; -import { - Logger, - LogLevel -} from './logging'; const logger = Logger.instance; diff --git a/src/license-client.ts b/src/license-client.ts index 7e82bba..698f204 100644 --- a/src/license-client.ts +++ b/src/license-client.ts @@ -13,11 +13,16 @@ export enum LicenseType { } export class LicensingClient { - private unityHub: UnityHub = new UnityHub(); + private readonly unityHub: UnityHub = new UnityHub(); + private readonly logger: Logger = Logger.instance; + private licenseClientPath: string | undefined; private licenseVersion: string | undefined; - private logger: Logger = Logger.instance; + /** + * Creates an instance of LicensingClient. + * @param licenseVersion The license version to use (e.g., '4.x', '5.x', '6.x'). If undefined, defaults to '6.x'. + */ constructor(licenseVersion: string | undefined = undefined) { this.licenseVersion = licenseVersion; } @@ -283,10 +288,22 @@ export class LicensingClient { }); } + /** + * Displays the version of the licensing client to the console. + */ public async Version(): Promise { await this.exec(['--version']); } + /** + * Activates a Unity license. + * @param licenseType The type of license to activate. + * @param servicesConfig The services config path for floating licenses. + * @param serial The license serial number. + * @param username The Unity ID username. + * @param password The Unity ID password. + * @throws Error if activation fails or required parameters are missing. + */ public async Activate(licenseType: LicenseType, servicesConfig: string | undefined = undefined, serial: string | undefined = undefined, username: string | undefined = undefined, password: string | undefined = undefined): Promise { let activeLicenses = await this.showEntitlements(); @@ -356,6 +373,12 @@ export class LicensingClient { } } + /** + * Deactivates a Unity license. + * @param licenseType The type of license to deactivate. + * @returns A promise that resolves when the license is deactivated. + * @throws Error if deactivation fails. + */ public async Deactivate(licenseType: LicenseType): Promise { if (licenseType === LicenseType.floating) { return; diff --git a/src/logging.ts b/src/logging.ts index dd87da1..88004d3 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -8,16 +8,14 @@ export enum LogLevel { export class Logger { public logLevel: LogLevel = LogLevel.INFO; - private _ci: string | undefined; - static instance: Logger = new Logger(); + private readonly _ci: string | undefined; + static readonly instance: Logger = new Logger(); private constructor() { if (process.env.GITHUB_ACTIONS) { this._ci = 'GITHUB_ACTIONS'; this.logLevel = process.env.ACTIONS_STEP_DEBUG === 'true' ? LogLevel.DEBUG : LogLevel.CI; } - - Logger.instance = this; } /** diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 98a1c97..a4389cc 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -2,11 +2,11 @@ import * as fs from 'fs'; import * as path from 'path'; import { Logger } from './logging'; import { - getArgumentValueAsString, - killChildProcesses, + GetArgumentValueAsString, + KillChildProcesses, ProcInfo, - readPidFile, - tryKillProcess + ReadPidFile, + TryKillProcess } from './utilities'; import { spawn, @@ -19,14 +19,19 @@ export interface EditorCommand { } export class UnityEditor { - public editorRootPath: string; + public readonly editorRootPath: string; - private procInfo: ProcInfo | undefined; - private pidFile: string; - private logger: Logger = Logger.instance; - private autoAddNoGraphics: boolean; + private readonly logger: Logger = Logger.instance; + private readonly procInfo: ProcInfo | undefined; + private readonly pidFile: string; + private readonly autoAddNoGraphics: boolean; - constructor(public editorPath: string) { + /** + * Initializes a new instance of the UnityEditor class. + * @param editorPath The path to the Unity Editor installation. + * @throws Will throw an error if the editor path is invalid or not executable. + */ + constructor(public readonly editorPath: string) { if (!fs.existsSync(editorPath)) { throw new Error(`The Unity Editor path does not exist: ${editorPath}`); } @@ -188,7 +193,7 @@ export class UnityEditor { command.args.push('-logFile', this.GenerateLogFilePath(command.projectPath)); } - const logPath: string = getArgumentValueAsString('-logFile', command.args); + const logPath: string = GetArgumentValueAsString('-logFile', command.args); let unityProcess: ChildProcessByStdio; @@ -232,9 +237,9 @@ export class UnityEditor { fs.mkdirSync(pidDir, { recursive: true }); } else { try { - var existingProcInfo = await readPidFile(this.pidFile); + var existingProcInfo = await ReadPidFile(this.pidFile); if (existingProcInfo) { - const killedPid = await tryKillProcess(existingProcInfo); + const killedPid = await TryKillProcess(existingProcInfo); if (killedPid) { this.logger.warn(`Killed existing Unity process with pid: ${killedPid}`); } @@ -318,8 +323,8 @@ export class UnityEditor { private async tryKillEditorProcess(): Promise { if (this.procInfo) { - await tryKillProcess(this.procInfo); - await killChildProcesses(this.procInfo); + await TryKillProcess(this.procInfo); + await KillChildProcesses(this.procInfo); } else { this.logger.debug('No Unity process info available to kill.'); } diff --git a/src/unity-hub.ts b/src/unity-hub.ts index f181da7..b16cfaf 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -24,31 +24,30 @@ import { UnityReleasesClient } from '@rage-against-the-pixel/unity-releases-api/ import { UnityEditor } from './unity-editor'; export class UnityHub { - public executable: string; - public rootDirectory: string; - public editorInstallationDirectory: string; - public editorFileExtension: string; + /** The path to the Unity Hub executable. */ + public readonly executable: string; + /** The root directory of the Unity Hub installation. */ + public readonly rootDirectory: string; + /** The file extension for the Unity editor executable. */ + public readonly editorFileExtension: string; - private logger: Logger = Logger.instance; + private readonly logger: Logger = Logger.instance; constructor() { switch (process.platform) { case 'win32': this.executable = process.env.UNITY_HUB_PATH || 'C:\\Program Files\\Unity Hub\\Unity Hub.exe'; this.rootDirectory = path.join(this.executable, '../'); - this.editorInstallationDirectory = 'C:\\Program Files\\Unity\\Hub\\Editor\\'; this.editorFileExtension = '\\Editor\\Unity.exe'; break; case 'darwin': this.executable = process.env.UNITY_HUB_PATH || '/Applications/Unity Hub.app/Contents/MacOS/Unity Hub'; this.rootDirectory = path.join(this.executable, '../../../'); - this.editorInstallationDirectory = '/Applications/Unity/Hub/Editor/'; this.editorFileExtension = '/Unity.app/Contents/MacOS/Unity'; break; case 'linux': this.executable = process.env.UNITY_HUB_PATH || '/opt/unityhub/unityhub'; this.rootDirectory = path.join(this.executable, '../'); - this.editorInstallationDirectory = `${process.env.HOME}/Unity/Hub/Editor/`; this.editorFileExtension = '/Editor/Unity'; break; default: @@ -145,6 +144,7 @@ export class UnityHub { /** * Prints the installed Unity Hub version. + * @returns The installed Unity Hub version. */ public async Version(): Promise { const version = await this.getInstalledHubVersion(); @@ -154,6 +154,7 @@ export class UnityHub { /** * Installs or updates the Unity Hub. * If the Unity Hub is already installed, it will be updated to the latest version. + * @returns The path to the Unity Hub executable. */ public async Install(): Promise { let isInstalled = false; @@ -195,6 +196,7 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); } } + await fs.promises.access(this.executable, fs.constants.X_OK); return this.executable; } @@ -354,7 +356,7 @@ chmod -R 777 "$hubPath"`]); /** * Returns the path where the Unity editors will be installed. - * @returns {Promise} The install path. + * @returns The editor install path. */ public async GetInstallPath(): Promise { const result = (await this.Exec(['install-path', '--get'])).trim(); @@ -368,7 +370,7 @@ chmod -R 777 "$hubPath"`]); /** * Sets the path where Unity editors will be installed. - * @param installPath The path to set. + * @param installPath The install path to set when installing Unity editors. */ public async SetInstallPath(installPath: string): Promise { await fs.promises.mkdir(installPath, { recursive: true }); @@ -377,7 +379,7 @@ chmod -R 777 "$hubPath"`]); /** * Locate and associate an installed editor from a stipulated path. - * @param editorPath + * @param editorPath The path to the Unity Editor installation. */ public async AddEditor(editorPath: string): Promise { await fs.promises.access(editorPath, fs.constants.R_OK | fs.constants.X_OK); @@ -942,6 +944,10 @@ done } } + /** + * Get the mapping of Unity platform targets to their corresponding module identifiers for the current OS. + * @returns A map of Unity platform targets to their corresponding module identifiers for the current OS. + */ public static GetPlatformTargetModuleMap(): { [key: string]: string } { const osType = os.type(); let moduleMap: { [key: string]: string }; diff --git a/src/unity-project.ts b/src/unity-project.ts index c38f37e..7adf8e7 100644 --- a/src/unity-project.ts +++ b/src/unity-project.ts @@ -1,11 +1,11 @@ import os from 'os'; import fs from 'fs'; import path from 'path'; -import { Logger } from './logging'; import { ResolveGlobToPath } from './utilities'; import { UnityVersion } from './unity-version'; export class UnityProject { + /** The default modules to include in a new Unity project. */ public static readonly DefaultModules: string[] = (() => { switch (os.type()) { case 'Linux': return ['linux-il2cpp']; @@ -15,6 +15,7 @@ export class UnityProject { } })(); + /** A map of build targets to their corresponding Unity Hub module names. */ public static readonly BuildTargetModuleMap: { [key: string]: string } = (() => { switch (os.type()) { case 'Linux': return { @@ -46,11 +47,17 @@ export class UnityProject { } })(); - private logger: Logger = Logger.instance; - + /** The path to the ProjectVersion.txt file within the Unity project. */ public readonly projectVersionPath: string; + + /** The Unity version used by the project. */ public readonly version: UnityVersion; + /** + * Initializes a new instance of the UnityProject class. + * @param projectPath The path to the Unity project. + * @throws Will throw an error if the project path is invalid or if the ProjectVersion.txt file cannot be found or read. + */ constructor(public readonly projectPath: string) { fs.accessSync(projectPath, fs.constants.R_OK); this.projectVersionPath = path.join(this.projectPath, 'ProjectSettings', 'ProjectVersion.txt'); @@ -73,6 +80,12 @@ export class UnityProject { this.version = new UnityVersion(match.groups.version, match.groups.changeset, undefined); } + /** + * Gets the Unity project located at the specified path, or the current working directory if no path is provided. + * @param projectPath The path to the Unity project. If undefined, the current working directory is used. + * @returns The UnityProject instance representing the project at the specified path. + * @throws Will throw an error if the project path is invalid or if the ProjectVersion.txt file cannot be found or read. + */ public static async GetProject(projectPath: string | undefined = undefined): Promise { if (!projectPath) { projectPath = process.cwd(); diff --git a/src/unity-version.ts b/src/unity-version.ts index ed67edd..e2d1fe4 100644 --- a/src/unity-version.ts +++ b/src/unity-version.ts @@ -8,12 +8,12 @@ import { } from 'semver'; export class UnityVersion { - public version: string; - public changeset: string | null | undefined; - public architecture: 'X86_64' | 'ARM64'; + public readonly version: string; + public readonly changeset: string | null | undefined; + public readonly architecture: 'X86_64' | 'ARM64'; - private semVer: SemVer; - private logger = Logger.instance; + private readonly semVer: SemVer; + private readonly logger = Logger.instance; constructor( version: string, diff --git a/src/utilities.ts b/src/utilities.ts index 4b17205..75ee1c6 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -122,6 +122,12 @@ export async function Exec(command: string, args: string[], options: ExecOptions return output; } +/** + * Downloads a file from a URL to a specified path. + * @param url The URL to download from. + * @param downloadPath The path to save the downloaded file. + * @throws An error if the download fails or the file is not accessible after download. + */ export async function DownloadFile(url: string, downloadPath: string): Promise { logger.debug(`Downloading from ${url} to ${downloadPath}...`); await fs.promises.mkdir(path.dirname(downloadPath), { recursive: true }); @@ -142,6 +148,11 @@ export async function DownloadFile(url: string, downloadPath: string): Promise { logger.debug(`Attempting to delete directory: ${targetPath}...`); if (targetPath && targetPath.length > 0 && fs.existsSync(targetPath)) { @@ -149,6 +160,12 @@ export async function DeleteDirectory(targetPath: string | undefined): Promise { const fileHandle = await fs.promises.open(filePath, 'r'); try { @@ -159,6 +176,11 @@ export async function ReadFileContents(filePath: string): Promise { } } +/** + * Gets the path to a temporary directory. + * @returns The path to a temporary directory. + * @remarks Falls back to the system temp directory if no environment variables are set. + */ export function GetTempDir(): string { if (process.env['RUNNER_TEMP']) { return process.env['RUNNER_TEMP']!; @@ -179,7 +201,7 @@ export function GetTempDir(): string { * @param args The list of command line arguments. * @returns The value of the argument or an error if not found. */ -export function getArgumentValueAsString(value: string, args: string[]): string { +export function GetArgumentValueAsString(value: string, args: string[]): string { const index = args.indexOf(value); if (index === -1 || index === args.length - 1) { @@ -200,7 +222,7 @@ export interface ProcInfo { * @param procInfo The process information containing the PID. * @returns The PID of the killed process, or undefined if no process was killed. */ -export async function tryKillProcess(procInfo: ProcInfo): Promise { +export async function TryKillProcess(procInfo: ProcInfo): Promise { let pid: number | undefined; try { @@ -219,7 +241,13 @@ export async function tryKillProcess(procInfo: ProcInfo): Promise { +/** + * Reads a PID file and returns the process information. + * @param pidFilePath The path to the PID file. + * @returns The process information, or undefined if the file does not exist or cannot be read. + * @remarks The PID file is deleted after reading. + */ +export async function ReadPidFile(pidFilePath: string): Promise { let procInfo: ProcInfo | undefined; try { if (!fs.existsSync(pidFilePath)) { @@ -250,7 +278,11 @@ export async function readPidFile(pidFilePath: string): Promise { +/** + * Kills all child processes of the given process. + * @param procInfo The process information of the parent process. + */ +export async function KillChildProcesses(procInfo: ProcInfo): Promise { logger.debug(`Killing child processes of ${procInfo.name} with pid: ${procInfo.pid}...`); try { if (process.platform === 'win32') { @@ -268,7 +300,7 @@ export async function killChildProcesses(procInfo: ProcInfo): Promise { const name: string = parts[2]!; if (ppid === procInfo.pid) { - await tryKillProcess({ pid, ppid, name }); + await TryKillProcess({ pid, ppid, name }); } } } From 824b773aec17813667a1849916e70f4751e6969c Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 27 Sep 2025 17:16:27 -0400 Subject: [PATCH 4/7] fix readonly --- src/unity-editor.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/unity-editor.ts b/src/unity-editor.ts index a4389cc..fae50af 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -22,10 +22,11 @@ export class UnityEditor { public readonly editorRootPath: string; private readonly logger: Logger = Logger.instance; - private readonly procInfo: ProcInfo | undefined; private readonly pidFile: string; private readonly autoAddNoGraphics: boolean; + private procInfo: ProcInfo | undefined; + /** * Initializes a new instance of the UnityEditor class. * @param editorPath The path to the Unity Editor installation. From 01296f26b50c939db884fe8c8c0a3153c29f2b2d Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 27 Sep 2025 18:18:20 -0400 Subject: [PATCH 5/7] remove temp pidFile updated hub install command visibility --- src/unity-editor.ts | 22 ---------------------- src/unity-hub.ts | 28 ++++++++++++++-------------- src/utilities.ts | 2 +- 3 files changed, 15 insertions(+), 37 deletions(-) diff --git a/src/unity-editor.ts b/src/unity-editor.ts index fae50af..473d2aa 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -22,7 +22,6 @@ export class UnityEditor { public readonly editorRootPath: string; private readonly logger: Logger = Logger.instance; - private readonly pidFile: string; private readonly autoAddNoGraphics: boolean; private procInfo: ProcInfo | undefined; @@ -39,7 +38,6 @@ export class UnityEditor { fs.accessSync(editorPath, fs.constants.X_OK); this.editorRootPath = UnityEditor.GetEditorRootPath(editorPath); - this.pidFile = path.join(process.env.RUNNER_TEMP || process.env.USERPROFILE || '.', '.unity', 'unity-editor-process-id.txt'); const match = editorPath.match(/(?\d+)\.(?\d+)\.(?\d+)/); @@ -231,26 +229,6 @@ export class UnityEditor { onPid({ pid: processId, ppid: process.pid, name: this.editorPath }); this.logger.debug(`Unity process started with pid: ${processId}`); - // make sure the directory for the PID file exists - const pidDir = path.dirname(this.pidFile); - - if (!fs.existsSync(pidDir)) { - fs.mkdirSync(pidDir, { recursive: true }); - } else { - try { - var existingProcInfo = await ReadPidFile(this.pidFile); - if (existingProcInfo) { - const killedPid = await TryKillProcess(existingProcInfo); - if (killedPid) { - this.logger.warn(`Killed existing Unity process with pid: ${killedPid}`); - } - } - } catch { - // PID file does not exist, continue - } - } - // Write the PID to the PID file - fs.writeFileSync(this.pidFile, String(processId)); const logPollingInterval = 100; // milliseconds // Wait for log file to appear while (!fs.existsSync(logPath)) { diff --git a/src/unity-hub.ts b/src/unity-hub.ts index b16cfaf..4f4bcbd 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -201,6 +201,7 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); } private async installHub(): Promise { + this.logger.ci(`Installing Unity Hub...`); switch (process.platform) { case 'win32': { const url = 'https://public-cdn.cloud.unity3d.com/hub/prod/UnityHubSetup.exe'; @@ -210,7 +211,7 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); this.logger.info(`Running Unity Hub installer...`); try { - await Exec(downloadPath, ['/S'], { silent: true }); + await Exec(downloadPath, ['/S'], { silent: true, showCommand: true }); } finally { if (fs.statSync(downloadPath).isFile()) { await fs.promises.unlink(downloadPath); @@ -223,7 +224,6 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); const baseUrl = 'https://public-cdn.cloud.unity3d.com/hub/prod'; const url = `${baseUrl}/UnityHubSetup-${process.arch}.dmg`; const downloadPath = path.join(GetTempDir(), `UnityHubSetup-${process.arch}.dmg`); - this.logger.info(`Downloading Unity Hub from ${url} to ${downloadPath}`); await DownloadFile(url, downloadPath); await fs.promises.chmod(downloadPath, 0o777); @@ -232,7 +232,7 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); this.logger.debug(`Mounting DMG...`); try { - const output = await Exec('hdiutil', ['attach', downloadPath, '-nobrowse'], { silent: true }); + const output = await Exec('hdiutil', ['attach', downloadPath, '-nobrowse'], { silent: true, showCommand: true }); // can be "/Volumes/Unity Hub 3.13.1-arm64" or "/Volumes/Unity Hub 3.13.1" const mountPointMatch = output.match(/\/Volumes\/Unity Hub.*$/m); @@ -248,16 +248,16 @@ sudo apt-get install -y --no-install-recommends --only-upgrade unityhub`]); await fs.promises.access(appPath, fs.constants.R_OK | fs.constants.X_OK); if (fs.existsSync('/Applications/Unity Hub.app')) { - await Exec('sudo', ['rm', '-rf', '/Applications/Unity Hub.app'], { silent: true }); + await Exec('sudo', ['rm', '-rf', '/Applications/Unity Hub.app'], { silent: true, showCommand: true }); } - await Exec('sudo', ['cp', '-R', appPath, '/Applications/Unity Hub.app'], { silent: true }); - await Exec('sudo', ['chmod', '777', '/Applications/Unity Hub.app/Contents/MacOS/Unity Hub'], { silent: true }); - await Exec('sudo', ['mkdir', '-p', '/Library/Application Support/Unity'], { silent: true }); - await Exec('sudo', ['chmod', '777', '/Library/Application Support/Unity'], { silent: true }); + await Exec('sudo', ['cp', '-R', appPath, '/Applications/Unity Hub.app'], { silent: true, showCommand: true }); + await Exec('sudo', ['chmod', '777', '/Applications/Unity Hub.app/Contents/MacOS/Unity Hub'], { silent: true, showCommand: true }); + await Exec('sudo', ['mkdir', '-p', '/Library/Application Support/Unity'], { silent: true, showCommand: true }); + await Exec('sudo', ['chmod', '777', '/Library/Application Support/Unity'], { silent: true, showCommand: true }); } finally { try { if (mountPoint && mountPoint.length > 0) { - await Exec('hdiutil', ['detach', mountPoint, '-quiet'], { silent: true }); + await Exec('hdiutil', ['detach', mountPoint, '-quiet'], { silent: true, showCommand: true }); } } finally { if (fs.statSync(downloadPath).isFile()) { @@ -295,7 +295,7 @@ chmod -R 777 "$hubPath"`]); } await fs.promises.access(this.executable, fs.constants.X_OK); - this.logger.info(`Unity Hub installed successfully.`); + this.logger.debug(`Unity Hub install complete`); } private async getInstalledHubVersion(): Promise { @@ -861,7 +861,7 @@ done this.logger.info(`Running Unity ${unityVersion.toString()} installer...`); try { - await Exec(installerPath, ['/S', `/D=${installPath}`, '-Wait', '-NoNewWindow'], { silent: true }); + await Exec(installerPath, ['/S', `/D=${installPath}`, '-Wait', '-NoNewWindow'], { silent: true, showCommand: true }); } catch (error) { this.logger.error(`Failed to install Unity ${unityVersion.toString()}: ${error}`); } finally { @@ -885,7 +885,7 @@ done let mountPoint = ''; try { - const output = await Exec('hdiutil', ['attach', installerPath, '-nobrowse'], { silent: true }); + const output = await Exec('hdiutil', ['attach', installerPath, '-nobrowse'], { silent: true, showCommand: true }); const mountPointMatch = output.match(/\/Volumes\/Unity Installer.*$/m); if (!mountPointMatch || mountPointMatch.length === 0) { @@ -899,7 +899,7 @@ done await fs.promises.access(pkgPath, fs.constants.R_OK); this.logger.debug(`Found .pkg installer: ${pkgPath}`); - await Exec('sudo', ['installer', '-pkg', pkgPath, '-target', '/', '-verboseR'], { silent: true }); + await Exec('sudo', ['installer', '-pkg', pkgPath, '-target', '/', '-verboseR'], { silent: true, showCommand: true }); const unityAppPath = path.join('/Applications', 'Unity'); const targetPath = path.join(installDir, `Unity ${unityVersion.version}`); @@ -928,7 +928,7 @@ done } finally { try { if (mountPoint && mountPoint.length > 0) { - await Exec('hdiutil', ['detach', mountPoint, '-quiet'], { silent: true }); + await Exec('hdiutil', ['detach', mountPoint, '-quiet'], { silent: true, showCommand: true }); } } finally { await fs.promises.unlink(installerPath); diff --git a/src/utilities.ts b/src/utilities.ts index 75ee1c6..4af2335 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -129,7 +129,7 @@ export async function Exec(command: string, args: string[], options: ExecOptions * @throws An error if the download fails or the file is not accessible after download. */ export async function DownloadFile(url: string, downloadPath: string): Promise { - logger.debug(`Downloading from ${url} to ${downloadPath}...`); + logger.ci(`Downloading from ${url} to ${downloadPath}...`); await fs.promises.mkdir(path.dirname(downloadPath), { recursive: true }); await new Promise((resolve, reject) => { const file = fs.createWriteStream(downloadPath, { mode: 0o755 }); From 4df060980c210c42c6a850e1c20c581a3cc0dda4 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 27 Sep 2025 18:24:07 -0400 Subject: [PATCH 6/7] cleanup sigint and sigterm handlers --- src/android-sdk.ts | 15 ++++++++++++--- src/license-client.ts | 15 ++++++++++++--- src/unity-editor.ts | 1 + src/unity-hub.ts | 15 ++++++++++++--- src/utilities.ts | 15 ++++++++++++--- 5 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index ac50386..5f22479 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -129,8 +129,10 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st env: { ...process.env, JAVA_HOME: javaPath } }); - process.once('SIGINT', () => child.kill('SIGINT')); - process.once('SIGTERM', () => child.kill('SIGTERM')); + const sigintHandler = () => child.kill('SIGINT'); + const sigtermHandler = () => child.kill('SIGTERM'); + process.once('SIGINT', sigintHandler); + process.once('SIGTERM', sigtermHandler); child.stdout.on('data', (data: Buffer) => { const chunk = data.toString(); output += chunk; @@ -147,9 +149,16 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st output += chunk; process.stderr.write(chunk); }); - child.on('error', (error: Error) => reject(error)); + child.on('error', (error: Error) => { + process.stdout.write('\n'); + process.removeListener('SIGINT', sigintHandler); + process.removeListener('SIGTERM', sigtermHandler); + reject(error); + }); child.on('close', (code: number | null) => { process.stdout.write('\n'); + process.removeListener('SIGINT', sigintHandler); + process.removeListener('SIGTERM', sigtermHandler); resolve(code === null ? 0 : code); }); }); diff --git a/src/license-client.ts b/src/license-client.ts index 698f204..1e91299 100644 --- a/src/license-client.ts +++ b/src/license-client.ts @@ -250,13 +250,22 @@ export class LicensingClient { stdio: ['ignore', 'pipe', 'pipe'] }); - process.once('SIGINT', () => child.kill('SIGINT')); - process.once('SIGTERM', () => child.kill('SIGTERM')); + const sigintHandler = () => child.kill('SIGINT'); + const sigtermHandler = () => child.kill('SIGTERM'); + process.once('SIGINT', sigintHandler); + process.once('SIGTERM', sigtermHandler); child.stdout.on('data', processOutput); child.stderr.on('data', processOutput); - child.on('error', (error) => reject(error)); + child.on('error', (error) => { + process.stdout.write('\n'); + process.removeListener('SIGINT', sigintHandler); + process.removeListener('SIGTERM', sigtermHandler); + reject(error); + }); child.on('close', (code) => { process.stdout.write('\n'); + process.removeListener('SIGINT', sigintHandler); + process.removeListener('SIGTERM', sigtermHandler); resolve(code === null ? 0 : code); }); }); diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 473d2aa..da20122 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -121,6 +121,7 @@ export class UnityEditor { isCancelled = true; await this.tryKillEditorProcess(); }; + process.once('SIGINT', onCancel); process.once('SIGTERM', onCancel); let exitCode: number | undefined; diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 4f4bcbd..e679cef 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -92,13 +92,22 @@ export class UnityHub { stdio: ['ignore', 'pipe', 'pipe'], }); - process.once('SIGINT', () => child.kill('SIGINT')); - process.once('SIGTERM', () => child.kill('SIGTERM')); + const sigintHandler = () => child.kill('SIGINT'); + const sigtermHandler = () => child.kill('SIGTERM'); + process.once('SIGINT', sigintHandler); + process.once('SIGTERM', sigtermHandler); child.stdout.on('data', processOutput); child.stderr.on('data', processOutput); - child.on('error', (error) => reject(error)); + child.on('error', (error) => { + process.stdout.write('\n'); + process.removeListener('SIGINT', sigintHandler); + process.removeListener('SIGTERM', sigtermHandler); + reject(error); + }); child.on('close', (code) => { process.stdout.write('\n'); + process.removeListener('SIGINT', sigintHandler); + process.removeListener('SIGTERM', sigtermHandler); resolve(code === null ? 0 : code); }); }); diff --git a/src/utilities.ts b/src/utilities.ts index 4af2335..efa160a 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -97,13 +97,22 @@ export async function Exec(command: string, args: string[], options: ExecOptions env: process.env, stdio: ['ignore', 'pipe', 'pipe'], }); - process.once('SIGINT', () => child.kill('SIGINT')); - process.once('SIGTERM', () => child.kill('SIGTERM')); + const sigintHandler = () => child.kill('SIGINT'); + const sigtermHandler = () => child.kill('SIGTERM'); + process.once('SIGINT', sigintHandler); + process.once('SIGTERM', sigtermHandler); child.stdout.on('data', processOutput); child.stderr.on('data', processOutput); - child.on('error', (error) => reject(error)); + child.on('error', (error) => { + process.stdout.write('\n'); + process.removeListener('SIGINT', sigintHandler); + process.removeListener('SIGTERM', sigtermHandler); + reject(error); + }); child.on('close', (code) => { process.stdout.write('\n'); + process.removeListener('SIGINT', sigintHandler); + process.removeListener('SIGTERM', sigtermHandler); resolve(code === null ? 0 : code); }); }); From 8fbcb5fdda9dc5be02ecbca1ad3d93aeb57422c0 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 27 Sep 2025 18:30:07 -0400 Subject: [PATCH 7/7] more cleanup --- src/unity-editor.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/unity-editor.ts b/src/unity-editor.ts index da20122..d4b02d1 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -138,7 +138,10 @@ export class UnityEditor { exitCode = 1; } } finally { + process.removeListener('SIGINT', onCancel); + process.removeListener('SIGTERM', onCancel); this.logger.endGroup(); + if (!isCancelled) { await this.tryKillEditorProcess();