From 502793b09ff812a94b90aede5949450bf3a62bbe Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 5 Dec 2022 00:19:51 -0700 Subject: [PATCH] [jrubyscripting] Implement dependency tracking (#13810) * [jrubyscripting] implement dependency tracking watchers had to be refactored similar to jsscripting. it supports watching any directory referenced from RUBYLIB, as well as the gem home. it properly excludes lib and gem home (as well as other gem homes if you have multiple jruby versions installed) from loading as regular scripts. this is a breaking change if you don't have RUBYLIB explicitly configured, and you are using the old default directory. it's expected that the detection of what files and gems any given script uses will be self-identified by the script, presumably by the helper library. JRubyScriptEngineConfiguration was largely refactored as part of this. * CONFIGURATION_PARAMETERS was renamed, and is no longer static, since it's modified every time the configuration is changed * OptionalConfigurationElement was simplified since default values are always provided now. this also simplified lots of other code that accesses the current settings. Signed-off-by: Cody Cutrer --- .../README.md | 22 +- .../JRubyScriptEngineConfiguration.java | 298 ++++++++++-------- .../internal/JRubyScriptEngineFactory.java | 87 ++++- .../internal/watch/BidiSetBag.java | 111 +++++++ .../watch/JRubyDependencyTracker.java | 106 +++++++ .../internal/watch/JRubyGemWatchService.java | 74 +++++ .../internal/watch/JRubyLibWatchService.java | 59 ++++ .../watch/JRubyScriptFileWatcher.java | 80 +++++ .../main/resources/OH-INF/config/config.xml | 8 +- .../resources/OH-INF/i18n/jruby.properties | 4 +- 10 files changed, 690 insertions(+), 159 deletions(-) create mode 100644 bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/BidiSetBag.java create mode 100644 bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyDependencyTracker.java create mode 100644 bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyGemWatchService.java create mode 100644 bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyLibWatchService.java create mode 100644 bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyScriptFileWatcher.java diff --git a/bundles/org.openhab.automation.jrubyscripting/README.md b/bundles/org.openhab.automation.jrubyscripting/README.md index 5225683bc4a61..d57aafde3ac4e 100644 --- a/bundles/org.openhab.automation.jrubyscripting/README.md +++ b/bundles/org.openhab.automation.jrubyscripting/README.md @@ -8,15 +8,15 @@ After installing this add-on, you will find configuration options in the openHAB Alternatively, JRuby configuration parameters may be set by creating a `jruby.cfg` file in `conf/services/` -| Parameter | Default | Description | -| ----------------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| org.openhab.automation.jrubyscripting:gem_home | $OPENHAB_CONF/scripts/lib/ruby/gem_home | Location ruby gems will be installed and loaded, directory will be created if missing and gem installs are specified | -| org.openhab.automation.jrubyscripting:rubylib | $OPENHAB_CONF/automation/lib/ruby/ | Search path for user libraries. Separate each path with a colon (semicolon in Windows). | -| org.openhab.automation.jrubyscripting:local_context | singlethread | The local context holds Ruby runtime, name-value pairs for sharing variables between Java and Ruby. See [this](https://github.com/jruby/jruby/wiki/RedBridge#Context_Instance_Type) for options and details | -| org.openhab.automation.jrubyscripting:local_variables | transient | Defines how variables are shared between Ruby and Java. See [this](https://github.com/jruby/jruby/wiki/RedBridge#local-variable-behavior-options) for options and details | -| org.openhab.automation.jrubyscripting:gems | | A comma separated list of [Ruby Gems](https://rubygems.org/) to install. | -| org.openhab.automation.jrubyscripting:require | | A comma separated list of script names to be required by the JRuby Scripting Engine at the beginning of user scripts. | -| org.openhab.automation.jrubyscripting:check_update | true | Check RubyGems for updates to the above gems when OpenHAB starts or JRuby settings are changed. Otherwise it will try to fulfil the requirements with locally installed gems, and you can manage them yourself with an external Ruby by setting the same GEM_HOME. | +| Parameter | Default | Description | +| ----------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| org.openhab.automation.jrubyscripting:gem_home | $OPENHAB_CONF/automation/ruby/.gem/{RUBY_ENGINE_VERSION} | Location Ruby Gems will be installed to and loaded from. Directory will be created if necessary. You can use `{RUBY_ENGINE_VERSION}`, `{RUBY_ENGINE}` and/or `{RUBY_VERSION}` replacements in this value to automatically point to a new directory when the addon is updated with a new version of JRuby. | +| org.openhab.automation.jrubyscripting:rubylib | $OPENHAB_CONF/automation/ruby/lib | Search path for user libraries. Separate each path with a colon (semicolon in Windows). | +| org.openhab.automation.jrubyscripting:local_context | singlethread | The local context holds Ruby runtime, name-value pairs for sharing variables between Java and Ruby. See [this](https://github.com/jruby/jruby/wiki/RedBridge#Context_Instance_Type) for options and details | +| org.openhab.automation.jrubyscripting:local_variables | transient | Defines how variables are shared between Ruby and Java. See [this](https://github.com/jruby/jruby/wiki/RedBridge#local-variable-behavior-options) for options and details | +| org.openhab.automation.jrubyscripting:gems | | A comma separated list of [Ruby Gems](https://rubygems.org/) to install. | +| org.openhab.automation.jrubyscripting:require | | A comma separated list of script names to be required by the JRuby Scripting Engine at the beginning of user scripts. | +| org.openhab.automation.jrubyscripting:check_update | true | Check RubyGems for updates to the above gems when OpenHAB starts or JRuby settings are changed. Otherwise it will try to fulfil the requirements with locally installed gems, and you can manage them yourself with an external Ruby by setting the same GEM_HOME. | ## Ruby Gems @@ -42,7 +42,7 @@ org.openhab.automation.jrubyscripting:gems=library= >= 2.2.0; < 3.0, another-gem When this add-on is installed, you can select JRuby as a scripting language when creating a script action within the rule editor of the UI. -Alternatively, you can create scripts in the `automation/jsr223/ruby/personal` configuration directory. +Alternatively, you can create scripts in the `automation/ruby` configuration directory. If you create an empty file called `test.rb`, you will see a log line with information similar to: ```text @@ -62,7 +62,7 @@ log:set DEBUG org.openhab.automation.jrubyscripting All [ScriptExtensions]({{base}}/configuration/jsr223.html#scriptextension-objects-all-jsr223-languages) are available in JRuby with the following exceptions/modifications: - The `File` variable, referencing `java.io.File` is not available as it conflicts with Ruby's File class preventing Ruby from initializing -- Globals `scriptExtension`, `automationManager`, `ruleRegistry`, `items`, `voice`, `rules`, `things`, `events`, `itemRegistry`, `ir`, `actions`, `se`, `audio`, `lifecycleTracker` are prepended with a `$` (e.g. `$automationManager`) making them available as global objects in Ruby. +- Globals `scriptExtension`, `automationManager`, `ruleRegistry`, `items`, `voice`, `rules`, `things`, `events`, `itemRegistry`, `ir`, `actions`, `se`, `audio`, `lifecycleTracker` are prepended with a `$` (e.g. `$automationManager`) making them available as global variables in Ruby. ## Script Examples diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineConfiguration.java b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineConfiguration.java index f5263329ed60a..b490aa5edc177 100644 --- a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineConfiguration.java +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineConfiguration.java @@ -13,21 +13,21 @@ package org.openhab.automation.jrubyscripting.internal; import java.io.File; -import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; import javax.script.ScriptException; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.jruby.runtime.Constants; import org.openhab.core.OpenHAB; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,45 +43,46 @@ public class JRubyScriptEngineConfiguration { private final Logger logger = LoggerFactory.getLogger(JRubyScriptEngineConfiguration.class); - private static final Path DEFAULT_GEM_HOME = Paths.get(OpenHAB.getConfigFolder(), "scripts", "lib", "ruby", - "gem_home"); + private static final String RUBY_ENGINE_REPLACEMENT = "{RUBY_ENGINE}"; + private static final String RUBY_ENGINE_VERSION_REPLACEMENT = "{RUBY_ENGINE_VERSION}"; + private static final String RUBY_VERSION_REPLACEMENT = "{RUBY_VERSION}"; + private static final List REPLACEMENTS = List.of(RUBY_ENGINE_REPLACEMENT, RUBY_ENGINE_VERSION_REPLACEMENT, + RUBY_VERSION_REPLACEMENT); - private static final Path DEFAULT_RUBYLIB = Paths.get(OpenHAB.getConfigFolder(), "automation", "lib", "ruby"); + private static final String DEFAULT_GEM_HOME = Paths + .get(OpenHAB.getConfigFolder(), "automation", "ruby", ".gem", RUBY_ENGINE_VERSION_REPLACEMENT).toString(); + private static final String DEFAULT_RUBYLIB = Paths.get(OpenHAB.getConfigFolder(), "automation", "ruby", "lib") + .toString(); - private static final String GEM_HOME = "gem_home"; - private static final String RUBYLIB = "rubylib"; - private static final String GEMS = "gems"; - private static final String REQUIRE = "require"; - private static final String CHECK_UPDATE = "check_update"; + private static final String GEM_HOME_CONFIG_KEY = "gem_home"; + private static final String RUBYLIB_CONFIG_KEY = "rubylib"; + private static final String GEMS_CONFIG_KEY = "gems"; + private static final String REQUIRE_CONFIG_KEY = "require"; + private static final String CHECK_UPDATE_CONFIG_KEY = "check_update"; // Map of configuration parameters - private static final Map CONFIGURATION_PARAMETERS = Map.ofEntries( + private final Map configurationParameters = Map.ofEntries( Map.entry("local_context", - new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.SYSTEM_PROPERTY) - .mappedTo("org.jruby.embed.localcontext.scope").defaultValue("singlethread").build()), + new OptionalConfigurationElement(OptionalConfigurationElement.Type.SYSTEM_PROPERTY, "singlethread", + "org.jruby.embed.localcontext.scope")), Map.entry("local_variable", - new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.SYSTEM_PROPERTY) - .mappedTo("org.jruby.embed.localvariable.behavior").defaultValue("transient").build()), + new OptionalConfigurationElement(OptionalConfigurationElement.Type.SYSTEM_PROPERTY, "transient", + "org.jruby.embed.localvariable.behavior")), - Map.entry(GEM_HOME, - new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT) - .mappedTo("GEM_HOME").defaultValue(DEFAULT_GEM_HOME.toString()).build()), + Map.entry(GEM_HOME_CONFIG_KEY, + new OptionalConfigurationElement(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT, + DEFAULT_GEM_HOME, "GEM_HOME")), - Map.entry(RUBYLIB, - new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT) - .mappedTo("RUBYLIB").defaultValue(DEFAULT_RUBYLIB.toString()).build()), + Map.entry(RUBYLIB_CONFIG_KEY, + new OptionalConfigurationElement(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT, + DEFAULT_RUBYLIB, "RUBYLIB")), - Map.entry(GEMS, new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.GEM).build()), + Map.entry(GEMS_CONFIG_KEY, new OptionalConfigurationElement("")), - Map.entry(REQUIRE, - new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.REQUIRE).build()), + Map.entry(REQUIRE_CONFIG_KEY, new OptionalConfigurationElement("")), - Map.entry(CHECK_UPDATE, - new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.CHECK_UPDATE).build())); - - private static final Map> CONFIGURATION_TYPE_MAP = CONFIGURATION_PARAMETERS - .values().stream().collect(Collectors.groupingBy(v -> v.type)); + Map.entry(CHECK_UPDATE_CONFIG_KEY, new OptionalConfigurationElement("true"))); /** * Update configuration @@ -91,8 +92,14 @@ public class JRubyScriptEngineConfiguration { */ void update(Map config, ScriptEngineFactory factory) { logger.trace("JRuby Script Engine Configuration: {}", config); + configurationParameters.forEach((k, v) -> v.clearValue()); config.forEach(this::processConfigValue); - configureScriptEngine(factory); + + configureSystemProperties(); + + ScriptEngine engine = factory.getScriptEngine(); + configureRubyEnvironment(engine); + configureGems(engine); } /** @@ -102,47 +109,74 @@ void update(Map config, ScriptEngineFactory factory) { * @param value Configuration value */ private void processConfigValue(String key, Object value) { - OptionalConfigurationElement configurationElement = CONFIGURATION_PARAMETERS.get(key); + OptionalConfigurationElement configurationElement = configurationParameters.get(key); if (configurationElement != null) { - configurationElement.setValue(value.toString()); + configurationElement.setValue(value.toString().trim()); } else { logger.debug("Ignoring unexpected configuration key: {}", key); } } /** - * Configure the ScriptEngine + * Gets a single configuration element. + */ + private String get(String key) { + OptionalConfigurationElement configElement = configurationParameters.get(key); + + return Objects.requireNonNull(configElement).getValue(); + } + + /** + * Gets the concrete gem home to install gems into for this version of JRuby. * - * @param factory Script Engine to configure + * {RUBY_ENGINE} and {RUBY_VERSION} are replaced with their current actual values. */ - void configureScriptEngine(ScriptEngineFactory factory) { - configureSystemProperties(); + public String getSpecificGemHome() { + String gemHome = get(GEM_HOME_CONFIG_KEY); + if (gemHome.isEmpty()) { + return gemHome; + } - ScriptEngine engine = factory.getScriptEngine(); - configureRubyEnvironment(engine); - configureGems(engine); + gemHome = gemHome.replace(RUBY_ENGINE_REPLACEMENT, Constants.ENGINE); + gemHome = gemHome.replace(RUBY_ENGINE_VERSION_REPLACEMENT, Constants.VERSION); + gemHome = gemHome.replace(RUBY_VERSION_REPLACEMENT, Constants.RUBY_VERSION); + return new File(gemHome).toString(); } /** - * Makes Gem home directory if it does not exist + * Get the base for all possible gem homes. + * + * If the configured gem home contains {RUBY_ENGINE} or {RUBY_VERSION}, + * the path is cut off at that point. This means a single configuration + * value will include the gem homes for all parallel-installed ruby + * versions. + * */ - private void ensureGemHomeExists() { - OptionalConfigurationElement gemHomeConfigElement = CONFIGURATION_PARAMETERS.get(GEM_HOME); - if (gemHomeConfigElement == null) { - return; + public String getGemHomeBase() { + String gemHome = get(GEM_HOME_CONFIG_KEY); + + for (String replacement : REPLACEMENTS) { + int loc = gemHome.indexOf(replacement); + if (loc != -1) { + gemHome = gemHome.substring(0, loc); + } } - Optional gemHome = gemHomeConfigElement.getValue(); - if (gemHome.isPresent()) { - File gemHomeDirectory = new File(gemHome.get()); - if (!gemHomeDirectory.exists()) { - logger.debug("gem_home directory does not exist, creating"); - if (!gemHomeDirectory.mkdirs()) { - logger.warn("Error creating gem_home directory"); - } + return new File(gemHome).toString(); + } + + /** + * Makes Gem home directory if it does not exist + */ + private boolean ensureGemHomeExists(String gemHome) { + File gemHomeDirectory = new File(gemHome); + if (!gemHomeDirectory.exists()) { + logger.debug("gem_home directory does not exist, creating"); + if (!gemHomeDirectory.mkdirs()) { + logger.warn("Error creating gem_home directory"); + return false; } - } else { - logger.debug("Gem install requested without gem_home specified, not ensuring gem_home path exists"); } + return true; } /** @@ -151,25 +185,30 @@ private void ensureGemHomeExists() { * @param engine Engine to install gems */ private synchronized void configureGems(ScriptEngine engine) { - ensureGemHomeExists(); + String gems = get(GEMS_CONFIG_KEY); + if (gems.isEmpty()) { + return; + } - OptionalConfigurationElement gemsConfigElement = CONFIGURATION_PARAMETERS.get(GEMS); - if (gemsConfigElement == null || !gemsConfigElement.getValue().isPresent()) { + String gemHome = getSpecificGemHome(); + if (gemHome.isEmpty()) { + logger.warn("Gem install requested with empty gem_home, not installing gems."); return; } - boolean checkUpdate = true; - OptionalConfigurationElement updateConfigElement = CONFIGURATION_PARAMETERS.get(CHECK_UPDATE); - if (updateConfigElement != null && updateConfigElement.getValue().isPresent()) { - checkUpdate = updateConfigElement.getValue().get().equals("true"); + + if (!ensureGemHomeExists(gemHome)) { + return; } - String[] gems = gemsConfigElement.getValue().get().split(","); + boolean checkUpdate = "true".equals(get(CHECK_UPDATE_CONFIG_KEY)); + + String[] gemsArray = gems.split(","); // Set update_native_env_enabled to false so that bundler doesn't leak // into other script engines String gemCommand = "require 'jruby'\nJRuby.runtime.instance_config.update_native_env_enabled = false\nrequire 'bundler/inline'\nrequire 'openssl'\n\ngemfile(" + checkUpdate + ") do\n" + " source 'https://rubygems.org/'\n"; int validGems = 0; - for (String gem : gems) { + for (String gem : gemsArray) { gem = gem.trim(); String[] versions = {}; if (gem.contains("=")) { @@ -212,21 +251,21 @@ private synchronized void configureGems(ScriptEngine engine) { * @param engine Engine to insert the require statements */ public void injectRequire(ScriptEngine engine) { - OptionalConfigurationElement requireConfigElement = CONFIGURATION_PARAMETERS.get(REQUIRE); - if (requireConfigElement == null || !requireConfigElement.getValue().isPresent()) { + String requires = get(REQUIRE_CONFIG_KEY); + + if (requires.isEmpty()) { return; } - Stream.of(requireConfigElement.getValue().get().split(",")).map(s -> s.trim()).filter(s -> !s.isEmpty()) - .forEach(script -> { - final String requireStatement = String.format("require '%s'", script); - try { - logger.trace("Injecting require statement: {}", requireStatement); - engine.eval(requireStatement); - } catch (ScriptException e) { - logger.warn("Error evaluating `{}`", requireStatement, unwrap(e)); - } - }); + Stream.of(requires.split(",")).map(s -> s.trim()).filter(s -> !s.isEmpty()).forEach(script -> { + final String requireStatement = String.format("require '%s'", script); + try { + logger.trace("Injecting require statement: {}", requireStatement); + engine.eval(requireStatement); + } catch (ScriptException e) { + logger.warn("Error evaluating `{}`", requireStatement, unwrap(e)); + } + }); } /** @@ -234,20 +273,30 @@ public void injectRequire(ScriptEngine engine) { * * @param engine Engine in which to configure environment */ - public ScriptEngine configureRubyEnvironment(ScriptEngine engine) { + public void configureRubyEnvironment(ScriptEngine scriptEngine) { getConfigurationElements(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT).forEach(configElement -> { - final String environmentSetting = String.format("ENV['%s']='%s'", configElement.mappedTo().get(), - configElement.getValue().get()); + String value; + if ("GEM_HOME".equals(configElement.mappedTo().get())) { + // this value has to be post-processed to handle replacements. + value = getSpecificGemHome(); + } else { + value = configElement.getValue(); + } + scriptEngine.put("__key", configElement.mappedTo().get()); + scriptEngine.put("__value", value); + logger.trace("Setting Ruby environment ENV['{}''] = '{}'", configElement.mappedTo().get(), value); + try { - logger.trace("Setting Ruby environment with code: {} ", environmentSetting); - engine.eval(environmentSetting); + scriptEngine.eval("ENV[__key] = __value"); } catch (ScriptException e) { logger.warn("Error setting Ruby environment", unwrap(e)); } + // clean up our temporary variables + scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE).remove("__key"); + scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE).remove("__value"); }); - configureRubyLib(engine); - return engine; + configureRubyLib(scriptEngine); } /** @@ -257,24 +306,27 @@ public ScriptEngine configureRubyEnvironment(ScriptEngine engine) { * @param engine Engine in which to configure environment */ private void configureRubyLib(ScriptEngine engine) { - OptionalConfigurationElement rubyLibConfigElement = CONFIGURATION_PARAMETERS.get(RUBYLIB); - if (rubyLibConfigElement == null) { - return; - } - - Optional rubyLib = rubyLibConfigElement.getValue(); - if (rubyLib.isPresent() && !rubyLib.get().trim().isEmpty()) { + String rubyLib = get(RUBYLIB_CONFIG_KEY); + if (!rubyLib.isEmpty()) { final String code = "$LOAD_PATH.unshift *ENV['RUBYLIB']&.split(File::PATH_SEPARATOR)" + // "&.reject(&:empty?)" + // "&.reject { |path| $LOAD_PATH.include?(path) }"; // try { engine.eval(code); } catch (ScriptException exception) { - logger.warn("Error setting $LOAD_PATH from RUBYLIB='{}'", rubyLib.get(), unwrap(exception)); + logger.warn("Error setting $LOAD_PATH from RUBYLIB='{}'", rubyLib, unwrap(exception)); } } } + public List getRubyLibPaths() { + String rubyLib = get(RUBYLIB_CONFIG_KEY); + if (rubyLib.isEmpty()) { + return List.of(); + } + return List.of(rubyLib.split(File.pathSeparator)); + } + /** * Configure system properties * @@ -283,17 +335,14 @@ private void configureRubyLib(ScriptEngine engine) { private void configureSystemProperties() { getConfigurationElements(OptionalConfigurationElement.Type.SYSTEM_PROPERTY).forEach(configElement -> { String systemProperty = configElement.mappedTo().get(); - String propertyValue = configElement.getValue().get(); + String propertyValue = configElement.getValue(); logger.trace("Setting system property ({}) to ({})", systemProperty, propertyValue); System.setProperty(systemProperty, propertyValue); }); } - private Stream getConfigurationElements( - OptionalConfigurationElement.Type configurationType) { - return CONFIGURATION_TYPE_MAP - .getOrDefault(configurationType, Collections. emptyList()).stream() - .filter(element -> element.getValue().isPresent()); + private Stream getConfigurationElements(OptionalConfigurationElement.Type type) { + return configurationParameters.values().stream().filter(element -> element.type.equals(type)); } /** @@ -314,61 +363,42 @@ private Throwable unwrap(Throwable e) { * Inner static companion class for configuration elements */ private static class OptionalConfigurationElement { + private enum Type { + SYSTEM_PROPERTY, + RUBY_ENVIRONMENT, + OTHER + } - private final Optional defaultValue; + private final String defaultValue; private final Optional mappedTo; private final Type type; private Optional value; - private OptionalConfigurationElement(Type type, @Nullable String mappedTo, @Nullable String defaultValue) { + private OptionalConfigurationElement(String defaultValue) { + this(Type.OTHER, defaultValue, null); + } + + private OptionalConfigurationElement(Type type, String defaultValue, @Nullable String mappedTo) { this.type = type; - this.defaultValue = Optional.ofNullable(defaultValue); + this.defaultValue = defaultValue; this.mappedTo = Optional.ofNullable(mappedTo); value = Optional.empty(); } - private Optional getValue() { - return value.or(() -> defaultValue); + private String getValue() { + return value.orElse(defaultValue); } private void setValue(String value) { this.value = Optional.of(value); } - private Optional mappedTo() { - return mappedTo; + private void clearValue() { + this.value = Optional.empty(); } - private enum Type { - SYSTEM_PROPERTY, - RUBY_ENVIRONMENT, - GEM, - REQUIRE, - CHECK_UPDATE, - } - - private static class Builder { - private final Type type; - private @Nullable String defaultValue = null; - private @Nullable String mappedTo = null; - - private Builder(Type type) { - this.type = type; - } - - private Builder mappedTo(String mappedTo) { - this.mappedTo = mappedTo; - return this; - } - - private Builder defaultValue(String value) { - this.defaultValue = value; - return this; - } - - private OptionalConfigurationElement build() { - return new OptionalConfigurationElement(type, mappedTo, defaultValue); - } + private Optional mappedTo() { + return mappedTo; } } } diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineFactory.java b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineFactory.java index 5d74d7b2fd2a1..a53fb534bb1f6 100644 --- a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineFactory.java +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineFactory.java @@ -12,9 +12,11 @@ */ package org.openhab.automation.jrubyscripting.internal; +import java.io.File; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -24,13 +26,20 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.automation.jrubyscripting.internal.watch.JRubyDependencyTracker; import org.openhab.core.automation.module.script.AbstractScriptEngineFactory; +import org.openhab.core.automation.module.script.ScriptDependencyTracker; import org.openhab.core.automation.module.script.ScriptEngineFactory; +import org.openhab.core.automation.module.script.ScriptExtensionManagerWrapper; import org.openhab.core.config.core.ConfigurableService; import org.osgi.framework.Constants; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; /** * This is an implementation of a {@link ScriptEngineFactory} for Ruby. @@ -43,14 +52,14 @@ + "=org.openhab.automation.jrubyscripting") @ConfigurableService(category = "automation", label = "JRuby Scripting", description_uri = "automation:jruby") public class JRubyScriptEngineFactory extends AbstractScriptEngineFactory { - private final JRubyScriptEngineConfiguration configuration = new JRubyScriptEngineConfiguration(); private final javax.script.ScriptEngineFactory factory = new org.jruby.embed.jsr223.JRubyEngineFactory(); - private final List scriptTypes = Stream - .concat(factory.getExtensions().stream(), factory.getMimeTypes().stream()) - .collect(Collectors.toUnmodifiableList()); + private final List scriptTypes = Stream.concat(Objects.requireNonNull(factory.getExtensions()).stream(), + Objects.requireNonNull(factory.getMimeTypes()).stream()).collect(Collectors.toUnmodifiableList()); + + private JRubyDependencyTracker jrubyDependencyTracker; // Adds $ in front of a set of variables so that Ruby recognizes them as global variables private static Map.Entry mapGlobalPresets(Map.Entry entry) { @@ -62,16 +71,24 @@ private static Map.Entry mapGlobalPresets(Map.Entry config) { - configuration.update(config, factory); + public JRubyScriptEngineFactory(Map config) { + jrubyDependencyTracker = new JRubyDependencyTracker(this); + modified(config); + } + + @Deactivate + protected void deactivate() { + jrubyDependencyTracker.deactivate(); } // The modified call updates configuration for the automation @Modified protected void modified(Map config) { configuration.update(config, factory); + // Re-initialize the dependency tracker's watchers. + jrubyDependencyTracker.deactivate(); + jrubyDependencyTracker.activate(); } @Override @@ -99,6 +116,15 @@ public void scopeValues(ScriptEngine scriptEngine, Map scopeValu importClassesToRuby(scriptEngine, partitionedMap.getOrDefault(true, new HashMap<>())); super.scopeValues(scriptEngine, partitionedMap.getOrDefault(false, new HashMap<>())); + Object scriptExtension = scopeValues.get("scriptExtension"); + if (scriptExtension instanceof ScriptExtensionManagerWrapper) { + ScriptExtensionManagerWrapper wrapper = (ScriptExtensionManagerWrapper) scriptExtension; + // we inject like this instead of using the script context, because + // this is executed _before_ the dependency tracker is added to the script context. + // But we need this set up before we inject our requires + scriptEngine.put("$dependencyListener", jrubyDependencyTracker.getTracker(wrapper.getScriptIdentifier())); + } + // scopeValues is called twice. The first call only passed 'se'. The second call passed the rest of the // presets, including 'ir'. We wait for the second call before running the require statements. if (scopeValues.containsKey("ir")) { @@ -120,7 +146,50 @@ private void importClassesToRuby(ScriptEngine scriptEngine, Map @Override public @Nullable ScriptEngine createScriptEngine(String scriptType) { - return scriptTypes.contains(scriptType) ? configuration.configureRubyEnvironment(factory.getScriptEngine()) - : null; + if (!scriptTypes.contains(scriptType)) { + return null; + } + ScriptEngine engine = factory.getScriptEngine(); + configuration.configureRubyEnvironment(engine); + return engine; + } + + @Override + public @Nullable ScriptDependencyTracker getDependencyTracker() { + return jrubyDependencyTracker; + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC, unbind = "removeChangeTracker") + public void addChangeTracker(ScriptDependencyTracker.Listener listener) { + jrubyDependencyTracker.addChangeTracker(listener); + } + + public void removeChangeTracker(ScriptDependencyTracker.Listener listener) { + jrubyDependencyTracker.removeChangeTracker(listener); + } + + public List getRubyLibPaths() { + return configuration.getRubyLibPaths(); + } + + public boolean isFileInLoadPath(String file) { + for (String path : getRubyLibPaths()) { + if (file.startsWith(new File(path).toString() + File.separator)) { + return true; + } + } + return false; + } + + public String getGemHome() { + return configuration.getSpecificGemHome(); + } + + public boolean isFileInGemHome(String file) { + String gemHome = configuration.getGemHomeBase(); + if (gemHome.isEmpty()) { + return false; + } + return file.startsWith(gemHome + File.separator); } } diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/BidiSetBag.java b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/BidiSetBag.java new file mode 100644 index 0000000000000..015a1eb634f7a --- /dev/null +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/BidiSetBag.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.jrubyscripting.internal.watch; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +// Copy of org.openhab.core.automation.module.script.rulesupport.internal.loader.collection.BidiSetBag + +/** + * Bidirectional bag of unique elements. A map allowing multiple, unique values to be stored against a single key. + * Provides optimized lookup of values for a key, as well as keys referencing a value. + * + * @author Jonathan Gilbert - Initial contribution + * @author Jan N. Klug - Make implementation thread-safe + * @param Type of Key + * @param Type of Value + */ +@NonNullByDefault +public class BidiSetBag { + + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + private final Map> keyToValues = new HashMap<>(); + private final Map> valueToKeys = new HashMap<>(); + + public void put(K key, V value) { + lock.writeLock().lock(); + try { + keyToValues.computeIfAbsent(key, k -> new HashSet<>()).add(value); + valueToKeys.computeIfAbsent(value, v -> new HashSet<>()).add(key); + } finally { + lock.writeLock().unlock(); + } + } + + public Set getValues(K key) { + lock.readLock().lock(); + try { + Set values = keyToValues.getOrDefault(key, Set.of()); + return Collections.unmodifiableSet(values); + } finally { + lock.readLock().unlock(); + } + } + + public Set getKeys(V value) { + lock.readLock().lock(); + try { + Set keys = valueToKeys.getOrDefault(value, Set.of()); + return Collections.unmodifiableSet(keys); + } finally { + lock.readLock().unlock(); + } + } + + public Set removeKey(K key) { + lock.writeLock().lock(); + try { + Set values = keyToValues.remove(key); + if (values != null) { + for (V value : values) { + valueToKeys.computeIfPresent(value, (k, v) -> { + v.remove(key); + return v; + }); + } + return values; + } else { + return Set.of(); + } + } finally { + lock.writeLock().unlock(); + } + } + + public Set removeValue(V value) { + lock.writeLock().lock(); + try { + Set keys = valueToKeys.remove(value); + if (keys != null) { + for (K key : keys) { + keyToValues.computeIfPresent(key, (k, v) -> { + v.remove(value); + return v; + }); + } + return keys; + } else { + return Set.of(); + } + } finally { + lock.writeLock().unlock(); + } + } +} diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyDependencyTracker.java b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyDependencyTracker.java new file mode 100644 index 0000000000000..0ce059b001688 --- /dev/null +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyDependencyTracker.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.jrubyscripting.internal.watch; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.jrubyscripting.internal.JRubyScriptEngineFactory; +import org.openhab.core.automation.module.script.ScriptDependencyTracker; +import org.openhab.core.service.AbstractWatchService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tracks Ruby dependencies + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public class JRubyDependencyTracker implements ScriptDependencyTracker { + private final Logger logger = LoggerFactory.getLogger(JRubyDependencyTracker.class); + + private final Set dependencyChangeListeners = ConcurrentHashMap.newKeySet(); + + private final BidiSetBag scriptToLibs = new BidiSetBag<>(); + + private final JRubyScriptEngineFactory scriptEngineFactory; + private final List dependencyWatchServices = new ArrayList<>(); + + public JRubyDependencyTracker(final JRubyScriptEngineFactory scriptEngineFactory) { + this.scriptEngineFactory = scriptEngineFactory; + } + + public void activate() { + String gemHome = scriptEngineFactory.getGemHome(); + if (!gemHome.isEmpty()) { + dependencyWatchServices.add(new JRubyGemWatchService(gemHome, this)); + } + for (String libPath : scriptEngineFactory.getRubyLibPaths()) { + dependencyWatchServices.add(new JRubyLibWatchService(libPath, this)); + } + for (AbstractWatchService dependencyWatchService : dependencyWatchServices) { + dependencyWatchService.activate(); + } + } + + public void deactivate() { + for (AbstractWatchService dependencyWatchService : dependencyWatchServices) { + dependencyWatchService.deactivate(); + } + dependencyWatchServices.clear(); + } + + void dependencyChanged(String dependency) { + Set scripts = new HashSet<>(scriptToLibs.getKeys(dependency)); // take a copy as it will change as we + logger.debug("{} changed; reimporting {} scripts...", dependency, scripts.size()); + for (String scriptUrl : scripts) { + for (ScriptDependencyTracker.Listener listener : dependencyChangeListeners) { + try { + listener.onDependencyChange(scriptUrl); + } catch (Exception e) { + logger.warn("Failed to notify tracker of dependency change: {}: {}", e.getClass(), e.getMessage()); + } + } + } + } + + @Override + public Consumer getTracker(String scriptId) { + return dependencyPath -> startTracking(scriptId, dependencyPath); + } + + @Override + public void removeTracking(String scriptId) { + scriptToLibs.removeKey(scriptId); + } + + protected void startTracking(String scriptId, String libPath) { + scriptToLibs.put(scriptId, libPath); + } + + public void addChangeTracker(ScriptDependencyTracker.Listener listener) { + logger.trace("adding change tracker listener {}", listener); + dependencyChangeListeners.add(listener); + } + + public void removeChangeTracker(ScriptDependencyTracker.Listener listener) { + logger.trace("removing change tracker listener {}", listener); + dependencyChangeListeners.remove(listener); + } +} diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyGemWatchService.java b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyGemWatchService.java new file mode 100644 index 0000000000000..9cd59b72dd887 --- /dev/null +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyGemWatchService.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.jrubyscripting.internal.watch; + +import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; + +import java.nio.file.Path; +import java.nio.file.WatchEvent; +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.service.AbstractWatchService; + +/** + * Watches a gem home + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public class JRubyGemWatchService extends AbstractWatchService { + + private static final String GEMSPEC = ".gemspec"; + + private JRubyDependencyTracker dependencyTracker; + + JRubyGemWatchService(String path, JRubyDependencyTracker dependencyTracker) { + super(path); + this.dependencyTracker = dependencyTracker; + } + + @Override + protected boolean watchSubDirectories() { + return true; + } + + @Override + protected WatchEvent.Kind @Nullable [] getWatchEventKinds(Path path) { + return new WatchEvent.Kind[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY }; + } + + @Override + protected void processWatchEvent(WatchEvent watchEvent, WatchEvent.Kind kind, Path path) { + String file = path.toFile().getName(); + if (file.endsWith(GEMSPEC)) { + // This seems really lazy, but you can't definitively tell the name + // of a gem from the gemspec's filename. It's simply too ambiguous with version + // numbers and platforms allowed to have `-` and `_` characters as well. RubyGems + // doesn't do it either - it either has the name already, and searches for + // `-*.gemspec`, or it completely lists the all files on disk. Either way + // it then executes the gemspec to get full details. We can't do that here in + // pure Java and without a JRubyEngine available. So just punt and invalidate + // _all_ subsets of hyphens. Worst case we invalidate a "parent" gem that didn't + // need to be invalidated, but oh well, that just means a script reloads sometimes + // when it didn't absolutely need to. + String[] parts = file.split("-"); + for (int i = 0; i < parts.length - 1; ++i) { + dependencyTracker.dependencyChanged("gem:" + String.join("-", Arrays.copyOf(parts, i + 1))); + } + } + } +} diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyLibWatchService.java b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyLibWatchService.java new file mode 100644 index 0000000000000..859280c9129f3 --- /dev/null +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyLibWatchService.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.jrubyscripting.internal.watch; + +import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.WatchEvent; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.service.AbstractWatchService; + +/** + * Watches a Ruby lib dir + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public class JRubyLibWatchService extends AbstractWatchService { + private JRubyDependencyTracker dependencyTracker; + + JRubyLibWatchService(String path, JRubyDependencyTracker dependencyTracker) { + super(path); + this.dependencyTracker = dependencyTracker; + } + + @Override + protected boolean watchSubDirectories() { + return true; + } + + @Override + protected WatchEvent.Kind @Nullable [] getWatchEventKinds(Path path) { + return new WatchEvent.Kind[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY }; + } + + @Override + protected void processWatchEvent(WatchEvent watchEvent, WatchEvent.Kind kind, Path path) { + File file = path.toFile(); + if (!file.isHidden() && (kind.equals(ENTRY_DELETE) + || (file.canRead() && (kind.equals(ENTRY_CREATE) || kind.equals(ENTRY_MODIFY))))) { + dependencyTracker.dependencyChanged(file.getPath()); + } + } +} diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyScriptFileWatcher.java b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyScriptFileWatcher.java new file mode 100644 index 0000000000000..392050df171b0 --- /dev/null +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/watch/JRubyScriptFileWatcher.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.jrubyscripting.internal.watch; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.WatchEvent; +import java.util.Objects; + +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.automation.jrubyscripting.internal.JRubyScriptEngineFactory; +import org.openhab.core.automation.module.script.ScriptDependencyTracker; +import org.openhab.core.automation.module.script.ScriptEngineFactory; +import org.openhab.core.automation.module.script.ScriptEngineManager; +import org.openhab.core.automation.module.script.rulesupport.loader.AbstractScriptFileWatcher; +import org.openhab.core.automation.module.script.rulesupport.loader.ScriptFileReference; +import org.openhab.core.service.ReadyService; +import org.osgi.framework.Constants; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Monitors /automation/ruby for Ruby files, but not libraries in lib or gems + * + * @author Cody Cutrer - Initial contribution + */ +@Component(immediate = true, service = ScriptDependencyTracker.Listener.class) +public class JRubyScriptFileWatcher extends AbstractScriptFileWatcher { + private final Logger logger = LoggerFactory.getLogger(JRubyScriptFileWatcher.class); + + private static final String FILE_DIRECTORY = "automation" + File.separator + "ruby"; + + private final JRubyScriptEngineFactory scriptEngineFactory; + + @Activate + public JRubyScriptFileWatcher(final @Reference ScriptEngineManager manager, + final @Reference ReadyService readyService, final @Reference(target = "(" + Constants.SERVICE_PID + + "=org.openhab.automation.jrubyscripting)") ScriptEngineFactory scriptEngineFactory) { + super(manager, readyService, FILE_DIRECTORY); + + this.scriptEngineFactory = (JRubyScriptEngineFactory) scriptEngineFactory; + } + + @Override + protected void importFile(ScriptFileReference ref) { + if (isIgnored(ref.getScriptFileURL().getFile())) { + return; + } + super.importFile(ref); + } + + @Override + protected void processWatchEvent(@Nullable WatchEvent event, WatchEvent.@Nullable Kind kind, + @Nullable Path path) { + if (Objects.nonNull(path)) { + logger.trace("looking at {}", path); + if (!isIgnored(path.toString())) { + logger.trace("and propagating it"); + super.processWatchEvent(event, kind, path); + } + } + } + + private boolean isIgnored(String path) { + return scriptEngineFactory.isFileInGemHome(path) || scriptEngineFactory.isFileInLoadPath(path); + } +} diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/config/config.xml index ef80653a3fcd1..c548ff69b04b2 100644 --- a/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/config/config.xml @@ -48,15 +48,17 @@ - OPENHAB_CONF/scripts/lib/ruby/gem_home" when not specified. + {RUBY_ENGINE_VERSION}, {RUBY_ENGINE} and/or {RUBY_VERSION} replacements in this value to automatically point to + a new directory when the addon is updated with a new version of JRuby. + Defaults to "OPENHAB_CONF/automation/ruby/.gem/{RUBY_ENGINE_VERSION}" when not specified. ]]> OPENHAB_CONF/automation/lib/ruby" when not specified.]]> + "OPENHAB_CONF/automation/ruby/lib" when not specified.]]> diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/i18n/jruby.properties b/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/i18n/jruby.properties index 8e54c06f65699..2d2d7e45cb63e 100644 --- a/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/i18n/jruby.properties +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/resources/OH-INF/i18n/jruby.properties @@ -3,7 +3,7 @@ automation.config.jruby.check_update.description = Check RubyGems for updates to automation.config.jruby.check_update.option.true = Check For Updates automation.config.jruby.check_update.option.false = Do Not Check For Updates automation.config.jruby.gem_home.label = GEM_HOME -automation.config.jruby.gem_home.description = Location Ruby Gems will be installed and loaded, directory will be created if missing and gem installs are specified. Defaults to "OPENHAB_CONF/scripts/lib/ruby/gem_home" when not specified. +automation.config.jruby.gem_home.description = Location Ruby Gems will be installed to and loaded from. Directory will be created if necessary. You can use {RUBY_ENGINE_VERSION}, {RUBY_ENGINE} and/or {RUBY_VERSION} replacements in this value to automatically point to a new directory when the addon is updated with a new version of JRuby. Defaults to "OPENHAB_CONF/automation/ruby/.gem/{RUBY_ENGINE_VERSION}" when not specified. automation.config.jruby.gems.label = Ruby Gems automation.config.jruby.gems.description = A comma separated list of Ruby Gems to install. Versions may be constrained by separating with an = and then the standard RubyGems version constraint, such as "openhab-scripting=~>4.0". automation.config.jruby.group.environment.label = Ruby Environment @@ -26,7 +26,7 @@ automation.config.jruby.local_variable.option.global = Global automation.config.jruby.require.label = Require Scripts automation.config.jruby.require.description = A comma separated list of script names to be required by the JRuby Scripting Engine before running user scripts. automation.config.jruby.rubylib.label = RUBYLIB -automation.config.jruby.rubylib.description = Search path for user libraries. Separate each path with a colon (semicolon in Windows). Defaults to "OPENHAB_CONF/automation/lib/ruby" when not specified. +automation.config.jruby.rubylib.description = Search path for user libraries. Separate each path with a colon (semicolon in Windows). Defaults to "OPENHAB_CONF/automation/ruby/lib" when not specified. # service