Skip to content

Commit 30388b0

Browse files
unity-cli@v1.6.8 (#56)
- fix a race condition between setting services configuration and acquire license request. Waits for license client to recognize license server before attempting to acquire.
1 parent 61747ec commit 30388b0

File tree

4 files changed

+117
-6
lines changed

4 files changed

+117
-6
lines changed

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rage-against-the-pixel/unity-cli",
3-
"version": "1.6.7",
3+
"version": "1.6.8",
44
"description": "A command line utility for the Unity Game Engine.",
55
"author": "RageAgainstThePixel",
66
"license": "MIT",

src/license-client.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,80 @@ export class LicensingClient {
468468
await this.exec(['--showContext']);
469469
}
470470

471+
private async getClientLogSize(): Promise<number> {
472+
try {
473+
const stats = await fs.promises.stat(LicensingClient.ClientLogPath());
474+
return stats.size;
475+
}
476+
catch (error) {
477+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
478+
return 0;
479+
}
480+
481+
throw error;
482+
}
483+
}
484+
485+
private async waitForLicenseServerConfiguration(timeoutMs: number = 30_000, pollIntervalMs: number = 1_000): Promise<void> {
486+
const logPath = LicensingClient.ClientLogPath();
487+
const configuredPattern = /Floating license server URL is:\s*(?<url>[^\s]+)\s*\(via config file\)/;
488+
const notConfiguredPattern = /Floating license server is not configured/;
489+
const deadline = Date.now() + timeoutMs;
490+
let offset = await this.getClientLogSize();
491+
let remainder = '';
492+
493+
while (Date.now() < deadline) {
494+
let newChunk = '';
495+
496+
try {
497+
const stats = await fs.promises.stat(logPath);
498+
499+
if (stats.size > offset) {
500+
const length = stats.size - offset;
501+
const handle = await fs.promises.open(logPath, 'r');
502+
503+
try {
504+
const buffer = Buffer.alloc(length);
505+
await handle.read(buffer, 0, length, offset);
506+
newChunk = buffer.toString('utf-8');
507+
offset = stats.size;
508+
}
509+
finally {
510+
await handle.close();
511+
}
512+
}
513+
}
514+
catch (error) {
515+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
516+
this.logger.debug(`Failed to inspect licensing client log: ${error}`);
517+
}
518+
}
519+
520+
if (newChunk.length > 0) {
521+
remainder += newChunk;
522+
const lines = remainder.split(/\r?\n/);
523+
remainder = lines.pop() ?? '';
524+
525+
for (const line of lines) {
526+
const configuredMatch = line.match(configuredPattern);
527+
528+
if (configuredMatch && configuredMatch.groups?.url) {
529+
this.logger.info(`License server configured with URL: ${configuredMatch.groups.url}`);
530+
return;
531+
}
532+
533+
if (notConfiguredPattern.test(line)) {
534+
this.logger.warn('Floating license server is not configured. Waiting for configuration...');
535+
}
536+
}
537+
}
538+
539+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
540+
}
541+
542+
throw new Error(`Timed out waiting for floating license server configuration. Check '${logPath}' for details.`);
543+
}
544+
471545
/**
472546
* Activates a Unity license.
473547
* @param options The activation options including license type, services config, serial, username, and password.
@@ -498,6 +572,8 @@ export class LicensingClient {
498572

499573
switch (options.licenseType) {
500574
case LicenseType.floating: {
575+
await this.Context();
576+
await this.waitForLicenseServerConfiguration();
501577
const output = await this.exec([`--acquire-floating`], true);
502578
const tokenMatch = output.match(/with token:\s*"(?<token>[\w-]+)"/);
503579

tests/license-client.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import * as fs from 'fs';
2+
import * as os from 'os';
3+
import * as path from 'path';
14
import { LicensingClient, LicenseType } from '../src/license-client';
25

36
afterEach(() => {
@@ -40,6 +43,8 @@ describe('LicensingClient floating activation order', () => {
4043
const client = new LicensingClient();
4144
const setupSpy = jest.spyOn(client as any, 'setupServicesConfig').mockResolvedValue('/tmp/services-config.json');
4245
const entitlementsSpy = jest.spyOn(client, 'GetActiveEntitlements').mockResolvedValue([]);
46+
jest.spyOn(client, 'Context').mockResolvedValue();
47+
jest.spyOn(client as any, 'waitForLicenseServerConfiguration').mockResolvedValue(undefined);
4348
jest.spyOn(client as any, 'exec').mockResolvedValue('Successfully acquired with token: "token-123"');
4449

4550
await client.Activate({
@@ -52,3 +57,33 @@ describe('LicensingClient floating activation order', () => {
5257
expect(entitlementsSpy.mock.invocationCallOrder[0]).toBeGreaterThan(setupSpy.mock.invocationCallOrder[0]);
5358
});
5459
});
60+
61+
describe('LicensingClient waitForLicenseServerConfiguration', () => {
62+
const createTempLog = () => {
63+
const tempFile = path.join(os.tmpdir(), `unity-cli-log-${Date.now()}-${Math.random()}`);
64+
fs.writeFileSync(tempFile, 'initial line\n');
65+
return tempFile;
66+
};
67+
68+
it('resolves once floating server URL appears in logs', async () => {
69+
const client = new LicensingClient();
70+
const tempLog = createTempLog();
71+
jest.spyOn(LicensingClient, 'ClientLogPath').mockReturnValue(tempLog);
72+
73+
setTimeout(() => {
74+
fs.appendFileSync(tempLog, '\nFloating license server URL is: https://example.com (via config file)\n');
75+
}, 10);
76+
77+
await (client as any).waitForLicenseServerConfiguration(500, 10);
78+
fs.rmSync(tempLog, { force: true });
79+
});
80+
81+
it('rejects when floating server URL never appears', async () => {
82+
const client = new LicensingClient();
83+
const tempLog = createTempLog();
84+
jest.spyOn(LicensingClient, 'ClientLogPath').mockReturnValue(tempLog);
85+
86+
await expect((client as any).waitForLicenseServerConfiguration(200, 10)).rejects.toThrow(/Timed out waiting for floating license server configuration/);
87+
fs.rmSync(tempLog, { force: true });
88+
});
89+
});

0 commit comments

Comments
 (0)