Pass full shell path (IDEA-161309)
[idea/community.git] / plugins / terminal / src / org / jetbrains / plugins / terminal / LocalTerminalDirectRunner.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jetbrains.plugins.terminal;
17
18 import com.google.common.collect.Lists;
19 import com.intellij.execution.TaskExecutor;
20 import com.intellij.execution.configurations.EncodingEnvironmentUtil;
21 import com.intellij.execution.process.ProcessAdapter;
22 import com.intellij.execution.process.ProcessEvent;
23 import com.intellij.execution.process.ProcessHandler;
24 import com.intellij.execution.process.ProcessWaitFor;
25 import com.intellij.openapi.diagnostic.Logger;
26 import com.intellij.openapi.project.Project;
27 import com.intellij.openapi.roots.ProjectRootManager;
28 import com.intellij.openapi.util.SystemInfo;
29 import com.intellij.openapi.vfs.CharsetToolkit;
30 import com.intellij.openapi.vfs.VirtualFile;
31 import com.intellij.util.ArrayUtil;
32 import com.intellij.util.concurrency.AppExecutorUtil;
33 import com.intellij.util.containers.HashMap;
34 import com.jediterm.pty.PtyProcessTtyConnector;
35 import com.jediterm.terminal.TtyConnector;
36 import com.pty4j.PtyProcess;
37 import org.jetbrains.annotations.NotNull;
38 import org.jetbrains.annotations.Nullable;
39
40 import java.io.File;
41 import java.io.IOException;
42 import java.io.OutputStream;
43 import java.net.URI;
44 import java.net.URL;
45 import java.nio.charset.Charset;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.concurrent.ExecutionException;
49 import java.util.concurrent.Future;
50
51 /**
52  * @author traff
53  */
54 public class LocalTerminalDirectRunner extends AbstractTerminalRunner<PtyProcess> {
55   private static final Logger LOG = Logger.getInstance(LocalTerminalDirectRunner.class);
56
57   private final Charset myDefaultCharset;
58
59   public LocalTerminalDirectRunner(Project project) {
60     super(project);
61     myDefaultCharset = CharsetToolkit.UTF8_CHARSET;
62   }
63
64   private static boolean hasLoginArgument(String name) {
65     return name.equals("bash") || name.equals("sh") || name.equals("zsh");
66   }
67
68   private static String getShellName(String path) {
69     return new File(path).getName();
70   }
71
72   private static String findRCFile(String shellName) {
73     if (shellName != null) {
74       if ("bash".equals(shellName)) {
75         shellName = "sh";
76       }
77       try {
78
79         URL resource = LocalTerminalDirectRunner.class.getClassLoader().getResource("jediterm-" + shellName + ".in");
80         if (resource != null) {
81           URI uri = resource.toURI();
82           return uri.getPath();
83         }
84       }
85       catch (Exception e) {
86         LOG.warn("Unable to find " + "jediterm-" + shellName + ".in configuration file", e);
87       }
88     }
89     return null;
90   }
91
92   @NotNull
93   public static LocalTerminalDirectRunner createTerminalRunner(Project project) {
94     return new LocalTerminalDirectRunner(project);
95   }
96
97   @Override
98   protected PtyProcess createProcess(@Nullable String directory) throws ExecutionException {
99     Map<String, String> envs = new HashMap<>(System.getenv());
100     if (!SystemInfo.isWindows) {
101       envs.put("TERM", "xterm-256color");
102     }
103     EncodingEnvironmentUtil.setLocaleEnvironmentIfMac(envs, myDefaultCharset);
104
105     String[] command = getCommand();
106
107     for (LocalTerminalCustomizer customizer : LocalTerminalCustomizer.EP_NAME.getExtensions()) {
108       command = customizer.customizeCommandAndEnvironment(myProject, command, envs);
109
110       if (directory == null) {
111         directory = customizer.getDefaultFolder();
112       }
113     }
114
115     try {
116       return PtyProcess.exec(command, envs, directory != null ? directory : currentProjectFolder());
117     }
118     catch (IOException e) {
119       throw new ExecutionException(e);
120     }
121   }
122
123   private String currentProjectFolder() {
124     final ProjectRootManager projectRootManager = ProjectRootManager.getInstance(myProject);
125
126     final VirtualFile[] roots = projectRootManager.getContentRoots();
127     if (roots.length == 1) {
128       roots[0].getCanonicalPath();
129     }
130     final VirtualFile baseDir = myProject.getBaseDir();
131     return baseDir == null ? null : baseDir.getCanonicalPath();
132   }
133
134   @Override
135   protected ProcessHandler createProcessHandler(final PtyProcess process) {
136     return new PtyProcessHandler(process, getCommand()[0]);
137   }
138
139   @Override
140   protected TtyConnector createTtyConnector(PtyProcess process) {
141     return new PtyProcessTtyConnector(process, myDefaultCharset);
142   }
143
144   @Override
145   public String runningTargetName() {
146     return "Local Terminal";
147   }
148
149   @Override
150   protected String getTerminalConnectionName(PtyProcess process) {
151     return "Local Terminal";
152   }
153
154
155   public String[] getCommand() {
156
157     String shellPath = TerminalOptionsProvider.getInstance().getShellPath();
158
159     return getCommand(shellPath);
160   }
161
162   @NotNull
163   public static String[] getCommand(String shellPath) {
164     if (SystemInfo.isUnix) {
165       List<String> command = Lists.newArrayList(shellPath.split(" "));
166
167       String shellCommand = command.get(0);
168       String shellName = command.size() > 0 ? getShellName(shellCommand) : null;
169
170
171       if (shellName != null) {
172         command.remove(0);
173
174         List<String> result = Lists.newArrayList(shellCommand);
175
176         String rcFilePath = findRCFile(shellName);
177
178
179         if (rcFilePath != null &&
180             TerminalOptionsProvider.getInstance().shellIntegration() &&
181             (shellName.equals("bash") || shellName.equals("sh"))) {
182           result.add("--rcfile");
183           result.add(rcFilePath);
184         }
185
186         if (!loginOrInteractive(command)) {
187           if (hasLoginArgument(shellName) && SystemInfo.isMac) {
188             result.add("--login");
189           }
190           result.add("-i");
191         }
192
193         result.addAll(command);
194         return ArrayUtil.toStringArray(result);
195       }
196       else {
197         return ArrayUtil.toStringArray(command);
198       }
199     }
200     else {
201       return new String[]{shellPath};
202     }
203   }
204
205   private static boolean loginOrInteractive(List<String> command) {
206     return command.contains("-i") || command.contains("--login") || command.contains("-l");
207   }
208
209   private static class PtyProcessHandler extends ProcessHandler implements TaskExecutor {
210
211     private final PtyProcess myProcess;
212     private final ProcessWaitFor myWaitFor;
213
214     public PtyProcessHandler(PtyProcess process, @NotNull String presentableName) {
215       myProcess = process;
216       myWaitFor = new ProcessWaitFor(process, this, presentableName);
217     }
218
219     @Override
220     public void startNotify() {
221       addProcessListener(new ProcessAdapter() {
222         @Override
223         public void startNotified(ProcessEvent event) {
224           try {
225             myWaitFor.setTerminationCallback(integer -> notifyProcessTerminated(integer));
226           }
227           finally {
228             removeProcessListener(this);
229           }
230         }
231       });
232
233       super.startNotify();
234     }
235
236     @Override
237     protected void destroyProcessImpl() {
238       myProcess.destroy();
239     }
240
241     @Override
242     protected void detachProcessImpl() {
243       destroyProcessImpl();
244     }
245
246     @Override
247     public boolean detachIsDefault() {
248       return false;
249     }
250
251     @Override
252     public boolean isSilentlyDestroyOnClose() {
253       return true;
254     }
255
256     @Nullable
257     @Override
258     public OutputStream getProcessInput() {
259       return myProcess.getOutputStream();
260     }
261
262     @NotNull
263     @Override
264     public Future<?> executeTask(@NotNull Runnable task) {
265       return AppExecutorUtil.getAppExecutorService().submit(task);
266     }
267   }
268 }