diff --git a/package-lock.json b/package-lock.json index e827a85..7fa26a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.6.6", + "version": "1.6.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.6.6", + "version": "1.6.7", "license": "MIT", "dependencies": { "@electron/asar": "^4.0.1", @@ -2269,9 +2269,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.31", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz", - "integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==", + "version": "2.8.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz", + "integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index d2299f1..d2442ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.6.7", + "version": "1.6.8", "description": "A command line utility for the Unity Game Engine.", "author": "RageAgainstThePixel", "license": "MIT", diff --git a/src/license-client.ts b/src/license-client.ts index b2fadb3..05aab81 100644 --- a/src/license-client.ts +++ b/src/license-client.ts @@ -468,6 +468,80 @@ export class LicensingClient { await this.exec(['--showContext']); } + private async getClientLogSize(): Promise { + try { + const stats = await fs.promises.stat(LicensingClient.ClientLogPath()); + return stats.size; + } + catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return 0; + } + + throw error; + } + } + + private async waitForLicenseServerConfiguration(timeoutMs: number = 30_000, pollIntervalMs: number = 1_000): Promise { + const logPath = LicensingClient.ClientLogPath(); + const configuredPattern = /Floating license server URL is:\s*(?[^\s]+)\s*\(via config file\)/; + const notConfiguredPattern = /Floating license server is not configured/; + const deadline = Date.now() + timeoutMs; + let offset = await this.getClientLogSize(); + let remainder = ''; + + while (Date.now() < deadline) { + let newChunk = ''; + + try { + const stats = await fs.promises.stat(logPath); + + if (stats.size > offset) { + const length = stats.size - offset; + const handle = await fs.promises.open(logPath, 'r'); + + try { + const buffer = Buffer.alloc(length); + await handle.read(buffer, 0, length, offset); + newChunk = buffer.toString('utf-8'); + offset = stats.size; + } + finally { + await handle.close(); + } + } + } + catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + this.logger.debug(`Failed to inspect licensing client log: ${error}`); + } + } + + if (newChunk.length > 0) { + remainder += newChunk; + const lines = remainder.split(/\r?\n/); + remainder = lines.pop() ?? ''; + + for (const line of lines) { + const configuredMatch = line.match(configuredPattern); + + if (configuredMatch && configuredMatch.groups?.url) { + this.logger.info(`License server configured with URL: ${configuredMatch.groups.url}`); + return; + } + + if (notConfiguredPattern.test(line)) { + this.logger.warn('Floating license server is not configured. Waiting for configuration...'); + } + } + } + + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); + } + + throw new Error(`Timed out waiting for floating license server configuration. Check '${logPath}' for details.`); + } + /** * Activates a Unity license. * @param options The activation options including license type, services config, serial, username, and password. @@ -498,6 +572,8 @@ export class LicensingClient { switch (options.licenseType) { case LicenseType.floating: { + await this.Context(); + await this.waitForLicenseServerConfiguration(); const output = await this.exec([`--acquire-floating`], true); const tokenMatch = output.match(/with token:\s*"(?[\w-]+)"/); diff --git a/tests/license-client.test.ts b/tests/license-client.test.ts index 69724d7..b5dfe5a 100644 --- a/tests/license-client.test.ts +++ b/tests/license-client.test.ts @@ -1,3 +1,6 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; import { LicensingClient, LicenseType } from '../src/license-client'; afterEach(() => { @@ -40,6 +43,8 @@ describe('LicensingClient floating activation order', () => { const client = new LicensingClient(); const setupSpy = jest.spyOn(client as any, 'setupServicesConfig').mockResolvedValue('/tmp/services-config.json'); const entitlementsSpy = jest.spyOn(client, 'GetActiveEntitlements').mockResolvedValue([]); + jest.spyOn(client, 'Context').mockResolvedValue(); + jest.spyOn(client as any, 'waitForLicenseServerConfiguration').mockResolvedValue(undefined); jest.spyOn(client as any, 'exec').mockResolvedValue('Successfully acquired with token: "token-123"'); await client.Activate({ @@ -52,3 +57,33 @@ describe('LicensingClient floating activation order', () => { expect(entitlementsSpy.mock.invocationCallOrder[0]).toBeGreaterThan(setupSpy.mock.invocationCallOrder[0]); }); }); + +describe('LicensingClient waitForLicenseServerConfiguration', () => { + const createTempLog = () => { + const tempFile = path.join(os.tmpdir(), `unity-cli-log-${Date.now()}-${Math.random()}`); + fs.writeFileSync(tempFile, 'initial line\n'); + return tempFile; + }; + + it('resolves once floating server URL appears in logs', async () => { + const client = new LicensingClient(); + const tempLog = createTempLog(); + jest.spyOn(LicensingClient, 'ClientLogPath').mockReturnValue(tempLog); + + setTimeout(() => { + fs.appendFileSync(tempLog, '\nFloating license server URL is: https://example.com (via config file)\n'); + }, 10); + + await (client as any).waitForLicenseServerConfiguration(500, 10); + fs.rmSync(tempLog, { force: true }); + }); + + it('rejects when floating server URL never appears', async () => { + const client = new LicensingClient(); + const tempLog = createTempLog(); + jest.spyOn(LicensingClient, 'ClientLogPath').mockReturnValue(tempLog); + + await expect((client as any).waitForLicenseServerConfiguration(200, 10)).rejects.toThrow(/Timed out waiting for floating license server configuration/); + fs.rmSync(tempLog, { force: true }); + }); +});