Skip to content

Commit 0182c98

Browse files
authored
Merge pull request #17 from NoTaskStudios/feature/hub-event-emitter
Feature/hub event emitter
2 parents edea05a + 78c7d26 commit 0182c98

File tree

5 files changed

+240
-27
lines changed

5 files changed

+240
-27
lines changed

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,34 @@ await UnityHub.addEditor("2022.3.60f1", undefined, [UnityModules.AndroidBuildSup
6969
await UnityHub.addModule("2022.3.60f1", [UnityModules.IOSBuildSupport]);
7070
```
7171

72+
### Installation Events
73+
74+
```typescript
75+
import { UnityHub, UnityModules, InstallerEventType } from "unity-cli-tools";
76+
77+
// Install with event tracking (addEditor returns an event emitter)
78+
const installer = await UnityHub.addEditor("2022.3.60f1");
79+
80+
// Get a promise that resolves when installation completes
81+
const installation = installer.completed;
82+
83+
// Or listen to specific events
84+
installer.on(InstallerEventType.Progress, (events) => {
85+
console.log("Progress:", events.map(e => `${e.module}: ${e.status} ${e.progress || 0}%`));
86+
});
87+
88+
installer.on(InstallerEventType.Error, (error) => {
89+
console.error("Installation error:", error);
90+
});
91+
92+
installer.on(InstallerEventType.Completed, (events) => {
93+
console.log("Installation completed!");
94+
});
95+
96+
// Cancel installation if needed
97+
installer.Cancel();
98+
```
99+
72100
### Projects Management
73101

74102
```typescript
@@ -225,6 +253,30 @@ console.log(executionResult);
225253
| `ChineseTraditional` | Traditional Chinese language pack |
226254
| `Chinese` | Chinese language pack (legacy) |
227255

256+
### InstallerStatus
257+
258+
| Constant | Description |
259+
| ------------------ | ------------------------------- |
260+
| `Queued` | Queued for download |
261+
| `Validating` | Validating download |
262+
| `InProgress` | Installation in progress |
263+
| `Downloading` | Downloading installation files |
264+
| `QueuedInstall` | Queued for install |
265+
| `ValidatingInstall`| Validating installation |
266+
| `Installing` | Installing |
267+
| `Verifying` | Verifying installation |
268+
| `Installed` | Installed successfully |
269+
| `Error` | Installation error |
270+
271+
### InstallerEventType
272+
273+
| Constant | Description |
274+
| ------------ | ------------------------------------------- |
275+
| `Progress` | Installation progress update event |
276+
| `Error` | Installation error event |
277+
| `Completed` | Installation completed successfully event |
278+
| `Cancelled` | Installation cancelled by user event |
279+
228280

229281
## Configuration
230282

src/events/hubEventEmitter.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { EventEmitter } from "events";
2+
import { InstallerEventType, InstallerEvent, InstallerStatus } from "../types/unity.ts";
3+
import { UnityHubEventParser } from "./hubEventParser.ts";
4+
5+
export interface InstallerEmitter extends EventEmitter {
6+
on(event: InstallerEventType.Progress, listener: (info: InstallerEvent[]) => void): this;
7+
on(event: InstallerEventType.Error, listener: (error: Error) => void): this;
8+
on(event: InstallerEventType.Completed, listener: (info: InstallerEvent[]) => void): this;
9+
on(event: InstallerEventType.Cancelled, listener: (info: InstallerEvent[]) => void): this;
10+
emit(event: InstallerEventType.Progress, info: InstallerEvent[]): boolean;
11+
emit(event: InstallerEventType.Error, info: InstallerEvent[]): boolean;
12+
emit(event: InstallerEventType.Completed, info: InstallerEvent[]): boolean;
13+
emit(event: InstallerEventType.Cancelled, info: InstallerEvent[]): boolean;
14+
15+
readonly completed: Promise<InstallerEvent[]>;
16+
}
17+
18+
export class UnityHubInstallerEvent extends EventEmitter implements InstallerEmitter {
19+
#moduleTracker: Map<string, InstallerStatus> = new Map();
20+
21+
public constructor() {
22+
super();
23+
}
24+
25+
public completed: Promise<InstallerEvent[]> = new Promise((resolve, reject) => {
26+
this.on(InstallerEventType.Completed, (events) => resolve(events));
27+
this.on(InstallerEventType.Error, (error) => reject(error));
28+
this.on(InstallerEventType.Cancelled, (events) => reject(new Error("Cancelled")));
29+
});
30+
31+
/**
32+
* Parses the raw event string and emits the appropriate event.
33+
* @param raw - The raw event string from Unity Hub.
34+
* @returns {void}
35+
*/
36+
public Progress(raw: string): void {
37+
const events = UnityHubEventParser.parseUnityHubEvent(raw);
38+
if (events.length === 0) return;
39+
40+
this.#Error(events);
41+
42+
const progressEvents = events.filter((e) => e.status !== InstallerStatus.Error);
43+
44+
if (progressEvents.length === 0) return;
45+
this.emit(InstallerEventType.Progress, progressEvents);
46+
this.#updateModuleTracker(events);
47+
this.#Complete(progressEvents);
48+
}
49+
50+
#Error(events: InstallerEvent[]): void {
51+
const errorEvents = events.filter((e) => e.status === InstallerStatus.Error);
52+
53+
if (errorEvents.length === 0) return;
54+
this.emit(InstallerEventType.Error, errorEvents);
55+
}
56+
57+
#Complete(events: InstallerEvent[]): void {
58+
const installed = events.filter((e) => e.status === InstallerStatus.Installed);
59+
if (installed.length > 0 && installed.length === events.length) {
60+
this.emit(InstallerEventType.Completed, installed);
61+
}
62+
}
63+
64+
/**
65+
* Emits a cancelled event.
66+
* @returns {void}
67+
*/
68+
public Cancel(): void {
69+
//Cancel operation
70+
this.#moduleTracker.clear();
71+
this.#Cancelled([]);
72+
}
73+
74+
#Cancelled(event: InstallerEvent[]): void {
75+
this.emit(InstallerEventType.Cancelled, event);
76+
}
77+
78+
#updateModuleTracker(events: InstallerEvent[]): void {
79+
for (const event of events) {
80+
this.#moduleTracker.set(event.module, event.status);
81+
}
82+
}
83+
}

src/events/hubEventParser.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { InstallerEvent, InstallerStatus } from "../types/unity.ts";
2+
3+
export class UnityHubEventParser {
4+
private static errorPatterns = [/Error:.*/];
5+
6+
public static parseUnityHubEvent(event: string): InstallerEvent[] {
7+
const events: InstallerEvent[] = [];
8+
const lines = event.split("\n");
9+
10+
for (const line of lines) {
11+
const errorLine = this.checkForErrors(line);
12+
if (errorLine) {
13+
events.push(errorLine);
14+
continue;
15+
}
16+
}
17+
18+
const pattern = /^\[(?<module>[^\]]+)\]\s+(?<status>.+?)(?:(?:\s+(?<progress>\d+(?:\.\d+)?))%)?\.*$/;
19+
20+
for (const line of lines) {
21+
const match = line.match(pattern);
22+
if (match?.groups) {
23+
const { module, status, progress } = match.groups;
24+
events.push({
25+
module: module.trim(),
26+
status: status.replace(/\.\.\.$/, "").trim() as InstallerStatus,
27+
progress: progress ? parseFloat(progress) : status !== InstallerStatus.Downloading ? null : 0,
28+
});
29+
}
30+
}
31+
32+
return events;
33+
}
34+
35+
private static checkForErrors(line: string): InstallerEvent | null {
36+
for (const pattern of this.errorPatterns) {
37+
if (pattern.test(line)) {
38+
return {
39+
module: "UnityHub",
40+
status: InstallerStatus.Error,
41+
error: line.trim(),
42+
};
43+
}
44+
}
45+
return null;
46+
}
47+
}

src/types/unity.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,42 @@ export enum UnityBuildTarget {
220220
/** VisionOS build target */
221221
VisionOS = "VisionOS",
222222
}
223+
224+
/**
225+
* Enum for Unity installation statuses
226+
* These values correspond to the different states of a Unity installation process
227+
*/
228+
export enum InstallerStatus {
229+
Queued = "queued for download",
230+
Validating = "validating download",
231+
InProgress = "in progress",
232+
Downloading = "downloading",
233+
QueuedInstall = "queued for install",
234+
ValidatingInstall = "validation installation",
235+
Installing = "installing",
236+
Verifying = "verifying",
237+
Installed = "installed successfully",
238+
Error = "Error",
239+
}
240+
241+
/**
242+
* Interface representing an installer event
243+
* Contains information about the status of a module installation process
244+
*/
245+
export interface InstallerEvent {
246+
module: string;
247+
status: InstallerStatus;
248+
progress?: number | null;
249+
error?: string | null;
250+
}
251+
252+
/**
253+
* Enum for installer event types
254+
* These values correspond to the different types of events that can occur during the installation process
255+
*/
256+
export enum InstallerEventType {
257+
Progress = "progress",
258+
Error = "error",
259+
Completed = "completed",
260+
Cancelled = "cancelled",
261+
}

src/unityHub.ts

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "./types/unity.js";
1111
import { CommandOptions, CommandResult, executeCommand } from "./utils/commandExecutor.js";
1212
import { getUnityChangeset } from "unity-changeset";
13+
import { UnityHubInstallerEvent } from "./events/hubEventEmitter.ts";
1314

1415
/**
1516
* Class for interacting with Unity Hub via command line interface
@@ -210,15 +211,15 @@ class UnityHub {
210211
* @param {string} editorVersion - Unity version to add modules to (e.g. "2022.3.60f1")
211212
* @param {ModuleId[]} modules - Array of module IDs to add
212213
* @param {boolean} [childModules=false] - Whether to include child modules
213-
* @returns {Promise<void>} Resolves when modules are added successfully
214+
* @returns {UnityHubInstallerEvent} Event emitter for installation progress
214215
* @throws Will throw an error if module addition fails
215216
* @public
216217
*/
217218
public static async addModule(
218219
editorVersion: string,
219220
modules: ModuleId[],
220221
childModules: boolean = true
221-
): Promise<void> {
222+
): Promise<UnityHubInstallerEvent> {
222223
try {
223224
console.debug(`Adding module ${modules} to Unity ${editorVersion}`);
224225

@@ -234,19 +235,15 @@ class UnityHub {
234235
throw new Error("No module IDs provided.");
235236
}
236237

237-
const { stderr } = await this.execUnityHubCommand(args, {
238+
const installerEmitter = new UnityHubInstallerEvent();
239+
this.execUnityHubCommand(args, {
238240
reject: false,
239-
onStderr: (data: string) => {
240-
console.warn(`Unity Hub stderr: ${data}`);
241-
},
242-
onStdout: (data: string) => {
243-
console.debug(`Unity Hub stdout: ${data}`);
244-
},
241+
//onStderr: (data: string) => installerEmitter.Progress(data),
242+
onStdout: (data: string) => installerEmitter.Progress(data),
243+
}).catch((error) => {
244+
console.error(`Error adding module ${modules} to Unity ${editorVersion}:`, error);
245245
});
246-
247-
if (stderr) {
248-
console.warn(`Add module command warning/error: ${stderr}`);
249-
}
246+
return installerEmitter;
250247
} catch (error) {
251248
console.error(`Error adding module ${modules} to Unity ${editorVersion}:`, error);
252249
throw error;
@@ -259,15 +256,15 @@ class UnityHub {
259256
* @param {string} [changeset] - Optional specific changeset to install
260257
* @param {ModuleId[]} [modules=[]] - Optional array of modules to install with the editor
261258
* @param {EditorArchitecture} [architecture=EditorArchitecture.x86_64] - Optional architecture (x86_64 or arm64)
262-
* @returns {Promise<void>} Resolves when editor installation begins successfully
259+
* @returns {UnityHubInstallerEvent} Event emitter for installation progress
263260
* @throws Will throw an error if installation fails to start
264261
* @public
265262
*/
266263
public static async addEditor(
267264
version: string,
268265
modules: ModuleId[] = [],
269266
architecture?: EditorArchitecture
270-
): Promise<void> {
267+
): Promise<UnityHubInstallerEvent> {
271268
try {
272269
const data = await getUnityChangeset(version);
273270
const args = ["install", "-v", version];
@@ -288,21 +285,16 @@ class UnityHub {
288285

289286
args.push("--architecture", architecture);
290287

291-
const { stdout, stderr } = await this.execUnityHubCommand(args, {
288+
const installerEmitter = new UnityHubInstallerEvent();
289+
this.execUnityHubCommand(args, {
292290
reject: false,
293-
onStderr: (data: string) => {
294-
console.warn(`Unity Hub stderr: ${data}`);
295-
},
296-
onStdout: (data: string) => {
297-
console.debug(`Unity Hub stdout: ${data}`);
298-
},
291+
//onStderr: (data: string) => installerEmitter.Error(data),
292+
onStdout: (data: string) => installerEmitter.Progress(data),
293+
}).catch((error) => {
294+
console.error(`Error installing Unity ${version}:`, error);
299295
});
300296

301-
if (stderr) {
302-
throw new Error(`Error installing Unity ${version}: ${stderr}`);
303-
}
304-
305-
console.debug(`Unity ${version}. ${stdout}`);
297+
return installerEmitter;
306298
} catch (error) {
307299
console.error(error);
308300
throw error;

0 commit comments

Comments
 (0)