Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AddonFinderProcess fixes #4061

Merged
merged 12 commits into from
Jan 31, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
import static org.openhab.core.config.discovery.addon.AddonFinderConstants.ADDON_SUGGESTION_FINDER;

import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.addon.AddonDiscoveryMethod;
import org.openhab.core.addon.AddonInfo;
import org.openhab.core.addon.AddonMatchProperty;
Expand All @@ -46,35 +47,52 @@ public class ProcessAddonFinder extends BaseAddonFinder {
public static final String SERVICE_NAME = SERVICE_TYPE + ADDON_SUGGESTION_FINDER;

private static final String COMMAND = "command";
private static final String COMMAND_LINE = "commandLine";
private static final Set<String> SUPPORTED_PROPERTIES = Set.of(COMMAND, COMMAND_LINE);

private final Logger logger = LoggerFactory.getLogger(ProcessAddonFinder.class);

// get list of running processes visible to openHAB,
// also tries to mitigate differences on different operating systems
String getProcessCommandProcess(ProcessHandle h) {
Optional<String> command = h.info().command();
if (command.isPresent()) {
return command.get();
}
Optional<String[]> args = h.info().arguments();
if (args.isEmpty()) {
return "";
}
String[] argsArray = args.get();
if (argsArray.length < 1) {
return "";
/**
* Private class to extract match property parameters from a {@link ProcessHandle.Info} object.
* Tries to mitigate differences on different operating systems.
*/
protected static class ProcessInfo {
protected final @Nullable String command;
protected final @Nullable String commandLine;

/**
* Initializes the command and commandLine fields.
* If the command field is not present, it parses the first token in the command line.
*/
protected ProcessInfo(ProcessHandle.Info info) {
commandLine = info.commandLine().orElse(null);
String cmd = info.command().orElse(null);
if ((cmd == null || cmd.isEmpty()) && commandLine != null) {
cmd = Objects.requireNonNull(commandLine);
andrewfg marked this conversation as resolved.
Show resolved Hide resolved
String[] args = info.arguments().orElse(null);
if (args != null) {
for (int i = args.length - 1; i >= 0; i--) {
int index = cmd.lastIndexOf(args[i]);
if (index >= 0) {
cmd = cmd.substring(0, index);
}
}
andrewfg marked this conversation as resolved.
Show resolved Hide resolved
}
cmd = cmd.stripTrailing();
}
command = cmd;
andrewfg marked this conversation as resolved.
Show resolved Hide resolved
}
return argsArray[0];
}

@Override
public Set<AddonInfo> getSuggestedAddons() {
logger.trace("ProcessAddonFinder::getSuggestedAddons");
Set<AddonInfo> result = new HashSet<>();
Set<String> processList;
Set<ProcessInfo> processInfos;

try {
processList = ProcessHandle.allProcesses().map(this::getProcessCommandProcess)
.filter(Predicate.not(String::isEmpty)).collect(Collectors.toUnmodifiableSet());
processInfos = ProcessHandle.allProcesses().map(process -> new ProcessInfo(process.info()))
andrewfg marked this conversation as resolved.
Show resolved Hide resolved
.collect(Collectors.toUnmodifiableSet());
} catch (SecurityException | UnsupportedOperationException unused) {
logger.info("Cannot obtain process list, suggesting add-ons based on running processes is not possible");
return result;
Expand All @@ -84,28 +102,30 @@ public Set<AddonInfo> getSuggestedAddons() {
for (AddonDiscoveryMethod method : candidate.getDiscoveryMethods().stream()
.filter(method -> SERVICE_TYPE.equals(method.getServiceType())).toList()) {

List<AddonMatchProperty> matchProperties = method.getMatchProperties();
List<AddonMatchProperty> commands = matchProperties.stream()
.filter(amp -> COMMAND.equals(amp.getName())).toList();

if (matchProperties.size() != commands.size()) {
logger.warn("Add-on '{}' addon.xml file contains unsupported 'match-property'", candidate.getUID());
}
Map<String, Pattern> matchProperties = method.getMatchProperties().stream()
.collect(Collectors.toMap(AddonMatchProperty::getName, AddonMatchProperty::getPattern));

if (commands.isEmpty()) {
logger.warn("Add-on '{}' addon.xml file does not specify match property \"{}\"", candidate.getUID(),
COMMAND);
if (matchProperties.isEmpty()) {
logger.warn("Add-on '{}' addon.xml file contains no 'match-property'", candidate.getUID());
andrewfg marked this conversation as resolved.
Show resolved Hide resolved
break;
}

// now check if a process matches the pattern defined in addon.xml
logger.trace("Checking candidate: {}", candidate.getUID());
Set<String> propertyNames = new HashSet<>(matchProperties.keySet());
boolean noSupportedProperty = !propertyNames.removeAll(SUPPORTED_PROPERTIES);

for (AddonMatchProperty command : commands) {
logger.trace("Candidate {}, pattern \"{}\"", candidate.getUID(), command.getRegex());
boolean match = processList.stream().anyMatch(c -> command.getPattern().matcher(c).matches());
if (!propertyNames.isEmpty()) {
logger.warn("Add-on '{}' addon.xml file contains unsupported 'match-property' [{}]",
andrewfg marked this conversation as resolved.
Show resolved Hide resolved
candidate.getUID(), String.join(",", propertyNames));

if (match) {
if (noSupportedProperty) {
break;
}
}

logger.trace("Checking candidate: {}", candidate.getUID());
for (ProcessInfo processInfo : processInfos) {
if (propertyMatches(matchProperties, COMMAND, processInfo.command)
&& propertyMatches(matchProperties, COMMAND_LINE, processInfo.commandLine)) {
result.add(candidate);
logger.debug("Suggested add-on found: {}", candidate.getUID());
break;
Expand Down