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