Merge remote-tracking branch 'origin/master'
[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.util.SystemInfo;
28 import com.intellij.openapi.util.io.FileUtil;
29 import com.intellij.openapi.util.text.StringUtil;
30 import com.intellij.openapi.vfs.CharsetToolkit;
31 import com.intellij.util.ArrayUtil;
32 import com.intellij.util.EnvironmentUtil;
33 import com.intellij.util.concurrency.AppExecutorUtil;
34 import com.intellij.util.containers.HashMap;
35 import com.jediterm.pty.PtyProcessTtyConnector;
36 import com.jediterm.terminal.TtyConnector;
37 import com.pty4j.PtyProcess;
38 import com.pty4j.util.PtyUtil;
39 import org.jetbrains.annotations.NotNull;
40 import org.jetbrains.annotations.Nullable;
41
42 import java.io.File;
43 import java.io.IOException;
44 import java.io.OutputStream;
45 import java.net.URI;
46 import java.net.URL;
47 import java.nio.charset.Charset;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.concurrent.ExecutionException;
51 import java.util.concurrent.Future;
52
53 /**
54  * @author traff
55  */
56 public class LocalTerminalDirectRunner extends AbstractTerminalRunner<PtyProcess> {
57   private static final Logger LOG = Logger.getInstance(LocalTerminalDirectRunner.class);
58   public static final String JEDITERM_USER_RCFILE = "JEDITERM_USER_RCFILE";
59   public static final String ZDOTDIR = "ZDOTDIR";
60
61   private final Charset myDefaultCharset;
62
63   public LocalTerminalDirectRunner(Project project) {
64     super(project);
65     myDefaultCharset = CharsetToolkit.UTF8_CHARSET;
66   }
67
68   private static boolean hasLoginArgument(String name) {
69     return name.equals("bash") || name.equals("sh") || name.equals("zsh");
70   }
71
72   private static String getShellName(String path) {
73     return new File(path).getName();
74   }
75
76   private static String findRCFile(String shellName) {
77     if (shellName != null) {
78       if ("sh".equals(shellName)) {
79         shellName = "bash";
80       }
81       try {
82
83         String rcfile = "jediterm-" + shellName + ".in";
84         if ("zsh".equals(shellName)) {
85           rcfile = ".zshrc";
86         }
87         URL resource = LocalTerminalDirectRunner.class.getClassLoader().getResource(rcfile);
88         if (resource != null && "jar".equals(resource.getProtocol())) {
89           File file = new File(new File(PtyUtil.getJarContainingFolderPath(LocalTerminalDirectRunner.class)).getParent(), rcfile);
90           if (file.exists()) {
91             return file.getAbsolutePath();
92           }
93         }
94         if (resource != null) {
95           URI uri = resource.toURI();
96           return uri.getPath();
97         }
98       }
99       catch (Exception e) {
100         LOG.warn("Unable to find " + "jediterm-" + shellName + ".in configuration file", e);
101       }
102     }
103     return null;
104   }
105
106   @NotNull
107   public static LocalTerminalDirectRunner createTerminalRunner(Project project) {
108     return new LocalTerminalDirectRunner(project);
109   }
110
111   @Override
112   protected PtyProcess createProcess(@Nullable String directory) throws ExecutionException {
113     Map<String, String> envs = new HashMap<>(System.getenv());
114     if (!SystemInfo.isWindows) {
115       envs.put("TERM", "xterm-256color");
116     }
117     EncodingEnvironmentUtil.setLocaleEnvironmentIfMac(envs, myDefaultCharset);
118
119     String[] command = getCommand(envs);
120
121     for (LocalTerminalCustomizer customizer : LocalTerminalCustomizer.EP_NAME.getExtensions()) {
122       try {
123         command = customizer.customizeCommandAndEnvironment(myProject, command, envs);
124
125         if (directory == null) {
126           directory = customizer.getDefaultFolder();
127         }
128       }
129       catch (Exception e) {
130         LOG.error("Exception during customization of the terminal session", e);
131       }
132     }
133
134     try {
135       return PtyProcess.exec(command, envs, directory != null
136                                             ? directory
137                                             : TerminalProjectOptionsProvider.Companion.getInstance(myProject).getStartingDirectory());
138     }
139     catch (IOException e) {
140       throw new ExecutionException(e);
141     }
142   }
143
144   @Override
145   protected ProcessHandler createProcessHandler(final PtyProcess process) {
146     return new PtyProcessHandler(process, getShellPath());
147   }
148
149   @Override
150   protected TtyConnector createTtyConnector(PtyProcess process) {
151     return new PtyProcessTtyConnector(process, myDefaultCharset);
152   }
153
154   @Override
155   public String runningTargetName() {
156     return "Local Terminal";
157   }
158
159   @Override
160   protected String getTerminalConnectionName(PtyProcess process) {
161     return "Local Terminal";
162   }
163
164
165   public String[] getCommand(Map<String, String> envs) {
166
167     String shellPath = getShellPath();
168
169     return getCommand(shellPath, envs, TerminalOptionsProvider.getInstance().shellIntegration());
170   }
171
172   private String getShellPath() {
173     return TerminalProjectOptionsProvider.Companion.getInstance(myProject).getShellPath();
174   }
175
176   @NotNull
177   public static String[] getCommand(String shellPath, Map<String, String> envs, boolean shellIntegration) {
178     if (SystemInfo.isUnix) {
179       List<String> command = Lists.newArrayList(shellPath.split(" "));
180
181       String shellCommand = command.get(0);
182       String shellName = command.size() > 0 ? getShellName(shellCommand) : null;
183
184
185       if (shellName != null) {
186         command.remove(0);
187
188         List<String> result = Lists.newArrayList(shellCommand);
189
190         String rcFilePath = findRCFile(shellName);
191
192         if (rcFilePath != null &&
193             shellIntegration) {
194           if (shellName.equals("bash") || (SystemInfo.isMac && shellName.equals("sh"))) {
195             addRcFileArgument(envs, command, result, rcFilePath, "--rcfile");
196           }
197           else if (shellName.equals("zsh")) {
198             if (StringUtil.isNotEmpty(EnvironmentUtil.getEnvironmentMap().get(ZDOTDIR))) {
199               File zshRc = new File(FileUtil.expandUserHome(envs.get(ZDOTDIR)), ".zshrc");
200               if (zshRc.exists()) {
201                 envs.put(JEDITERM_USER_RCFILE, zshRc.getAbsolutePath());
202               }
203             }
204             envs.put(ZDOTDIR, new File(rcFilePath).getParent());
205           }
206         }
207
208         if (!loginOrInteractive(command)) {
209           if (hasLoginArgument(shellName) && SystemInfo.isMac) {
210             result.add("--login");
211           }
212           result.add("-i");
213         }
214
215         result.addAll(command);
216         return ArrayUtil.toStringArray(result);
217       }
218       else {
219         return ArrayUtil.toStringArray(command);
220       }
221     }
222     else {
223       return new String[]{shellPath};
224     }
225   }
226
227   private static void addRcFileArgument(Map<String, String> envs,
228                                         List<String> command,
229                                         List<String> result,
230                                         String rcFilePath, String rcfileOption) {
231     result.add(rcfileOption);
232     result.add(rcFilePath);
233     int idx = command.indexOf(rcfileOption);
234     if (idx >= 0) {
235       command.remove(idx);
236       if (idx < command.size()) {
237         envs.put(JEDITERM_USER_RCFILE, FileUtil.expandUserHome(command.get(idx)));
238         command.remove(idx);
239       }
240     }
241   }
242
243   private static boolean loginOrInteractive(List<String> command) {
244     return command.contains("-i") || command.contains("--login") || command.contains("-l");
245   }
246
247   private static class PtyProcessHandler extends ProcessHandler implements TaskExecutor {
248
249     private final PtyProcess myProcess;
250     private final ProcessWaitFor myWaitFor;
251
252     public PtyProcessHandler(PtyProcess process, @NotNull String presentableName) {
253       myProcess = process;
254       myWaitFor = new ProcessWaitFor(process, this, presentableName);
255     }
256
257     @Override
258     public void startNotify() {
259       addProcessListener(new ProcessAdapter() {
260         @Override
261         public void startNotified(ProcessEvent event) {
262           try {
263             myWaitFor.setTerminationCallback(integer -> notifyProcessTerminated(integer));
264           }
265           finally {
266             removeProcessListener(this);
267           }
268         }
269       });
270
271       super.startNotify();
272     }
273
274     @Override
275     protected void destroyProcessImpl() {
276       myProcess.destroy();
277     }
278
279     @Override
280     protected void detachProcessImpl() {
281       destroyProcessImpl();
282     }
283
284     @Override
285     public boolean detachIsDefault() {
286       return false;
287     }
288
289     @Override
290     public boolean isSilentlyDestroyOnClose() {
291       return true;
292     }
293
294     @Nullable
295     @Override
296     public OutputStream getProcessInput() {
297       return myProcess.getOutputStream();
298     }
299
300     @NotNull
301     @Override
302     public Future<?> executeTask(@NotNull Runnable task) {
303       return AppExecutorUtil.getAppExecutorService().submit(task);
304     }
305   }
306 }