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