Skip to content

Commit 61747ec

Browse files
unity-cli@v1.6.7 (#55)
- refactored license client resolver and order of operations when setting config json
1 parent 05b48c4 commit 61747ec

File tree

4 files changed

+96
-49
lines changed

4 files changed

+96
-49
lines changed

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.6",
3+
"version": "1.6.7",
44
"description": "A command line utility for the Unity Game Engine.",
55
"author": "RageAgainstThePixel",
66
"license": "MIT",

src/license-client.ts

Lines changed: 52 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import * as path from 'path';
44
import { spawn } from 'child_process';
55
import { Logger } from './logging';
66
import { UnityHub } from './unity-hub';
7-
import { ResolveGlobToPath } from './utilities';
7+
import {
8+
ResolveGlobToPath,
9+
tryParseJson
10+
} from './utilities';
811

912
export enum LicenseType {
1013
personal = 'personal',
@@ -119,45 +122,59 @@ export class LicensingClient {
119122
return path.join(servicesConfigDirectory, 'services-config.json');
120123
}
121124

122-
private tryParseJson(content: string | undefined): string | undefined {
123-
if (!content) {
124-
return undefined;
125-
}
126-
127-
try {
128-
JSON.parse(content);
129-
return content;
130-
} catch {
131-
return undefined;
132-
}
133-
}
134-
135125
private resolveServicesConfigContent(input: string): string {
136126
const trimmedInput = input.trim();
137127

138128
if (trimmedInput.length === 0) {
139129
throw new Error('Services config value is empty. Provide a file path, JSON, or base64 encoded JSON string.');
140130
}
141131

142-
const directJson = this.tryParseJson(trimmedInput);
132+
const rawJson = tryParseJson(trimmedInput);
143133

144-
if (directJson) {
145-
return directJson;
134+
if (rawJson) {
135+
return rawJson;
146136
}
147137

148-
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
149-
if (base64Regex.test(trimmedInput)) {
150-
const decoded = Buffer.from(trimmedInput, 'base64').toString('utf-8').trim();
151-
const decodedJson = this.tryParseJson(decoded);
138+
try {
139+
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
140+
141+
if (base64Regex.test(trimmedInput)) {
142+
const decoded = Buffer.from(trimmedInput, 'base64').toString('utf-8').trim();
143+
const decodedJson = tryParseJson(decoded);
152144

153-
if (decodedJson) {
154-
return decodedJson;
145+
if (decodedJson) {
146+
return decodedJson;
147+
}
148+
} else {
149+
throw new Error('Input does not match base64 format.');
155150
}
156151
}
152+
catch (error) {
153+
throw new Error(`Failed to decode services config as base64: ${error}`);
154+
}
157155

158156
throw new Error('Services config value is not a valid JSON string or base64 encoded JSON string.');
159157
}
160158

159+
private async setupServicesConfig(configSource: string): Promise<string> {
160+
const servicesConfigPath = this.servicesConfigPath();
161+
162+
if (fs.existsSync(configSource)) {
163+
fs.copyFileSync(configSource, servicesConfigPath);
164+
}
165+
else {
166+
const configContent = this.resolveServicesConfigContent(configSource);
167+
fs.writeFileSync(servicesConfigPath, configContent, { encoding: 'utf-8' });
168+
}
169+
170+
if (process.platform !== 'win32') {
171+
fs.chmodSync(servicesConfigPath, 0o644);
172+
}
173+
174+
fs.accessSync(servicesConfigPath, fs.constants.R_OK);
175+
return servicesConfigPath;
176+
}
177+
161178
/**
162179
* Gets the path to the Unity Licensing Client log file.
163180
* @see https://docs.unity.com/en-us/licensing-server/troubleshooting-client#logs
@@ -459,8 +476,19 @@ export class LicensingClient {
459476
* @throws Error if activation fails or required parameters are missing.
460477
*/
461478
public async Activate(options: ActivateOptions, skipEntitlementCheck: boolean = false): Promise<string | undefined> {
479+
let servicesConfigPath: string | undefined;
480+
481+
if (options.licenseType === LicenseType.floating) {
482+
if (!options.servicesConfig) {
483+
throw new Error('Services config path is required for floating license activation');
484+
}
485+
486+
servicesConfigPath = await this.setupServicesConfig(options.servicesConfig);
487+
this.logger.debug(`Using services config at: ${servicesConfigPath}`);
488+
}
489+
462490
if (!skipEntitlementCheck) {
463-
let activeLicenses = await this.GetActiveEntitlements();
491+
const activeLicenses = await this.GetActiveEntitlements();
464492

465493
if (activeLicenses.includes(options.licenseType)) {
466494
this.logger.info(`License of type '${options.licenseType}' is already active, skipping activation`);
@@ -470,27 +498,6 @@ export class LicensingClient {
470498

471499
switch (options.licenseType) {
472500
case LicenseType.floating: {
473-
if (!options.servicesConfig) {
474-
throw new Error('Services config path is required for floating license activation');
475-
}
476-
477-
const servicesConfigPath = this.servicesConfigPath();
478-
479-
if (fs.existsSync(options.servicesConfig)) {
480-
fs.copyFileSync(options.servicesConfig, servicesConfigPath);
481-
}
482-
else {
483-
const configContent = this.resolveServicesConfigContent(options.servicesConfig);
484-
fs.writeFileSync(servicesConfigPath, configContent, { encoding: 'utf-8' });
485-
}
486-
487-
if (process.platform !== 'win32') {
488-
fs.chmodSync(servicesConfigPath, 0o644);
489-
}
490-
491-
fs.accessSync(servicesConfigPath, fs.constants.R_OK);
492-
this.logger.debug(`Using services config at: ${servicesConfigPath}`);
493-
494501
const output = await this.exec([`--acquire-floating`], true);
495502
const tokenMatch = output.match(/with token:\s*"(?<token>[\w-]+)"/);
496503

src/utilities.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,4 +505,17 @@ export async function isProcessElevated(): Promise<boolean> {
505505
"(New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)"
506506
], { silent: true, showCommand: false });
507507
return output.trim().toLowerCase() === 'true';
508+
}
509+
510+
export function tryParseJson(content: string | undefined): string | undefined {
511+
if (!content) {
512+
return undefined;
513+
}
514+
515+
try {
516+
JSON.parse(content);
517+
return content;
518+
} catch {
519+
return undefined;
520+
}
508521
}

tests/license-client.test.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { LicensingClient } from '../src/license-client';
1+
import { LicensingClient, LicenseType } from '../src/license-client';
2+
3+
afterEach(() => {
4+
jest.restoreAllMocks();
5+
});
26

37
describe('LicensingClient services config handling', () => {
48
const invokeResolver = (input: string) => {
@@ -17,11 +21,34 @@ describe('LicensingClient services config handling', () => {
1721
expect(invokeResolver(encoded)).toBe(json);
1822
});
1923

20-
it('rejects invalid inline config', () => {
21-
expect(() => invokeResolver('not-a-valid-config')).toThrow('Services config value is not a valid JSON string or base64 encoded JSON string.');
24+
it('rejects invalid inline config even if it looks like base64', () => {
25+
// "YWJjZA==" decodes to "abcd", which is not valid JSON
26+
expect(() => invokeResolver('YWJjZA==')).toThrow('Services config value is not a valid JSON string or base64 encoded JSON string.');
27+
});
28+
29+
it('throws when inline config does not match base64 format', () => {
30+
expect(() => invokeResolver('not-a-valid-config')).toThrow(/Input does not match base64 format/);
2231
});
2332

2433
it('rejects empty inline config', () => {
2534
expect(() => invokeResolver(' ')).toThrow('Services config value is empty. Provide a file path, JSON, or base64 encoded JSON string.');
2635
});
2736
});
37+
38+
describe('LicensingClient floating activation order', () => {
39+
it('prepares services config before checking entitlements', async () => {
40+
const client = new LicensingClient();
41+
const setupSpy = jest.spyOn(client as any, 'setupServicesConfig').mockResolvedValue('/tmp/services-config.json');
42+
const entitlementsSpy = jest.spyOn(client, 'GetActiveEntitlements').mockResolvedValue([]);
43+
jest.spyOn(client as any, 'exec').mockResolvedValue('Successfully acquired with token: "token-123"');
44+
45+
await client.Activate({
46+
licenseType: LicenseType.floating,
47+
servicesConfig: '{"floatingServer":"https://example.com"}'
48+
});
49+
50+
expect(setupSpy).toHaveBeenCalledTimes(1);
51+
expect(entitlementsSpy).toHaveBeenCalledTimes(1);
52+
expect(entitlementsSpy.mock.invocationCallOrder[0]).toBeGreaterThan(setupSpy.mock.invocationCallOrder[0]);
53+
});
54+
});

0 commit comments

Comments
 (0)