2 * Copyright 2000-2015 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.util;
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;
36 import java.nio.charset.Charset;
38 import java.util.concurrent.*;
40 import static java.util.Collections.unmodifiableMap;
42 public class EnvironmentUtil {
43 private static final Logger LOG = Logger.getInstance("#com.intellij.util.EnvironmentUtil");
45 private static final int SHELL_ENV_READING_TIMEOUT = 20000;
47 private static final String LANG = "LANG";
48 private static final String LC_ALL = "LC_ALL";
49 private static final String LC_CTYPE = "LC_CTYPE";
51 private static final Future<Map<String, String>> ourEnvGetter;
53 if (SystemInfo.isMac && "unlocked".equals(System.getProperty("__idea.mac.env.lock")) && Registry.is("idea.fix.mac.env")) {
54 ExecutorService executor = Executors.newSingleThreadExecutor(ConcurrencyUtil.newNamedThreadFactory("Shell Env Loader"));
55 ourEnvGetter = executor.submit(new Callable<Map<String, String>>() {
57 public Map<String, String> call() throws Exception {
58 return unmodifiableMap(setCharsetVar(getShellEnv()));
64 ourEnvGetter = new FixedFuture<Map<String, String>>(getSystemEnv());
68 private static final NotNullLazyValue<Map<String, String>> ourEnvironment = new AtomicNotNullLazyValue<Map<String, String>>() {
71 protected Map<String, String> compute() {
73 return ourEnvGetter.get();
76 LOG.warn("can't get shell environment", t);
77 return getSystemEnv();
82 private static Map<String, String> getSystemEnv() {
83 if (SystemInfo.isWindows) {
84 return unmodifiableMap(new THashMap<String, String>(System.getenv(), CaseInsensitiveStringHashingStrategy.INSTANCE));
87 return System.getenv();
91 private EnvironmentUtil() { }
93 public static boolean isEnvironmentReady() {
94 return ourEnvGetter.isDone();
98 * A wrapper layer around {@link System#getenv()}.
100 * On Windows, the returned map is case-insensitive (i.e. {@code map.get("Path") == map.get("PATH")} holds).
102 * On Mac OS X things are complicated.<br/>
103 * An app launched by a GUI launcher (Finder, Dock, Spotlight etc.) receives a pretty empty and useless environment,
104 * since standard Unix ways of setting variables via e.g. ~/.profile do not work. What's more important, there are no
105 * sane alternatives. This causes a lot of user complaints about tools working in a terminal not working when launched
106 * from the IDE. To ease their pain, the IDE loads a shell environment (see {@link #getShellEnv()} for gory details)
107 * and returns it as the result.<br/>
108 * And one more thing (c): locale variables on OS X are usually set by a terminal app - meaning they are missing
109 * even from a shell environment above. This again causes user complaints about tools being unable to output anything
110 * outside ASCII range when launched from the IDE. Resolved by adding LC_CTYPE variable to the map if it doesn't contain
111 * explicitly set locale variables (LANG/LC_ALL/LC_CTYPE). See {@link #setCharsetVar(Map)} for details.
113 * @return unmodifiable map of the process environment.
116 public static Map<String, String> getEnvironmentMap() {
117 return ourEnvironment.getValue();
121 * Same as {@code getEnvironmentMap().get(name)}.
122 * Returns value for the passed environment variable name, or null if no such variable found.
124 * @see #getEnvironmentMap()
127 public static String getValue(@NotNull String name) {
128 return getEnvironmentMap().get(name);
132 * Same as {@code flattenEnvironment(getEnvironmentMap())}.
133 * Returns an environment as an array of "NAME=VALUE" strings.
135 * @see #getEnvironmentMap()
137 public static String[] getEnvironment() {
138 return flattenEnvironment(getEnvironmentMap());
141 public static String[] flattenEnvironment(Map<String, String> environment) {
142 String[] array = new String[environment.size()];
144 for (Map.Entry<String, String> entry : environment.entrySet()) {
145 array[i++] = entry.getKey() + "=" + entry.getValue();
150 private static Map<String, String> getShellEnv() throws Exception {
151 String shell = System.getenv("SHELL");
152 if (shell == null || !new File(shell).canExecute()) {
153 throw new Exception("shell:" + shell);
156 File reader = FileUtil.findFirstThatExist(
157 PathManager.getBinPath() + "/printenv.py",
158 PathManager.getHomePath() + "/community/bin/mac/printenv.py",
159 PathManager.getHomePath() + "/bin/mac/printenv.py"
161 if (reader == null) {
162 throw new Exception("bin:" + PathManager.getBinPath());
165 File envFile = FileUtil.createTempFile("intellij-shell-env.", ".tmp", false);
167 String[] command = {shell, "-l", "-i", "-c", ("'" + reader.getAbsolutePath() + "' '" + envFile.getAbsolutePath() + "'")};
168 LOG.info("loading shell env: " + StringUtil.join(command, " "));
170 Process process = Runtime.getRuntime().exec(command);
171 StreamGobbler stdoutGobbler = new StreamGobbler(process.getInputStream(), "stdout");
172 stdoutGobbler.start();
173 StreamGobbler stderrGobbler = new StreamGobbler(process.getErrorStream(), "stderr");
174 stderrGobbler.start();
175 int rv = waitAndTerminateAfter(process, SHELL_ENV_READING_TIMEOUT);
176 stdoutGobbler.logIfNotEmpty();
177 stderrGobbler.logIfNotEmpty();
179 String lines = FileUtil.loadFile(envFile);
180 if (rv != 0 || lines.isEmpty()) {
181 throw new Exception("rv:" + rv + " text:" + lines.length());
183 return parseEnv(lines);
186 FileUtil.delete(envFile);
190 private static Map<String, String> parseEnv(String text) throws Exception {
191 Set<String> toIgnore = new HashSet<String>(Arrays.asList("_", "PWD", "SHLVL"));
192 Map<String, String> env = System.getenv();
193 Map<String, String> newEnv = new HashMap<String, String>();
195 String[] lines = text.split("\0");
196 for (String line : lines) {
197 int pos = line.indexOf('=');
199 throw new Exception("malformed:" + line);
201 String name = line.substring(0, pos);
202 if (!toIgnore.contains(name)) {
203 newEnv.put(name, line.substring(pos + 1));
205 else if (env.containsKey(name)) {
206 newEnv.put(name, env.get(name));
210 LOG.info("shell environment loaded (" + newEnv.size() + " vars)");
214 private static int waitAndTerminateAfter(@NotNull Process process, int timeoutMillis) {
215 Integer exitCode = waitFor(process, timeoutMillis);
216 if (exitCode != null) {
219 LOG.warn("shell env loader is timed out");
220 UnixProcessManager.sendSigIntToProcessTree(process);
221 exitCode = waitFor(process, 1000);
222 if (exitCode != null) {
225 LOG.warn("failed to terminate shell env loader process gracefully, terminating forcibly");
226 UnixProcessManager.sendSigKillToProcessTree(process);
227 exitCode = waitFor(process, 1000);
228 if (exitCode != null) {
231 LOG.warn("failed to kill shell env loader");
236 private static Integer waitFor(@NotNull Process process, int timeoutMillis) {
237 long stop = System.currentTimeMillis() + timeoutMillis;
238 while (System.currentTimeMillis() < stop) {
239 TimeoutUtil.sleep(100);
241 return process.exitValue();
243 catch (IllegalThreadStateException ignore) { }
248 private static Map<String, String> setCharsetVar(@NotNull Map<String, String> env) {
249 if (!isCharsetVarDefined(env)) {
250 Locale locale = Locale.getDefault();
251 Charset charset = CharsetToolkit.getDefaultSystemCharset();
252 String language = locale.getLanguage();
253 String country = locale.getCountry();
254 String value = (language.isEmpty() || country.isEmpty() ? "en_US" : language + '_' + country) + '.' + charset.name();
255 env.put(LC_CTYPE, value);
256 LOG.info("LC_CTYPE=" + value);
261 private static boolean isCharsetVarDefined(@NotNull Map<String, String> env) {
262 return !env.isEmpty() && (env.containsKey(LANG) || env.containsKey(LC_ALL) || env.containsKey(LC_CTYPE));
265 public static void inlineParentOccurrences(@NotNull Map<String, String> envs) {
266 Map<String, String> parentParams = new HashMap<String, String>(System.getenv());
267 for (Map.Entry<String, String> entry : envs.entrySet()) {
268 String key = entry.getKey();
269 String value = entry.getValue();
271 String parentVal = parentParams.get(key);
272 if (parentVal != null && containsEnvKeySubstitution(key, value)) {
273 envs.put(key, value.replace("$" + key + "$", parentVal));
279 private static boolean containsEnvKeySubstitution(final String envKey, final String val) {
280 return ArrayUtil.find(val.split(File.pathSeparator), "$" + envKey + "$") != -1;
284 static Map<String, String> testLoader() {
286 return getShellEnv();
288 catch (Exception e) {
289 throw new RuntimeException(e);
294 static Map<String, String> testParser(@NotNull String lines) {
296 return parseEnv(lines);
298 catch (Exception e) {
299 throw new RuntimeException(e);
303 private static class StreamGobbler implements Runnable {
304 private final BufferedReader myReader;
305 private final String myStreamType;
306 private final StringBuffer myBuffer = new StringBuffer();
307 private final Semaphore myFinishSemaphore = new Semaphore(0);
309 public StreamGobbler(@NotNull InputStream stream, @NotNull String streamType) {
310 myReader = new BufferedReader(new InputStreamReader(stream));
311 myStreamType = streamType;
314 public void start() {
315 Thread thread = new Thread(this, "shell process " + myStreamType + " reader");
323 while ((ch = myReader.read()) != -1) {
324 myBuffer.append((char)ch);
327 catch (IOException e) {
328 LOG.warn("Error reading shell process " + myStreamType, e);
331 myFinishSemaphore.release(1);
336 private void close() {
340 catch (IOException e) {
341 LOG.warn("Error closing shell process " + myStreamType, e);
346 private String getText() {
348 if (!myFinishSemaphore.tryAcquire(1, 2, TimeUnit.SECONDS)) {
349 LOG.warn("closing shell process " + myStreamType + " forcibly");
353 catch (InterruptedException e) {
356 return myBuffer.toString();
359 public void logIfNotEmpty() {
360 String text = getText();
361 if (!text.isEmpty()) {
362 LOG.info("shell process " + myStreamType + ":" + StringUtil.trimEnd(text, '\n'));