a32135e4bd4049b9c35e197bca77d53c414fd2a7
[idea/community.git] / platform / util / src / com / intellij / util / EnvironmentUtil.java
1 /*
2  * Copyright 2000-2015 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 com.intellij.util;
17
18 import com.intellij.execution.process.UnixProcessManager;
19 import com.intellij.openapi.application.PathManager;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.intellij.openapi.util.AtomicNotNullLazyValue;
22 import com.intellij.openapi.util.NotNullLazyValue;
23 import com.intellij.openapi.util.SystemInfo;
24 import com.intellij.openapi.util.io.FileUtil;
25 import com.intellij.openapi.util.registry.Registry;
26 import com.intellij.openapi.util.text.StringUtil;
27 import com.intellij.openapi.vfs.CharsetToolkit;
28 import com.intellij.util.concurrency.FixedFuture;
29 import com.intellij.util.text.CaseInsensitiveStringHashingStrategy;
30 import gnu.trove.THashMap;
31 import org.jetbrains.annotations.NotNull;
32 import org.jetbrains.annotations.Nullable;
33 import org.jetbrains.annotations.TestOnly;
34
35 import java.io.File;
36 import java.nio.charset.Charset;
37 import java.util.*;
38 import java.util.concurrent.Callable;
39 import java.util.concurrent.ExecutorService;
40 import java.util.concurrent.Executors;
41 import java.util.concurrent.Future;
42
43 import static java.util.Collections.unmodifiableMap;
44
45 public class EnvironmentUtil {
46   private static final Logger LOG = Logger.getInstance("#com.intellij.util.EnvironmentUtil");
47
48   private static final int SHELL_ENV_READING_TIMEOUT = 20000;
49
50   private static final String LANG = "LANG";
51   private static final String LC_ALL = "LC_ALL";
52   private static final String LC_CTYPE = "LC_CTYPE";
53
54   private static final Future<Map<String, String>> ourEnvGetter;
55   static {
56     if (SystemInfo.isMac && "unlocked".equals(System.getProperty("__idea.mac.env.lock")) && Registry.is("idea.fix.mac.env")) {
57       ExecutorService executor = Executors.newSingleThreadExecutor(ConcurrencyUtil.newNamedThreadFactory("Shell Env Loader"));
58       ourEnvGetter = executor.submit(new Callable<Map<String, String>>() {
59         @Override
60         public Map<String, String> call() throws Exception {
61           return unmodifiableMap(setCharsetVar(getShellEnv()));
62         }
63       });
64       executor.shutdown();
65     }
66     else {
67       ourEnvGetter = new FixedFuture<Map<String, String>>(getSystemEnv());
68     }
69   }
70
71   private static final NotNullLazyValue<Map<String, String>> ourEnvironment = new AtomicNotNullLazyValue<Map<String, String>>() {
72     @NotNull
73     @Override
74     protected Map<String, String> compute() {
75       try {
76         return ourEnvGetter.get();
77       }
78       catch (Throwable t) {
79         LOG.warn("can't get shell environment", t);
80         return getSystemEnv();
81       }
82     }
83   };
84
85   private static Map<String, String> getSystemEnv() {
86     if (SystemInfo.isWindows) {
87       return unmodifiableMap(new THashMap<String, String>(System.getenv(), CaseInsensitiveStringHashingStrategy.INSTANCE));
88     }
89     else {
90       return System.getenv();
91     }
92   }
93
94   private EnvironmentUtil() { }
95
96   public static boolean isEnvironmentReady() {
97     return ourEnvGetter.isDone();
98   }
99
100   /**
101    * A wrapper layer around {@link System#getenv()}.
102    * <p>
103    * On Windows, the returned map is case-insensitive (i.e. {@code map.get("Path") == map.get("PATH")} holds).
104    * <p>
105    * On Mac OS X things are complicated.<br/>
106    * An app launched by a GUI launcher (Finder, Dock, Spotlight etc.) receives a pretty empty and useless environment,
107    * since standard Unix ways of setting variables via e.g. ~/.profile do not work. What's more important, there are no
108    * sane alternatives. This causes a lot of user complaints about tools working in a terminal not working when launched
109    * from the IDE. To ease their pain, the IDE loads a shell environment (see {@link #getShellEnv()} for gory details)
110    * and returns it as the result.<br/>
111    * And one more thing (c): locale variables on OS X are usually set by a terminal app - meaning they are missing
112    * even from a shell environment above. This again causes user complaints about tools being unable to output anything
113    * outside ASCII range when launched from the IDE. Resolved by adding LC_CTYPE variable to the map if it doesn't contain
114    * explicitly set locale variables (LANG/LC_ALL/LC_CTYPE). See {@link #setCharsetVar(Map)} for details.
115    *
116    * @return unmodifiable map of the process environment.
117    */
118   @NotNull
119   public static Map<String, String> getEnvironmentMap() {
120     return ourEnvironment.getValue();
121   }
122
123   /**
124    * Same as {@code getEnvironmentMap().get(name)}.
125    * Returns value for the passed environment variable name, or null if no such variable found.
126    *
127    * @see #getEnvironmentMap()
128    */
129   @Nullable
130   public static String getValue(@NotNull String name) {
131     return getEnvironmentMap().get(name);
132   }
133
134   /**
135    * Same as {@code flattenEnvironment(getEnvironmentMap())}.
136    * Returns an environment as an array of "NAME=VALUE" strings.
137    *
138    * @see #getEnvironmentMap()
139    */
140   public static String[] getEnvironment() {
141     return flattenEnvironment(getEnvironmentMap());
142   }
143
144   public static String[] flattenEnvironment(Map<String, String> environment) {
145     String[] array = new String[environment.size()];
146     int i = 0;
147     for (Map.Entry<String, String> entry : environment.entrySet()) {
148       array[i++] = entry.getKey() + "=" + entry.getValue();
149     }
150     return array;
151   }
152
153   private static Map<String, String> getShellEnv() throws Exception {
154     String shell = System.getenv("SHELL");
155     if (shell == null || !new File(shell).canExecute()) {
156       throw new Exception("shell:" + shell);
157     }
158
159     File reader = FileUtil.findFirstThatExist(
160       PathManager.getBinPath() + "/printenv.py",
161       PathManager.getHomePath() + "/community/bin/mac/printenv.py",
162       PathManager.getHomePath() + "/bin/mac/printenv.py"
163     );
164     if (reader == null) {
165       throw new Exception("bin:" + PathManager.getBinPath());
166     }
167
168     File envFile = FileUtil.createTempFile("intellij-shell-env.", ".tmp", false);
169     try {
170       String[] command = {shell, "-l", "-i", "-c", ("'" + reader.getAbsolutePath() + "' '" + envFile.getAbsolutePath() + "'")};
171       LOG.info("loading shell env: " + StringUtil.join(command, " "));
172
173       Process process = Runtime.getRuntime().exec(command);
174       int rv = waitAndTerminateAfter(process, SHELL_ENV_READING_TIMEOUT);
175
176       String lines = FileUtil.loadFile(envFile);
177       if (rv != 0 || lines.isEmpty()) {
178         throw new Exception("rv:" + rv + " text:" + lines.length());
179       }
180       return parseEnv(lines);
181     }
182     finally {
183       FileUtil.delete(envFile);
184     }
185   }
186
187   private static Map<String, String> parseEnv(String text) throws Exception {
188     Set<String> toIgnore = new HashSet<String>(Arrays.asList("_", "PWD", "SHLVL"));
189     Map<String, String> env = System.getenv();
190     Map<String, String> newEnv = new HashMap<String, String>();
191
192     String[] lines = text.split("\0");
193     for (String line : lines) {
194       int pos = line.indexOf('=');
195       if (pos <= 0) {
196         throw new Exception("malformed:" + line);
197       }
198       String name = line.substring(0, pos);
199       if (!toIgnore.contains(name)) {
200         newEnv.put(name, line.substring(pos + 1));
201       }
202       else if (env.containsKey(name)) {
203         newEnv.put(name, env.get(name));
204       }
205     }
206
207     LOG.info("shell environment loaded (" + newEnv.size() + " vars)");
208     return newEnv;
209   }
210
211   private static int waitAndTerminateAfter(@NotNull Process process, int timeoutMillis) {
212     Integer exitCode = waitFor(process, timeoutMillis);
213     if (exitCode != null) {
214       return exitCode;
215     }
216     LOG.warn("shell env loader is timed out");
217     UnixProcessManager.sendSigIntToProcessTree(process);
218     exitCode = waitFor(process, 1000);
219     if (exitCode != null) {
220       return exitCode;
221     }
222     LOG.warn("failed to terminate shell env loader process gracefully, terminating forcibly");
223     UnixProcessManager.sendSigKillToProcessTree(process);
224     exitCode = waitFor(process, 1000);
225     if (exitCode != null) {
226       return exitCode;
227     }
228     LOG.warn("failed to kill shell env loader");
229     return -1;
230   }
231
232   @Nullable
233   private static Integer waitFor(@NotNull Process process, int timeoutMillis) {
234     long stop = System.currentTimeMillis() + timeoutMillis;
235     while (System.currentTimeMillis() < stop) {
236       TimeoutUtil.sleep(100);
237       try {
238         return process.exitValue();
239       }
240       catch (IllegalThreadStateException ignore) { }
241     }
242     return null;
243   }
244
245   private static Map<String, String> setCharsetVar(@NotNull Map<String, String> env) {
246     if (!isCharsetVarDefined(env)) {
247       Locale locale = Locale.getDefault();
248       Charset charset = CharsetToolkit.getDefaultSystemCharset();
249       String language = locale.getLanguage();
250       String country = locale.getCountry();
251       String value = (language.isEmpty() || country.isEmpty() ? "en_US" : language + '_' + country) + '.' + charset.name();
252       env.put(LC_CTYPE, value);
253       LOG.info("LC_CTYPE=" + value);
254     }
255     return env;
256   }
257
258   private static boolean isCharsetVarDefined(@NotNull Map<String, String> env) {
259     return !env.isEmpty() && (env.containsKey(LANG) || env.containsKey(LC_ALL) || env.containsKey(LC_CTYPE));
260   }
261
262   public static void inlineParentOccurrences(@NotNull Map<String, String> envs) {
263     Map<String, String> parentParams = new HashMap<String, String>(System.getenv());
264     for (Map.Entry<String, String> entry : envs.entrySet()) {
265       String key = entry.getKey();
266       String value = entry.getValue();
267       if (value != null) {
268         String parentVal = parentParams.get(key);
269         if (parentVal != null && containsEnvKeySubstitution(key, value)) {
270           envs.put(key, value.replace("$" + key + "$", parentVal));
271         }
272       }
273     }
274   }
275
276   private static boolean containsEnvKeySubstitution(final String envKey, final String val) {
277     return ArrayUtil.find(val.split(File.pathSeparator), "$" + envKey + "$") != -1;
278   }
279
280   @TestOnly
281   static Map<String, String> testLoader() {
282     try {
283       return getShellEnv();
284     }
285     catch (Exception e) {
286       throw new RuntimeException(e);
287     }
288   }
289
290   @TestOnly
291   static Map<String, String> testParser(@NotNull String lines) {
292     try {
293       return parseEnv(lines);
294     }
295     catch (Exception e) {
296       throw new RuntimeException(e);
297     }
298   }
299 }