diff --git a/NOTICE b/NOTICE index fab54b9d8..42f325890 100644 --- a/NOTICE +++ b/NOTICE @@ -1 +1,5 @@ Copyright 2014-2018 Appium Contributors + +This product includes software adapted from Selenium (https://www.selenium.dev/), +licensed under the Apache License, Version 2.0. See individual source files under +io.appium.java_client.internal for details of which classes are adapted and from where. diff --git a/src/main/java/io/appium/java_client/internal/process/ExecutableFinder.java b/src/main/java/io/appium/java_client/internal/process/ExecutableFinder.java new file mode 100644 index 000000000..6cb2b9a4d --- /dev/null +++ b/src/main/java/io/appium/java_client/internal/process/ExecutableFinder.java @@ -0,0 +1,106 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.internal.process; + +import org.jspecify.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +/** + * Locates a named executable by scanning the PATH. + * Adapted from Selenium's {@code org.openqa.selenium.os.ExecutableFinder} + * (Apache License 2.0), vendored here to drop the {@code selenium-os} dependency and + * trimmed to bare-name PATH lookup, the only way this class is actually called. + */ +public class ExecutableFinder { + + private static final boolean IS_WINDOWS = osNameContains("win"); + private static final boolean IS_MAC = osNameContains("mac"); + + private static final List ENDINGS = IS_WINDOWS + ? List.of("", ".cmd", ".exe", ".com", ".bat") + : singletonList(""); + + private static boolean osNameContains(String needle) { + return System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains(needle); + } + + /** + * Finds a named executable by scanning the PATH. On Windows, common executable + * endings (".com", ".bat" and ".exe") are tried in addition to the bare name. + * + * @param named the name of the executable to find, e.g. "node" + * @return the absolute path to the executable, or {@code null} if no match is found + */ + @Nullable + public String find(String named) { + List pathSegments = new ArrayList<>(fromEnvironment()); + if (IS_MAC) { + pathSegments.addAll(macSpecificPathSegments()); + } + + for (String pathSegment : pathSegments) { + for (String ending : ENDINGS) { + File file = new File(pathSegment, named + ending); + if (canExecute(file)) { + return file.getAbsolutePath(); + } + } + } + return null; + } + + private List fromEnvironment() { + String pathName = "PATH"; + Map env = System.getenv(); + if (!env.containsKey(pathName)) { + for (String key : env.keySet()) { + if (pathName.equalsIgnoreCase(key)) { + pathName = key; + break; + } + } + } + String path = env.get(pathName); + return path != null ? List.of(path.split(File.pathSeparator)) : emptyList(); + } + + private List macSpecificPathSegments() { + File pathFile = new File("/etc/paths"); + if (pathFile.exists()) { + try { + return Files.readAllLines(pathFile.toPath()); + } catch (IOException e) { + // Ignore, PATH env var segments are still used + } + } + return emptyList(); + } + + private static boolean canExecute(File file) { + return file.exists() && !file.isDirectory() && file.canExecute(); + } +} diff --git a/src/main/java/io/appium/java_client/service/local/AppiumServiceBuilder.java b/src/main/java/io/appium/java_client/service/local/AppiumServiceBuilder.java index b9337212a..4534942fe 100644 --- a/src/main/java/io/appium/java_client/service/local/AppiumServiceBuilder.java +++ b/src/main/java/io/appium/java_client/service/local/AppiumServiceBuilder.java @@ -20,6 +20,7 @@ import com.google.gson.GsonBuilder; import io.appium.java_client.android.options.context.SupportsChromedriverExecutableOption; import io.appium.java_client.android.options.signing.SupportsKeystoreOptions; +import io.appium.java_client.internal.process.ExecutableFinder; import io.appium.java_client.remote.MobileBrowserType; import io.appium.java_client.remote.options.SupportsAppOption; import io.appium.java_client.service.local.flags.GeneralServerFlag; @@ -28,7 +29,6 @@ import org.jspecify.annotations.Nullable; import org.openqa.selenium.Capabilities; import org.openqa.selenium.Platform; -import org.openqa.selenium.os.ExecutableFinder; import org.openqa.selenium.remote.Browser; import org.openqa.selenium.remote.service.DriverService; diff --git a/src/test/java/io/appium/java_client/internal/process/ExecutableFinderTest.java b/src/test/java/io/appium/java_client/internal/process/ExecutableFinderTest.java new file mode 100644 index 000000000..0ad5eb40d --- /dev/null +++ b/src/test/java/io/appium/java_client/internal/process/ExecutableFinderTest.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.internal.process; + +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class ExecutableFinderTest { + + @Test + void returnsNullWhenNothingMatches() { + String named = "definitely-not-a-real-executable-" + UUID.randomUUID(); + + assertNull(new ExecutableFinder().find(named)); + } + + @Test + void findsAnExecutableOnPath() { + // The JVM running this test was itself launched via a PATH-resolvable "java", + // so this exercises the real PATH-scanning branch without mocking the environment. + assertNotNull(new ExecutableFinder().find("java")); + } +}