@@ -5,8 +5,6 @@ import type { IExtensionFeed, ExtensionMetadata, ExtensionModule } from "./exten
5
5
6
6
import { Logger } from "core/Misc/logger" ;
7
7
8
- import { Assert } from "../misc/assert" ;
9
-
10
8
/**
11
9
* Represents a loaded extension.
12
10
*/
@@ -44,6 +42,21 @@ export interface IExtension {
44
42
addStateChangedHandler ( handler : ( ) => void ) : IDisposable ;
45
43
}
46
44
45
+ /**
46
+ * Provides information about an extension installation failure.
47
+ */
48
+ export type InstallFailedInfo = {
49
+ /**
50
+ * The metadata of the extension that failed to install.
51
+ */
52
+ extension : ExtensionMetadata ;
53
+
54
+ /**
55
+ * The error that occurred during the installation.
56
+ */
57
+ error : unknown ;
58
+ } ;
59
+
47
60
type InstalledExtension = {
48
61
metadata : ExtensionMetadata ;
49
62
feed : IExtensionFeed ;
@@ -91,18 +104,24 @@ export class ExtensionManager implements IDisposable {
91
104
92
105
private constructor (
93
106
private readonly _serviceContainer : ServiceContainer ,
94
- private readonly _feeds : readonly IExtensionFeed [ ]
107
+ private readonly _feeds : readonly IExtensionFeed [ ] ,
108
+ private readonly _onInstallFailed : ( info : InstallFailedInfo ) => void
95
109
) { }
96
110
97
111
/**
98
112
* Creates a new instance of the ExtensionManager.
99
113
* This will automatically rehydrate previously installed and enabled extensions.
100
114
* @param serviceContainer The service container to use.
101
115
* @param feeds The extension feeds to include.
116
+ * @param onInstallFailed A callback that is called when an extension installation fails.
102
117
* @returns A promise that resolves to the new instance of the ExtensionManager.
103
118
*/
104
- public static async CreateAsync ( serviceContainer : ServiceContainer , feeds : readonly IExtensionFeed [ ] ) {
105
- const extensionManager = new ExtensionManager ( serviceContainer , feeds ) ;
119
+ public static async CreateAsync (
120
+ serviceContainer : ServiceContainer ,
121
+ feeds : readonly IExtensionFeed [ ] ,
122
+ onInstallFailed : ( info : InstallFailedInfo ) => void
123
+ ) : Promise < ExtensionManager > {
124
+ const extensionManager = new ExtensionManager ( serviceContainer , feeds , onInstallFailed ) ;
106
125
107
126
// Rehydrate installed extensions.
108
127
const installedExtensionNames = JSON . parse ( localStorage . getItem ( InstalledExtensionsKey ) ?? "[]" ) as string [ ] ;
@@ -125,7 +144,17 @@ export class ExtensionManager implements IDisposable {
125
144
// Load installed and enabled extensions.
126
145
const enablePromises : Promise < void > [ ] = [ ] ;
127
146
for ( const extension of extensionManager . _installedExtensions . values ( ) ) {
128
- enablePromises . push ( extensionManager . _enableAsync ( extension . metadata , false ) ) ;
147
+ enablePromises . push (
148
+ ( async ( ) => {
149
+ try {
150
+ await extensionManager . _enableAsync ( extension . metadata , false , false ) ;
151
+ } catch {
152
+ // If enabling the extension fails, uninstall it. The extension install fail callback will still be called,
153
+ // so the owner of the ExtensionManager instance can decide what to do with the error.
154
+ await extensionManager . _uninstallAsync ( extension . metadata , false ) ;
155
+ }
156
+ } ) ( )
157
+ ) ;
129
158
}
130
159
131
160
await Promise . all ( enablePromises ) ;
@@ -215,6 +244,16 @@ export class ExtensionManager implements IDisposable {
215
244
installedExtension . isStateChanging = true ;
216
245
this . _installedExtensions . set ( metadata . name , installedExtension ) ;
217
246
247
+ try {
248
+ // Enable the extension.
249
+ await this . _enableAsync ( metadata , true , true ) ;
250
+ } catch ( error ) {
251
+ this . _installedExtensions . delete ( metadata . name ) ;
252
+ throw error ;
253
+ } finally {
254
+ ! isNestedStateChange && ( installedExtension . isStateChanging = false ) ;
255
+ }
256
+
218
257
// Mark the extension as being installed.
219
258
localStorage . setItem (
220
259
GetExtensionInstalledKey ( GetExtensionIdentity ( feed . name , metadata . name ) ) ,
@@ -227,16 +266,6 @@ export class ExtensionManager implements IDisposable {
227
266
InstalledExtensionsKey ,
228
267
JSON . stringify ( Array . from ( this . _installedExtensions . values ( ) ) . map ( ( extension ) => GetExtensionIdentity ( extension . feed . name , extension . metadata . name ) ) )
229
268
) ;
230
-
231
- try {
232
- // Enable the extension.
233
- await this . _enableAsync ( metadata , true ) ;
234
- } catch ( error ) {
235
- this . _installedExtensions . delete ( metadata . name ) ;
236
- throw error ;
237
- } finally {
238
- ! isNestedStateChange && ( installedExtension . isStateChanging = false ) ;
239
- }
240
269
}
241
270
242
271
return installedExtension ;
@@ -263,7 +292,7 @@ export class ExtensionManager implements IDisposable {
263
292
}
264
293
}
265
294
266
- private async _enableAsync ( metadata : ExtensionMetadata , isNestedStateChange : boolean ) : Promise < void > {
295
+ private async _enableAsync ( metadata : ExtensionMetadata , isInitialInstall : boolean , isNestedStateChange : boolean ) : Promise < void > {
267
296
const installedExtension = this . _installedExtensions . get ( metadata . name ) ;
268
297
if ( installedExtension && ( isNestedStateChange || ! installedExtension . isStateChanging ) ) {
269
298
try {
@@ -274,7 +303,9 @@ export class ExtensionManager implements IDisposable {
274
303
installedExtension . extensionModule = await installedExtension . feed . getExtensionModuleAsync ( metadata . name ) ;
275
304
}
276
305
277
- Assert ( installedExtension . extensionModule ) ;
306
+ if ( ! installedExtension . extensionModule ) {
307
+ throw new Error ( `Unable to load extension module for "${ metadata . name } " from feed "${ installedExtension . feed . name } ".` ) ;
308
+ }
278
309
279
310
// Register the ServiceDefinitions.
280
311
let servicesRegistrationToken : Nullable < IDisposable > = null ;
@@ -288,6 +319,12 @@ export class ExtensionManager implements IDisposable {
288
319
servicesRegistrationToken ?. dispose ( ) ;
289
320
} ,
290
321
} ;
322
+ } catch ( error : unknown ) {
323
+ this . _onInstallFailed ( {
324
+ extension : metadata ,
325
+ error,
326
+ } ) ;
327
+ throw error ;
291
328
} finally {
292
329
! isNestedStateChange && ( installedExtension . isStateChanging = false ) ;
293
330
}
0 commit comments