Add printenv for linux (PY-15085)
[idea/community.git] / platform / util / src / com / intellij / util / EnvironmentUtil.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 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.AppExecutorUtil;
29 import com.intellij.util.concurrency.FixedFuture;
30 import com.intellij.util.io.BaseOutputReader;
31 import com.intellij.util.text.CaseInsensitiveStringHashingStrategy;
32 import gnu.trove.THashMap;
33 import org.jetbrains.annotations.NotNull;
34 import org.jetbrains.annotations.Nullable;
35 import org.jetbrains.annotations.TestOnly;
36
37 import java.io.File;
38 import java.io.InputStream;
39 import java.nio.charset.Charset;
40 import java.util.*;
41 import java.util.concurrent.Callable;
42 import java.util.concurrent.Future;
43
44 import static java.util.Collections.unmodifiableMap;
45
46 public class EnvironmentUtil {
47   private static final Logger LOG = Logger.getInstance("#com.intellij.util.EnvironmentUtil");
48
49   private static final int SHELL_ENV_READING_TIMEOUT = 20000;
50
51   private static final String LANG = "LANG";
52   private static final String LC_ALL = "LC_ALL";
53   private static final String LC_CTYPE = "LC_CTYPE";
54
55   private static final Future<Map<String, String>> ourEnvGetter;
56
57   static {
58     if (SystemInfo.isMac && "unlocked".equals(System.getProperty("__idea.mac.env.lock")) && Registry.is("idea.fix.mac.env")) {
59       ourEnvGetter = AppExecutorUtil.getAppExecutorService().submit(new Callable<Map<String, String>>() {
60         @Override
61         public Map<String, String> call() throws Exception {
62           return unmodifiableMap(setCharsetVar(getShellEnv()));
63         }
64       });
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
97   public static boolean isEnvironmentReady() {
98     return ourEnvGetter.isDone();
99   }
100
101   /**
102    * A wrapper layer around {@link System#getenv()}.
103    * <p>
104    * On Windows, the returned map is case-insensitive (i.e. {@code map.get("Path") == map.get("PATH")} holds).
105    * <p>
106    * On Mac OS X things are complicated.<br/>
107    * An app launched by a GUI launcher (Finder, Dock, Spotlight etc.) receives a pretty empty and useless environment,
108    * since standard Unix ways of setting variables via e.g. ~/.profile do not work. What's more important, there are no
109    * sane alternatives. This causes a lot of user complaints about tools working in a terminal not working when launched
110    * from the IDE. To ease their pain, the IDE loads a shell environment (see {@link #getShellEnv()} for gory details)
111    * and returns it as the result.<br/>
112    * And one more thing (c): locale variables on OS X are usually set by a terminal app - meaning they are missing
113    * even from a shell environment above. This again causes user complaints about tools being unable to output anything
114    * outside ASCII range when launched from the IDE. Resolved by adding LC_CTYPE variable to the map if it doesn't contain
115    * explicitly set locale variables (LANG/LC_ALL/LC_CTYPE). See {@link #setCharsetVar(Map)} for details.
116    *
117    * @return unmodifiable map of the process environment.
118    */
119   @NotNull
120   public static Map<String, String> getEnvironmentMap() {
121     return ourEnvironment.getValue();
122   }
123
124   /**
125    * Same as {@code getEnvironmentMap().get(name)}.
126    * Returns value for the passed environment variable name, or null if no such variable found.
127    *
128    * @see #getEnvironmentMap()
129    */
130   @Nullable
131   public static String getValue(@NotNull String name) {
132     return getEnvironmentMap().get(name);
133   }
134
135   /**
136    * Same as {@code flattenEnvironment(getEnvironmentMap())}.
137    * Returns an environment as an array of "NAME=VALUE" strings.
138    *
139    * @see #getEnvironmentMap()
140    */
141   public static String[] getEnvironment() {
142     return flattenEnvironment(getEnvironmentMap());
143   }
144
145   public static String[] flattenEnvironment(@NotNull Map<String, String> environment) {
146     String[] array = new String[environment.size()];
147     int i = 0;
148     for (Map.Entry<String, String> entry : environment.entrySet()) {
149       array[i++] = entry.getKey() + "=" + entry.getValue();
150     }
151     return array;
152   }
153
154   private static final String DISABLE_OMZ_AUTO_UPDATE = "DISABLE_AUTO_UPDATE";
155
156   private static Map<String, String> getShellEnv() throws Exception {
157     return new ShellEnvReader().readShellEnv();
158   }
159
160
161   public static class ShellEnvReader {
162
163     public Map<String, String> readShellEnv() throws Exception {
164       String os = SystemInfo.isLinux ? "linux" : "mac";
165       File reader = FileUtil.findFirstThatExist(
166         PathManager.getBinPath() + "/printenv.py",
167         PathManager.getHomePath() + "/community/bin/" + os + "/printenv.py",
168         PathManager.getHomePath() + "/bin/" + os + "/printenv.py");
169       if (reader == null) {
170         throw new Exception("bin:" + PathManager.getBinPath());
171       }
172
173       File envFile = FileUtil.createTempFile("intellij-shell-env.", ".tmp", false);
174       try {
175         List<String> command = getShellProcessCommand();
176         command.add("-c");
177         command.add("'" + reader.getAbsolutePath() + "' '" + envFile.getAbsolutePath() + "'");
178
179         LOG.info("loading shell env: " + StringUtil.join(command, " "));
180
181         return runProcessAndReadEnvs(command, envFile, "\0");
182       }
183       finally {
184         FileUtil.delete(envFile);
185       }
186     }
187
188     @NotNull
189     protected static Map<String, String> runProcessAndReadEnvs(@NotNull List<String> command, @NotNull File envFile, String lineSeparator)
190       throws Exception {
191       ProcessBuilder builder = new ProcessBuilder(command).redirectErrorStream(true);
192       builder.environment().put(DISABLE_OMZ_AUTO_UPDATE, "true");
193       Process process = builder.start();
194       StreamGobbler gobbler = new StreamGobbler(process.getInputStream());
195       int rv = waitAndTerminateAfter(process, SHELL_ENV_READING_TIMEOUT);
196       gobbler.stop();
197
198       String lines = FileUtil.loadFile(envFile);
199       if (rv != 0 || lines.isEmpty()) {
200         throw new Exception("rv:" + rv + " text:" + lines.length() + " out:" + StringUtil.trimEnd(gobbler.getText(), '\n'));
201       }
202       return parseEnv(lines, lineSeparator);
203     }
204
205     protected List<String> getShellProcessCommand() throws Exception {
206       String shell = getShell();
207
208       if (shell == null || !new File(shell).canExecute()) {
209         throw new Exception("shell:" + shell);
210       }
211
212       return new ArrayList<String>(Arrays.asList(shell, "-l", "-i"));
213     }
214
215     @Nullable
216     protected String getShell() throws Exception {
217       return System.getenv("SHELL");
218     }
219   }
220
221   @NotNull
222   private static Map<String, String> parseEnv(String text, String lineSeparator) throws Exception {
223     Set<String> toIgnore = new HashSet<String>(Arrays.asList("_", "PWD", "SHLVL", DISABLE_OMZ_AUTO_UPDATE));
224     Map<String, String> env = System.getenv();
225     Map<String, String> newEnv = new HashMap<String, String>();
226
227     String[] lines = text.split(lineSeparator);
228     for (String line : lines) {
229       int pos = line.indexOf('=');
230       if (pos <= 0) {
231         throw new Exception("malformed:" + line);
232       }
233       String name = line.substring(0, pos);
234       if (!toIgnore.contains(name)) {
235         newEnv.put(name, line.substring(pos + 1));
236       }
237       else if (env.containsKey(name)) {
238         newEnv.put(name, env.get(name));
239       }
240     }
241
242     LOG.info("shell environment loaded (" + newEnv.size() + " vars)");
243     return newEnv;
244   }
245
246   private static int waitAndTerminateAfter(@NotNull Process process, int timeoutMillis) {
247     Integer exitCode = waitFor(process, timeoutMillis);
248     if (exitCode != null) {
249       return exitCode;
250     }
251     LOG.warn("shell env loader is timed out");
252     UnixProcessManager.sendSigIntToProcessTree(process);
253     exitCode = waitFor(process, 1000);
254     if (exitCode != null) {
255       return exitCode;
256     }
257     LOG.warn("failed to terminate shell env loader process gracefully, terminating forcibly");
258     UnixProcessManager.sendSigKillToProcessTree(process);
259     exitCode = waitFor(process, 1000);
260     if (exitCode != null) {
261       return exitCode;
262     }
263     LOG.warn("failed to kill shell env loader");
264     return -1;
265   }
266
267   @Nullable
268   private static Integer waitFor(@NotNull Process process, int timeoutMillis) {
269     long stop = System.currentTimeMillis() + timeoutMillis;
270     while (System.currentTimeMillis() < stop) {
271       TimeoutUtil.sleep(100);
272       try {
273         return process.exitValue();
274       }
275       catch (IllegalThreadStateException ignore) {
276       }
277     }
278     return null;
279   }
280
281   private static Map<String, String> setCharsetVar(@NotNull Map<String, String> env) {
282     if (!isCharsetVarDefined(env)) {
283       Locale locale = Locale.getDefault();
284       Charset charset = CharsetToolkit.getDefaultSystemCharset();
285       String language = locale.getLanguage();
286       String country = locale.getCountry();
287       String value = (language.isEmpty() || country.isEmpty() ? "en_US" : language + '_' + country) + '.' + charset.name();
288       env.put(LC_CTYPE, value);
289       LOG.info("LC_CTYPE=" + value);
290     }
291     return env;
292   }
293
294   private static boolean isCharsetVarDefined(@NotNull Map<String, String> env) {
295     return !env.isEmpty() && (env.containsKey(LANG) || env.containsKey(LC_ALL) || env.containsKey(LC_CTYPE));
296   }
297
298   public static void inlineParentOccurrences(@NotNull Map<String, String> envs) {
299     Map<String, String> parentParams = new HashMap<String, String>(System.getenv());
300     for (Map.Entry<String, String> entry : envs.entrySet()) {
301       String key = entry.getKey();
302       String value = entry.getValue();
303       if (value != null) {
304         String parentVal = parentParams.get(key);
305         if (parentVal != null && containsEnvKeySubstitution(key, value)) {
306           envs.put(key, value.replace("$" + key + "$", parentVal));
307         }
308       }
309     }
310   }
311
312   private static boolean containsEnvKeySubstitution(final String envKey, final String val) {
313     return ArrayUtil.find(val.split(File.pathSeparator), "$" + envKey + "$") != -1;
314   }
315
316   @TestOnly
317   static Map<String, String> testLoader() {
318     try {
319       return getShellEnv();
320     }
321     catch (Exception e) {
322       throw new RuntimeException(e);
323     }
324   }
325
326   @TestOnly
327   static Map<String, String> testParser(@NotNull String lines) {
328     try {
329       return parseEnv(lines, "\0");
330     }
331     catch (Exception e) {
332       throw new RuntimeException(e);
333     }
334   }
335
336   private static class StreamGobbler extends BaseOutputReader {
337     private static final Options OPTIONS = new Options() {
338       @Override
339       public SleepingPolicy policy() {
340         return SleepingPolicy.BLOCKING;
341       }
342
343       @Override
344       public boolean splitToLines() {
345         return false;
346       }
347     };
348
349     private final StringBuffer myBuffer;
350
351     public StreamGobbler(@NotNull InputStream stream) {
352       super(stream, CharsetToolkit.getDefaultSystemCharset(), OPTIONS);
353       myBuffer = new StringBuffer();
354       start("stdout/stderr streams of shell env loading process");
355     }
356
357     @NotNull
358     @Override
359     protected Future<?> executeOnPooledThread(@NotNull Runnable runnable) {
360       return AppExecutorUtil.getAppExecutorService().submit(runnable);
361     }
362
363     @Override
364     protected void onTextAvailable(@NotNull String text) {
365       myBuffer.append(text);
366     }
367
368     @NotNull
369     public String getText() {
370       return myBuffer.toString();
371     }
372   }
373 }