Browse Source

support Task cancellation in plugin UI

David Sehnal 5 years ago
parent
commit
04d39e0e8d

+ 4 - 0
src/mol-plugin/context.ts

@@ -176,6 +176,10 @@ export class PluginContext {
         return this.tasks.run(task);
     }
 
+    requestTaskAbort(task: Task<any> | number, reason?: string) {
+        this.tasks.requestAbort(task, reason);
+    }
+
     dispose() {
         if (this.disposed) return;
         this.commands.dispose();

+ 1 - 1
src/mol-plugin/skin/base/components/tasks.scss

@@ -120,7 +120,7 @@
             > button {
                 display: inline-block;       
                 margin-top: -3px;
-                font-size: 140%;
+                // font-size: 140%;
             }
         }        
     }

+ 1 - 3
src/mol-plugin/ui/plugin.tsx

@@ -141,9 +141,7 @@ export class ViewportWrapper extends PluginUIComponent {
                 <StateSnapshotViewportControls />
             </div>
             <ViewportControls />
-            <div style={{ position: 'absolute', left: '10px', bottom: '10px' }}>
-                <BackgroundTaskProgress />
-            </div>
+            <BackgroundTaskProgress />
             <div className='msp-highlight-toast-wrapper'>
                 <LociLabels />
                 <Toasts />

+ 2 - 0
src/mol-plugin/ui/state/common.tsx

@@ -144,6 +144,8 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
         this.setState({ busy: true });
         try {
             await this.applyAction();
+        } catch {
+            // eat errors because they should be handled elsewhere
         } finally {
             this.busy.next(false);
         }

+ 16 - 4
src/mol-plugin/ui/task.tsx

@@ -10,6 +10,7 @@ import { OrderedMap } from 'immutable';
 import { TaskManager } from '../../mol-plugin/util/task-manager';
 import { filter } from 'rxjs/operators';
 import { Progress } from '../../mol-task';
+import { IconButton } from './controls/common';
 
 export class BackgroundTaskProgress extends PluginUIComponent<{ }, { tracked: OrderedMap<number, TaskManager.ProgressEvent> }> {
     componentDidMount() {
@@ -24,13 +25,18 @@ export class BackgroundTaskProgress extends PluginUIComponent<{ }, { tracked: Or
     state = { tracked: OrderedMap<number, TaskManager.ProgressEvent>() };
 
     render() {
-        return <div>
+        return <div className='msp-background-tasks'>
             {this.state.tracked.valueSeq().map(e => <ProgressEntry key={e!.id} event={e!} />)}
         </div>;
     }
 }
 
 class ProgressEntry extends PluginUIComponent<{ event: TaskManager.ProgressEvent }> {
+
+    abort = () => {
+        this.plugin.requestTaskAbort(this.props.event.id, 'User Request');
+    }
+
     render() {
         const root = this.props.event.progress.root;
         const subtaskCount = countSubtasks(this.props.event.progress.root) - 1;
@@ -39,9 +45,15 @@ class ProgressEntry extends PluginUIComponent<{ event: TaskManager.ProgressEvent
             : <>[{root.progress.current}/{root.progress.max}]</>;
         const subtasks = subtaskCount > 0
             ? <>[{subtaskCount} subtask(s)]</>
-            : void 0
-        return <div>
-            {root.progress.message} {pr} {subtasks}
+            : void 0;
+
+        return <div className='msp-task-state'>
+            <div>
+                {root.progress.canAbort && <IconButton onClick={this.abort} icon='abort' title='Abort' />}
+                <div>
+                    {root.progress.message} {pr} {subtasks}
+                </div>
+            </div>
         </div>;
     }
 }

+ 8 - 7
src/mol-task/execution/observable.ts

@@ -27,7 +27,7 @@ export function ExecuteInContext<T>(ctx: RuntimeContext, task: Task<T>) {
 }
 
 export function ExecuteObservableChild<T>(ctx: RuntimeContext, task: Task<T>, progress?: string | Partial<RuntimeContext.ProgressUpdate>) {
-    return (ctx as ObservableRuntimeContext).runChild(task, progress);
+    return (ctx as ObservableRuntimeContext).runChild(task as ExposedTask<T>, progress);
 }
 
 export namespace ExecuteObservable {
@@ -99,7 +99,7 @@ async function execute<T>(task: ExposedTask<T>, ctx: ObservableRuntimeContext) {
         UserTiming.markEnd(task)
         UserTiming.measure(task)
         if (ctx.info.abortToken.abortRequested) {
-            abort(ctx.info, ctx.node);
+            abort(ctx.info);
         }
         return ret;
     } catch (e) {
@@ -108,20 +108,21 @@ async function execute<T>(task: ExposedTask<T>, ctx: ObservableRuntimeContext) {
             if (ctx.node.children.length > 0) {
                 await new Promise(res => { ctx.onChildrenFinished = res; });
             }
-            if (task.onAbort) task.onAbort();
+            if (task.onAbort) {
+                task.onAbort();
+            }
         }
         if (ExecuteObservable.PRINT_ERRORS_TO_STD_ERR) console.error(e);
         throw e;
     }
 }
 
-function abort(info: ProgressInfo, node: Progress.Node) {
+function abort(info: ProgressInfo) {
     if (!info.abortToken.treeAborted) {
         info.abortToken.treeAborted = true;
         abortTree(info.root);
         notifyObserver(info, now());
     }
-
     throw Task.Aborted(info.abortToken.reason);
 }
 
@@ -157,7 +158,7 @@ class ObservableRuntimeContext implements RuntimeContext {
 
     private checkAborted() {
         if (this.info.abortToken.abortRequested) {
-            abort(this.info, this.node);
+            abort(this.info);
         }
     }
 
@@ -205,7 +206,7 @@ class ObservableRuntimeContext implements RuntimeContext {
         return Scheduler.immediatePromise();
     }
 
-    async runChild<T>(task: Task<T>, progress?: string | Partial<RuntimeContext.ProgressUpdate>): Promise<T> {
+    async runChild<T>(task: ExposedTask<T>, progress?: string | Partial<RuntimeContext.ProgressUpdate>): Promise<T> {
         this.updateProgress(progress);
 
         // Create a new child context and add it to the progress tree.

+ 2 - 2
src/mol-task/task.ts

@@ -54,9 +54,9 @@ namespace Task {
         }
     }
 
-    export interface Aborted { isAborted: true, reason: string }
+    export interface Aborted { isAborted: true, reason: string, toString(): string }
     export function isAbort(e: any): e is Aborted { return !!e && !!e.isAborted; }
-    export function Aborted(reason: string): Aborted { return { isAborted: true, reason }; }
+    export function Aborted(reason: string): Aborted { return { isAborted: true, reason, toString() { return `Aborted${reason ? ': ' + reason : ''}`; } }; }
 
     export function create<T>(name: string, f: (ctx: RuntimeContext) => Promise<T>, onAbort?: () => void): Task<T> {
         return new Impl(name, f, onAbort);

+ 35 - 29
src/mol-util/data-source.ts

@@ -91,13 +91,19 @@ function readData(ctx: RuntimeContext, action: string, data: XMLHttpRequest | Fi
             reject(error ? error : 'Failed.');
         };
 
-        data.onabort = () => reject(Task.Aborted(''));
-
+        let hasError = false;
         data.onprogress = (e: ProgressEvent) => {
-            if (e.lengthComputable) {
-                ctx.update({ message: action, isIndeterminate: false, current: e.loaded, max: e.total });
-            } else {
-                ctx.update({ message: `${action} ${(e.loaded / 1024 / 1024).toFixed(2)} MB`, isIndeterminate: true });
+            if (!ctx.shouldUpdate || hasError) return;
+
+            try {
+                if (e.lengthComputable) {
+                    ctx.update({ message: action, isIndeterminate: false, current: e.loaded, max: e.total });
+                } else {
+                    ctx.update({ message: `${action} ${(e.loaded / 1024 / 1024).toFixed(2)} MB`, isIndeterminate: true });
+                }
+            } catch (e) {
+                hasError = true;
+                reject(e);
             }
         }
         data.onload = (e: any) => resolve(e);
@@ -178,36 +184,36 @@ async function processAjax(ctx: RuntimeContext, asUint8Array: boolean, decompres
 function ajaxGetInternal(title: string | undefined, url: string, type: 'json' | 'xml' | 'string' | 'binary', decompressGzip: boolean, body?: string): Task<string | Uint8Array> {
     let xhttp: XMLHttpRequest | undefined = void 0;
     return Task.create(title ? title : 'Download', async ctx => {
-        try {
-            const asUint8Array = type === 'binary';
-            if (!asUint8Array && decompressGzip) {
-                throw 'Decompress is only available when downloading binary data.';
-            }
+        const asUint8Array = type === 'binary';
+        if (!asUint8Array && decompressGzip) {
+            throw 'Decompress is only available when downloading binary data.';
+        }
 
-            xhttp = RequestPool.get();
+        xhttp = RequestPool.get();
 
-            xhttp.open(body ? 'post' : 'get', url, true);
-            xhttp.responseType = asUint8Array ? 'arraybuffer' : 'text';
-            xhttp.send(body);
+        xhttp.open(body ? 'post' : 'get', url, true);
+        xhttp.responseType = asUint8Array ? 'arraybuffer' : 'text';
+        xhttp.send(body);
 
-            ctx.update({ message: 'Waiting for server...', canAbort: true });
-            const e = await readData(ctx, 'Downloading...', xhttp, asUint8Array);
-            const result = await processAjax(ctx, asUint8Array, decompressGzip, e)
+        await ctx.update({ message: 'Waiting for server...', canAbort: true });
+        const e = await readData(ctx, 'Downloading...', xhttp, asUint8Array);
+        xhttp = void 0;
+        const result = await processAjax(ctx, asUint8Array, decompressGzip, e)
 
-            if (type === 'json') {
-                ctx.update({ message: 'Parsing JSON...', canAbort: false });
-                return JSON.parse(result);
-            } else if (type === 'xml') {
-                ctx.update({ message: 'Parsing XML...', canAbort: false });
-                return parseXml(result);
-            }
+        if (type === 'json') {
+            await ctx.update({ message: 'Parsing JSON...', canAbort: false });
+            return JSON.parse(result);
+        } else if (type === 'xml') {
+            await ctx.update({ message: 'Parsing XML...', canAbort: false });
+            return parseXml(result);
+        }
 
-            return result;
-        } finally {
+        return result;
+    }, () => {
+        if (xhttp) {
+            xhttp.abort();
             xhttp = void 0;
         }
-    }, () => {
-        if (xhttp) xhttp.abort();
     });
 }