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