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