Skip to content

Commit f525fc7

Browse files
committed
frontend: kraken: add cancellation support for extension install and update
1 parent c3358c6 commit f525fc7

3 files changed

Lines changed: 100 additions & 26 deletions

File tree

core/frontend/src/components/kraken/KrakenManager.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ export async function setManifestSourceOrder(identifier: string, order: number):
196196
export async function installExtension(
197197
extension: InstalledExtensionData,
198198
progressHandler: (event: any) => void,
199+
signal?: AbortSignal,
199200
): Promise<void> {
200201
await back_axios({
201202
url: `${KRAKEN_API_V2_URL}/extension/install`,
@@ -211,6 +212,7 @@ export async function installExtension(
211212
},
212213
timeout: 600000,
213214
onDownloadProgress: progressHandler,
215+
signal,
214216
})
215217
}
216218

@@ -250,6 +252,18 @@ export async function uninstallExtension(identifier: string): Promise<void> {
250252
})
251253
}
252254

255+
/**
256+
* Uninstall a specific version of an extension by its identifier and tag, uses API v2
257+
* @param {string} identifier The identifier of the extension
258+
* @param {string} tag The tag of the version to uninstall
259+
*/
260+
export async function uninstallExtensionVersion(identifier: string, tag: string): Promise<void> {
261+
await back_axios({
262+
method: 'DELETE',
263+
url: `${KRAKEN_API_V2_URL}/extension/${identifier}/${tag}`,
264+
})
265+
}
266+
253267
/**
254268
* Restart an extension by its identifier, uses API v2
255269
* @param {string} identifier The identifier of the extension
@@ -272,12 +286,14 @@ export async function updateExtensionToVersion(
272286
identifier: string,
273287
version: string,
274288
progressHandler: (event: any) => void,
289+
signal?: AbortSignal,
275290
): Promise<void> {
276291
await back_axios({
277292
url: `${KRAKEN_API_V2_URL}/extension/${identifier}/${version}`,
278293
method: 'PUT',
279294
timeout: 120000,
280295
onDownloadProgress: progressHandler,
296+
signal,
281297
})
282298
}
283299

@@ -370,6 +386,7 @@ export async function finalizeExtension(
370386
extension: InstalledExtensionData,
371387
tempTag: string,
372388
progressHandler: (event: any) => void,
389+
signal?: AbortSignal,
373390
): Promise<void> {
374391
await back_axios({
375392
method: 'POST',
@@ -385,6 +402,7 @@ export async function finalizeExtension(
385402
},
386403
timeout: 120000,
387404
onDownloadProgress: progressHandler,
405+
signal,
388406
})
389407
}
390408

@@ -430,6 +448,7 @@ export default {
430448
enableExtension,
431449
disableExtension,
432450
uninstallExtension,
451+
uninstallExtensionVersion,
433452
restartExtension,
434453
listContainers,
435454
getContainersStats,

core/frontend/src/components/utils/PullProgress.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@
5050
</v-expansion-panel>
5151
</v-expansion-panels>
5252
</v-card-text>
53+
<v-card-actions v-if="cancelable" class="justify-end">
54+
<v-btn color="primary" @click="$emit('cancel')">
55+
Cancel
56+
</v-btn>
57+
</v-card-actions>
5358
</v-card>
5459
</v-dialog>
5560
</template>
@@ -80,6 +85,10 @@ export default Vue.extend({
8085
type: String,
8186
required: true,
8287
},
88+
cancelable: {
89+
type: Boolean,
90+
default: false,
91+
},
8392
},
8493
data() {
8594
return {

core/frontend/src/views/ExtensionManagerView.vue

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
:download="download_percentage"
88
:extraction="extraction_percentage"
99
:statustext="status_text"
10+
:cancelable="!!active_abort_controller"
11+
@cancel="cancelInstallOperation"
1012
/>
1113
<v-dialog
1214
v-model="show_dialog"
@@ -510,6 +512,7 @@ export default Vue.extend({
510512
active_operation_identifier: localStorage.getItem(ACTIVE_OPERATION_KEY) as null | string,
511513
active_operation_type: (localStorage.getItem(ACTIVE_OPERATION_TYPE_KEY) ?? null) as null | 'install' | 'update',
512514
session: null as Session | null,
515+
active_abort_controller: null as null | AbortController,
513516
}
514517
},
515518
computed: {
@@ -619,6 +622,8 @@ export default Vue.extend({
619622
clearInterval(this.metrics_interval)
620623
this.stopUploadKeepAlive()
621624
this.session = null
625+
this.active_abort_controller?.abort()
626+
this.active_abort_controller = null
622627
},
623628
methods: {
624629
async initializeZenohSession() {
@@ -635,6 +640,25 @@ export default Vue.extend({
635640
clearEditedExtension() {
636641
this.edited_extension = null
637642
},
643+
beginInstallOperation(): AbortController {
644+
this.active_abort_controller?.abort()
645+
const controller = new AbortController()
646+
this.active_abort_controller = controller
647+
return controller
648+
},
649+
cancelInstallOperation(): void {
650+
this.active_abort_controller?.abort()
651+
},
652+
showAlertError(error: unknown): void {
653+
this.alerter = true
654+
this.alerter_error = String(error)
655+
},
656+
finishInstallOperation(): void {
657+
this.active_abort_controller = null
658+
this.clearInstallingState()
659+
this.resetPullOutput()
660+
this.fetchInstalledExtensions()
661+
},
638662
setInstallFromFilePhase(phase: TarInstallPhase) {
639663
this.install_from_file_phase = phase
640664
if (phase !== 'error') {
@@ -791,24 +815,30 @@ export default Vue.extend({
791815
async update(extension: InstalledExtensionData, version: string) {
792816
this.setInstallingState(extension.identifier, 'update')
793817
this.show_pull_output = true
794-
const tracker = this.getTracker()
818+
const controller = this.beginInstallOperation()
819+
const tracker = this.getTracker(controller.signal)
795820
kraken.updateExtensionToVersion(
796821
extension.identifier,
797822
version,
798823
(progressEvent) => this.handleDownloadProgress(progressEvent.event, tracker),
824+
controller.signal,
799825
)
800826
.then(() => {
801-
this.fetchInstalledExtensions()
802827
notifier.pushSuccess('EXTENSION_UPDATE_SUCCESS', `${extension.name} updated successfully.`, true)
803828
})
804-
.catch((error) => {
805-
this.alerter = true
806-
this.alerter_error = String(error)
829+
.catch(async (error) => {
830+
if (axios.isCancel(error)) {
831+
if (controller !== this.active_abort_controller) return
832+
await kraken.uninstallExtensionVersion(extension.identifier, version)
833+
.catch(() => { /* version may not be registered yet */ })
834+
notifier.pushInfo('EXTENSION_UPDATE_CANCELLED', 'Extension update was cancelled.', true)
835+
return
836+
}
837+
this.showAlertError(error)
807838
notifier.pushBackError('EXTENSION_UPDATE_FAIL', error)
808839
})
809840
.finally(() => {
810-
this.clearInstallingState()
811-
this.resetPullOutput()
841+
if (controller === this.active_abort_controller) this.finishInstallOperation()
812842
})
813843
},
814844
metricsFor(extension: InstalledExtensionData): { cpu: number, memory: number} | Record<string, never> {
@@ -929,24 +959,30 @@ export default Vue.extend({
929959
this.setInstallingState(extension.identifier, 'install')
930960
this.show_dialog = false
931961
this.show_pull_output = true
932-
const tracker = this.getTracker()
962+
const controller = this.beginInstallOperation()
963+
const tracker = this.getTracker(controller.signal)
933964
934965
kraken.installExtension(
935966
extension,
936967
(progressEvent) => this.handleDownloadProgress(progressEvent.event, tracker),
968+
controller.signal,
937969
)
938970
.then(() => {
939-
this.fetchInstalledExtensions()
940971
notifier.pushSuccess('EXTENSION_INSTALL_SUCCESS', `${extension.name} installed successfully.`, true)
941972
})
942-
.catch((error) => {
943-
this.alerter = true
944-
this.alerter_error = String(error)
945-
notifier.pushBackError('EXTENSIONS_INSTALL_FAIL', error)
973+
.catch(async (error) => {
974+
if (axios.isCancel(error)) {
975+
if (controller !== this.active_abort_controller) return
976+
await kraken.uninstallExtensionVersion(extension.identifier, extension.tag)
977+
.catch(() => { /* version may not be registered yet */ })
978+
notifier.pushInfo('EXTENSION_INSTALL_CANCELLED', 'Extension install was cancelled.', true)
979+
return
980+
}
981+
this.showAlertError(error)
982+
notifier.pushBackError('EXTENSION_INSTALL_FAIL', error)
946983
})
947984
.finally(() => {
948-
this.clearInstallingState()
949-
this.resetPullOutput()
985+
if (controller === this.active_abort_controller) this.finishInstallOperation()
950986
})
951987
},
952988
async performActionFromModal(
@@ -1055,17 +1091,17 @@ export default Vue.extend({
10551091
temp[extension.identifier].loading = loading
10561092
this.installed_extensions = temp
10571093
},
1058-
getTracker(): PullTracker {
1094+
getTracker(signal: AbortSignal): PullTracker {
10591095
return new PullTracker(
10601096
() => {
10611097
setTimeout(() => {
10621098
this.show_pull_output = false
10631099
}, 1000)
10641100
},
10651101
(error) => {
1066-
this.alerter = true
1067-
this.alerter_error = String(error)
1068-
notifier.pushBackError('EXTENSIONS_INSTALL_FAIL', error)
1102+
if (signal.aborted) return
1103+
this.showAlertError(error)
1104+
notifier.pushBackError('EXTENSION_INSTALL_FAIL', error)
10691105
this.show_pull_output = false
10701106
},
10711107
)
@@ -1119,7 +1155,8 @@ export default Vue.extend({
11191155
}
11201156
11211157
this.show_pull_output = true
1122-
const tracker = this.getTracker()
1158+
const controller = this.beginInstallOperation()
1159+
const tracker = this.getTracker(controller.signal)
11231160
this.setInstallFromFilePhase('installing')
11241161
this.install_from_file_install_progress = 0
11251162
this.install_from_file_status_text = 'Starting installation...'
@@ -1129,20 +1166,29 @@ export default Vue.extend({
11291166
extension,
11301167
this.upload_temp_tag,
11311168
(progressEvent) => this.handleDownloadProgress(progressEvent.event, tracker),
1169+
controller.signal,
11321170
)
11331171
this.setInstallFromFilePhase('success')
11341172
this.install_from_file_status_text = 'Extension installed successfully'
11351173
this.stopUploadKeepAlive()
11361174
this.upload_temp_tag = null
11371175
this.upload_metadata = null
1138-
this.fetchInstalledExtensions()
11391176
} catch (error) {
1140-
this.applyInstallFromFileError(String(error))
1141-
this.alerter = true
1142-
this.alerter_error = String(error)
1143-
notifier.pushBackError('EXTENSION_FINALIZE_FAIL', error)
1177+
if (axios.isCancel(error)) {
1178+
if (controller === this.active_abort_controller) {
1179+
this.setInstallFromFilePhase('ready')
1180+
this.install_from_file_status_text = ''
1181+
await kraken.uninstallExtensionVersion(extension.identifier, extension.tag)
1182+
.catch(() => { /* version may not be registered yet */ })
1183+
notifier.pushInfo('EXTENSION_INSTALL_CANCELLED', 'Installation from file was cancelled.', true)
1184+
}
1185+
} else {
1186+
this.applyInstallFromFileError(String(error))
1187+
this.showAlertError(error)
1188+
notifier.pushBackError('EXTENSION_FINALIZE_FAIL', error)
1189+
}
11441190
} finally {
1145-
this.resetPullOutput()
1191+
if (controller === this.active_abort_controller) this.finishInstallOperation()
11461192
}
11471193
},
11481194
setInstallingState(identifier: string, action: 'install' | 'update'): void {

0 commit comments

Comments
 (0)