Xterm view for OS terminal in Eclipse
This commit is contained in:
6
eclipse-extensions/org.springframework.ide.eclipse.xterm/.gitignore
vendored
Normal file
6
eclipse-extensions/org.springframework.ide.eclipse.xterm/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/target/
|
||||
/.settings/
|
||||
.DS_Store
|
||||
/**/.DS_Store
|
||||
.project
|
||||
.classpath
|
||||
@@ -0,0 +1,20 @@
|
||||
Manifest-Version: 1.0
|
||||
Bundle-ManifestVersion: 2
|
||||
Bundle-Name: org.springframework.ide.eclipse.xterm
|
||||
Bundle-SymbolicName: org.springframework.ide.eclipse.xterm;singleton:=true
|
||||
Bundle-Version: 4.8.1.qualifier
|
||||
Automatic-Module-Name: org.springframework.ide.eclipse.xterm
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
|
||||
Bundle-Activator: org.springframework.ide.eclipse.xterm.XtermPlugin
|
||||
Eclipse-BundleShape: dir
|
||||
Require-Bundle: org.eclipse.ui;bundle-version="3.115.0",
|
||||
org.eclipse.core.runtime,
|
||||
org.eclipse.core.resources,
|
||||
org.apache.httpcomponents.httpclient,
|
||||
org.apache.httpcomponents.httpcore,
|
||||
org.eclipse.wildwebdeveloper.embedder.node
|
||||
Import-Package: javax.inject
|
||||
Bundle-ClassPath: .
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Export-Package: org.springframework.ide.eclipse.xterm,
|
||||
org.springframework.ide.eclipse.xterm.views
|
||||
@@ -0,0 +1,7 @@
|
||||
source.. = src/
|
||||
output.. = bin/
|
||||
bin.includes = META-INF/,\
|
||||
.,\
|
||||
plugin.xml,\
|
||||
css/,\
|
||||
icons/
|
||||
@@ -0,0 +1,9 @@
|
||||
/* See bug 466075 about the pseudo-selector ":org-springframework-tooling-ls-eclipse-commons" */
|
||||
IEclipsePreferences#org-eclipse-ui-workbench:org-springframework-ide-eclipse-xterm {
|
||||
preferences:
|
||||
'org.springframework.ide.eclipse.xterm.background=41,41,41'
|
||||
'org.springframework.ide.eclipse.xterm.foreground=255,255,255'
|
||||
'org.springframework.ide.eclipse.xterm.selection=235,235,235'
|
||||
'org.springframework.ide.eclipse.xterm.cursor=255,255,255'
|
||||
'org.springframework.ide.eclipse.xterm.cursorAccent=255,255,255'
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 555 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 398 B |
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://web.resource.org/cc/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" version="1.1" baseProfile="full" width="100px" height="100px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" id="svg_document" style="zoom: 1;"><!-- Created with macSVG - https://macsvg.org/ - https://github.com/dsward2/macsvg/ --><title id="svg_document_title">Untitled.svg</title><defs id="svg_document_defs"></defs><g id="main_group"><rect stroke="#000000" id="rect1" stroke-width="nonepx" x="0px" rx="20px" width="100px" y="0px" fill="0" transform="" ry="20px" height="97.116px"></rect></g><polyline points="15,30 40,50 15,70 " stroke="white" id="polyline1" stroke-width="15px" fill="none" stroke-linecap="round" stroke-linejoin="round" transform=""></polyline><line stroke="white" y1="70px" id="line1" stroke-width="15px" x1="55px" y2="70px" stroke-linecap="round" x2="80px" transform=""></line></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 653 B |
@@ -0,0 +1,173 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?eclipse version="3.4"?>
|
||||
<plugin>
|
||||
<extension
|
||||
point="org.eclipse.ui.views">
|
||||
<view
|
||||
category="org.eclipse.ui"
|
||||
class="org.springframework.ide.eclipse.xterm.views.TerminalView"
|
||||
icon="icons/terminal.png"
|
||||
id="org.springframework.ide.eclipse.xterm.views.TerminalView"
|
||||
inject="true"
|
||||
name="Xterm">
|
||||
</view>
|
||||
</extension>
|
||||
<extension
|
||||
point="org.eclipse.e4.ui.css.swt.theme">
|
||||
<stylesheet
|
||||
uri="css/terminal-dark-theme.css">
|
||||
<themeid
|
||||
refid="org.eclipse.e4.ui.css.theme.e4_dark">
|
||||
</themeid>
|
||||
</stylesheet>
|
||||
</extension>
|
||||
<extension
|
||||
point="org.eclipse.ui.themes">
|
||||
<colorDefinition
|
||||
categoryId="org.springframework.ide.eclipse.xterm"
|
||||
id="org.springframework.ide.eclipse.xterm.background"
|
||||
isEditable="true"
|
||||
label="Background Color"
|
||||
value="255,255,255">
|
||||
</colorDefinition>
|
||||
<colorDefinition
|
||||
categoryId="org.springframework.ide.eclipse.xterm"
|
||||
id="org.springframework.ide.eclipse.xterm.foreground"
|
||||
label="Foreground Color"
|
||||
value="0,0,0">
|
||||
</colorDefinition>
|
||||
<colorDefinition
|
||||
categoryId="org.springframework.ide.eclipse.xterm"
|
||||
id="org.springframework.ide.eclipse.xterm.selection"
|
||||
label="Selection Color"
|
||||
value="45,45,45">
|
||||
</colorDefinition>
|
||||
<colorDefinition
|
||||
categoryId="org.springframework.ide.eclipse.xterm"
|
||||
id="org.springframework.ide.eclipse.xterm.cursor"
|
||||
label="Cursor Color"
|
||||
value="0,0,0">
|
||||
</colorDefinition>
|
||||
<colorDefinition
|
||||
categoryId="org.springframework.ide.eclipse.xterm"
|
||||
id="org.springframework.ide.eclipse.xterm.cursorAccent"
|
||||
label="Cursor Accent Color"
|
||||
value="0,0,0">
|
||||
</colorDefinition>
|
||||
<themeElementCategory
|
||||
id="org.springframework.ide.eclipse.xterm"
|
||||
label="Xterm">
|
||||
<description>
|
||||
Xterm terminal apperance theme settings
|
||||
</description>
|
||||
</themeElementCategory>
|
||||
<fontDefinition
|
||||
categoryId="org.springframework.ide.eclipse.xterm"
|
||||
defaultsTo="org.eclipse.jface.textfont"
|
||||
id="org.springframework.ide.eclipse.xterm.font"
|
||||
label="Font">
|
||||
</fontDefinition>
|
||||
</extension>
|
||||
<extension
|
||||
point="org.eclipse.ui.menus">
|
||||
<menuContribution
|
||||
allPopups="false"
|
||||
locationURI="popup:org.eclipse.ui.popup.any?before=group.edit">
|
||||
<command
|
||||
commandId="org.springframework.ide.eclipse.xterm.open"
|
||||
icon="icons/terminal.png"
|
||||
id="org.springframework.ide.eclipse.xterm.open"
|
||||
label="Open Xterm"
|
||||
style="push"
|
||||
tooltip="Open terminal for the folder">
|
||||
<visibleWhen
|
||||
checkEnabled="false">
|
||||
<and>
|
||||
<count
|
||||
value="1">
|
||||
</count>
|
||||
<iterate>
|
||||
<or>
|
||||
<adapt
|
||||
type="org.eclipse.core.resources.IProject">
|
||||
<test
|
||||
property="org.eclipse.core.resources.open"
|
||||
value="true">
|
||||
</test>
|
||||
</adapt>
|
||||
<and>
|
||||
<instanceof
|
||||
value="org.eclipse.core.resources.IProject">
|
||||
</instanceof>
|
||||
<test
|
||||
property="org.eclipse.core.resources.open"
|
||||
value="true">
|
||||
</test>
|
||||
</and>
|
||||
</or>
|
||||
</iterate>
|
||||
</and>
|
||||
</visibleWhen>
|
||||
</command>
|
||||
</menuContribution>
|
||||
</extension>
|
||||
<extension
|
||||
point="org.eclipse.ui.handlers">
|
||||
<handler
|
||||
class="org.springframework.ide.eclipse.xterm.views.OpenTerminalHandler"
|
||||
commandId="org.springframework.ide.eclipse.xterm.open">
|
||||
<enabledWhen>
|
||||
<and>
|
||||
<count
|
||||
value="1">
|
||||
</count>
|
||||
<iterate>
|
||||
<or>
|
||||
<adapt
|
||||
type="org.eclipse.core.resources.IProject">
|
||||
<test
|
||||
property="org.eclipse.core.resources.open"
|
||||
value="true">
|
||||
</test>
|
||||
</adapt>
|
||||
<and>
|
||||
<instanceof
|
||||
value="org.eclipse.core.resources.IProject">
|
||||
</instanceof>
|
||||
<test
|
||||
property="org.eclipse.core.resources.open"
|
||||
value="true">
|
||||
</test>
|
||||
</and>
|
||||
</or>
|
||||
</iterate>
|
||||
</and>
|
||||
</enabledWhen>
|
||||
</handler>
|
||||
</extension>
|
||||
<extension
|
||||
point="org.eclipse.ui.commands">
|
||||
<command
|
||||
categoryId="org.eclipse.ui.category.navigate"
|
||||
defaultHandler="org.springframework.ide.eclipse.xterm.views.OpenTerminalHandler"
|
||||
description="Open terminal for folder"
|
||||
id="org.springframework.ide.eclipse.xterm.open"
|
||||
name="Open Terminal">
|
||||
</command>
|
||||
</extension>
|
||||
<extension
|
||||
point="org.eclipse.core.runtime.preferences">
|
||||
<initializer
|
||||
class="org.springframework.ide.eclipse.xterm.views.XtermPreferencesInitializer">
|
||||
</initializer>
|
||||
</extension>
|
||||
<extension
|
||||
point="org.eclipse.ui.preferencePages">
|
||||
<page
|
||||
class="org.springframework.ide.eclipse.xterm.views.XtermPreferencePage"
|
||||
id="org.springframework.ide.eclipse.xterm."
|
||||
name="Xterm">
|
||||
</page>
|
||||
</extension>
|
||||
|
||||
</plugin>
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="https://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot.ide</groupId>
|
||||
<artifactId>org.springframework.boot.ide</artifactId>
|
||||
<version>4.8.1-SNAPSHOT</version>
|
||||
<relativePath>../../eclipse-distribution/pom.xml</relativePath>
|
||||
</parent>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
<artifactId>org.springframework.ide.eclipse.xterm</artifactId>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.eclipse.tycho</groupId>
|
||||
<artifactId>target-platform-configuration</artifactId>
|
||||
<version>${tycho-version}</version>
|
||||
<configuration>
|
||||
<resolver>p2</resolver>
|
||||
<pomDependencies>ignore</pomDependencies>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.eclipse.tycho</groupId>
|
||||
<artifactId>tycho-compiler-plugin</artifactId>
|
||||
<version>${tycho-version}</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.eclipse.tycho</groupId>
|
||||
<artifactId>tycho-source-plugin</artifactId>
|
||||
<version>${tycho-version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>plugin-source</id>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>plugin-source</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.eclipse.tycho</groupId>
|
||||
<artifactId>tycho-p2-plugin</artifactId>
|
||||
<version>${tycho-version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>second-generate-p2-metadata</id>
|
||||
<goals>
|
||||
<goal>p2-metadata</goal>
|
||||
</goals>
|
||||
<phase>verify</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,169 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2020 Pivotal, Inc.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* https://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* Pivotal, Inc. - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.eclipse.xterm;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
import org.eclipse.core.runtime.CoreException;
|
||||
import org.eclipse.core.runtime.IStatus;
|
||||
import org.eclipse.core.runtime.OperationCanceledException;
|
||||
import org.eclipse.core.runtime.Status;
|
||||
import org.eclipse.ui.IWorkbenchPage;
|
||||
import org.eclipse.ui.PlatformUI;
|
||||
import org.eclipse.ui.plugin.AbstractUIPlugin;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.springframework.ide.eclipse.xterm.views.TerminalView;
|
||||
|
||||
public class XtermPlugin extends AbstractUIPlugin {
|
||||
|
||||
private static XtermPlugin plugin;
|
||||
|
||||
private static final String PLUGIN_ID = "org.springframework.ide.eclipse.xterm";
|
||||
|
||||
public static final String BG_COLOR = "org.springframework.ide.eclipse.xterm.background";
|
||||
public static final String FG_COLOR = "org.springframework.ide.eclipse.xterm.foreground";
|
||||
public static final String SELECTION_COLOR = "org.springframework.ide.eclipse.xterm.selection";
|
||||
public static final String CURSOR_COLOR = "org.springframework.ide.eclipse.xterm.cursor";
|
||||
public static final String CURSOR_ACCENT_COLOR = "org.springframework.ide.eclipse.xterm.cursorAccent";
|
||||
public static final String FONT = "org.springframework.ide.eclipse.xterm.font";
|
||||
|
||||
public static final String PREFS_DEFAULT_SHELL_CMD = "org.springframework.ide.eclipse.xterm.defaultShellCmd";
|
||||
|
||||
private static final List<String> NO_SHOW_EXCEPTIONS = Arrays.asList(
|
||||
"ValueParseException",
|
||||
"CoreException"
|
||||
);
|
||||
|
||||
private XtermServiceProcessManager serviceManager = new XtermServiceProcessManager();
|
||||
|
||||
@Override
|
||||
public void start(BundleContext bundle) throws Exception {
|
||||
plugin = this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(BundleContext bundle) throws Exception {
|
||||
serviceManager.stopService();
|
||||
plugin = null;
|
||||
}
|
||||
|
||||
public static XtermPlugin getDefault() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
public static void log(String m, Throwable t) {
|
||||
getDefault().getLog().error(m, t);
|
||||
}
|
||||
|
||||
public void openTerminalView(String cmd, String cwd) {
|
||||
try {
|
||||
IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
|
||||
String terminalId = UUID.randomUUID().toString();
|
||||
TerminalView terminalView = (TerminalView) page.showView(TerminalView.ID, terminalId, IWorkbenchPage.VIEW_ACTIVATE);
|
||||
terminalView.startTerminal(terminalId, cmd, cwd);
|
||||
} catch (Exception e) {
|
||||
XtermPlugin.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
public CompletableFuture<String> xtermUrl(long timeout) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return serviceManager.serviceUrl(Duration.ofSeconds(10));
|
||||
} catch (Throwable t) {
|
||||
throw new CompletionException(t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void log(Throwable e) {
|
||||
if (isCancelation(e)) {
|
||||
//Don't log canceled operations, those aren't real errors.
|
||||
return;
|
||||
}
|
||||
try {
|
||||
XtermPlugin.getDefault().getLog().log(status(e));
|
||||
} catch (NullPointerException npe) {
|
||||
//Can happen if errors are trying to be logged during Eclipse's shutdown
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isCancelation(Throwable e) {
|
||||
if (e==null) {
|
||||
return false;
|
||||
}
|
||||
Throwable cause = e.getCause();
|
||||
boolean isCancel = (
|
||||
e instanceof OperationCanceledException ||
|
||||
e instanceof InterruptedException ||
|
||||
e instanceof CancellationException ||
|
||||
(
|
||||
e instanceof CoreException &&
|
||||
((CoreException)e).getStatus().getSeverity()==IStatus.CANCEL
|
||||
)
|
||||
);
|
||||
return isCancel || (
|
||||
cause!=null && //avoid npe's on recursive check
|
||||
cause!=e && //avoid infinite recursion on e == cause
|
||||
isCancelation(cause)
|
||||
);
|
||||
}
|
||||
|
||||
private static IStatus status(Throwable e) {
|
||||
if (isCancelation(e)) {
|
||||
return Status.CANCEL_STATUS;
|
||||
}
|
||||
return status(IStatus.ERROR, e);
|
||||
}
|
||||
|
||||
private static IStatus status(int severity, Throwable e) {
|
||||
if (e instanceof CoreException) {
|
||||
IStatus status = ((CoreException) e).getStatus();
|
||||
if (status != null && status.getSeverity() == severity) {
|
||||
Throwable ee = status.getException();
|
||||
if (ee != null) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Status(severity, XtermPlugin.PLUGIN_ID, getMessage(e), e);
|
||||
}
|
||||
|
||||
private static String getMessage(Throwable e) {
|
||||
// The message of nested exception is usually more interesting than the
|
||||
// one on top.
|
||||
Throwable cause = getDeepestCause(e);
|
||||
String errorType = cause.getClass().getSimpleName();
|
||||
String msg = cause.getMessage();
|
||||
if (NO_SHOW_EXCEPTIONS.contains(errorType) && msg!=null) {
|
||||
return msg;
|
||||
}
|
||||
return errorType + ": " + msg;
|
||||
}
|
||||
|
||||
private static Throwable getDeepestCause(Throwable e) {
|
||||
Throwable cause = e;
|
||||
Throwable parent = e.getCause();
|
||||
while (parent != null && parent != e) {
|
||||
cause = parent;
|
||||
parent = cause.getCause();
|
||||
}
|
||||
return cause;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2020 Pivotal, Inc.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* https://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* Pivotal, Inc. - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.eclipse.xterm;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.ClientProtocolException;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.eclipse.core.runtime.FileLocator;
|
||||
import org.eclipse.core.runtime.Path;
|
||||
import org.eclipse.core.runtime.Platform;
|
||||
import org.eclipse.wildwebdeveloper.embedder.node.NodeJSManager;
|
||||
import org.osgi.framework.Bundle;
|
||||
|
||||
|
||||
class XtermServiceProcessManager {
|
||||
|
||||
private static final int INVALID_PORT = -1;
|
||||
|
||||
private static final String NODEJS_SYSTEM_PROPERTY = "org.springframework.xterm.nodejs";
|
||||
|
||||
private HttpClient httpClient = HttpClientBuilder.create().build();
|
||||
|
||||
private Process process;
|
||||
|
||||
private int port = INVALID_PORT;
|
||||
|
||||
synchronized void startProcess() throws IOException {
|
||||
port = findFreePort();
|
||||
Bundle bundle = XtermPlugin.getDefault().getBundle();
|
||||
URL url = FileLocator.find(bundle, new Path("/lib/node_modules/node-xterm/terminal-server.js"), null);
|
||||
// URL url = FileLocator.find(bundle, new Path("/lib/terminal-server.js"), null);
|
||||
// URL url = FileLocator.find(bundle, new Path("/lib/node-xterm-macos"), null);
|
||||
// URL url = FileLocator.find(bundle, new Path("/lib/node-xterm-win.exe"), null);
|
||||
url = FileLocator.toFileURL(url);
|
||||
try {
|
||||
File serverJsFile = new File(url.toURI());
|
||||
if (serverJsFile == null || !serverJsFile.exists()) {
|
||||
throw new IllegalStateException("Cannot find file " + serverJsFile + ". Cannot start xterm service!");
|
||||
}
|
||||
File nodeJs = null;
|
||||
if (System.getProperty(NODEJS_SYSTEM_PROPERTY) != null) {
|
||||
nodeJs = new File(System.getProperty(NODEJS_SYSTEM_PROPERTY));
|
||||
} else {
|
||||
nodeJs = NodeJSManager.getNodeJsLocation();
|
||||
}
|
||||
if (nodeJs == null || !nodeJs.exists()) {
|
||||
throw new IllegalStateException("Cannot find NodeJS executable at '" + nodeJs + "'. Cannot start xterm service!");
|
||||
}
|
||||
ProcessBuilder builder = new ProcessBuilder(
|
||||
nodeJs.toString(),
|
||||
serverJsFile.toString(),
|
||||
"--server.port=" + port,
|
||||
"--terminal.pty.shutdown=delay", // terminal pty process destroyed right after sockets closed
|
||||
"--terminal.pty.shutdown-delay=5",
|
||||
"--terminal.auto-shutdown.on=true", // terminal app can shutdown itself if not used
|
||||
"--terminal.auto-shutdown.delay=30" // terminal app shuts itself down in not used for 30 sec
|
||||
);
|
||||
|
||||
File logFile = Platform.getStateLocation(XtermPlugin.getDefault().getBundle()).append("xterm-log.log").toFile();
|
||||
builder.redirectError(logFile);
|
||||
builder.redirectOutput(logFile);
|
||||
|
||||
process = builder.start();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized private void waitUntilStarted(Duration timeout) throws TimeoutException, InterruptedException {
|
||||
long start = System.currentTimeMillis();
|
||||
long timeoutMillis = timeout.toMillis();
|
||||
do {
|
||||
if (System.currentTimeMillis() - start > timeoutMillis) {
|
||||
throw new TimeoutException("Timed out waiting for Xterm service to start");
|
||||
}
|
||||
Thread.sleep(150);
|
||||
} while (!isStarted(port));
|
||||
}
|
||||
|
||||
synchronized private boolean isStarted(int port) {
|
||||
HttpGet request = new HttpGet("http://localhost:" + port);
|
||||
try {
|
||||
HttpResponse response = httpClient.execute(request);
|
||||
if (response.getStatusLine().getStatusCode() == 200) {
|
||||
return true;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
synchronized void stopService() {
|
||||
if (process != null && process.isAlive()) {
|
||||
if (port > 0) {
|
||||
HttpPost request = new HttpPost("http://localhost:" + port + "/shutdown");
|
||||
request.setHeader("Content-Type", "application/json");
|
||||
try {
|
||||
HttpResponse response = httpClient.execute(request);
|
||||
int code = response.getStatusLine().getStatusCode();
|
||||
if (code != 200) {
|
||||
process.destroy();
|
||||
}
|
||||
} catch (ClientProtocolException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
process.destroy();
|
||||
}
|
||||
} else {
|
||||
process.destroy();
|
||||
}
|
||||
}
|
||||
process = null;
|
||||
}
|
||||
|
||||
synchronized String serviceUrl(Duration timeout) throws Exception {
|
||||
if (port == INVALID_PORT && process != null && process.isAlive()) {
|
||||
process.destroy();
|
||||
process = null;
|
||||
}
|
||||
if (process == null || !process.isAlive()) {
|
||||
startProcess();
|
||||
try {
|
||||
waitUntilStarted(timeout);
|
||||
} catch (Exception e) {
|
||||
this.port = INVALID_PORT;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return "http://localhost:" + port;
|
||||
}
|
||||
|
||||
private static int findFreePort() throws IOException {
|
||||
ServerSocket socket = new ServerSocket(0);
|
||||
try {
|
||||
return socket.getLocalPort();
|
||||
} finally {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2020 Pivotal, Inc.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* https://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* Pivotal, Inc. - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.eclipse.xterm.views;
|
||||
|
||||
import org.eclipse.core.commands.AbstractHandler;
|
||||
import org.eclipse.core.commands.ExecutionEvent;
|
||||
import org.eclipse.core.commands.ExecutionException;
|
||||
import org.eclipse.core.commands.IHandler;
|
||||
import org.eclipse.core.resources.IProject;
|
||||
import org.eclipse.core.runtime.IAdaptable;
|
||||
import org.eclipse.ui.handlers.HandlerUtil;
|
||||
import org.springframework.ide.eclipse.xterm.XtermPlugin;
|
||||
|
||||
public class OpenTerminalHandler extends AbstractHandler implements IHandler {
|
||||
|
||||
@Override
|
||||
public Object execute(ExecutionEvent event) throws ExecutionException {
|
||||
try {
|
||||
Object obj = HandlerUtil.getCurrentStructuredSelection(event).getFirstElement();
|
||||
IProject project = null;
|
||||
if (obj instanceof IProject) {
|
||||
project = (IProject) obj;
|
||||
} else if (obj instanceof IAdaptable) {
|
||||
project = ((IAdaptable)obj).getAdapter(IProject.class);
|
||||
}
|
||||
if (project == null) {
|
||||
throw new ExecutionException("Cannot find folder for element " + obj);
|
||||
}
|
||||
String cwd = project.getLocation().toOSString();
|
||||
XtermPlugin.getDefault().openTerminalView(null, cwd);
|
||||
} catch (Exception e) {
|
||||
throw new ExecutionException("Failed to open terminal", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2020 Pivotal, Inc.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* https://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* Pivotal, Inc. - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.eclipse.xterm.views;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
|
||||
import org.springframework.ide.eclipse.xterm.XtermPlugin;
|
||||
|
||||
class SimpleUriBuilder {
|
||||
|
||||
private final StringBuilder builder;
|
||||
|
||||
private int numparams = 0;
|
||||
|
||||
public SimpleUriBuilder(String baseUrl) {
|
||||
builder = new StringBuilder(baseUrl);
|
||||
}
|
||||
|
||||
public void addParameter(String name, String paramValue) {
|
||||
try {
|
||||
if (numparams==0) {
|
||||
builder.append("?");
|
||||
} else {
|
||||
builder.append("&");
|
||||
}
|
||||
numparams++;
|
||||
|
||||
builder.append(URLEncoder.encode(name, "UTF-8"));
|
||||
if (paramValue!=null) {
|
||||
builder.append("=");
|
||||
builder.append(URLEncoder.encode(paramValue, "UTF-8"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
XtermPlugin.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2020 Pivotal, Inc.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* https://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* Pivotal, Inc. - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.eclipse.xterm.views;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.eclipse.core.resources.ResourcesPlugin;
|
||||
import org.eclipse.jface.action.Action;
|
||||
import org.eclipse.jface.action.IMenuManager;
|
||||
import org.eclipse.jface.action.IToolBarManager;
|
||||
import org.eclipse.jface.dialogs.MessageDialog;
|
||||
import org.eclipse.jface.resource.ColorRegistry;
|
||||
import org.eclipse.jface.util.IPropertyChangeListener;
|
||||
import org.eclipse.jface.util.PropertyChangeEvent;
|
||||
import org.eclipse.swt.SWT;
|
||||
import org.eclipse.swt.browser.Browser;
|
||||
import org.eclipse.swt.graphics.Font;
|
||||
import org.eclipse.swt.graphics.RGB;
|
||||
import org.eclipse.swt.graphics.RGBA;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.eclipse.swt.widgets.Display;
|
||||
import org.eclipse.ui.IActionBars;
|
||||
import org.eclipse.ui.IMemento;
|
||||
import org.eclipse.ui.IViewSite;
|
||||
import org.eclipse.ui.IWorkbench;
|
||||
import org.eclipse.ui.PartInitException;
|
||||
import org.eclipse.ui.PlatformUI;
|
||||
import org.eclipse.ui.part.ViewPart;
|
||||
import org.eclipse.ui.themes.ITheme;
|
||||
import org.springframework.ide.eclipse.xterm.XtermPlugin;
|
||||
|
||||
public class TerminalView extends ViewPart {
|
||||
|
||||
private static final String CWD_PARAM = "cwd";
|
||||
private static final String CMD_PARAM = "cmd";
|
||||
private static final String TERMINAL_ID_PARAM = "terminal";
|
||||
private static final String DEFAULT_TERMINAL_ID = "default";
|
||||
private static final String ERROR_DIALOG_TITLE = "Error Opening Xterm";
|
||||
private static final String ERROR_DIALOG_MESSAGE = "Failed to determine if Xterm service is running.\n";
|
||||
|
||||
/**
|
||||
* The ID of the view as specified by the extension.
|
||||
*/
|
||||
public static final String ID = "org.springframework.ide.eclipse.xterm.views.TerminalView";
|
||||
|
||||
@Inject IWorkbench workbench;
|
||||
|
||||
private Action refreshAction;
|
||||
|
||||
private Browser browser;
|
||||
|
||||
private String terminalId = DEFAULT_TERMINAL_ID;
|
||||
|
||||
private String cmd;
|
||||
|
||||
private String cwd;
|
||||
|
||||
private boolean isNewView;
|
||||
|
||||
private final IPropertyChangeListener PROPERTY_LISTENER = new IPropertyChangeListener() {
|
||||
|
||||
@Override
|
||||
public void propertyChange(PropertyChangeEvent event) {
|
||||
switch (event.getProperty()) {
|
||||
case XtermPlugin.BG_COLOR:
|
||||
case XtermPlugin.FG_COLOR:
|
||||
case XtermPlugin.SELECTION_COLOR:
|
||||
case XtermPlugin.CURSOR_COLOR:
|
||||
case XtermPlugin.CURSOR_ACCENT_COLOR:
|
||||
case XtermPlugin.FONT:
|
||||
refresh();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@Override
|
||||
public void createPartControl(Composite parent) {
|
||||
PlatformUI.getWorkbench().getThemeManager().addPropertyChangeListener(PROPERTY_LISTENER);
|
||||
browser = new Browser(parent, SWT.CHROMIUM);
|
||||
makeActions();
|
||||
contributeToActionBars();
|
||||
if (isNewView) {
|
||||
// Non-default Xterm views are initialized with #startTerminal(...) call which will use Xterm service with the right parameters
|
||||
if (isDefault()) {
|
||||
navigateToTerminal(terminalId, null, ResourcesPlugin.getWorkspace().getRoot().getLocation().toOSString());
|
||||
}
|
||||
} else {
|
||||
navigateToTerminal(terminalId, cmd, cwd == null ? ResourcesPlugin.getWorkspace().getRoot().getLocation().toOSString() : cwd);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDefault() {
|
||||
// Secondary id is present only for non-default Terminal views
|
||||
return getViewSite().getSecondaryId() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveState(IMemento memento) {
|
||||
if (terminalId != null) {
|
||||
memento.putString(TERMINAL_ID_PARAM, terminalId);
|
||||
if (cmd != null) {
|
||||
memento.putString(CMD_PARAM, cmd);
|
||||
}
|
||||
if (cwd != null) {
|
||||
memento.putString(CWD_PARAM, cwd);
|
||||
}
|
||||
}
|
||||
super.saveState(memento);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(IViewSite site, IMemento memento) throws PartInitException {
|
||||
super.init(site, memento);
|
||||
isNewView = memento == null;
|
||||
if (memento != null) {
|
||||
terminalId = memento.getString(TERMINAL_ID_PARAM);
|
||||
if (terminalId == null) {
|
||||
terminalId = DEFAULT_TERMINAL_ID;
|
||||
} else {
|
||||
cmd = memento.getString(CMD_PARAM);
|
||||
cwd = memento.getString(CWD_PARAM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> refresh() {
|
||||
return navigateToTerminal(terminalId, null, ResourcesPlugin.getWorkspace().getRoot().getLocation().toOSString());
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> navigateToTerminal(final String terminalId, final String cmd, final String cwd) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
String serviceUrl = XtermPlugin.getDefault().xtermUrl(10_000).get();
|
||||
if (Display.getCurrent() != null) {
|
||||
if (browser != null && !browser.isDisposed() && terminalId.equals(TerminalView.this.terminalId)) {
|
||||
browser.setUrl(createUrl(serviceUrl, terminalId, cmd, cwd));
|
||||
}
|
||||
} else {
|
||||
Display display = PlatformUI.getWorkbench().getDisplay();
|
||||
if (display != null && !display.isDisposed()) {
|
||||
display.asyncExec(() -> {
|
||||
if (browser != null && !browser.isDisposed() && terminalId.equals(TerminalView.this.terminalId)) {
|
||||
String url = createUrl(serviceUrl, terminalId, cmd, cwd);
|
||||
browser.setUrl(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (ExecutionException e) {
|
||||
// TODO show error page in the browser
|
||||
XtermPlugin.log(e);
|
||||
if (e.getCause() != null) {
|
||||
StringBuilder errorMessage = new StringBuilder(ERROR_DIALOG_MESSAGE);
|
||||
errorMessage.append('\n');
|
||||
errorMessage.append(e.getCause().getMessage());
|
||||
if (Display.getCurrent() != null) {
|
||||
MessageDialog.openError(Display.getCurrent().getActiveShell(), ERROR_DIALOG_TITLE, errorMessage.toString());
|
||||
} else {
|
||||
Display display = PlatformUI.getWorkbench().getDisplay();
|
||||
if (display != null && !display.isDisposed()) {
|
||||
display.asyncExec(() ->
|
||||
MessageDialog.openError(Display.getCurrent().getActiveShell(), ERROR_DIALOG_TITLE, errorMessage.toString())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
XtermPlugin.log(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> startTerminal(String terminalId, String cmd, String cwd) {
|
||||
this.terminalId = terminalId;
|
||||
this.cmd = cmd;
|
||||
this.cwd = cwd;
|
||||
return navigateToTerminal(terminalId, cmd, cwd);
|
||||
}
|
||||
|
||||
private String createUrl(String serviceUrl, String terminalId, String cmd, String cwd) {
|
||||
SimpleUriBuilder urlBuilder = new SimpleUriBuilder(serviceUrl + "/terminal/" + terminalId);
|
||||
|
||||
ITheme theme = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme();
|
||||
ColorRegistry colorRegistry = theme.getColorRegistry();
|
||||
Font font = theme.getFontRegistry().get(XtermPlugin.FONT);
|
||||
|
||||
urlBuilder.addParameter("bg", rgbToUrlParameter(colorRegistry.get(XtermPlugin.BG_COLOR).getRGB()));
|
||||
urlBuilder.addParameter("fg", rgbToUrlParameter(colorRegistry.get(XtermPlugin.FG_COLOR).getRGB()));
|
||||
RGB selectionColor = colorRegistry.get(XtermPlugin.SELECTION_COLOR).getRGB();
|
||||
// add transparency to selection color
|
||||
urlBuilder.addParameter("selection", rgbaToUrlParameter(new RGBA(selectionColor.red, selectionColor.green, selectionColor.blue, 51)));
|
||||
urlBuilder.addParameter("cursor", rgbToUrlParameter(colorRegistry.get(XtermPlugin.CURSOR_COLOR).getRGB()));
|
||||
urlBuilder.addParameter("cursorAccent", rgbToUrlParameter(colorRegistry.get(XtermPlugin.CURSOR_ACCENT_COLOR).getRGB()));
|
||||
|
||||
urlBuilder.addParameter("fontFamily", font.getFontData()[0].getName());
|
||||
urlBuilder.addParameter("fontSize", String.valueOf(font.getFontData()[0].getHeight()));
|
||||
|
||||
if (cmd != null && !cmd.isEmpty()) {
|
||||
urlBuilder.addParameter(CMD_PARAM, cmd);
|
||||
} else {
|
||||
urlBuilder.addParameter(CMD_PARAM,
|
||||
XtermPlugin.getDefault().getPreferenceStore().getString(XtermPlugin.PREFS_DEFAULT_SHELL_CMD));
|
||||
}
|
||||
if (cwd != null && !cwd.isEmpty()) {
|
||||
urlBuilder.addParameter(CWD_PARAM, cwd);
|
||||
}
|
||||
|
||||
return urlBuilder.toString();
|
||||
|
||||
}
|
||||
|
||||
private static String rgbToUrlParameter(RGB rgb) {
|
||||
StringBuilder sb = new StringBuilder("rgb(");
|
||||
sb.append(rgb.red);
|
||||
sb.append(",");
|
||||
sb.append(rgb.green);
|
||||
sb.append(",");
|
||||
sb.append(rgb.blue);
|
||||
sb.append(")");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String rgbaToUrlParameter(RGBA rgba) {
|
||||
StringBuilder sb = new StringBuilder("rgba(");
|
||||
sb.append(rgba.rgb.red);
|
||||
sb.append(",");
|
||||
sb.append(rgba.rgb.green);
|
||||
sb.append(",");
|
||||
sb.append(rgba.rgb.blue);
|
||||
sb.append(",");
|
||||
sb.append(rgba.alpha / 255.0);
|
||||
sb.append(")");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void contributeToActionBars() {
|
||||
IActionBars bars = getViewSite().getActionBars();
|
||||
fillLocalPullDown(bars.getMenuManager());
|
||||
fillLocalToolBar(bars.getToolBarManager());
|
||||
}
|
||||
|
||||
private void fillLocalPullDown(IMenuManager manager) {
|
||||
manager.add(refreshAction);
|
||||
}
|
||||
|
||||
private void fillLocalToolBar(IToolBarManager manager) {
|
||||
manager.add(refreshAction);
|
||||
}
|
||||
|
||||
private void makeActions() {
|
||||
refreshAction = new Action() {
|
||||
public void run() {
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
refreshAction.setText("Refresh");
|
||||
refreshAction.setToolTipText("Refresh Terminal");
|
||||
refreshAction.setImageDescriptor(XtermPlugin.imageDescriptorFromPlugin(XtermPlugin.getDefault().getBundle().getSymbolicName(), "icons/refresh.png"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFocus() {
|
||||
browser.setFocus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
PlatformUI.getWorkbench().getThemeManager().removePropertyChangeListener(PROPERTY_LISTENER);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2020 Pivotal, Inc.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* https://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* Pivotal, Inc. - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.eclipse.xterm.views;
|
||||
|
||||
import org.eclipse.jface.preference.FieldEditorPreferencePage;
|
||||
import org.eclipse.jface.preference.StringFieldEditor;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.eclipse.ui.IWorkbench;
|
||||
import org.eclipse.ui.IWorkbenchPreferencePage;
|
||||
import org.springframework.ide.eclipse.xterm.XtermPlugin;
|
||||
|
||||
public class XtermPreferencePage extends FieldEditorPreferencePage implements IWorkbenchPreferencePage {
|
||||
|
||||
@Override
|
||||
public void init(IWorkbench workbench) {
|
||||
setDescription("Xterm terminal preferences");
|
||||
setPreferenceStore(XtermPlugin.getDefault().getPreferenceStore());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createFieldEditors() {
|
||||
Composite parent = getFieldEditorParent();
|
||||
addField(new StringFieldEditor(XtermPlugin.PREFS_DEFAULT_SHELL_CMD, "Shell Command", parent));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2020 Pivotal, Inc.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* https://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* Pivotal, Inc. - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.eclipse.xterm.views;
|
||||
|
||||
import org.eclipse.core.runtime.Platform;
|
||||
import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer;
|
||||
import org.springframework.ide.eclipse.xterm.XtermPlugin;
|
||||
|
||||
public class XtermPreferencesInitializer extends AbstractPreferenceInitializer {
|
||||
|
||||
public XtermPreferencesInitializer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initializeDefaultPreferences() {
|
||||
XtermPlugin.getDefault().getPreferenceStore().setDefault(XtermPlugin.PREFS_DEFAULT_SHELL_CMD, Platform.OS_WIN32.equals(Platform.getOS()) ? "cmd.exe" : "/bin/bash");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user