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