e0a4be324acd95b75953358fb2283c198a9ddb1c
[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.util.concurrency.FixedFuture;
28 import com.intellij.util.text.CaseInsensitiveStringHashingStrategy;
29 import gnu.trove.THashMap;
30 import org.jetbrains.annotations.NotNull;
31 import org.jetbrains.annotations.Nullable;
32 import org.jetbrains.annotations.TestOnly;
33
34 import java.io.File;
35 import java.util.*;
36 import java.util.concurrent.Callable;
37 import java.util.concurrent.ExecutorService;
38 import java.util.concurrent.Executors;
39 import java.util.concurrent.Future;
40
41 public class EnvironmentUtil {
42   private static final Logger LOG = Logger.getInstance("#com.intellij.util.EnvironmentUtil");
43
44   private static final int SHELL_ENV_READING_TIMEOUT = 20000;
45
46   private static final Future<Map<String, String>> ourEnvGetter;
47   static {
48     if (SystemInfo.isMac && "unlocked".equals(System.getProperty("__idea.mac.env.lock")) && Registry.is("idea.fix.mac.env")) {
49       ExecutorService executor = Executors.newSingleThreadExecutor(ConcurrencyUtil.newNamedThreadFactory("get shell env"));
50       ourEnvGetter = executor.submit(new Callable<Map<String, String>>() {
51         @Override
52         public Map<String, String> call() throws Exception {
53           try {
54             return getShellEnv();
55           }
56           catch (Throwable t) {
57             LOG.warn("can't get shell environment", t);
58             return System.getenv();
59           }
60         }
61       });
62       executor.shutdown();
63     }
64     else {
65       ourEnvGetter = new FixedFuture<Map<String, String>>(System.getenv());
66     }
67   }
68
69   private static final NotNullLazyValue<Map<String, String>> ourEnvironment = new AtomicNotNullLazyValue<Map<String, String>>() {
70     @NotNull
71     @Override
72     protected Map<String, String> compute() {
73       try {
74         return ourEnvGetter.get();
75       }
76       catch (Exception e) {
77         LOG.warn(e);
78         return System.getenv();
79       }
80     }
81   };
82
83   private static final NotNullLazyValue<Map<String, String>> ourEnvironmentOsSpecific = new AtomicNotNullLazyValue<Map<String, String>>() {
84     @NotNull
85     @Override
86     protected Map<String, String> compute() {
87       Map<String, String> env = ourEnvironment.getValue();
88       if (SystemInfo.isWindows) {
89         env = Collections.unmodifiableMap(new THashMap<String, String>(env, CaseInsensitiveStringHashingStrategy.INSTANCE));
90       }
91       return env;
92     }
93   };
94
95   private EnvironmentUtil() { }
96
97   public static boolean isEnvironmentReady() {
98     return ourEnvGetter.isDone();
99   }
100
101   /**
102    * Returns the process environment.
103    * On Mac OS X a shell (Terminal.app) environment is returned (unless disabled by a system property).
104    *
105    * @return unmodifiable map of the process environment.
106    */
107   @NotNull
108   public static Map<String, String> getEnvironmentMap() {
109     return ourEnvironment.getValue();
110   }
111
112   /**
113    * Returns value for the passed environment variable name.
114    * The passed environment variable name is handled in a case-sensitive or case-insensitive manner depending on OS.<p>
115    * For example, on Windows <code>getValue("Path")</code> will return the same result as <code>getValue("PATH")</code>.
116    *
117    * @param name environment variable name
118    * @return value of the environment variable or null if no such variable found
119    */
120   @Nullable
121   public static String getValue(@NotNull String name) {
122     return ourEnvironmentOsSpecific.getValue().get(name);
123   }
124
125   public static String[] getEnvironment() {
126     return flattenEnvironment(getEnvironmentMap());
127   }
128
129   public static String[] flattenEnvironment(Map<String, String> environment) {
130     String[] array = new String[environment.size()];
131     int i = 0;
132     for (Map.Entry<String, String> entry : environment.entrySet()) {
133       array[i++] = entry.getKey() + "=" + entry.getValue();
134     }
135     return array;
136   }
137
138   private static Map<String, String> getShellEnv() throws Exception {
139     String shell = System.getenv("SHELL");
140     if (shell == null || !new File(shell).canExecute()) {
141       throw new Exception("shell:" + shell);
142     }
143
144     File reader = FileUtil.findFirstThatExist(
145       PathManager.getBinPath() + "/printenv.py",
146       PathManager.getHomePath() + "/community/bin/mac/printenv.py",
147       PathManager.getHomePath() + "/bin/mac/printenv.py"
148     );
149     if (reader == null) {
150       throw new Exception("bin:" + PathManager.getBinPath());
151     }
152
153     File envFile = FileUtil.createTempFile("intellij-shell-env", null, false);
154     try {
155       String[] command = {shell, "-l", "-i", "-c", ("'" + reader.getAbsolutePath() + "' '" + envFile.getAbsolutePath() + "'")};
156       LOG.info("loading shell env: " + StringUtil.join(command, " "));
157
158       Process process = Runtime.getRuntime().exec(command);
159       ProcessKiller processKiller = new ProcessKiller(process);
160       processKiller.killAfter(SHELL_ENV_READING_TIMEOUT);
161       int rv = process.waitFor();
162       processKiller.stopWaiting();
163
164       String lines = FileUtil.loadFile(envFile);
165       if (rv != 0 || lines.isEmpty()) {
166         throw new Exception("rv:" + rv + " text:" + lines.length());
167       }
168       return parseEnv(lines);
169     }
170     finally {
171       FileUtil.delete(envFile);
172     }
173   }
174
175   private static Map<String, String> parseEnv(String text) throws Exception {
176     Set<String> toIgnore = new HashSet<String>(Arrays.asList("_", "PWD", "SHLVL"));
177     Map<String, String> env = System.getenv();
178     Map<String, String> newEnv = new HashMap<String, String>();
179
180     String[] lines = text.split("\0");
181     for (String line : lines) {
182       int pos = line.indexOf('=');
183       if (pos <= 0) {
184         throw new Exception("malformed:" + line);
185       }
186       String name = line.substring(0, pos);
187       if (!toIgnore.contains(name)) {
188         newEnv.put(name, line.substring(pos + 1));
189       }
190       else if (env.containsKey(name)) {
191         newEnv.put(name, env.get(name));
192       }
193     }
194
195     LOG.info("shell environment loaded (" + newEnv.size() + " vars)");
196     return Collections.unmodifiableMap(newEnv);
197   }
198
199
200   private static class ProcessKiller {
201     private final Process myProcess;
202     private final Object myWaiter = new Object();
203
204     public ProcessKiller(Process process) {
205       myProcess = process;
206     }
207
208     public void killAfter(long timeout) {
209       final long stop = System.currentTimeMillis() + timeout;
210       new Thread("kill after") {
211         @Override
212         public void run() {
213           synchronized (myWaiter) {
214             while (System.currentTimeMillis() < stop) {
215               try {
216                 myProcess.exitValue();
217                 break;
218               }
219               catch (IllegalThreadStateException ignore) { }
220
221               try {
222                 myWaiter.wait(100);
223               }
224               catch (InterruptedException ignore) { }
225             }
226           }
227
228           try {
229             myProcess.exitValue();
230           }
231           catch (IllegalThreadStateException e) {
232             UnixProcessManager.sendSigKillToProcessTree(myProcess);
233             LOG.warn("timed out");
234           }
235         }
236       }.start();
237     }
238
239     public void stopWaiting() {
240       synchronized (myWaiter) {
241         myWaiter.notifyAll();
242       }
243     }
244   }
245
246   public static void inlineParentOccurrences(@NotNull Map<String, String> envs) {
247     Map<String, String> parentParams = new HashMap<String, String>(System.getenv());
248     for (Map.Entry<String, String> entry : envs.entrySet()) {
249       String key = entry.getKey();
250       String value = entry.getValue();
251       if (value != null) {
252         String parentVal = parentParams.get(key);
253         if (parentVal != null && containsEnvKeySubstitution(key, value)) {
254           envs.put(key, value.replace("$" + key + "$", parentVal));
255         }
256       }
257     }
258   }
259
260   private static boolean containsEnvKeySubstitution(final String envKey, final String val) {
261     return ArrayUtil.find(val.split(File.pathSeparator), "$" + envKey + "$") != -1;
262   }
263
264   @TestOnly
265   static Map<String, String> testLoader() {
266     try {
267       return getShellEnv();
268     }
269     catch (Exception e) {
270       throw new RuntimeException(e);
271     }
272   }
273
274   @TestOnly
275   static Map<String, String> testParser(@NotNull String lines) {
276     try {
277       return parseEnv(lines);
278     }
279     catch (Exception e) {
280       throw new RuntimeException(e);
281     }
282   }
283 }