How to stop a TaskCommandAction

If a task provides a TaskCommandAction for the run action that is persistent, how do I communicate to the API how that task can be stopped?

For example, assume that the linked Command will start a Process. How do I tell the API that when the user clicks Stop, that process should be terminated? Currently, nothing happens when Stop is clicked. I assume I need to return something from the command, but I do not find any information about what that might be.

Edit: Experiments have shown that a Process that got started gets terminated when the Command implementation returns (I am not sure what exactly happens, but I see that onStdout is not called anymore even though the process should still generate output). That leaves me wondering how I can implement a persistent action at all.

There’s currently no API for this, but I will consider this a feature request! For the time being, it’s probably better to use a TaskProcessAction. If you need to perform operations in your JavaScript sandbox to set it up on run, you can use a resolvable action through TaskResolvableAction.

Thanks for clarifying this. Sadly, the TaskProcessAction is not suitable for what I’m doing.

I want run latexmk in continuous preview mode to render a PDF from the LaTeX source in the editor, so that every change automatically updates the PDF. I cannot do this as TaskProcessAction for these reasons:

  • An Issue Matcher I register for that action will generate an issue if the source has an error, but will not remove the issue when the user fixes it. I have found no way to tell the issue matcher to clear the issues when latexmk re-runs the build (that would need to be recognized from latexmk’s output).
  • The PDF preview has the ability to emit source file and line when I click into the PDF and I want Nova to navigate to that position. Since this does not seem to be able via Nova’s command line (my other question about that remains unanswered) I figured I’ll run a Process that reads a named pipe where the previewer can write its source locations into. For this to work, I need to run that Process along with the external command. I did try to start this process in a TaskResolvableAction while returning a TaskProcessAction to start the external command, but the Process was silently terminated when the TaskResolvableAction returned.

Generally, I’d like to recommend to specify in the documentation of Process how and when it will be terminated. The current behavior of it being silently shut down (even without calling onDidExit) is quite surprising.

What I am wondering about is that Process does have this JSON-RPC API which implies it is designed for more complex things that run for a longer time, but a Process lifetime seems to be severely limited, only lasting until the command returns. Is there some way to use a Process I am overlooking where it can last a longer period of time?

Processes are terminated if they are collected by the garbage collector. That is likely what is happening here for you.

You’ll need to hold a strong reference to it somewhere to keep it active.

Indeed, holding a strong reference will keep the process running.

I’ll post my example code here for reference if someone else has this problem. What this code does is to define a task template that is to be instantiated in Project Settings. Clicking Run will then create a named pipe test.fifo and start a Process reading from that pipe and logging received lines. It will tell Nova to execute the script test.sh as TaskProcessAction. That script will repeatedly write „spam“ to test.fifo and delete the named pipe when it is stopped via Nova. The Process reading the named pipe will end automatically since the named pipe closes when the last writer vanishes.

I recommend not clicking on the test.fifo file in the Files Sidebar as Nova currently freezes if you do that (I did already file a bug).

Scripts/main.js

nova.assistants.registerTaskAssistant({
    provideTasks: function() { return null; },
    resolveTaskAction: function(context) {
        return new Promise((resolve, reject) => {
           let mkfifo = new Process("/usr/bin/mkfifo", {
               args: ["test.fifo"],
               cwd: nova.workspace.path
           });
           mkfifo.onDidExit((status) => {
               if (status == 0) {
                   globalThis.curProc = new Process("/bin/cat", {
                       args: ["test.fifo"],
                       cwd: nova.workspace.path
                   });
                   globalThis.curProc.onStdout((line) => {
                       console.log("received line: " + line);
                   });
                   globalThis.curProc.start();
                   resolve(new TaskProcessAction("/bin/sh", {
                       args: [nova.path.join(nova.extension.path, "Scripts", "test.sh")],
                       cwd: nova.workspace.path
                   }));
               } else {
                   reject("FIFO creation failed with exit status " + status);
               }
           });
           mkfifo.start();
        });
    }
}, {identifier: "org.flyx.test.resolver"});

Scripts/test.sh

trap "rm test.fifo; kill 0" SIGINT SIGTERM EXIT
perl -e 'open my $f, ">", "test.fifo"; $f->autoflush(1); for (;1;) {print $f "spam.\n"; sleep 1}'

extension.json

{
    "identifier": "org.flyx.test",
    "name": "test",
    "organization": "Felix Krause",
    "description": "Lorem ipsum, dolor sit amet.",
    "version": "1.0",
    "categories": ["tasks"],
    
    "main": "main.js",
    
    "entitlements": {
        "filesystem": "readwrite", "process": true
    },
    
    "taskTemplates": {
        "test123": {
            "name": "ProcessTest",
            "description": "foo",
            "tasks": {
                "run": {
                    "resolve": "org.flyx.test.resolver",
                    "persistent": true
                }
            }
        }
    }
}
1 Like