Execute command in the terminal from plugin action

Answered

Hello.

I`m developing a plugin for simplify some routine commands executing by clicking on action.

Now I`m using com.intellij.execution.configurations.GeneralCommandLine class for executing and it`s works fine. But would be great to open the terminal panel in idea and execute command in it.

Is it possible from plugin code?

23 comments
Comment actions Permalink
Official comment

Hey,

To run a command in Terminal tool window, you can use approach used by "Shell Script" plugin. It runs a script inside a shell process in Terminal tool window. See ShTerminalRunner and org.jetbrains.plugins.terminal.ShellTerminalWidget#executeCommand. API is available in 193 major version only.

Comment actions Permalink

Hi Sergey - is there no way to achieve this pre 193? (even if it means using something lower level) Specifically I'm wondering for versions 2019.1 and up.

0
Comment actions Permalink

Hi Etan, nope, there is no such capability in pre 193.

0
Comment actions Permalink

This is great. Thank you. We are on 2020.2.3 so it works for us.

Question: I would like to attach a processListener to it so can do some other stuff when process is finished. Any ideas? 

0
Comment actions Permalink

Once you have `ShellTerminalWidget` instance:

ProcessTtyConnector connector = getProcessTtyConnector(shellTerminalWidget.getTtyConnector());
if (connector != null) {
Process process = connector.getProcess();
process.waitFor();
// do some stuff
}

 

0
Comment actions Permalink

Hi Sergey

The process is done only when the terminal tab is closed. I was looking for more about when a particular command that is sent to the process is finished.

The following does help

if (shellTerminalWidget.hasRunningCommands()) 

But if I am running a shell script or a python script, that is waiting for user input, this does not really work. It says, no running commands when the command is waiting for user input.

I guess I can look at the output and look for prompt to determine if a command is done.

But if you know of any better way to accomplish this, I ll appreciate that. 
Thanks

0
Comment actions Permalink

Errata..

(shellTerminalWidget.hasRunningCommands())  seems to be working even for the above mentioned case now.  I ll dig in more. 

 

But it would have been great to be able to attach a listener (get a processHandler for each command) to the command to get notified of command started, terminated etc. 

0
Comment actions Permalink

Ah, got it. No, there is no such API. I guess the best way is to listen for changes in terminal output and run `shellTerminalWidget.hasRunningCommands()`:

 

shellTerminalWidget.getTerminalTextBuffer().addModelListener(() -> {
if (shellTerminalWidget.hasRunningCommands()) {
// a process is running
}
else {
// no processes are running
}
});
0
Comment actions Permalink

The model listener works but then the listener is attached to that terminal tab.. anything else run in future in that terminal sends events to the listener even after the main command that I was interested in, has finished. Easy solution but extra events fired every time.

Another approach I tried and that worked was starting a background process once the command is started, keep monitoring until shellTerminalWidget.hasRunningCommands().. once it returns false, I send the notification to the listener.

What do you think about this approach? Am I missing something here or are there any other things I should be cautious of using this approach?

 

ProgressManager.getInstance()
.run(new CommandMonitoringBackgroundTask(shellTerminalWidget, processListener, project, commandLineString));
static class CommandMonitoringBackgroundTask extends Task.Backgroundable {

ShellTerminalWidget shellTerminalWidget;
ProcessListener processListener;
String commandline;

CommandMonitoringBackgroundTask(ShellTerminalWidget shellTerminalWidget, ProcessListener processListener,
Project project, String commandLineString) {
super(project, commandLineString, true);
this.processListener = processListener;
this.shellTerminalWidget = shellTerminalWidget;
this.commandline = commandLineString;
}

@Override
public void run(@NotNull ProgressIndicator indicator) {
if (shellTerminalWidget.hasRunningCommands()) {
while (shellTerminalWidget.hasRunningCommands()) {
// wait until done
}
}
processListener.processTerminated(new ProcessEvent(new EmptyProcessHandler());
}

 

0
Comment actions Permalink

Looks good to me.

The question is how `wait until done` is implemented. For example, a straightforward approach would be like this:

while (shellTerminalWidget.hasRunningCommands()) {
Thread.sleep(100);
}

However, seems it'd be better to check `shellTerminalWidget.hasRunningCommands()` only when terminal text is changed (assuming that shell prompt will be shown when a shell command is terminated). This would be a bit more performance friendly as `hasRunningCommands` is not free. Also, `CommandMonitoringBackgroundTask` would likely finish as soon as the command is terminated, thus no delay. To listen to terminal text changes, you would need `shellTerminalWidget.getTerminalTextBuffer().addModelListener(modelListener)`. When `modelListener` is no longer needed, it can be removed with `shellTerminalWidget.getTerminalTextBuffer().removeModelListener(modelListener)`.

0
Comment actions Permalink

I agree that 'hasRunningCommands' should be called on the model change. But we have to remove the modelListener once there are no running commands and that needs to happen in the modelChanged() method. 

Unfortunately doing this in the modelChanged() will lead to ConcurrentModificationException as the backing list is an ArrayList for modelListeners in TerminalTextBuffer class

this.myListeners = Lists.newArrayList();


Seems like I choose one of the 2 options:

1. Have expensive call to hasRunningCommands() continuously (but some commands are going to run for 10mins. for example 'build' in terminal)

2. Keep the model listeners attached and keep notifying them for any other changes in the terminal.

I feel like 2nd option is the better one. 
And if I go with the 2nd option, I don't think I need to do this in the background process either. 

0
Comment actions Permalink

Indeed, ConcurrentModificationException is possible in 2020.2.*, it's fixed in 2020.3 by replacing "Lists.newArrayList()" with "new CopyOnWriteArrayList<>()".

Even with model listener, I'd still recommend to call "hasRunningCommands()" in a background thread. Because the model might be updated surprisingly frequently. Ideally, implement some throttling when calling "hasRunningCommands()", e.g. no more than once every 100 ms.

I think keeping model listener attached shouldn't harm performance much.

0
Comment actions Permalink

Hey Sergey!
I try to use this way, but it give Caused by: java.lang.ClassNotFoundException: org.jetbrains.plugins.terminal.ShellTerminalWidget PluginClassLoader(plugin=PluginDescriptor(name=Auto Tester, id=it.unisa.HelloWorldPlugin, descriptorPath=plugin.xml, path=~/Downloads/HelloWorldPlugin-main/build/idea-sandbox/plugins/HelloWorldPlugin, version=0.1.SNAPSHOT, package=null), packagePrefix=null, instanceId=22, state=active) error, how I can it solve? (I'm use

id 'org.jetbrains.intellij' version '0.7.2')
0
Comment actions Permalink

Hi! What's the version of the IntelliJ Platform IDE that will be used to build the plugin? See `intellij.version` property in your gradle script (https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties). I'd recommend to update `org.jetbrains.intellij` to the latest version which is 1.1.4 as of now.

Have you declared dependency on `org.jetbrains.plugins.terminal` plugin in your plugin.xml? See https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html#dependency-declaration-in-pluginxml

0
Comment actions Permalink

Thank you very much, I missed add dependences to the xml))))

0
Comment actions Permalink

Hi Sergey.

There is an action in my plugin that essentially executes a command in terminal. Following your answer, I implemented the action like the following:

public class MyAction extends BaseAction {
@Override
protected void performAction(AnActionEvent event) {
    Project project = event.getProject();
    TerminalView terminalView = TerminalView.getInstance(project);
    String command = "the command";
    try {
        terminalView.createLocalShellWidget(project.getBasePath(), "Name").executeCommand(command);
    } catch (IOException err) {
        err.printStackTrace();
    }
  }

}

 

The issue is that each time the action is performed, a new terminal session is created. The behavior I want is that the command is executed in the previous terminal session. In the case the action is performed for the first time, I want a terminal session created and the command is executed there.

How can I achieve that?

0
Comment actions Permalink

Hi,

A new terminal sessions is created on each action execution, because terminalView.createLocalShellWidget is called each time. You just need to reuse previously created instances. For example, take a look how Shell Script execution in terminal is implemented (com.intellij.sh.run.terminal.ShTerminalRunner).

0
Comment actions Permalink

Thank you, Sergey!

I have achieved what I wanted by adapting the example of ShTerminalRunner. The way I get a previously created terminal session is like

Content content = contentManager.findContent("tab name");
JBTerminalWidget widget = TerminalView.getWidgetByContent(content);

 

While looking into ShTerminalRunner I found two issues.

One issue is about format of working directory. A path returned by Project#getBasePath is system-independent and is like "F:/path/to/project" on my Windows machine. While a path returned by TerminalWorkingDirectoryManager#getWorkingDirectory is system-dependent and is like "F:\path\to\project\". So I think comparing two strings of working directory here is problematic.

Another issue is about ContentManager#getSelectedContent. I observed that if initially a terminal session was created by TerminalView#createLocalShellWidget and was focused on, and then a call of ContentManager#getSelectedContent would return null. So it seems that one has to call ContentManager#setSelectedContent to have a content selected. Is that the case?

 

0
Comment actions Permalink

I can't seem to declare the dependency:

0
Comment actions Permalink

Mark, please make sure `Terminal` plugin is configured for project. For example, if you're using Gradle with gradle-intellij-plugin, add

intellij {
...
plugins = ['org.jetbrains.plugins.terminal']
}

See https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html#2-project-setup for the details.

0
Comment actions Permalink

Unfortunately, even with that, it still shows it as unresolved.

0
Comment actions Permalink

Hi Zjsdut,

Sorry for the long delay. Thanks for reporting problem about comparing two paths. It will be fixed in 2022.2 I guess (IDEA-290641).

Couldn't reproduce the problem with TerminalView#createLocalShellWidget and ContentManager#getSelectedContent, as I can see contentManager.setSelectedContent is called always for newly created tabs.

0
Comment actions Permalink

Mark, please share an example project where the problem is reproduced.

0

Please sign in to leave a comment.