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?

1
29 comments
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.

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

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

0

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

Once you have `ShellTerminalWidget` instance:

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

 

0

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

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

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

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

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

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

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

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

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

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

0

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

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

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

I can't seem to declare the dependency:

0

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

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

0

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

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

0

I'm using a project generated from the template project that can be found at:
JetBrains/intellij-platform-plugin-template: Template repository for creating plugins for IntelliJ Platform (github.com)

With this configuration, add the dependency to gradle.properties:

And then to plugin.xml:

0

Here's a fully working kotlin example for an action that will call "ng help" in the same "Your Tab Name" terminal window when you right click in the project view and select "Open in Local Terminal"


import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.terminal.JBTerminalWidget
import com.tcubedstudios.angularstudio.terminal.utils.getEventProject
import org.jetbrains.plugins.terminal.ShellTerminalWidget
import org.jetbrains.plugins.terminal.TerminalToolWindowFactory
import org.jetbrains.plugins.terminal.TerminalView
import java.io.IOException


// https://intellij-support.jetbrains.com/hc/en-us/community/posts/360005329339-Execute-command-in-the-terminal-from-plugin-action?page=1#community_comment_6186561275794
// https://github.com/JetBrains/intellij-community/blob/95ab6a1ecfdf49754f7eb5a81984cdc2c4fa0ca5/plugins/sh/src/com/intellij/sh/run/ShTerminalRunner.java
class RunCommandInLocalTerminalAction: DumbAwareAction() {//BaseAction in example
companion object {
val LOGGY = Logger.getInstance(RunCommandInLocalTerminalAction::class.java)
val TAB_NAME = "Your Tab Name"
}

override fun update(event: AnActionEvent) {
val project = event.getEventProject()
event.presentation.isEnabledAndVisible = true//project != null
}

override fun actionPerformed(event: AnActionEvent) {
when (val project = event.getEventProject()) {
null -> LOGGY.error("Cannot run command in local terminal. Project is null")
else -> {
try {
val terminalView = TerminalView.getInstance(project)
val window = ToolWindowManager.getInstance(project).getToolWindow(TerminalToolWindowFactory.TOOL_WINDOW_ID)
val contentManager = window?.contentManager

val widget = when (val content = contentManager?.findContent(TAB_NAME)) {
null -> terminalView.createLocalShellWidget(project.basePath, TAB_NAME)
else -> TerminalView.getWidgetByContent(content) as ShellTerminalWidget
}

widget.executeCommand("ng help")

} catch (e: IOException) {
LOGGY.error("Cannot run command in local terminal. Error:$e")
}
}
}
}
}

fun AnActionEvent?.getEventProject(): Project? = AnAction.getEventProject(this)
0

TerminalView (1) (scheduled for removal in a future release)

Could you please provide any references for replacement?

0

Please use `org.jetbrains.plugins.terminal.TerminalToolWindowManager` instead of `TerminalView`.

0

if the command contains ‘sudo’ , eg: ‘sudo apt install xxx',   

  it ask user input password for root, how to avoid this step ?   

I meaning execute command that contains ‘sudo'  does not ask user input password

 

0

No, it's not possible to execute `sudo …` command without prompting for a password..

0

Please sign in to leave a comment.