From 6e9bacb07ed39e259588fca2e4ba41d22e1da554 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Thu, 3 Jul 2025 14:31:54 +0200 Subject: [PATCH] [platform] replacing `PathExecLazyValue` (IJPL-7387 prerequisite) GitOrigin-RevId: edc1f4884790a873780346c3d6e7e7540ea3d9d6 --- .../openapi/fileTypes/NativeFileType.java | 14 +++--- .../ide/browsers/BrowserLauncherAppless.kt | 17 +++---- .../ide/actions/CreateDesktopEntryAction.java | 7 +-- .../ide/actions/RevealFileAction.java | 23 +++++----- .../actions/GoToWslHomeAction.java | 14 +++--- .../actions/GotoDesktopDirAction.java | 13 +++--- .../PathEnvironmentVariableUtilTest.java | 27 +++++++++++ platform/platform-util-io/api-dump.txt | 1 + .../PathEnvironmentVariableUtil.java | 37 +++++++++++++-- .../sudo/LocalSudoCommandProvider.kt | 14 ++---- .../com/intellij/execution/util/ExecUtil.kt | 40 ++++++++-------- platform/util/api-dump.txt | 2 + .../com/intellij/openapi/util/SystemInfo.java | 46 ++++++++++++++----- .../openapi/util/io/PathExecLazyValue.java | 37 --------------- .../util/src/com/intellij/util/system/OS.java | 9 +++- .../openapi/util/io/PathExecLazyValueTest.kt | 24 ---------- 16 files changed, 175 insertions(+), 150 deletions(-) create mode 100644 platform/platform-tests/testSrc/com/intellij/execution/configurations/PathEnvironmentVariableUtilTest.java delete mode 100644 platform/util/src/com/intellij/openapi/util/io/PathExecLazyValue.java delete mode 100644 platform/util/testSrc/com/intellij/openapi/util/io/PathExecLazyValueTest.kt diff --git a/platform/ide-core/src/com/intellij/openapi/fileTypes/NativeFileType.java b/platform/ide-core/src/com/intellij/openapi/fileTypes/NativeFileType.java index 08161053ee96..4d1c44534b5f 100644 --- a/platform/ide-core/src/com/intellij/openapi/fileTypes/NativeFileType.java +++ b/platform/ide-core/src/com/intellij/openapi/fileTypes/NativeFileType.java @@ -1,18 +1,18 @@ -// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.openapi.fileTypes; import com.intellij.execution.configurations.GeneralCommandLine; +import com.intellij.execution.configurations.PathEnvironmentVariableUtil; import com.intellij.icons.AllIcons; import com.intellij.ide.IdeCoreBundle; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.system.OS; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import javax.swing.*; import java.util.ArrayList; -import java.util.List; public final class NativeFileType implements INativeFileType { public static final NativeFileType INSTANCE = new NativeFileType(); @@ -64,16 +64,16 @@ public final class NativeFileType implements INativeFileType { throw new IllegalArgumentException("Non-local file: " + file + "; FS=" + file.getFileSystem()); } - List commands = new ArrayList<>(); - if (SystemInfo.isWindows) { + var commands = new ArrayList(); + if (OS.CURRENT == OS.Windows) { //noinspection SpellCheckingInspection commands.add("rundll32.exe"); commands.add("url.dll,FileProtocolHandler"); } - else if (SystemInfo.isMac) { + else if (OS.CURRENT == OS.macOS) { commands.add("/usr/bin/open"); } - else if (SystemInfo.hasXdgOpen()) { + else if (PathEnvironmentVariableUtil.isOnPath("xdg-open")) { commands.add("xdg-open"); } else { diff --git a/platform/platform-api/src/com/intellij/ide/browsers/BrowserLauncherAppless.kt b/platform/platform-api/src/com/intellij/ide/browsers/BrowserLauncherAppless.kt index ef19bf4a2c73..60cbafc29820 100644 --- a/platform/platform-api/src/com/intellij/ide/browsers/BrowserLauncherAppless.kt +++ b/platform/platform-api/src/com/intellij/ide/browsers/BrowserLauncherAppless.kt @@ -4,6 +4,7 @@ package com.intellij.ide.browsers import com.intellij.execution.CommandLineUtil import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.configurations.PathEnvironmentVariableUtil import com.intellij.execution.process.CapturingProcessHandler import com.intellij.execution.util.ExecUtil import com.intellij.ide.BrowserUtil @@ -17,11 +18,11 @@ import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import com.intellij.openapi.util.NlsContexts.NotificationContent import com.intellij.openapi.util.NlsSafe -import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.vfs.StandardFileSystems import com.intellij.openapi.vfs.VfsUtil import com.intellij.util.PathUtil import com.intellij.util.io.URLUtil +import com.intellij.util.system.OS import kotlinx.coroutines.* import org.jetbrains.annotations.ApiStatus import java.awt.Desktop @@ -36,7 +37,7 @@ open class BrowserLauncherAppless : BrowserLauncher() { @JvmStatic fun canUseSystemDefaultBrowserPolicy(): Boolean = - isDesktopActionSupported(Desktop.Action.BROWSE) || SystemInfo.isWindows || SystemInfo.isMac || SystemInfo.hasXdgOpen() + isDesktopActionSupported(Desktop.Action.BROWSE) || OS.CURRENT == OS.Windows || OS.CURRENT == OS.macOS || PathEnvironmentVariableUtil.isOnPath("xdg-open") private fun isDesktopActionSupported(action: Desktop.Action): Boolean = Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(action) @@ -77,13 +78,13 @@ open class BrowserLauncherAppless : BrowserLauncher() { @Suppress("UsagesOfObsoleteApi") override fun browse(file: java.io.File) { val path = file.absolutePath - val absPath = if (SystemInfo.isWindows && path[0] != '/') "/${path}" else path + val absPath = if (OS.CURRENT == OS.Windows && path[0] != '/') "/${path}" else path browse("${StandardFileSystems.FILE_PROTOCOL_PREFIX}${absPath}", browser = null, project = null) } override fun browse(file: Path) { val path = file.toAbsolutePath().toString() - val absPath = if (SystemInfo.isWindows && path[0] != '/') "/${path}" else path + val absPath = if (OS.CURRENT == OS.Windows && path[0] != '/') "/${path}" else path browse("${StandardFileSystems.FILE_PROTOCOL_PREFIX}${absPath}", browser = null, project = null) } @@ -189,7 +190,7 @@ open class BrowserLauncherAppless : BrowserLauncher() { } catch (e: Exception) { LOG.warn("[${uri}]", e) - if (SystemInfo.isMac && e.message!!.contains("Error code: -10814")) { + if (OS.CURRENT == OS.macOS && e.message!!.contains("Error code: -10814")) { // if "No application knows how to open" the URL, there is no sense in retrying with the 'open' command return@launch } @@ -260,9 +261,9 @@ open class BrowserLauncherAppless : BrowserLauncher() { private val defaultBrowserCommand: List? get() = when { - SystemInfo.isWindows -> listOf(CommandLineUtil.getWinShellName(), "/c", "start", GeneralCommandLine.inescapableQuote("")) - SystemInfo.isMac -> listOf(ExecUtil.openCommandPath) - SystemInfo.hasXdgOpen() -> listOf("xdg-open") + OS.CURRENT == OS.Windows -> listOf(CommandLineUtil.getWinShellName(), "/c", "start", GeneralCommandLine.inescapableQuote("")) + OS.CURRENT == OS.macOS -> listOf(ExecUtil.openCommandPath) + PathEnvironmentVariableUtil.isOnPath("xdg-open") -> listOf("xdg-open") else -> null } diff --git a/platform/platform-impl/src/com/intellij/ide/actions/CreateDesktopEntryAction.java b/platform/platform-impl/src/com/intellij/ide/actions/CreateDesktopEntryAction.java index eb850fe7fa46..76fe09f5fda5 100644 --- a/platform/platform-impl/src/com/intellij/ide/actions/CreateDesktopEntryAction.java +++ b/platform/platform-impl/src/com/intellij/ide/actions/CreateDesktopEntryAction.java @@ -1,8 +1,9 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.ide.actions; import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; +import com.intellij.execution.configurations.PathEnvironmentVariableUtil; import com.intellij.execution.process.CapturingProcessHandler; import com.intellij.execution.util.ExecUtil; import com.intellij.ide.IdeBundle; @@ -22,12 +23,12 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.updateSettings.impl.ExternalUpdateManager; import com.intellij.openapi.util.NlsSafe; -import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.AppUIUtil; import com.intellij.ui.AppUIUtilKt; import com.intellij.util.ExceptionUtil; import com.intellij.util.Restarter; +import com.intellij.util.system.OS; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; @@ -44,7 +45,7 @@ public final class CreateDesktopEntryAction extends DumbAwareAction implements A private static final Logger LOG = Logger.getInstance(CreateDesktopEntryAction.class); public static boolean isAvailable() { - return SystemInfo.isUnix && !SystemInfo.isMac && !ExternalUpdateManager.isCreatingDesktopEntries() && SystemInfo.hasXdgOpen(); + return OS.isGenericUnix() &&!ExternalUpdateManager.isCreatingDesktopEntries() && PathEnvironmentVariableUtil.isOnPath("xdg-desktop-menu"); } @Override diff --git a/platform/platform-impl/src/com/intellij/ide/actions/RevealFileAction.java b/platform/platform-impl/src/com/intellij/ide/actions/RevealFileAction.java index c97e8716db0f..c62a01b29063 100644 --- a/platform/platform-impl/src/com/intellij/ide/actions/RevealFileAction.java +++ b/platform/platform-impl/src/com/intellij/ide/actions/RevealFileAction.java @@ -1,6 +1,7 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.ide.actions; +import com.intellij.execution.configurations.PathEnvironmentVariableUtil; import com.intellij.execution.process.CapturingProcessHandler; import com.intellij.execution.process.ProcessIOExecutorService; import com.intellij.ide.IdeBundle; @@ -22,10 +23,10 @@ import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.NlsActions.ActionText; import com.intellij.openapi.util.NlsContexts; import com.intellij.openapi.util.NlsSafe; -import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.newvfs.ArchiveFileSystem; import com.intellij.util.SystemProperties; +import com.intellij.util.system.OS; import com.sun.jna.Native; import com.sun.jna.Pointer; import com.sun.jna.platform.win32.*; @@ -107,12 +108,12 @@ public class RevealFileAction extends DumbAwareAction implements LightEditCompat /** Whether a system is able to open a directory in a file manager and highlight a file in it. */ public static boolean isSupported() { - return SystemInfo.isWindows || SystemInfo.isMac || Holder.fileManagerApp != null; + return OS.CURRENT == OS.Windows || OS.CURRENT == OS.macOS || Holder.fileManagerApp != null; } /** Whether a system is able to open a directory in a file manager. */ public static boolean isDirectoryOpenSupported() { - return SystemInfo.isWindows || SystemInfo.isMac || Holder.fileManagerApp != null; + return OS.CURRENT == OS.Windows || OS.CURRENT == OS.macOS || Holder.fileManagerApp != null; } public static @ActionText @NotNull String getActionName() { @@ -125,7 +126,7 @@ public class RevealFileAction extends DumbAwareAction implements LightEditCompat } private static @ActionText String getActionName(boolean skipDetection) { - return SystemInfo.isMac ? ActionsBundle.message("action.RevealIn.name.mac") : ActionsBundle.message("action.RevealIn.name.other", getFileManagerName(skipDetection)); + return OS.CURRENT == OS.macOS ? ActionsBundle.message("action.RevealIn.name.mac") : ActionsBundle.message("action.RevealIn.name.other", getFileManagerName(skipDetection)); } @Override @@ -143,8 +144,8 @@ public class RevealFileAction extends DumbAwareAction implements LightEditCompat } public static @NotNull @ActionText String getFileManagerName(boolean skipDetection) { - return SystemInfo.isMac ? IdeBundle.message("action.finder.text") : - SystemInfo.isWindows ? IdeBundle.message("action.explorer.text") : + return OS.CURRENT == OS.Windows ? IdeBundle.message("action.explorer.text") : + OS.CURRENT == OS.macOS ? IdeBundle.message("action.finder.text") : skipDetection ? IdeBundle.message("action.file.manager.text") : requireNonNullElseGet(Holder.fileManagerName, () -> IdeBundle.message("action.file.manager.text")); } @@ -202,7 +203,7 @@ public class RevealFileAction extends DumbAwareAction implements LightEditCompat var toSelect = _toSelect != null ? canonicalize(_toSelect).normalize().toString() : null; String fmApp; - if (SystemInfo.isWindows) { + if (OS.CURRENT == OS.Windows) { if (JnaLoader.isLoaded()) { openViaShellApi(dir, toSelect); } @@ -210,7 +211,7 @@ public class RevealFileAction extends DumbAwareAction implements LightEditCompat openViaExplorerCall(dir, toSelect); } } - else if (SystemInfo.isMac) { + else if (OS.CURRENT == OS.macOS) { if (toSelect != null) { spawn("open", "-R", toSelect); } @@ -229,7 +230,7 @@ public class RevealFileAction extends DumbAwareAction implements LightEditCompat spawn(fmApp, toSelect != null ? toSelect : dir); } } - else if (toSelect == null && SystemInfo.hasXdgOpen()) { + else if (toSelect == null && PathEnvironmentVariableUtil.isOnPath("xdg-open")) { spawn("xdg-open", dir); } else { @@ -298,7 +299,7 @@ public class RevealFileAction extends DumbAwareAction implements LightEditCompat ProcessIOExecutorService.INSTANCE.execute(() -> { try { - var process = SystemInfo.isWindows ? Runtime.getRuntime().exec(command[0]) : new ProcessBuilder(command).start(); + var process = OS.CURRENT == OS.Windows ? Runtime.getRuntime().exec(command[0]) : new ProcessBuilder(command).start(); new CapturingProcessHandler.Silent(process, null, command[0]) .runProcess(10000, false) .checkSuccess(LOG); @@ -315,7 +316,7 @@ public class RevealFileAction extends DumbAwareAction implements LightEditCompat static { String fmApp = null, fmName = null; - if (SystemInfo.hasXdgMime()) { + if (PathEnvironmentVariableUtil.isOnPath("xdg-mime")) { try (var reader = new ProcessBuilder("xdg-mime", "query", "default", "inode/directory").start().inputReader()) { var desktopEntryName = reader.readLine(); if (desktopEntryName != null && desktopEntryName.endsWith(".desktop")) { diff --git a/platform/platform-impl/src/com/intellij/openapi/fileChooser/actions/GoToWslHomeAction.java b/platform/platform-impl/src/com/intellij/openapi/fileChooser/actions/GoToWslHomeAction.java index e9bc1ad9dd32..8fb353824320 100644 --- a/platform/platform-impl/src/com/intellij/openapi/fileChooser/actions/GoToWslHomeAction.java +++ b/platform/platform-impl/src/com/intellij/openapi/fileChooser/actions/GoToWslHomeAction.java @@ -1,6 +1,7 @@ -// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.openapi.fileChooser.actions; +import com.intellij.execution.configurations.PathEnvironmentVariableUtil; import com.intellij.execution.wsl.WSLDistribution; import com.intellij.execution.wsl.WSLUtil; import com.intellij.execution.wsl.WslDistributionManager; @@ -18,7 +19,6 @@ import com.intellij.openapi.ui.Messages; import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.ui.popup.util.PopupUtil; import com.intellij.openapi.util.io.NioFiles; -import com.intellij.openapi.util.io.PathExecLazyValue; import com.intellij.ui.UIBundle; import org.jetbrains.annotations.NotNull; @@ -26,15 +26,15 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.function.Supplier; final class GoToWslHomeAction extends FileChooserAction implements LightEditCompatible { - private static final Supplier ourHasWsl = PathExecLazyValue.create("wsl.exe"); - @Override protected void update(@NotNull FileChooserPanel panel, @NotNull AnActionEvent e) { - var allowed = WSLUtil.isSystemCompatible() && ourHasWsl.get() && Experiments.getInstance().isFeatureEnabled("wsl.p9.show.roots.in.file.chooser"); - e.getPresentation().setEnabledAndVisible(allowed); + e.getPresentation().setEnabledAndVisible( + WSLUtil.isSystemCompatible() && + Experiments.getInstance().isFeatureEnabled("wsl.p9.show.roots.in.file.chooser") && + PathEnvironmentVariableUtil.isOnPath("wsl.exe") + ); } @Override diff --git a/platform/platform-impl/src/com/intellij/openapi/fileChooser/actions/GotoDesktopDirAction.java b/platform/platform-impl/src/com/intellij/openapi/fileChooser/actions/GotoDesktopDirAction.java index 784334c89e55..61ed5126c3fc 100644 --- a/platform/platform-impl/src/com/intellij/openapi/fileChooser/actions/GotoDesktopDirAction.java +++ b/platform/platform-impl/src/com/intellij/openapi/fileChooser/actions/GotoDesktopDirAction.java @@ -1,7 +1,8 @@ -// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.openapi.fileChooser.actions; import com.intellij.execution.configurations.GeneralCommandLine; +import com.intellij.execution.configurations.PathEnvironmentVariableUtil; import com.intellij.execution.util.ExecUtil; import com.intellij.ide.lightEdit.LightEditCompatible; import com.intellij.jna.JnaLoader; @@ -10,12 +11,12 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileChooser.FileChooserPanel; import com.intellij.openapi.fileChooser.FileSystemTree; import com.intellij.openapi.util.NullableLazyValue; -import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.mac.foundation.Foundation; import com.intellij.ui.mac.foundation.ID; import com.intellij.util.SystemProperties; +import com.intellij.util.system.OS; import com.sun.jna.platform.win32.*; import org.jetbrains.annotations.NotNull; @@ -65,7 +66,7 @@ final class GotoDesktopDirAction extends FileChooserAction implements LightEditC } private static Path getDesktopDirectory() { - if (SystemInfo.isWindows && JnaLoader.isLoaded()) { + if (OS.CURRENT == OS.Windows && JnaLoader.isLoaded()) { char[] path = new char[WinDef.MAX_PATH]; WinNT.HRESULT res = Shell32.INSTANCE.SHGetFolderPath(null, ShlObj.CSIDL_DESKTOP, null, ShlObj.SHGFP_TYPE_CURRENT, path); if (WinError.S_OK.equals(res)) { @@ -74,7 +75,7 @@ final class GotoDesktopDirAction extends FileChooserAction implements LightEditC return Path.of(new String(path, 0, len)); } } - else if (SystemInfo.isMac && JnaLoader.isLoaded()) { + else if (OS.CURRENT == OS.macOS && JnaLoader.isLoaded()) { ID manager = Foundation.invoke(Foundation.getObjcClass("NSFileManager"), "defaultManager"); ID url = Foundation.invoke(manager, "URLForDirectory:inDomain:appropriateForURL:create:error:", 12 /*NSDesktopDirectory*/, 1 /*NSUserDomainMask*/, null, false, null); @@ -83,8 +84,8 @@ final class GotoDesktopDirAction extends FileChooserAction implements LightEditC return Path.of(path); } } - else if (SystemInfo.hasXdgOpen()) { - String path = ExecUtil.execAndReadLine(new GeneralCommandLine("xdg-user-dir", "DESKTOP")); + else if (PathEnvironmentVariableUtil.isOnPath("xdg-user-dir")) { + var path = ExecUtil.execAndReadLine(new GeneralCommandLine("xdg-user-dir", "DESKTOP")); if (path != null && !path.isBlank()) { try { return Path.of(path); diff --git a/platform/platform-tests/testSrc/com/intellij/execution/configurations/PathEnvironmentVariableUtilTest.java b/platform/platform-tests/testSrc/com/intellij/execution/configurations/PathEnvironmentVariableUtilTest.java new file mode 100644 index 000000000000..b428a7a98c1e --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/execution/configurations/PathEnvironmentVariableUtilTest.java @@ -0,0 +1,27 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.execution.configurations; + +import com.intellij.util.system.OS; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +class PathEnvironmentVariableUtilTest { + @Test void isOnPathContract() { + assertThatCode(() -> PathEnvironmentVariableUtil.isOnPath("bad\\path")).isInstanceOf(IllegalArgumentException.class); + } + + @Test void isOnPathPositive() { + var shell = Path.of(OS.CURRENT == OS.Windows ? "C:\\Windows\\System32\\cmd.exe" : "/bin/sh"); + assumeTrue(Files.isExecutable(shell)); + assertThat(PathEnvironmentVariableUtil.isOnPath(shell.getFileName().toString())).isTrue(); + } + + @Test void isOnPathNegative() { + assertThat(PathEnvironmentVariableUtil.isOnPath("no-one-in-his-right-mind-names-an-exec-like-this")).isFalse(); + } +} diff --git a/platform/platform-util-io/api-dump.txt b/platform/platform-util-io/api-dump.txt index a537b4a2a71a..12b7dbe46e78 100644 --- a/platform/platform-util-io/api-dump.txt +++ b/platform/platform-util-io/api-dump.txt @@ -151,6 +151,7 @@ f:com.intellij.execution.configurations.PathEnvironmentVariableUtil - s:getPathDirs(java.lang.String):java.util.List - s:getPathVariableValue():java.lang.String - s:getWindowsExecutableFileExtensions():java.util.List +- s:isOnPath(java.lang.String):Z c:com.intellij.execution.configurations.PtyCommandLine - com.intellij.execution.configurations.GeneralCommandLine - com.intellij.execution.configurations.CommandLineWithSuspendedProcessCallback diff --git a/platform/platform-util-io/src/com/intellij/execution/configurations/PathEnvironmentVariableUtil.java b/platform/platform-util-io/src/com/intellij/execution/configurations/PathEnvironmentVariableUtil.java index c916571e807a..15e8a0bc315e 100644 --- a/platform/platform-util-io/src/com/intellij/execution/configurations/PathEnvironmentVariableUtil.java +++ b/platform/platform-util-io/src/com/intellij/execution/configurations/PathEnvironmentVariableUtil.java @@ -1,4 +1,4 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.execution.configurations; import com.intellij.openapi.util.SystemInfo; @@ -10,15 +10,19 @@ import org.jetbrains.annotations.*; import java.io.File; import java.io.FileFilter; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * A collection of utility methods for working with PATH environment variable. */ +@SuppressWarnings("ALL") public final class PathEnvironmentVariableUtil { - - private static final String PATH = "PATH"; + private static final Map ourOnPathCache = Collections.synchronizedMap(new HashMap<>()); private PathEnvironmentVariableUtil() { } @@ -143,7 +147,7 @@ public final class PathEnvironmentVariableUtil { * Retrieves the value of PATH environment variable */ public static @Nullable String getPathVariableValue() { - return EnvironmentUtil.getValue(PATH); + return EnvironmentUtil.getValue("PATH"); } public static @Nullable File findExecutableInPathOnAnyOS(@NotNull @NonNls String fileBaseName) { @@ -157,4 +161,29 @@ public final class PathEnvironmentVariableUtil { return findInPath(fileBaseName); } } + + /** + * Checks whether the given file is in one of the directories listed in the PATH environment variable. + * The first call might be slow, but the result is cached. + * + * @since 2025.3 + */ + public static boolean isOnPath(@NotNull String name) { + if (name.indexOf('\\') >= 0 || name.indexOf('/') >= 0) throw new IllegalArgumentException(name); + var result = ourOnPathCache.get(name); + if (result == null) { + result = Boolean.FALSE; + var path = getPathVariableValue(); + if (path != null) { + for (var dir : StringUtil.tokenize(path, File.pathSeparator)) { + if (Files.isExecutable(Path.of(dir, name))) { + result = Boolean.TRUE; + break; + } + } + } + ourOnPathCache.put(name, result); + } + return result; + } } diff --git a/platform/platform-util-io/src/com/intellij/execution/sudo/LocalSudoCommandProvider.kt b/platform/platform-util-io/src/com/intellij/execution/sudo/LocalSudoCommandProvider.kt index 13ddc1ef614e..5360c5226920 100644 --- a/platform/platform-util-io/src/com/intellij/execution/sudo/LocalSudoCommandProvider.kt +++ b/platform/platform-util-io/src/com/intellij/execution/sudo/LocalSudoCommandProvider.kt @@ -1,11 +1,11 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.execution.sudo import com.intellij.execution.CommandLineUtil import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.configurations.PathEnvironmentVariableUtil import com.intellij.execution.util.ExecUtil import com.intellij.openapi.application.PathManager -import com.intellij.openapi.util.io.PathExecLazyValue import com.intellij.openapi.util.text.StringUtil import com.intellij.util.io.IdeUtilIoBundle import com.intellij.util.system.OS @@ -14,10 +14,6 @@ import org.jetbrains.annotations.Nls @ApiStatus.Internal open class LocalSudoCommandProvider : SudoCommandProvider { - private val hasGkSudo = PathExecLazyValue.create("gksudo") - private val hasKdeSudo = PathExecLazyValue.create("kdesudo") - private val hasPkExec = PathExecLazyValue.create("pkexec") - override fun isAvailable(): Boolean = true override fun sudoCommand(wrappedCommand: GeneralCommandLine, prompt: @Nls String): GeneralCommandLine? { @@ -37,13 +33,13 @@ open class LocalSudoCommandProvider : SudoCommandProvider { do shell script ${escapedCommand} with prompt "${escapedPrompt}" with administrator privileges without altering line endings end tell""".trimIndent()) } - hasGkSudo.get() -> { + PathEnvironmentVariableUtil.isOnPath("gksudo") -> { GeneralCommandLine(listOf("gksudo", "--message", prompt, "--") + envCommand(wrappedCommand) + command) } - hasKdeSudo.get() -> { + PathEnvironmentVariableUtil.isOnPath("kdesudo") -> { GeneralCommandLine(listOf("kdesudo", "--comment", prompt, "--") + envCommand(wrappedCommand) + command) } - hasPkExec.get() -> { + PathEnvironmentVariableUtil.isOnPath("pkexec") -> { GeneralCommandLine(listOf("pkexec") + envCommand(wrappedCommand) + command) } ExecUtil.hasTerminalApp() -> { diff --git a/platform/platform-util-io/src/com/intellij/execution/util/ExecUtil.kt b/platform/platform-util-io/src/com/intellij/execution/util/ExecUtil.kt index c8ea36f0544a..2ce538fe30d2 100644 --- a/platform/platform-util-io/src/com/intellij/execution/util/ExecUtil.kt +++ b/platform/platform-util-io/src/com/intellij/execution/util/ExecUtil.kt @@ -4,21 +4,21 @@ package com.intellij.execution.util import com.intellij.execution.CommandLineUtil import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.configurations.PathEnvironmentVariableUtil import com.intellij.execution.process.* import com.intellij.execution.sudo.SudoCommandProvider import com.intellij.openapi.application.PathManager import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.progress.runBlockingMaybeCancellable import com.intellij.openapi.util.NlsSafe -import com.intellij.openapi.util.SystemInfoRt import com.intellij.openapi.util.io.FileUtil -import com.intellij.openapi.util.io.PathExecLazyValue import com.intellij.platform.eel.EelExecApi import com.intellij.platform.eel.provider.asEelPath import com.intellij.platform.eel.spawnProcess import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.io.IdeUtilIoBundle import com.intellij.util.io.SuperUserStatus +import com.intellij.util.system.OS import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.Nls import java.io.BufferedReader @@ -27,20 +27,19 @@ import java.io.InputStreamReader import java.nio.file.Path object ExecUtil { - private val hasGnomeTerminal = PathExecLazyValue.create("gnome-terminal") - @Suppress("SpellCheckingInspection") - private val hasKdeTerminal = PathExecLazyValue.create("konsole") - @Suppress("SpellCheckingInspection") - private val hasUrxvt = PathExecLazyValue.create("urxvt") - private val hasXTerm = PathExecLazyValue.create("xterm") + private val hasSupportedTerminals = lazy { + @Suppress("SpellCheckingInspection") + PathEnvironmentVariableUtil.isOnPath("konsole") || + PathEnvironmentVariableUtil.isOnPath("gnome-terminal") || + PathEnvironmentVariableUtil.isOnPath("urxvt") || + PathEnvironmentVariableUtil.isOnPath("xterm") + } @JvmStatic - val osascriptPath: @NlsSafe String - get() = "/usr/bin/osascript" + val osascriptPath: @NlsSafe String get() = "/usr/bin/osascript" @JvmStatic - val openCommandPath: @NlsSafe String - get() = "/usr/bin/open" + val openCommandPath: @NlsSafe String get() = "/usr/bin/open" @ApiStatus.Internal @JvmStatic @@ -167,17 +166,16 @@ object ExecUtil { @ApiStatus.Internal @JvmStatic - fun hasTerminalApp(): Boolean = - SystemInfoRt.isWindows || SystemInfoRt.isMac || hasKdeTerminal.get() || hasGnomeTerminal.get() || hasUrxvt.get() || hasXTerm.get() + fun hasTerminalApp(): Boolean = OS.CURRENT == OS.Windows || OS.CURRENT == OS.macOS || hasSupportedTerminals.value @ApiStatus.Internal @JvmStatic @Suppress("SpellCheckingInspection") fun getTerminalCommand(@Nls(capitalization = Nls.Capitalization.Title) title: String?, command: String): List<@NlsSafe String> = when { - SystemInfoRt.isWindows -> { + OS.CURRENT == OS.Windows -> { listOf(CommandLineUtil.getWinShellName(), "/c", "start", GeneralCommandLine.inescapableQuote(title?.replace('"', '\'') ?: ""), command) } - SystemInfoRt.isMac -> { + OS.CURRENT == OS.macOS -> { val prefix = if (title != null) "\"echo -n \" & " + escapeAppleScriptArgument("\\0033]0;${title}\\007") + " & \" ; \" & " else "" val script = prefix + "\"clear ; exec \" & " + escapeAppleScriptArgument(command) listOf(osascriptPath, "-e", """ @@ -187,24 +185,24 @@ object ExecUtil { |end tell """.trimMargin()) } - hasKdeTerminal.get() -> { + PathEnvironmentVariableUtil.isOnPath("konsole") -> { if (title != null) listOf("konsole", "-p", "tabtitle=\"${title.replace('"', '\'')}\"", "-e", command) else listOf("konsole", "-e", command) } - hasGnomeTerminal.get() -> { + PathEnvironmentVariableUtil.isOnPath("gnome-terminal") -> { if (title != null) listOf("gnome-terminal", "-t", title, "-x", command) else listOf("gnome-terminal", "-x", command) } - hasUrxvt.get() -> { + PathEnvironmentVariableUtil.isOnPath("urxvt") -> { if (title != null) listOf("urxvt", "-title", title, "-e", command) else listOf("urxvt", "-e", command) } - hasXTerm.get() -> { + PathEnvironmentVariableUtil.isOnPath("xterm") -> { if (title != null) listOf("xterm", "-T", title, "-e", command) else listOf("xterm", "-e", command) } else -> { - throw UnsupportedOperationException("Unsupported OS/desktop: ${SystemInfoRt.OS_NAME}/${System.getenv("XDG_CURRENT_DESKTOP")}") + throw UnsupportedOperationException("Unsupported OS/desktop: ${OS.CURRENT.name}/${System.getenv("XDG_CURRENT_DESKTOP")}") } } diff --git a/platform/util/api-dump.txt b/platform/util/api-dump.txt index f8fffc29b1b5..83b5a2b9dfb9 100644 --- a/platform/util/api-dump.txt +++ b/platform/util/api-dump.txt @@ -423,6 +423,8 @@ com.intellij.util.keyFMap.KeyFMap - a:minus(com.intellij.openapi.util.Key):com.intellij.util.keyFMap.KeyFMap - a:plus(com.intellij.openapi.util.Key,java.lang.Object):com.intellij.util.keyFMap.KeyFMap - a:size():I +e:com.intellij.util.system.OS +- s:isGenericUnix():Z com.intellij.util.xmlb.Accessor - a:getAnnotation(java.lang.Class):java.lang.annotation.Annotation - a:getGenericType():java.lang.reflect.Type diff --git a/platform/util/src/com/intellij/openapi/util/SystemInfo.java b/platform/util/src/com/intellij/openapi/util/SystemInfo.java index 587b57a31a0c..5902638bca9b 100644 --- a/platform/util/src/com/intellij/openapi/util/SystemInfo.java +++ b/platform/util/src/com/intellij/openapi/util/SystemInfo.java @@ -1,7 +1,6 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.openapi.util; -import com.intellij.openapi.util.io.PathExecLazyValue; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.util.text.Strings; import com.intellij.util.containers.ContainerUtil; @@ -12,8 +11,11 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.List; -import java.util.function.Supplier; + +import static com.intellij.openapi.util.NotNullLazyValue.lazy; /** * Provides information about operating system, system-wide settings, and Java Runtime. @@ -68,18 +70,16 @@ public final class SystemInfo { public static final boolean isWin11OrNewer = isWindows && isOsVersionAtLeast("11.0"); /** - * Set to true if we are running in a Wayland environment, either through - * XWayland or using Wayland directly. + * Set to true if we are running in a Wayland environment, either through XWayland or using Wayland directly. */ public static final boolean isWayland; - public static final boolean isXWindow = SystemInfoRt.isUnix && !SystemInfoRt.isMac; public static final boolean isGNOME, isKDE, isXfce, isI3; static { // http://askubuntu.com/questions/72549/how-to-determine-which-window-manager-is-running/227669#227669 // https://userbase.kde.org/KDE_System_Administration/Environment_Variables#KDE_FULL_SESSION - if (SystemInfoRt.isUnix && !SystemInfoRt.isMac) { + if (!isWindows && !isMac) { isWayland = System.getenv("WAYLAND_DISPLAY") != null; - @SuppressWarnings("SpellCheckingInspection") String desktop = System.getenv("XDG_CURRENT_DESKTOP"), gdmSession = System.getenv("GDMSESSION"); + @SuppressWarnings({"SpellCheckingInspection", "RedundantSuppression"}) String desktop = System.getenv("XDG_CURRENT_DESKTOP"), gdmSession = System.getenv("GDMSESSION"); isGNOME = desktop != null && desktop.contains("GNOME") || gdmSession != null && gdmSession.contains("gnome"); isKDE = !isGNOME && (desktop != null && desktop.contains("KDE") || System.getenv("KDE_FULL_SESSION") != null); isXfce = !isGNOME && !isKDE && (desktop != null && desktop.contains("XFCE")); @@ -94,14 +94,16 @@ public final class SystemInfo { public static final boolean isFileSystemCaseSensitive = SystemInfoRt.isFileSystemCaseSensitive; - private static final Supplier ourHasXdgOpen = SystemInfoRt.isUnix && !SystemInfoRt.isMac - ? PathExecLazyValue.create("xdg-open") : () -> false; + /** @deprecated use {@link com.intellij.execution.configurations.PathEnvironmentVariableUtil#isOnPath} instead */ + @Deprecated + @ApiStatus.ScheduledForRemoval public static boolean hasXdgOpen() { return ourHasXdgOpen.get(); } - private static final Supplier ourHasXdgMime = SystemInfoRt.isUnix && !SystemInfoRt.isMac - ? PathExecLazyValue.create("xdg-mime") : () -> false; + /** @deprecated use {@link com.intellij.execution.configurations.PathEnvironmentVariableUtil#isOnPath} instead */ + @Deprecated + @ApiStatus.ScheduledForRemoval public static boolean hasXdgMime() { return ourHasXdgMime.get(); } @@ -203,5 +205,25 @@ public final class SystemInfo { @Deprecated @ApiStatus.ScheduledForRemoval public static final boolean is64Bit = CpuArch.CURRENT.width == 64; + + /** @deprecated misleading; consider using {@link com.intellij.util.system.OS#isGenericUnix} instead, if appropriate */ + @Deprecated + @ApiStatus.ScheduledForRemoval + public static final boolean isXWindow = SystemInfoRt.isUnix && !SystemInfoRt.isMac; + + private static final NotNullLazyValue ourHasXdgOpen = isUnix && !isMac ? lazy(() -> isOnPath("xdg-open")) : NotNullLazyValue.createConstantValue(false); + private static final NotNullLazyValue ourHasXdgMime = isUnix && !isMac ? lazy(() -> isOnPath("xdg-mime")) : NotNullLazyValue.createConstantValue(false); + + private static boolean isOnPath(String name) { + String path = System.getenv("PATH"); + if (path != null) { + for (String dir : StringUtil.tokenize(path, ":")) { + if (Files.isExecutable(Paths.get(dir, name))) { + return true; + } + } + } + return false; + } // } diff --git a/platform/util/src/com/intellij/openapi/util/io/PathExecLazyValue.java b/platform/util/src/com/intellij/openapi/util/io/PathExecLazyValue.java deleted file mode 100644 index 5d41a24d41f0..000000000000 --- a/platform/util/src/com/intellij/openapi/util/io/PathExecLazyValue.java +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.intellij.openapi.util.io; - -import com.intellij.openapi.util.NlsSafe; -import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.util.text.Strings; -import com.intellij.util.EnvironmentUtil; -import com.intellij.util.concurrency.SynchronizedClearableLazy; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -import java.io.File; -import java.util.function.Supplier; - -@ApiStatus.Internal -public final class PathExecLazyValue { - private PathExecLazyValue() { } - - public static @NotNull Supplier create(@NlsSafe @NotNull String name) { - if (Strings.containsAnyChar(name, "/\\")) { - throw new IllegalArgumentException(name); - } - - return new SynchronizedClearableLazy<>(() -> { - String path = EnvironmentUtil.getValue("PATH"); - if (path != null) { - for (String dir : StringUtil.tokenize(path, File.pathSeparator)) { - if (new File(dir, name).canExecute()) { - return true; - } - } - } - - return false; - }); - } -} diff --git a/platform/util/src/com/intellij/util/system/OS.java b/platform/util/src/com/intellij/util/system/OS.java index 3231389b3c6b..6f7b3c291e63 100644 --- a/platform/util/src/com/intellij/util/system/OS.java +++ b/platform/util/src/com/intellij/util/system/OS.java @@ -1,4 +1,4 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.util.system; import com.intellij.execution.Platform; @@ -49,4 +49,11 @@ public enum OS { public @NotNull Platform getPlatform() { return this == Windows ? Platform.WINDOWS : Platform.UNIX; } + + /** + * Returns {@code true} if the current operating system is a generic Unix-like system (not Windows or macOS). + */ + public static boolean isGenericUnix() { + return CURRENT != Windows && CURRENT != macOS; + } } diff --git a/platform/util/testSrc/com/intellij/openapi/util/io/PathExecLazyValueTest.kt b/platform/util/testSrc/com/intellij/openapi/util/io/PathExecLazyValueTest.kt deleted file mode 100644 index 466918853f08..000000000000 --- a/platform/util/testSrc/com/intellij/openapi/util/io/PathExecLazyValueTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -package com.intellij.openapi.util.io - -import com.intellij.openapi.util.SystemInfo -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test -import java.io.File - -class PathExecLazyValueTest { - @Test fun positive() { - val shell = File(if (SystemInfo.isWindows) "C:\\Windows\\System32\\cmd.exe" else "/bin/sh") - assertTrue(shell.canExecute()) - assertTrue(PathExecLazyValue.create(shell.name).get()) - } - - @Test fun negative() { - assertFalse(PathExecLazyValue.create("no-one-in-his-right-mind-names-an-exec-like-this").get()) - } - - @Test(expected = IllegalArgumentException::class) fun contract() { - PathExecLazyValue.create("bad\\path") - } -} \ No newline at end of file