@rage-against-the-pixel/unity-cli 3.0.2 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -224
- package/dist/android-sdk.js +8 -1
- package/dist/android-sdk.js.map +1 -1
- package/dist/cli.js +180 -505
- package/dist/cli.js.map +1 -1
- package/dist/failure-markers.d.ts +17 -0
- package/dist/failure-markers.js +46 -0
- package/dist/failure-markers.js.map +1 -0
- package/dist/github-actions-ci.d.ts +1 -0
- package/dist/github-actions-ci.js +17 -3
- package/dist/github-actions-ci.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/license-client.d.ts +119 -2
- package/dist/license-client.js +737 -56
- package/dist/license-client.js.map +1 -1
- package/dist/logger-provider.d.ts +2 -0
- package/dist/logger-provider.js +3 -0
- package/dist/logger-provider.js.map +1 -1
- package/dist/logging.d.ts +5 -0
- package/dist/logging.js +47 -0
- package/dist/logging.js.map +1 -1
- package/dist/process-tracker.d.ts +71 -0
- package/dist/process-tracker.js +213 -0
- package/dist/process-tracker.js.map +1 -0
- package/dist/unity-cli.d.ts +97 -0
- package/dist/unity-cli.js +591 -0
- package/dist/unity-cli.js.map +1 -0
- package/dist/unity-editor.d.ts +25 -16
- package/dist/unity-editor.js +204 -107
- package/dist/unity-editor.js.map +1 -1
- package/dist/unity-installer.d.ts +139 -0
- package/dist/unity-installer.js +1018 -0
- package/dist/unity-installer.js.map +1 -0
- package/dist/unity-logging.d.ts +16 -1
- package/dist/unity-logging.js +55 -3
- package/dist/unity-logging.js.map +1 -1
- package/dist/unity-project.d.ts +15 -0
- package/dist/unity-project.js +80 -0
- package/dist/unity-project.js.map +1 -1
- package/dist/upm-cli.d.ts +8 -8
- package/dist/upm-cli.js +8 -8
- package/dist/upm-cli.js.map +1 -1
- package/dist/utilities.d.ts +27 -7
- package/dist/utilities.js +119 -63
- package/dist/utilities.js.map +1 -1
- package/package.json +10 -11
- package/dist/unity-hub.d.ts +0 -140
- package/dist/unity-hub.js +0 -1425
- package/dist/unity-hub.js.map +0 -1
package/dist/license-client.js
CHANGED
|
@@ -34,23 +34,56 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.LicensingClient = exports.LicenseType = void 0;
|
|
37
|
+
exports.LicensingClientDiagnostic = LicensingClientDiagnostic;
|
|
38
|
+
exports.GetLegacyLicenseVersion = GetLegacyLicenseVersion;
|
|
39
|
+
exports.ActivateLicense = ActivateLicense;
|
|
40
|
+
exports.IsSharedRunnerIdentity = IsSharedRunnerIdentity;
|
|
41
|
+
exports.ReturnLicense = ReturnLicense;
|
|
42
|
+
exports.PrintLicenseContext = PrintLicenseContext;
|
|
43
|
+
exports.GetLicenseFilePath = GetLicenseFilePath;
|
|
37
44
|
const os = __importStar(require("os"));
|
|
38
45
|
const fs = __importStar(require("fs"));
|
|
39
46
|
const path = __importStar(require("path"));
|
|
40
47
|
const child_process_1 = require("child_process");
|
|
41
48
|
const logging_1 = require("./logging");
|
|
42
|
-
const
|
|
49
|
+
const unity_cli_1 = require("./unity-cli");
|
|
43
50
|
const utilities_1 = require("./utilities");
|
|
51
|
+
const process_tracker_1 = require("./process-tracker");
|
|
44
52
|
var LicenseType;
|
|
45
53
|
(function (LicenseType) {
|
|
46
54
|
LicenseType["personal"] = "personal";
|
|
47
55
|
LicenseType["professional"] = "professional";
|
|
48
56
|
LicenseType["floating"] = "floating";
|
|
49
57
|
})(LicenseType || (exports.LicenseType = LicenseType = {}));
|
|
58
|
+
/**
|
|
59
|
+
* The v3 licensing client, which drives Unity.Licensing.Client directly.
|
|
60
|
+
* v4 uses it as the fallback when the official Unity CLI cannot activate or return a license,
|
|
61
|
+
* for example a personal license on a headless runner, and for Unity 4.x and 5.x editors.
|
|
62
|
+
*/
|
|
63
|
+
/** How long one Unity Licensing Client command may run. */
|
|
64
|
+
const LICENSING_CLIENT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
65
|
+
// literal alternations only, so matching stays linear on any line
|
|
66
|
+
const LICENSING_DIAGNOSTIC_PATTERN = /error|fail|seat|entitlement|activation|limit|denied|invalid|expired/i;
|
|
67
|
+
/**
|
|
68
|
+
* The lines of sanitized licensing client output that say why a command failed, such as a seat limit.
|
|
69
|
+
* @param lines The client output, already sanitized.
|
|
70
|
+
* @returns At most the last 3 matching lines, each cut to 300 characters, or undefined when none match.
|
|
71
|
+
*/
|
|
72
|
+
function LicensingClientDiagnostic(lines) {
|
|
73
|
+
const matches = lines
|
|
74
|
+
.map(line => line.trim())
|
|
75
|
+
.filter(line => line.length > 0 && LICENSING_DIAGNOSTIC_PATTERN.test(line))
|
|
76
|
+
.slice(-3)
|
|
77
|
+
.map(line => line.length > 300 ? `${line.slice(0, 300)}...` : line);
|
|
78
|
+
return matches.length > 0 ? matches.join('\n') : undefined;
|
|
79
|
+
}
|
|
50
80
|
class LicensingClient {
|
|
51
|
-
unityHub = new unity_hub_1.UnityHub();
|
|
52
81
|
logger = logging_1.Logger.instance;
|
|
53
82
|
licenseClientPath;
|
|
83
|
+
secrets = [];
|
|
84
|
+
isLicenseClientPatched = false;
|
|
85
|
+
/** Where the 4.x/5.x license link lives, and the folders the client may have written Unity_lic.ulf to. */
|
|
86
|
+
legacyLicense;
|
|
54
87
|
licenseVersion;
|
|
55
88
|
/**
|
|
56
89
|
* Creates an instance of LicensingClient.
|
|
@@ -59,20 +92,186 @@ class LicensingClient {
|
|
|
59
92
|
constructor(licenseVersion = undefined) {
|
|
60
93
|
this.licenseVersion = licenseVersion;
|
|
61
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Finds Unity.Licensing.Client: UNITY_LICENSING_CLIENT_PATH, then the copy the official Unity CLI manages,
|
|
97
|
+
* then the Hub's external modules folder, then the client bundled with the editor in UNITY_EDITOR_PATH.
|
|
98
|
+
* @returns The licensing client executable.
|
|
99
|
+
*/
|
|
62
100
|
async init() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
101
|
+
const licensingClientExecutable = process.platform === 'win32' ? 'Unity.Licensing.Client.exe' : 'Unity.Licensing.Client';
|
|
102
|
+
let licenseClientPath = process.env.UNITY_LICENSING_CLIENT_PATH;
|
|
103
|
+
const legacyVersion = this.licenseVersion ?? GetLegacyLicenseVersion();
|
|
104
|
+
if (!licenseClientPath && process.platform === 'darwin' && (legacyVersion === '4.x' || legacyVersion === '5.x')) {
|
|
105
|
+
// the macOS client the Unity CLI downloads cannot be patched for 4.x/5.x, so use the one Unity Hub bundled
|
|
106
|
+
licenseClientPath = await LicensingClient.getHubBundledClient();
|
|
66
107
|
}
|
|
67
|
-
|
|
68
|
-
|
|
108
|
+
if (!licenseClientPath) {
|
|
109
|
+
try {
|
|
110
|
+
const cli = await unity_cli_1.UnityCli.Get();
|
|
111
|
+
licenseClientPath = await cli.GetLicensingClientPath();
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
this.logger.debug(`The Unity CLI did not report a licensing client: ${error}`);
|
|
115
|
+
}
|
|
69
116
|
}
|
|
70
|
-
|
|
71
|
-
|
|
117
|
+
if (!licenseClientPath) {
|
|
118
|
+
const candidates = LicensingClient.externalModulesDirectories()
|
|
119
|
+
.map(directory => [directory, '**', licensingClientExecutable]);
|
|
120
|
+
const editorPath = process.env.UNITY_EDITOR_PATH;
|
|
121
|
+
if (editorPath && editorPath.length > 0) {
|
|
122
|
+
candidates.push([path.dirname(editorPath), '**', 'Licensing', 'Client', licensingClientExecutable]);
|
|
123
|
+
}
|
|
124
|
+
licenseClientPath = await (0, utilities_1.ResolvePathCandidates)(candidates);
|
|
125
|
+
}
|
|
126
|
+
if (!licenseClientPath) {
|
|
127
|
+
throw new Error('Failed to find Unity.Licensing.Client. Set UNITY_LICENSING_CLIENT_PATH to its location.');
|
|
128
|
+
}
|
|
129
|
+
await LicensingClient.ensureMacLicenseDirectory();
|
|
72
130
|
this.licenseClientPath = licenseClientPath;
|
|
73
131
|
await fs.promises.access(this.licenseClientPath, fs.constants.X_OK);
|
|
132
|
+
this.logger.debug(`Using Unity Licensing Client at ${this.licenseClientPath}`);
|
|
74
133
|
return this.licenseClientPath;
|
|
75
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Extracts the licensing client bundled with Unity Hub 3.12.0 on macOS. Its resolver is a separate assembly that
|
|
137
|
+
* the 4.x/5.x patch works on, unlike the single-file macOS client the Unity CLI downloads, whose licenses stay 6.x.
|
|
138
|
+
* Only the client is copied out; the Hub is not installed.
|
|
139
|
+
* @returns The extracted client executable.
|
|
140
|
+
*/
|
|
141
|
+
static async getHubBundledClient() {
|
|
142
|
+
const hubVersion = '3.12.0';
|
|
143
|
+
const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
|
|
144
|
+
const cacheDirectory = await LicensingClient.privateDirectory(`hub-client-${hubVersion}-${arch}`);
|
|
145
|
+
const findClient = async () => (await fs.promises.readdir(cacheDirectory, { recursive: true }))
|
|
146
|
+
.map(entry => path.join(cacheDirectory, entry.toString()))
|
|
147
|
+
.find(entry => path.basename(entry) === 'Unity.Licensing.Client' && fs.statSync(entry).isFile());
|
|
148
|
+
if (fs.existsSync(cacheDirectory)) {
|
|
149
|
+
const cached = await findClient();
|
|
150
|
+
if (cached) {
|
|
151
|
+
return cached;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const dmgPath = path.join((0, utilities_1.GetTempDir)(), `UnityHubSetup-${hubVersion}-${arch}.dmg`);
|
|
155
|
+
const mountPoint = path.join((0, utilities_1.GetTempDir)(), `UnityHub-${hubVersion}-${arch}`);
|
|
156
|
+
// pinned so a tampered download never reaches the client that receives the Unity ID
|
|
157
|
+
const dmgSha256 = {
|
|
158
|
+
'x64': '1b5e9698b6353cb814618ea12c6ca014d50fc33efe086da6b67e5b442a77bb3e',
|
|
159
|
+
'arm64': '0026d98a375d926090b144ca3d54e982612aa24a59404e004e333dc34c4b832a',
|
|
160
|
+
};
|
|
161
|
+
await (0, utilities_1.DownloadFile)(`https://public-cdn.cloud.unity3d.com/hub/${hubVersion}/UnityHubSetup-${arch}.dmg`, dmgPath, { sha256: dmgSha256[arch] });
|
|
162
|
+
await fs.promises.mkdir(mountPoint, { recursive: true });
|
|
163
|
+
try {
|
|
164
|
+
await (0, utilities_1.Exec)('hdiutil', ['attach', dmgPath, '-nobrowse', '-readonly', '-mountpoint', mountPoint], { silent: true, showCommand: true });
|
|
165
|
+
try {
|
|
166
|
+
const client = await (0, utilities_1.ResolveGlobToPath)([mountPoint, '**', 'Unity.Licensing.Client']);
|
|
167
|
+
const sourceDirectory = LicensingClient.clientRootDirectory(client);
|
|
168
|
+
await fs.promises.rm(cacheDirectory, { recursive: true, force: true });
|
|
169
|
+
await fs.promises.cp(sourceDirectory, cacheDirectory, { recursive: true, verbatimSymlinks: true });
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
await (0, utilities_1.Exec)('hdiutil', ['detach', mountPoint, '-quiet'], { silent: true, showCommand: true });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
await fs.promises.rm(dmgPath, { force: true });
|
|
177
|
+
}
|
|
178
|
+
const extracted = await findClient();
|
|
179
|
+
if (!extracted) {
|
|
180
|
+
throw new Error(`Failed to extract Unity.Licensing.Client from Unity Hub ${hubVersion}`);
|
|
181
|
+
}
|
|
182
|
+
logging_1.Logger.instance.info(`Using the Unity Licensing Client bundled with Unity Hub ${hubVersion}: ${extracted}`);
|
|
183
|
+
return extracted;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* On macOS the client writes Unity_lic.ulf to /Library/Application Support/Unity. When that folder is missing or not
|
|
187
|
+
* writable it asks for an administrator password in a dialog, which never returns on a headless runner.
|
|
188
|
+
* The Unity Hub installer used to create it; without the Hub, it is created here with non-interactive sudo.
|
|
189
|
+
*/
|
|
190
|
+
static async ensureMacLicenseDirectory() {
|
|
191
|
+
if (process.platform !== 'darwin') {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const directory = '/Library/Application Support/Unity';
|
|
195
|
+
try {
|
|
196
|
+
await fs.promises.access(directory, fs.constants.W_OK);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// missing or not writable
|
|
201
|
+
}
|
|
202
|
+
// owned by the current user rather than world-writable
|
|
203
|
+
const user = os.userInfo().username;
|
|
204
|
+
for (const args of [['-n', 'mkdir', '-p', directory], ['-n', 'chown', user, directory], ['-n', 'chmod', '755', directory]]) {
|
|
205
|
+
const exitCode = await new Promise((resolve) => {
|
|
206
|
+
const child = (0, child_process_1.spawn)('sudo', args, { stdio: 'ignore' });
|
|
207
|
+
child.on('error', () => resolve(1));
|
|
208
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
209
|
+
});
|
|
210
|
+
if (exitCode !== 0) {
|
|
211
|
+
logging_1.Logger.instance.warn(`Could not prepare ${directory} (sudo ${args.slice(1).join(' ')} exited with code ${exitCode}). Activation may wait for an administrator password.`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Gets a folder under ~/.unity-cli/licensing that only the current user can read and write, creating it when needed.
|
|
218
|
+
* Used instead of the shared temp folder for the patched licensing client and the license it writes.
|
|
219
|
+
* @param name The folder name.
|
|
220
|
+
*/
|
|
221
|
+
static async privateDirectory(name) {
|
|
222
|
+
const root = path.join(os.homedir(), '.unity-cli', 'licensing');
|
|
223
|
+
await fs.promises.mkdir(root, { recursive: true, mode: 0o700 });
|
|
224
|
+
if (process.platform !== 'win32') {
|
|
225
|
+
await fs.promises.chmod(root, 0o700);
|
|
226
|
+
}
|
|
227
|
+
return path.join(root, name);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The folder that holds the whole licensing client: the bundle root for a macOS bundle (…/Contents/MacOS/client), otherwise the executable's folder.
|
|
231
|
+
*/
|
|
232
|
+
static clientRootDirectory(clientPath) {
|
|
233
|
+
const macOSDirectory = path.dirname(clientPath);
|
|
234
|
+
if (path.basename(macOSDirectory) === 'MacOS' && path.basename(path.dirname(macOSDirectory)) === 'Contents') {
|
|
235
|
+
return path.dirname(path.dirname(macOSDirectory));
|
|
236
|
+
}
|
|
237
|
+
return macOSDirectory;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Lists the files under a directory that can hold compiled code: executables, libraries and extensionless binaries.
|
|
241
|
+
*/
|
|
242
|
+
static async listBinaries(directory) {
|
|
243
|
+
const results = [];
|
|
244
|
+
for (const entry of await fs.promises.readdir(directory, { withFileTypes: true, recursive: true })) {
|
|
245
|
+
if (!entry.isFile()) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const extension = path.extname(entry.name).toLowerCase();
|
|
249
|
+
// the macOS client is named Unity.Licensing.Client, so its extension reads as ".client"
|
|
250
|
+
const isClient = entry.name.startsWith('Unity.Licensing.') && !['.md', '.config', '.json', '.plist', '.xml', '.txt'].includes(extension);
|
|
251
|
+
if (isClient || ['', '.exe', '.dll', '.dylib', '.so'].includes(extension)) {
|
|
252
|
+
results.push(path.join(entry.parentPath, entry.name));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return results;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The folders the Unity Hub and the official Unity CLI download Unity.Licensing.Client to.
|
|
259
|
+
*/
|
|
260
|
+
static externalModulesDirectories() {
|
|
261
|
+
switch (process.platform) {
|
|
262
|
+
case 'win32':
|
|
263
|
+
return [path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'UnityHub', 'external-modules')];
|
|
264
|
+
case 'darwin':
|
|
265
|
+
return [path.join(os.homedir(), 'Library', 'Application Support', 'UnityHub', 'external-modules')];
|
|
266
|
+
default: {
|
|
267
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
268
|
+
return [
|
|
269
|
+
path.join(configHome, 'UnityHub', 'external-modules'),
|
|
270
|
+
path.join(configHome, 'unityhub', 'external-modules'),
|
|
271
|
+
];
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
76
275
|
getUnityCommonDir() {
|
|
77
276
|
const result = process.env['UNITY_COMMON_DIR'];
|
|
78
277
|
if (result) {
|
|
@@ -155,6 +354,11 @@ class LicensingClient {
|
|
|
155
354
|
}
|
|
156
355
|
throw new Error('Services config value is not a valid JSON string or base64 encoded JSON string.');
|
|
157
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Writes the floating license server's services-config.json where Unity's licensing reads it.
|
|
359
|
+
* @param configSource A path to the configuration file, raw JSON, or base64 encoded JSON.
|
|
360
|
+
* @returns The path the configuration was written to.
|
|
361
|
+
*/
|
|
158
362
|
async setupServicesConfig(configSource) {
|
|
159
363
|
const servicesConfigPath = this.servicesConfigPath();
|
|
160
364
|
if (fs.existsSync(configSource)) {
|
|
@@ -281,27 +485,8 @@ class LicensingClient {
|
|
|
281
485
|
}
|
|
282
486
|
async patchLicenseVersion() {
|
|
283
487
|
if (!this.licenseVersion) {
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
if (versionMatch) {
|
|
287
|
-
switch (versionMatch[1]) {
|
|
288
|
-
case '4': {
|
|
289
|
-
this.licenseVersion = '4.x';
|
|
290
|
-
break;
|
|
291
|
-
}
|
|
292
|
-
case '5': {
|
|
293
|
-
this.licenseVersion = '5.x';
|
|
294
|
-
break;
|
|
295
|
-
}
|
|
296
|
-
default: {
|
|
297
|
-
this.licenseVersion = '6.x'; // default to 6.x for any other
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
this.licenseVersion = '6.x'; // default to 6.x if not set
|
|
304
|
-
}
|
|
488
|
+
// use the editor in UNITY_EDITOR_PATH to determine the license version, defaulting to 6.x
|
|
489
|
+
this.licenseVersion = GetLegacyLicenseVersion() ?? '6.x';
|
|
305
490
|
}
|
|
306
491
|
if (this.licenseVersion !== '6.x') {
|
|
307
492
|
if (this.licenseVersion !== '5.x' && this.licenseVersion !== '4.x') {
|
|
@@ -312,32 +497,78 @@ class LicensingClient {
|
|
|
312
497
|
if (!this.licenseClientPath) {
|
|
313
498
|
this.licenseClientPath = await this.init();
|
|
314
499
|
}
|
|
315
|
-
if (this.licenseVersion === '6.x') {
|
|
316
|
-
return; // no patching needed
|
|
500
|
+
if (this.licenseVersion === '6.x' || this.isLicenseClientPatched) {
|
|
501
|
+
return; // no patching needed, or already done for this instance
|
|
317
502
|
}
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
503
|
+
const clientRoot = LicensingClient.clientRootDirectory(this.licenseClientPath);
|
|
504
|
+
// a private per-user folder rather than the shared temp folder: it holds a client that receives the Unity ID and the license it writes
|
|
505
|
+
const patchedDirectory = await LicensingClient.privateDirectory(`patched-client-${this.licenseVersion.replace('.', '_')}`);
|
|
506
|
+
const patchedClientPath = path.join(patchedDirectory, path.relative(clientRoot, this.licenseClientPath));
|
|
507
|
+
const sourceMarker = path.join(patchedDirectory, '.source-sha256');
|
|
508
|
+
const sourceHash = await (0, utilities_1.Sha256FileHex)(this.licenseClientPath);
|
|
509
|
+
const isCurrent = (await fs.promises.readFile(sourceMarker, 'utf-8').catch(() => '')).trim() === sourceHash;
|
|
510
|
+
if (isCurrent) {
|
|
511
|
+
this.logger.debug('Unity Licensing Client was already patched from the same client, reusing');
|
|
322
512
|
}
|
|
323
513
|
else {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
514
|
+
await fs.promises.rm(patchedDirectory, { recursive: true, force: true });
|
|
515
|
+
await fs.promises.mkdir(patchedDirectory, { recursive: true, mode: 0o700 });
|
|
516
|
+
// copy the whole client, then patch whichever binary holds the "6.x" license version:
|
|
517
|
+
// older clients ship it in Unity.Licensing.EntitlementResolver.dll, the single-file client the Unity CLI downloads embeds it in the executable
|
|
518
|
+
await fs.promises.cp(clientRoot, patchedDirectory, { recursive: true, verbatimSymlinks: true });
|
|
519
|
+
const patchedFiles = [];
|
|
520
|
+
// Windows and Linux builds hold the value as a UTF-16 string; the macOS build only as the
|
|
521
|
+
// UTF-8 constant k_UnityLicenseVersionCompatibility, so that one is matched with its name
|
|
522
|
+
const constantName = Buffer.concat([Buffer.from('k_UnityLicenseVersionCompatibility'), Buffer.from([0x06])]);
|
|
523
|
+
const patterns = [
|
|
524
|
+
[Buffer.from('6.x', 'utf16le'), Buffer.from(this.licenseVersion, 'utf16le')],
|
|
525
|
+
[Buffer.concat([constantName, Buffer.from('6.x')]), Buffer.concat([constantName, Buffer.from(this.licenseVersion)])],
|
|
526
|
+
];
|
|
527
|
+
const binaries = await LicensingClient.listBinaries(patchedDirectory);
|
|
528
|
+
// the UTF-8 constant is only patched when no UTF-16 value exists, which leaves the Windows and Linux patch as it was
|
|
529
|
+
for (const [searchValue, replaceValue] of patterns) {
|
|
530
|
+
for (const file of binaries) {
|
|
531
|
+
try {
|
|
532
|
+
await this.patchBinary(file, file, searchValue, replaceValue);
|
|
533
|
+
patchedFiles.push(file);
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
if (!(error instanceof Error && error.message === 'Could not find the search value')) {
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
329
540
|
}
|
|
330
|
-
|
|
331
|
-
|
|
541
|
+
if (patchedFiles.length > 0) {
|
|
542
|
+
break;
|
|
332
543
|
}
|
|
333
544
|
}
|
|
334
|
-
if (
|
|
335
|
-
|
|
545
|
+
if (patchedFiles.length === 0) {
|
|
546
|
+
const searched = (await LicensingClient.listBinaries(patchedDirectory)).map(file => path.relative(patchedDirectory, file));
|
|
547
|
+
await fs.promises.rm(patchedDirectory, { recursive: true, force: true });
|
|
548
|
+
throw new Error(`Could not find the 6.x license version in any Unity Licensing Client binary under ${clientRoot}. Searched:\n${searched.map(file => ` > ${file}`).join('\n')}`);
|
|
549
|
+
}
|
|
550
|
+
this.logger.debug(`Patched the license version in:\n${patchedFiles.map(file => ` > ${file}`).join('\n')}`);
|
|
551
|
+
await fs.promises.chmod(patchedClientPath, 0o755);
|
|
552
|
+
if (process.platform === 'darwin') {
|
|
553
|
+
// patching invalidates the signature, and macOS kills binaries with a broken one
|
|
554
|
+
const isBundle = fs.existsSync(path.join(patchedDirectory, 'Contents', 'MacOS'));
|
|
555
|
+
const targets = isBundle ? [patchedDirectory] : patchedFiles;
|
|
556
|
+
for (const target of targets) {
|
|
557
|
+
await new Promise((resolve, reject) => {
|
|
558
|
+
const child = (0, child_process_1.spawn)('codesign', ['--force', '--deep', '--sign', '-', target], { stdio: 'ignore' });
|
|
559
|
+
child.on('error', reject);
|
|
560
|
+
child.on('close', (code) => code === 0 ? resolve() : reject(new Error(`codesign failed with exit code ${code}`)));
|
|
561
|
+
});
|
|
562
|
+
}
|
|
336
563
|
}
|
|
564
|
+
// records which client the copy came from, so a newer client is patched again instead of reused
|
|
565
|
+
await fs.promises.writeFile(sourceMarker, sourceHash, { encoding: 'utf-8', mode: 0o600 });
|
|
337
566
|
}
|
|
338
|
-
this.licenseClientPath =
|
|
567
|
+
this.licenseClientPath = patchedClientPath;
|
|
568
|
+
this.isLicenseClientPatched = true;
|
|
339
569
|
const unityCommonDir = this.getUnityCommonDir();
|
|
340
570
|
const legacyLicenseFile = path.join(unityCommonDir, `Unity_v${this.licenseVersion}.ulf`);
|
|
571
|
+
this.legacyLicense ??= { linkPath: legacyLicenseFile, commonDirectory: unityCommonDir, patchedDirectory };
|
|
341
572
|
await fs.promises.mkdir(unityCommonDir, { recursive: true });
|
|
342
573
|
try {
|
|
343
574
|
await fs.promises.symlink(path.join(patchedDirectory, 'Unity_lic.ulf'), legacyLicenseFile);
|
|
@@ -360,24 +591,42 @@ class LicensingClient {
|
|
|
360
591
|
}
|
|
361
592
|
let output = '';
|
|
362
593
|
let exitCode = 0;
|
|
363
|
-
|
|
594
|
+
let timedOut = false;
|
|
595
|
+
let killedBy = null;
|
|
596
|
+
// values that must never reach a log, even outside CI where ::add-mask:: does nothing
|
|
597
|
+
const sensitiveFlags = ['--username', '--password', '--serial', '--return-floating'];
|
|
598
|
+
const argSecrets = args.filter((_, i) => i > 0 && sensitiveFlags.includes(args[i - 1]));
|
|
599
|
+
const secrets = [...this.secrets, ...argSecrets];
|
|
600
|
+
const redact = (text) => (0, utilities_1.redactSensitiveLiterals)(text, secrets);
|
|
601
|
+
this.logger.startGroup(`\x1b[34m${this.licenseClientPath} ${redact(args.join(' '))}\x1b[0m`);
|
|
602
|
+
const logOffset = await this.getClientLogSize();
|
|
364
603
|
await fs.promises.access(this.licenseClientPath, fs.constants.R_OK | fs.constants.X_OK);
|
|
365
604
|
try {
|
|
366
605
|
exitCode = await new Promise((resolve, reject) => {
|
|
367
606
|
fs.accessSync(this.licenseClientPath, fs.constants.R_OK | fs.constants.X_OK);
|
|
607
|
+
const startedAt = Date.now();
|
|
368
608
|
const child = (0, child_process_1.spawn)(this.licenseClientPath, args, {
|
|
369
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
609
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
610
|
+
...(0, process_tracker_1.OwnedSpawnOptions)(),
|
|
370
611
|
});
|
|
612
|
+
// the background instance the client can start stays reachable through this record
|
|
613
|
+
(0, process_tracker_1.TrackOwnedProcess)(child, startedAt);
|
|
371
614
|
const sigintHandler = () => child.kill('SIGINT');
|
|
372
615
|
const sigtermHandler = () => child.kill('SIGTERM');
|
|
373
616
|
process.once('SIGINT', sigintHandler);
|
|
374
617
|
process.once('SIGTERM', sigtermHandler);
|
|
618
|
+
// a client that waits on something that never comes must not hold a CI job until it times out
|
|
619
|
+
const timer = setTimeout(() => {
|
|
620
|
+
timedOut = true;
|
|
621
|
+
child.kill('SIGKILL');
|
|
622
|
+
}, LICENSING_CLIENT_TIMEOUT_MS);
|
|
375
623
|
let hasCleanedUpListeners = false;
|
|
376
624
|
function removeListeners() {
|
|
377
625
|
if (hasCleanedUpListeners) {
|
|
378
626
|
return;
|
|
379
627
|
}
|
|
380
628
|
hasCleanedUpListeners = true;
|
|
629
|
+
clearTimeout(timer);
|
|
381
630
|
process.removeListener('SIGINT', sigintHandler);
|
|
382
631
|
process.removeListener('SIGTERM', sigtermHandler);
|
|
383
632
|
}
|
|
@@ -391,17 +640,38 @@ class LicensingClient {
|
|
|
391
640
|
removeListeners();
|
|
392
641
|
reject(error);
|
|
393
642
|
});
|
|
394
|
-
|
|
643
|
+
// the client can start a background instance that inherits its stdout and stderr,
|
|
644
|
+
// so 'close' may never fire; settle on 'exit' and stop reading the pipes shortly after
|
|
645
|
+
let settled = false;
|
|
646
|
+
const settle = (code, signal) => {
|
|
647
|
+
if (settled) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
settled = true;
|
|
395
651
|
removeListeners();
|
|
396
|
-
|
|
652
|
+
killedBy = signal;
|
|
653
|
+
// a client killed by a signal did not finish its work, so it is a failure, not exit code 0
|
|
654
|
+
resolve(timedOut ? -1 : code === null ? (signal ? -2 : 0) : code);
|
|
655
|
+
};
|
|
656
|
+
child.on('close', settle);
|
|
657
|
+
child.on('exit', (code, signal) => {
|
|
658
|
+
setTimeout(() => {
|
|
659
|
+
child.stdout.destroy();
|
|
660
|
+
child.stderr.destroy();
|
|
661
|
+
settle(code, signal);
|
|
662
|
+
}, 1000);
|
|
397
663
|
});
|
|
398
664
|
});
|
|
399
665
|
}
|
|
400
666
|
finally {
|
|
667
|
+
if (timedOut) {
|
|
668
|
+
this.logger.error(`Unity Licensing Client did not finish within ${LICENSING_CLIENT_TIMEOUT_MS / 60000} minutes. Its log: ${LicensingClient.ClientLogPath()}`);
|
|
669
|
+
await this.printClientLogProblems(logOffset, secrets);
|
|
670
|
+
}
|
|
671
|
+
// any client output can carry emails and tokens, so it gets the same sanitizing as the client log
|
|
672
|
+
const maskedLines = this.maskSerialInOutput(output).split(/\r?\n/).map(line => LicensingClient.sanitizeLogLine(line, secrets));
|
|
401
673
|
if (!silent || exitCode !== 0) {
|
|
402
|
-
const
|
|
403
|
-
const splitLines = maskedOutput.split(/\r?\n/);
|
|
404
|
-
for (const line of splitLines) {
|
|
674
|
+
for (const line of maskedLines) {
|
|
405
675
|
if (line === undefined || line.length === 0) {
|
|
406
676
|
continue;
|
|
407
677
|
}
|
|
@@ -410,12 +680,61 @@ class LicensingClient {
|
|
|
410
680
|
}
|
|
411
681
|
this.logger.endGroup();
|
|
412
682
|
if (exitCode !== 0) {
|
|
413
|
-
|
|
683
|
+
let message = timedOut ? 'Timed out' : killedBy ? `Killed by ${killedBy}` : this.getExitCodeMessage(exitCode);
|
|
684
|
+
// Unity ID throttles sign-ins when many jobs activate with one account at once; naming it lets the
|
|
685
|
+
// failure be marked for a rerun once those jobs finish
|
|
686
|
+
if (/maximum same user attempt retry count/i.test(output)) {
|
|
687
|
+
message = 'Unity ID sign-in rate limit reached: too many sign-in attempts for this account';
|
|
688
|
+
}
|
|
689
|
+
// the client's own reason, such as a seat limit, lets the failure be classified; exit 17 alone does not say
|
|
690
|
+
const diagnostic = LicensingClientDiagnostic(maskedLines);
|
|
691
|
+
if (diagnostic) {
|
|
692
|
+
message += `\n${diagnostic}`;
|
|
693
|
+
}
|
|
414
694
|
throw new Error(`License command failed with exit code ${exitCode}: ${message}`);
|
|
415
695
|
}
|
|
416
696
|
}
|
|
417
697
|
return output;
|
|
418
698
|
}
|
|
699
|
+
/**
|
|
700
|
+
* Redacts the known secrets, email addresses and long token-like strings (JWTs, access and refresh tokens) from a client log line.
|
|
701
|
+
* The line is split on separator characters and each piece is checked on its own, which stays linear on any input.
|
|
702
|
+
*/
|
|
703
|
+
static sanitizeLogLine(line, secrets) {
|
|
704
|
+
return (0, utilities_1.SanitizeLogText)(line, secrets);
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Prints the last lines the client logged since `offset`, to show why a command hung.
|
|
708
|
+
* The log can hold account details and tokens, so every line is sanitized first.
|
|
709
|
+
*/
|
|
710
|
+
async printClientLogProblems(offset, secrets, maxLines = 40) {
|
|
711
|
+
try {
|
|
712
|
+
const content = await fs.promises.readFile(LicensingClient.ClientLogPath());
|
|
713
|
+
const lines = content.subarray(offset).toString('utf-8').split(/\r?\n/)
|
|
714
|
+
.filter(line => line.trim().length > 0)
|
|
715
|
+
.slice(-maxLines)
|
|
716
|
+
.map(line => LicensingClient.sanitizeLogLine(line, secrets));
|
|
717
|
+
if (lines.length > 0) {
|
|
718
|
+
this.logger.info(`Last lines the Unity Licensing Client logged (redacted):\n${lines.join('\n')}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
this.logger.debug(`Could not read the Unity Licensing Client log: ${error}`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Stops Unity.Licensing.Client instances that this job's unity-cli commands started, directly or through the
|
|
727
|
+
* official Unity CLI or an editor run. A command line activation can wait on such an instance indefinitely.
|
|
728
|
+
* Clients other jobs started are left alone, even on a shared self-hosted runner. Only done in CI.
|
|
729
|
+
*/
|
|
730
|
+
static async StopBackgroundClients() {
|
|
731
|
+
if (process.env.CI !== 'true') {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
// matched on the executable's own name, so a process that only mentions it on its command line is left alone
|
|
735
|
+
const clients = (await (0, process_tracker_1.FindOwnedProcesses)()).filter(proc => (0, process_tracker_1.IsExecutableNamed)(proc, 'Unity.Licensing.Client'));
|
|
736
|
+
await Promise.all(clients.map(client => (0, utilities_1.KillProcess)(client).catch(() => undefined)));
|
|
737
|
+
}
|
|
419
738
|
maskSerialInOutput(output) {
|
|
420
739
|
return output.replace(/([\w-]+-XXXX)/g, (_, serial) => {
|
|
421
740
|
const maskedSerial = serial.slice(0, -4) + `XXXX`;
|
|
@@ -529,11 +848,12 @@ class LicensingClient {
|
|
|
529
848
|
const output = await this.exec([`--acquire-floating`], true);
|
|
530
849
|
const tokenMatch = output.match(/with token:\s*"(?<token>[\w-]+)"/);
|
|
531
850
|
if (!tokenMatch || !tokenMatch.groups || !tokenMatch.groups['token']) {
|
|
532
|
-
throw new Error(`Failed to acquire floating license lease: No token found in output.\n ${output}`);
|
|
851
|
+
throw new Error(`Failed to acquire floating license lease: No token found in output.\n ${(0, utilities_1.SanitizeLogText)(output)}`);
|
|
533
852
|
}
|
|
534
853
|
const token = tokenMatch.groups['token'];
|
|
535
854
|
this.logger.CI_mask(token);
|
|
536
|
-
|
|
855
|
+
// the token is returned to the caller (and printed with --json); the log never shows it
|
|
856
|
+
this.logger.info((0, utilities_1.redactSensitiveLiterals)(output, [token]));
|
|
537
857
|
return token;
|
|
538
858
|
}
|
|
539
859
|
default: { // personal and professional license activation
|
|
@@ -604,6 +924,20 @@ class LicensingClient {
|
|
|
604
924
|
}
|
|
605
925
|
return licenses;
|
|
606
926
|
}
|
|
927
|
+
/**
|
|
928
|
+
* The machine binding values the licensing client reports for this machine (Legacy.MachineBinding1, 2, ...).
|
|
929
|
+
* @returns The values, or an empty list when the client cannot report them.
|
|
930
|
+
*/
|
|
931
|
+
async GetMachineBindings() {
|
|
932
|
+
try {
|
|
933
|
+
const context = await this.exec(['--showContext'], true);
|
|
934
|
+
return [...context.matchAll(/Legacy\.MachineBinding\d+[ \t]+(\S+)/g)].map(match => match[1]);
|
|
935
|
+
}
|
|
936
|
+
catch (error) {
|
|
937
|
+
this.logger.debug(`Failed to read the machine bindings: ${error}`);
|
|
938
|
+
return [];
|
|
939
|
+
}
|
|
940
|
+
}
|
|
607
941
|
async activateLicense(licenseType, username, password, serial = undefined) {
|
|
608
942
|
const args = [
|
|
609
943
|
`--activate-ulf`,
|
|
@@ -619,13 +953,37 @@ class LicensingClient {
|
|
|
619
953
|
if (licenseType === LicenseType.personal) {
|
|
620
954
|
args.push(`--include-personal`);
|
|
621
955
|
}
|
|
956
|
+
// keep redacting the account details in every later command's output, such as --showEntitlements
|
|
957
|
+
this.secrets.push(username, password, ...(serial ? [serial, serial.slice(0, -4) + 'XXXX'] : []));
|
|
622
958
|
await this.exec(args);
|
|
623
959
|
const activeLicenses = await this.GetActiveEntitlements();
|
|
624
960
|
if (!activeLicenses.includes(licenseType)) {
|
|
625
961
|
throw new Error(`Failed to activate license of type '${licenseType}'`);
|
|
626
962
|
}
|
|
963
|
+
await this.linkLegacyLicenseFile();
|
|
627
964
|
this.logger.info(`Successfully activated license of type '${licenseType}'`);
|
|
628
965
|
}
|
|
966
|
+
/**
|
|
967
|
+
* Points Unity_v4.x.ulf or Unity_v5.x.ulf at the license the client actually wrote. Clients that ignore
|
|
968
|
+
* UNITY_COMMON_DIR, such as the macOS one, write Unity_lic.ulf to the regular folder instead of the patched one.
|
|
969
|
+
*/
|
|
970
|
+
async linkLegacyLicenseFile() {
|
|
971
|
+
if (!this.legacyLicense) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const { linkPath, commonDirectory, patchedDirectory } = this.legacyLicense;
|
|
975
|
+
const candidates = [path.join(patchedDirectory, 'Unity_lic.ulf'), path.join(commonDirectory, 'Unity_lic.ulf')];
|
|
976
|
+
const licensePath = candidates.find(candidate => fs.existsSync(candidate));
|
|
977
|
+
if (!licensePath) {
|
|
978
|
+
this.logger.warn(`No Unity_lic.ulf was written to ${candidates.join(' or ')}`);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
await fs.promises.rm(linkPath, { force: true });
|
|
982
|
+
await fs.promises.symlink(licensePath, linkPath);
|
|
983
|
+
// the license version decides whether a 4.x or 5.x editor accepts the file
|
|
984
|
+
const licenseVersion = (await fs.promises.readFile(licensePath, 'utf-8')).match(/<LicenseVersion Value="([^"]+)"/)?.[1];
|
|
985
|
+
this.logger.info(`Linked ${linkPath} to ${licensePath} (license version ${licenseVersion ?? 'unknown'})`);
|
|
986
|
+
}
|
|
629
987
|
async returnLicense(licenseType, token) {
|
|
630
988
|
if (licenseType === LicenseType.floating) {
|
|
631
989
|
if (!token || token.length === 0) {
|
|
@@ -651,4 +1009,327 @@ class LicensingClient {
|
|
|
651
1009
|
}
|
|
652
1010
|
}
|
|
653
1011
|
exports.LicensingClient = LicensingClient;
|
|
1012
|
+
/**
|
|
1013
|
+
* Gets the license version a 4.x or 5.x editor in UNITY_EDITOR_PATH reads, or undefined for newer editors.
|
|
1014
|
+
* @param editorPath The editor executable. Defaults to UNITY_EDITOR_PATH.
|
|
1015
|
+
*/
|
|
1016
|
+
function GetLegacyLicenseVersion(editorPath = process.env.UNITY_EDITOR_PATH) {
|
|
1017
|
+
// anchored per path segment, so the match stays linear on any input
|
|
1018
|
+
for (const segment of (editorPath ?? '').split(/[\\/ ]/)) {
|
|
1019
|
+
const major = /^(\d{1,4})\.\d{1,4}\.\d/.exec(segment)?.[1];
|
|
1020
|
+
if (major === undefined) {
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
switch (major) {
|
|
1024
|
+
case '4':
|
|
1025
|
+
return '4.x';
|
|
1026
|
+
case '5':
|
|
1027
|
+
return '5.x';
|
|
1028
|
+
default:
|
|
1029
|
+
return undefined;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return undefined;
|
|
1033
|
+
}
|
|
1034
|
+
/** How long one official CLI license command may run before it falls back. */
|
|
1035
|
+
const UNITY_CLI_LICENSE_TIMEOUT_MS = 3 * 60 * 1000;
|
|
1036
|
+
/**
|
|
1037
|
+
* Activates a Unity license through the official Unity CLI first.
|
|
1038
|
+
* Falls back to the Unity Licensing Client with the Unity ID email and password when the CLI fails,
|
|
1039
|
+
* for example a personal license on a headless runner with no browser sign-in.
|
|
1040
|
+
* 4.x and 5.x editors read their own license file, so they always use the Unity Licensing Client.
|
|
1041
|
+
* @param options The activation options.
|
|
1042
|
+
* @returns The floating license token, when one is known.
|
|
1043
|
+
*/
|
|
1044
|
+
async function ActivateLicense(options) {
|
|
1045
|
+
const logger = logging_1.Logger.instance;
|
|
1046
|
+
const legacyLicenseVersion = GetLegacyLicenseVersion();
|
|
1047
|
+
if (legacyLicenseVersion) {
|
|
1048
|
+
logger.info(`Unity ${legacyLicenseVersion} editors read their own license file, activating with the Unity Licensing Client...`);
|
|
1049
|
+
await LicensingClient.StopBackgroundClients();
|
|
1050
|
+
return await new LicensingClient(legacyLicenseVersion).Activate(options);
|
|
1051
|
+
}
|
|
1052
|
+
const licenseFile = options.licenseFile ?? process.env.UNITY_LICENSE;
|
|
1053
|
+
if (licenseFile && licenseFile.trim().length > 0 &&
|
|
1054
|
+
(options.licenseType === LicenseType.personal || options.licenseType === LicenseType.professional)) {
|
|
1055
|
+
try {
|
|
1056
|
+
if (await activateFromLicenseFile(licenseFile)) {
|
|
1057
|
+
// lets a CI step tell whether it needs to store a fresh license
|
|
1058
|
+
logger.CI_setOutput('license-source', 'license-file');
|
|
1059
|
+
return undefined;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
catch (error) {
|
|
1063
|
+
logger.warn(`The license file could not be used, activating with the account instead.\n${error}`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
logger.CI_setOutput('license-source', 'account');
|
|
1067
|
+
let cliError;
|
|
1068
|
+
try {
|
|
1069
|
+
const result = await activateWithUnityCli(options);
|
|
1070
|
+
if (result.activated) {
|
|
1071
|
+
await logLicenseBinding();
|
|
1072
|
+
return result.token;
|
|
1073
|
+
}
|
|
1074
|
+
cliError = result.reason;
|
|
1075
|
+
}
|
|
1076
|
+
catch (error) {
|
|
1077
|
+
cliError = error;
|
|
1078
|
+
}
|
|
1079
|
+
logger.warn(`Unity CLI license activation failed, falling back to the Unity Licensing Client.\n${cliError}`);
|
|
1080
|
+
if (options.licenseType !== LicenseType.floating &&
|
|
1081
|
+
!options.username && !process.env.UNITY_USERNAME_BASE64) {
|
|
1082
|
+
throw new Error(`Unity CLI license activation failed, and no Unity ID email and password were given for the fallback.\n${cliError}`);
|
|
1083
|
+
}
|
|
1084
|
+
await LicensingClient.StopBackgroundClients();
|
|
1085
|
+
const token = await new LicensingClient().Activate(options);
|
|
1086
|
+
await logLicenseBinding();
|
|
1087
|
+
return token;
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Logs whether the license file now on disk names this machine's binding, which is what a cached copy is checked
|
|
1091
|
+
* against on the next run. Only a yes or no is logged, never the binding or the file.
|
|
1092
|
+
*/
|
|
1093
|
+
async function logLicenseBinding() {
|
|
1094
|
+
try {
|
|
1095
|
+
const content = await fs.promises.readFile(GetLicenseFilePath(), 'utf-8');
|
|
1096
|
+
const bindings = await new LicensingClient().GetMachineBindings();
|
|
1097
|
+
if (bindings.length > 0) {
|
|
1098
|
+
logging_1.Logger.instance.info(`The license file is bound to this machine's identity: ${bindings.some(binding => content.includes(binding)) ? 'yes' : 'no'}`);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
catch (error) {
|
|
1102
|
+
logging_1.Logger.instance.info(`Could not compare the license file with this machine's identity: ${error instanceof Error ? error.message : error}`);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Whether this is a GitHub-hosted macOS or Linux runner. Those report one machine identity to Unity across every job
|
|
1107
|
+
* and repo, so returning a license there revokes it for all of them. Windows and self-hosted runners have their own.
|
|
1108
|
+
*/
|
|
1109
|
+
function IsSharedRunnerIdentity() {
|
|
1110
|
+
return process.env.RUNNER_ENVIRONMENT === 'github-hosted' && (process.platform === 'darwin' || process.platform === 'linux');
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Returns a Unity license through the official Unity CLI first, then the Unity Licensing Client.
|
|
1114
|
+
* @param licenseType The type of license to return.
|
|
1115
|
+
* @param token The floating license token from activation. A floating license with a token is returned by the Unity Licensing Client.
|
|
1116
|
+
* @param when `auto` (the default) skips returning a personal or professional license on a shared runner identity.
|
|
1117
|
+
*/
|
|
1118
|
+
async function ReturnLicense(licenseType, token = undefined, when = 'auto') {
|
|
1119
|
+
try {
|
|
1120
|
+
if (when === 'never') {
|
|
1121
|
+
logging_1.Logger.instance.info('Skipping the license return (--when never).');
|
|
1122
|
+
}
|
|
1123
|
+
else if (when === 'auto' && licenseType !== LicenseType.floating && IsSharedRunnerIdentity()) {
|
|
1124
|
+
logging_1.Logger.instance.info('Skipping the license return: GitHub-hosted macOS and Linux runners share one machine identity, so a return would revoke the license for every job and repo on it. Use --when always to return anyway.');
|
|
1125
|
+
}
|
|
1126
|
+
else {
|
|
1127
|
+
await returnLicense(licenseType, token);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
finally {
|
|
1131
|
+
// returning is the last licensing step of a job, so stop what the licensing calls left running in the background
|
|
1132
|
+
await LicensingClient.StopBackgroundClients();
|
|
1133
|
+
await stopUnityCliHelpers();
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Stops background processes the official Unity CLI left running from its own folder, when this job's unity-cli
|
|
1138
|
+
* commands started them. Only done in CI.
|
|
1139
|
+
*/
|
|
1140
|
+
async function stopUnityCliHelpers() {
|
|
1141
|
+
const cliPath = process.env.UNITY_CLI_PATH;
|
|
1142
|
+
if (process.env.CI !== 'true' || !cliPath) {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
const cliDirectory = path.dirname(cliPath);
|
|
1146
|
+
const helpers = (await (0, process_tracker_1.FindOwnedProcesses)())
|
|
1147
|
+
.filter(proc => (0, process_tracker_1.IsPathUnder)(proc.executablePath ?? proc.commandLine.split(' ')[0] ?? '', cliDirectory));
|
|
1148
|
+
await Promise.all(helpers.map(helper => (0, utilities_1.KillProcess)(helper).catch(() => undefined)));
|
|
1149
|
+
}
|
|
1150
|
+
async function returnLicense(licenseType, token) {
|
|
1151
|
+
const logger = logging_1.Logger.instance;
|
|
1152
|
+
const legacyLicenseVersion = GetLegacyLicenseVersion();
|
|
1153
|
+
if (legacyLicenseVersion || (licenseType === LicenseType.floating && token)) {
|
|
1154
|
+
await LicensingClient.StopBackgroundClients();
|
|
1155
|
+
await new LicensingClient(legacyLicenseVersion).Deactivate(licenseType, token);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
try {
|
|
1159
|
+
const cli = await unity_cli_1.UnityCli.Get();
|
|
1160
|
+
const { exitCode, envelope } = await cli.RunJson(['license', 'return', '--yes'], { timeoutMs: UNITY_CLI_LICENSE_TIMEOUT_MS });
|
|
1161
|
+
if (exitCode === 0 && envelope?.success !== false && !(await isLicenseActive(cli))) {
|
|
1162
|
+
logger.info(`Successfully returned license of type '${licenseType}'`);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
logger.warn(`unity license return exited with code ${exitCode}${formatEnvelopeErrors(envelope)}, falling back to the Unity Licensing Client...`);
|
|
1166
|
+
}
|
|
1167
|
+
catch (error) {
|
|
1168
|
+
logger.warn(`Unity CLI license return failed, falling back to the Unity Licensing Client.\n${error}`);
|
|
1169
|
+
}
|
|
1170
|
+
if (licenseType === LicenseType.floating) {
|
|
1171
|
+
throw new Error('The Unity CLI could not return the floating license. Pass the token from activation to return it with the Unity Licensing Client.');
|
|
1172
|
+
}
|
|
1173
|
+
await LicensingClient.StopBackgroundClients();
|
|
1174
|
+
await new LicensingClient().Deactivate(licenseType);
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Prints the license state from the official Unity CLI, then the Unity Licensing Client context.
|
|
1178
|
+
*/
|
|
1179
|
+
async function PrintLicenseContext() {
|
|
1180
|
+
try {
|
|
1181
|
+
const cli = await unity_cli_1.UnityCli.Get();
|
|
1182
|
+
await cli.Run(['license', 'status'], { stream: true });
|
|
1183
|
+
}
|
|
1184
|
+
catch (error) {
|
|
1185
|
+
logging_1.Logger.instance.warn(`Failed to read the Unity CLI license status: ${error}`);
|
|
1186
|
+
}
|
|
1187
|
+
await new LicensingClient(GetLegacyLicenseVersion()).Context();
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Gets where Unity reads the machine's license file, Unity_lic.ulf.
|
|
1191
|
+
*/
|
|
1192
|
+
function GetLicenseFilePath() {
|
|
1193
|
+
switch (process.platform) {
|
|
1194
|
+
case 'win32':
|
|
1195
|
+
return path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'Unity', 'Unity_lic.ulf');
|
|
1196
|
+
case 'darwin':
|
|
1197
|
+
return path.join('/Library', 'Application Support', 'Unity', 'Unity_lic.ulf');
|
|
1198
|
+
default:
|
|
1199
|
+
return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'unity3d', 'Unity', 'Unity_lic.ulf');
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Activates from a cached license file: `unity license activate --file` first, then by placing the file where Unity reads it.
|
|
1204
|
+
* The file's contents are never logged.
|
|
1205
|
+
* @param licenseFile A path to the license file, its raw XML, or base64.
|
|
1206
|
+
* @returns True when a license is active afterwards.
|
|
1207
|
+
*/
|
|
1208
|
+
async function activateFromLicenseFile(licenseFile) {
|
|
1209
|
+
const logger = logging_1.Logger.instance;
|
|
1210
|
+
const trimmed = licenseFile.trim();
|
|
1211
|
+
let content;
|
|
1212
|
+
if (!trimmed.startsWith('<') && fs.existsSync(trimmed)) {
|
|
1213
|
+
content = await fs.promises.readFile(trimmed, 'utf-8');
|
|
1214
|
+
}
|
|
1215
|
+
else if (trimmed.startsWith('<')) {
|
|
1216
|
+
content = trimmed;
|
|
1217
|
+
}
|
|
1218
|
+
else if (/\.ulf$/i.test(trimmed) || /^([A-Za-z]:[\\/]|[\\/~.])/.test(trimmed)) {
|
|
1219
|
+
// base64 of a license starts with "PD" or "PE", so this is a path to a file that is not there
|
|
1220
|
+
throw new Error(`The license file was not found: ${trimmed}`);
|
|
1221
|
+
}
|
|
1222
|
+
else {
|
|
1223
|
+
content = Buffer.from(trimmed, 'base64').toString('utf-8');
|
|
1224
|
+
}
|
|
1225
|
+
if (!content.includes('<License') || !content.includes('</root>')) {
|
|
1226
|
+
throw new Error('The license file is not a Unity license (Unity_lic.ulf).');
|
|
1227
|
+
}
|
|
1228
|
+
// a license file is bound to the machine it was issued to: it still activates offline on another machine, but the
|
|
1229
|
+
// licensing servers resolve no entitlements for it, and the editor fails later. Runner images change that identity.
|
|
1230
|
+
const bindings = await new LicensingClient().GetMachineBindings();
|
|
1231
|
+
if (bindings.length > 0 && !bindings.some(binding => content.includes(binding))) {
|
|
1232
|
+
logger.warn('The cached license file was issued to a different machine identity, likely before a runner image change. Activating with the account instead.');
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
logger.info('Activating from the cached license file...');
|
|
1236
|
+
const tempDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'unity-license-'));
|
|
1237
|
+
const tempFile = path.join(tempDirectory, 'Unity_lic.ulf');
|
|
1238
|
+
try {
|
|
1239
|
+
await fs.promises.writeFile(tempFile, content, { encoding: 'utf-8', mode: 0o600 });
|
|
1240
|
+
try {
|
|
1241
|
+
const cli = await unity_cli_1.UnityCli.Get();
|
|
1242
|
+
const { exitCode, envelope } = await cli.RunJson(['license', 'activate', '--file', tempFile], { timeoutMs: UNITY_CLI_LICENSE_TIMEOUT_MS });
|
|
1243
|
+
if (exitCode === 0 && envelope?.success !== false && await isLicenseActive(cli)) {
|
|
1244
|
+
logger.info('Successfully activated from the cached license file with the Unity CLI');
|
|
1245
|
+
return true;
|
|
1246
|
+
}
|
|
1247
|
+
logger.warn(`unity license activate --file exited with code ${exitCode}${formatEnvelopeErrors(envelope)}, placing the license file directly...`);
|
|
1248
|
+
}
|
|
1249
|
+
catch (error) {
|
|
1250
|
+
logger.warn(`Unity CLI activation from the license file failed, placing the license file directly...\n${error}`);
|
|
1251
|
+
}
|
|
1252
|
+
const licensePath = GetLicenseFilePath();
|
|
1253
|
+
await fs.promises.mkdir(path.dirname(licensePath), { recursive: true });
|
|
1254
|
+
await fs.promises.copyFile(tempFile, licensePath);
|
|
1255
|
+
const active = await new LicensingClient().GetActiveEntitlements();
|
|
1256
|
+
if (active.length > 0) {
|
|
1257
|
+
logger.info(`Successfully activated from the cached license file (${active.join(', ')})`);
|
|
1258
|
+
return true;
|
|
1259
|
+
}
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
finally {
|
|
1263
|
+
await fs.promises.rm(tempDirectory, { recursive: true, force: true });
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
async function activateWithUnityCli(options) {
|
|
1267
|
+
const args = ['license', 'activate'];
|
|
1268
|
+
const redactLiterals = [];
|
|
1269
|
+
switch (options.licenseType) {
|
|
1270
|
+
case LicenseType.personal:
|
|
1271
|
+
args.push('--personal', '--accept-eula');
|
|
1272
|
+
break;
|
|
1273
|
+
case LicenseType.professional:
|
|
1274
|
+
if (!options.serial) {
|
|
1275
|
+
return { activated: false, reason: 'A serial is required to activate a professional license with the Unity CLI.' };
|
|
1276
|
+
}
|
|
1277
|
+
args.push('--serial', options.serial.trim());
|
|
1278
|
+
redactLiterals.push(options.serial.trim());
|
|
1279
|
+
break;
|
|
1280
|
+
case LicenseType.floating:
|
|
1281
|
+
if (!options.servicesConfig) {
|
|
1282
|
+
throw new Error('Services config path is required for floating license activation');
|
|
1283
|
+
}
|
|
1284
|
+
await new LicensingClient().setupServicesConfig(options.servicesConfig);
|
|
1285
|
+
args.push('--floating');
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1288
|
+
const cli = await unity_cli_1.UnityCli.Get();
|
|
1289
|
+
logging_1.Logger.instance.info(`Activating a ${options.licenseType} license with the Unity CLI...`);
|
|
1290
|
+
const { exitCode, envelope } = await cli.RunJson(args, { timeoutMs: UNITY_CLI_LICENSE_TIMEOUT_MS, redactLiterals });
|
|
1291
|
+
if (exitCode !== 0 || envelope?.success === false) {
|
|
1292
|
+
return { activated: false, reason: `unity license activate exited with code ${exitCode}${formatEnvelopeErrors(envelope)}` };
|
|
1293
|
+
}
|
|
1294
|
+
if (!(await isLicenseActive(cli))) {
|
|
1295
|
+
return { activated: false, reason: 'unity license activate succeeded, but unity license status reports no active license' };
|
|
1296
|
+
}
|
|
1297
|
+
const token = findToken(envelope?.data);
|
|
1298
|
+
if (token) {
|
|
1299
|
+
logging_1.Logger.instance.CI_mask(token);
|
|
1300
|
+
}
|
|
1301
|
+
logging_1.Logger.instance.info(`Successfully activated license of type '${options.licenseType}' with the Unity CLI`);
|
|
1302
|
+
return { activated: true, token };
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Reads whether a license is active. Throws when the CLI gives no clear answer (a timeout or an unparseable result),
|
|
1306
|
+
* so callers fall back instead of assuming the license is gone. LICENSE_NOT_ACTIVE exits 4 with active: false.
|
|
1307
|
+
*/
|
|
1308
|
+
async function isLicenseActive(cli) {
|
|
1309
|
+
const { exitCode, envelope } = await cli.RunJson(['license', 'status'], { timeoutMs: UNITY_CLI_LICENSE_TIMEOUT_MS });
|
|
1310
|
+
const active = envelope?.data?.active;
|
|
1311
|
+
if ((exitCode === 0 || exitCode === 4) && typeof active === 'boolean') {
|
|
1312
|
+
return active;
|
|
1313
|
+
}
|
|
1314
|
+
throw new Error(`unity license status exited with code ${exitCode} without a license state${formatEnvelopeErrors(envelope)}`);
|
|
1315
|
+
}
|
|
1316
|
+
function findToken(value) {
|
|
1317
|
+
if (!value || typeof value !== 'object') {
|
|
1318
|
+
return undefined;
|
|
1319
|
+
}
|
|
1320
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1321
|
+
if (typeof item === 'string' && /token/i.test(key) && item.length > 0) {
|
|
1322
|
+
return item;
|
|
1323
|
+
}
|
|
1324
|
+
const nested = findToken(item);
|
|
1325
|
+
if (nested) {
|
|
1326
|
+
return nested;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return undefined;
|
|
1330
|
+
}
|
|
1331
|
+
function formatEnvelopeErrors(envelope) {
|
|
1332
|
+
const errors = envelope?.errors ?? [];
|
|
1333
|
+
return errors.length > 0 ? `\n${errors.map(e => ` > ${e.code}: ${e.message}`).join('\n')}` : '';
|
|
1334
|
+
}
|
|
654
1335
|
//# sourceMappingURL=license-client.js.map
|