Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
76 changes: 76 additions & 0 deletions src/license-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,80 @@
await this.exec(['--showContext']);
}

private async getClientLogSize(): Promise<number> {
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<void> {
const logPath = LicensingClient.ClientLogPath();
const configuredPattern = /Floating license server URL is:\s*(?<url>[^\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.
Expand Down Expand Up @@ -498,6 +572,8 @@

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*"(?<token>[\w-]+)"/);

Expand Down
35 changes: 35 additions & 0 deletions tests/license-client.test.ts
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand Down Expand Up @@ -40,6 +43,8 @@
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({
Expand All @@ -52,3 +57,33 @@
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 });
});
});
Loading