[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 <cody@cutrer.us>
This commit is contained in:
Cody Cutrer 2022-12-05 00:19:51 -07:00 committed by GitHub
parent a0ac57cfd9
commit 2382fadaaa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 692 additions and 161 deletions

View File

@ -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/` Alternatively, JRuby configuration parameters may be set by creating a `jruby.cfg` file in `conf/services/`
| Parameter | Default | Description | | 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: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/lib/ruby/ | Search path for user libraries. Separate each path with a colon (semicolon in Windows). | | 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_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: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: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: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. | | 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 ## 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. 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: If you create an empty file called `test.rb`, you will see a log line with information similar to:
```text ```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: 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 - 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 ## Script Examples

View File

@ -13,21 +13,21 @@
package org.openhab.automation.jrubyscripting.internal; package org.openhab.automation.jrubyscripting.internal;
import java.io.File; import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.script.ScriptContext;
import javax.script.ScriptEngine; import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory; import javax.script.ScriptEngineFactory;
import javax.script.ScriptException; import javax.script.ScriptException;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.jruby.runtime.Constants;
import org.openhab.core.OpenHAB; import org.openhab.core.OpenHAB;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -43,45 +43,46 @@ public class JRubyScriptEngineConfiguration {
private final Logger logger = LoggerFactory.getLogger(JRubyScriptEngineConfiguration.class); private final Logger logger = LoggerFactory.getLogger(JRubyScriptEngineConfiguration.class);
private static final Path DEFAULT_GEM_HOME = Paths.get(OpenHAB.getConfigFolder(), "scripts", "lib", "ruby", private static final String RUBY_ENGINE_REPLACEMENT = "{RUBY_ENGINE}";
"gem_home"); private static final String RUBY_ENGINE_VERSION_REPLACEMENT = "{RUBY_ENGINE_VERSION}";
private static final String RUBY_VERSION_REPLACEMENT = "{RUBY_VERSION}";
private static final List<String> 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 GEM_HOME_CONFIG_KEY = "gem_home";
private static final String RUBYLIB = "rubylib"; private static final String RUBYLIB_CONFIG_KEY = "rubylib";
private static final String GEMS = "gems"; private static final String GEMS_CONFIG_KEY = "gems";
private static final String REQUIRE = "require"; private static final String REQUIRE_CONFIG_KEY = "require";
private static final String CHECK_UPDATE = "check_update"; private static final String CHECK_UPDATE_CONFIG_KEY = "check_update";
// Map of configuration parameters // Map of configuration parameters
private static final Map<String, OptionalConfigurationElement> CONFIGURATION_PARAMETERS = Map.ofEntries( private final Map<String, OptionalConfigurationElement> configurationParameters = Map.ofEntries(
Map.entry("local_context", Map.entry("local_context",
new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.SYSTEM_PROPERTY) new OptionalConfigurationElement(OptionalConfigurationElement.Type.SYSTEM_PROPERTY, "singlethread",
.mappedTo("org.jruby.embed.localcontext.scope").defaultValue("singlethread").build()), "org.jruby.embed.localcontext.scope")),
Map.entry("local_variable", Map.entry("local_variable",
new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.SYSTEM_PROPERTY) new OptionalConfigurationElement(OptionalConfigurationElement.Type.SYSTEM_PROPERTY, "transient",
.mappedTo("org.jruby.embed.localvariable.behavior").defaultValue("transient").build()), "org.jruby.embed.localvariable.behavior")),
Map.entry(GEM_HOME, Map.entry(GEM_HOME_CONFIG_KEY,
new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT) new OptionalConfigurationElement(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT,
.mappedTo("GEM_HOME").defaultValue(DEFAULT_GEM_HOME.toString()).build()), DEFAULT_GEM_HOME, "GEM_HOME")),
Map.entry(RUBYLIB, Map.entry(RUBYLIB_CONFIG_KEY,
new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT) new OptionalConfigurationElement(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT,
.mappedTo("RUBYLIB").defaultValue(DEFAULT_RUBYLIB.toString()).build()), DEFAULT_RUBYLIB, "RUBYLIB")),
Map.entry(GEMS, new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.GEM).build()), Map.entry(GEMS_CONFIG_KEY, new OptionalConfigurationElement("")),
Map.entry(REQUIRE, Map.entry(REQUIRE_CONFIG_KEY, new OptionalConfigurationElement("")),
new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.REQUIRE).build()),
Map.entry(CHECK_UPDATE, Map.entry(CHECK_UPDATE_CONFIG_KEY, new OptionalConfigurationElement("true")));
new OptionalConfigurationElement.Builder(OptionalConfigurationElement.Type.CHECK_UPDATE).build()));
private static final Map<OptionalConfigurationElement.Type, List<OptionalConfigurationElement>> CONFIGURATION_TYPE_MAP = CONFIGURATION_PARAMETERS
.values().stream().collect(Collectors.groupingBy(v -> v.type));
/** /**
* Update configuration * Update configuration
@ -91,8 +92,14 @@ public class JRubyScriptEngineConfiguration {
*/ */
void update(Map<String, Object> config, ScriptEngineFactory factory) { void update(Map<String, Object> config, ScriptEngineFactory factory) {
logger.trace("JRuby Script Engine Configuration: {}", config); logger.trace("JRuby Script Engine Configuration: {}", config);
configurationParameters.forEach((k, v) -> v.clearValue());
config.forEach(this::processConfigValue); config.forEach(this::processConfigValue);
configureScriptEngine(factory);
configureSystemProperties();
ScriptEngine engine = factory.getScriptEngine();
configureRubyEnvironment(engine);
configureGems(engine);
} }
/** /**
@ -102,47 +109,74 @@ public class JRubyScriptEngineConfiguration {
* @param value Configuration value * @param value Configuration value
*/ */
private void processConfigValue(String key, Object value) { private void processConfigValue(String key, Object value) {
OptionalConfigurationElement configurationElement = CONFIGURATION_PARAMETERS.get(key); OptionalConfigurationElement configurationElement = configurationParameters.get(key);
if (configurationElement != null) { if (configurationElement != null) {
configurationElement.setValue(value.toString()); configurationElement.setValue(value.toString().trim());
} else { } else {
logger.debug("Ignoring unexpected configuration key: {}", key); logger.debug("Ignoring unexpected configuration key: {}", key);
} }
} }
/** /**
* Configure the ScriptEngine * Gets a single configuration element.
*
* @param factory Script Engine to configure
*/ */
void configureScriptEngine(ScriptEngineFactory factory) { private String get(String key) {
configureSystemProperties(); OptionalConfigurationElement configElement = configurationParameters.get(key);
ScriptEngine engine = factory.getScriptEngine(); return Objects.requireNonNull(configElement).getValue();
configureRubyEnvironment(engine); }
configureGems(engine);
/**
* Gets the concrete gem home to install gems into for this version of JRuby.
*
* {RUBY_ENGINE} and {RUBY_VERSION} are replaced with their current actual values.
*/
public String getSpecificGemHome() {
String gemHome = get(GEM_HOME_CONFIG_KEY);
if (gemHome.isEmpty()) {
return gemHome;
}
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();
}
/**
* 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.
*
*/
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);
}
}
return new File(gemHome).toString();
} }
/** /**
* Makes Gem home directory if it does not exist * Makes Gem home directory if it does not exist
*/ */
private void ensureGemHomeExists() { private boolean ensureGemHomeExists(String gemHome) {
OptionalConfigurationElement gemHomeConfigElement = CONFIGURATION_PARAMETERS.get(GEM_HOME); File gemHomeDirectory = new File(gemHome);
if (gemHomeConfigElement == null) { if (!gemHomeDirectory.exists()) {
return; logger.debug("gem_home directory does not exist, creating");
} if (!gemHomeDirectory.mkdirs()) {
Optional<String> gemHome = gemHomeConfigElement.getValue(); logger.warn("Error creating gem_home directory");
if (gemHome.isPresent()) { return false;
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");
}
} }
} else {
logger.debug("Gem install requested without gem_home specified, not ensuring gem_home path exists");
} }
return true;
} }
/** /**
@ -151,25 +185,30 @@ public class JRubyScriptEngineConfiguration {
* @param engine Engine to install gems * @param engine Engine to install gems
*/ */
private synchronized void configureGems(ScriptEngine engine) { private synchronized void configureGems(ScriptEngine engine) {
ensureGemHomeExists(); String gems = get(GEMS_CONFIG_KEY);
if (gems.isEmpty()) {
OptionalConfigurationElement gemsConfigElement = CONFIGURATION_PARAMETERS.get(GEMS);
if (gemsConfigElement == null || !gemsConfigElement.getValue().isPresent()) {
return; return;
} }
boolean checkUpdate = true;
OptionalConfigurationElement updateConfigElement = CONFIGURATION_PARAMETERS.get(CHECK_UPDATE); String gemHome = getSpecificGemHome();
if (updateConfigElement != null && updateConfigElement.getValue().isPresent()) { if (gemHome.isEmpty()) {
checkUpdate = updateConfigElement.getValue().get().equals("true"); logger.warn("Gem install requested with empty gem_home, not installing gems.");
return;
} }
String[] gems = gemsConfigElement.getValue().get().split(","); if (!ensureGemHomeExists(gemHome)) {
return;
}
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 // Set update_native_env_enabled to false so that bundler doesn't leak
// into other script engines // into other script engines
String gemCommand = "require 'jruby'\nJRuby.runtime.instance_config.update_native_env_enabled = false\nrequire 'bundler/inline'\nrequire 'openssl'\n\ngemfile(" 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"; + checkUpdate + ") do\n" + " source 'https://rubygems.org/'\n";
int validGems = 0; int validGems = 0;
for (String gem : gems) { for (String gem : gemsArray) {
gem = gem.trim(); gem = gem.trim();
String[] versions = {}; String[] versions = {};
if (gem.contains("=")) { if (gem.contains("=")) {
@ -212,21 +251,21 @@ public class JRubyScriptEngineConfiguration {
* @param engine Engine to insert the require statements * @param engine Engine to insert the require statements
*/ */
public void injectRequire(ScriptEngine engine) { public void injectRequire(ScriptEngine engine) {
OptionalConfigurationElement requireConfigElement = CONFIGURATION_PARAMETERS.get(REQUIRE); String requires = get(REQUIRE_CONFIG_KEY);
if (requireConfigElement == null || !requireConfigElement.getValue().isPresent()) {
if (requires.isEmpty()) {
return; return;
} }
Stream.of(requireConfigElement.getValue().get().split(",")).map(s -> s.trim()).filter(s -> !s.isEmpty()) Stream.of(requires.split(",")).map(s -> s.trim()).filter(s -> !s.isEmpty()).forEach(script -> {
.forEach(script -> { final String requireStatement = String.format("require '%s'", script);
final String requireStatement = String.format("require '%s'", script); try {
try { logger.trace("Injecting require statement: {}", requireStatement);
logger.trace("Injecting require statement: {}", requireStatement); engine.eval(requireStatement);
engine.eval(requireStatement); } catch (ScriptException e) {
} catch (ScriptException e) { logger.warn("Error evaluating `{}`", requireStatement, unwrap(e));
logger.warn("Error evaluating `{}`", requireStatement, unwrap(e)); }
} });
});
} }
/** /**
@ -234,20 +273,30 @@ public class JRubyScriptEngineConfiguration {
* *
* @param engine Engine in which to configure environment * @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 -> { getConfigurationElements(OptionalConfigurationElement.Type.RUBY_ENVIRONMENT).forEach(configElement -> {
final String environmentSetting = String.format("ENV['%s']='%s'", configElement.mappedTo().get(), String value;
configElement.getValue().get()); 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 { try {
logger.trace("Setting Ruby environment with code: {} ", environmentSetting); scriptEngine.eval("ENV[__key] = __value");
engine.eval(environmentSetting);
} catch (ScriptException e) { } catch (ScriptException e) {
logger.warn("Error setting Ruby environment", unwrap(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); configureRubyLib(scriptEngine);
return engine;
} }
/** /**
@ -257,24 +306,27 @@ public class JRubyScriptEngineConfiguration {
* @param engine Engine in which to configure environment * @param engine Engine in which to configure environment
*/ */
private void configureRubyLib(ScriptEngine engine) { private void configureRubyLib(ScriptEngine engine) {
OptionalConfigurationElement rubyLibConfigElement = CONFIGURATION_PARAMETERS.get(RUBYLIB); String rubyLib = get(RUBYLIB_CONFIG_KEY);
if (rubyLibConfigElement == null) { if (!rubyLib.isEmpty()) {
return;
}
Optional<String> rubyLib = rubyLibConfigElement.getValue();
if (rubyLib.isPresent() && !rubyLib.get().trim().isEmpty()) {
final String code = "$LOAD_PATH.unshift *ENV['RUBYLIB']&.split(File::PATH_SEPARATOR)" + // final String code = "$LOAD_PATH.unshift *ENV['RUBYLIB']&.split(File::PATH_SEPARATOR)" + //
"&.reject(&:empty?)" + // "&.reject(&:empty?)" + //
"&.reject { |path| $LOAD_PATH.include?(path) }"; // "&.reject { |path| $LOAD_PATH.include?(path) }"; //
try { try {
engine.eval(code); engine.eval(code);
} catch (ScriptException exception) { } 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<String> getRubyLibPaths() {
String rubyLib = get(RUBYLIB_CONFIG_KEY);
if (rubyLib.isEmpty()) {
return List.of();
}
return List.of(rubyLib.split(File.pathSeparator));
}
/** /**
* Configure system properties * Configure system properties
* *
@ -283,17 +335,14 @@ public class JRubyScriptEngineConfiguration {
private void configureSystemProperties() { private void configureSystemProperties() {
getConfigurationElements(OptionalConfigurationElement.Type.SYSTEM_PROPERTY).forEach(configElement -> { getConfigurationElements(OptionalConfigurationElement.Type.SYSTEM_PROPERTY).forEach(configElement -> {
String systemProperty = configElement.mappedTo().get(); String systemProperty = configElement.mappedTo().get();
String propertyValue = configElement.getValue().get(); String propertyValue = configElement.getValue();
logger.trace("Setting system property ({}) to ({})", systemProperty, propertyValue); logger.trace("Setting system property ({}) to ({})", systemProperty, propertyValue);
System.setProperty(systemProperty, propertyValue); System.setProperty(systemProperty, propertyValue);
}); });
} }
private Stream<OptionalConfigurationElement> getConfigurationElements( private Stream<OptionalConfigurationElement> getConfigurationElements(OptionalConfigurationElement.Type type) {
OptionalConfigurationElement.Type configurationType) { return configurationParameters.values().stream().filter(element -> element.type.equals(type));
return CONFIGURATION_TYPE_MAP
.getOrDefault(configurationType, Collections.<OptionalConfigurationElement> emptyList()).stream()
.filter(element -> element.getValue().isPresent());
} }
/** /**
@ -314,61 +363,42 @@ public class JRubyScriptEngineConfiguration {
* Inner static companion class for configuration elements * Inner static companion class for configuration elements
*/ */
private static class OptionalConfigurationElement { private static class OptionalConfigurationElement {
private enum Type {
SYSTEM_PROPERTY,
RUBY_ENVIRONMENT,
OTHER
}
private final Optional<String> defaultValue; private final String defaultValue;
private final Optional<String> mappedTo; private final Optional<String> mappedTo;
private final Type type; private final Type type;
private Optional<String> value; private Optional<String> 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.type = type;
this.defaultValue = Optional.ofNullable(defaultValue); this.defaultValue = defaultValue;
this.mappedTo = Optional.ofNullable(mappedTo); this.mappedTo = Optional.ofNullable(mappedTo);
value = Optional.empty(); value = Optional.empty();
} }
private Optional<String> getValue() { private String getValue() {
return value.or(() -> defaultValue); return value.orElse(defaultValue);
} }
private void setValue(String value) { private void setValue(String value) {
this.value = Optional.of(value); this.value = Optional.of(value);
} }
private void clearValue() {
this.value = Optional.empty();
}
private Optional<String> mappedTo() { private Optional<String> mappedTo() {
return mappedTo; return mappedTo;
} }
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);
}
}
} }
} }

View File

@ -12,9 +12,11 @@
*/ */
package org.openhab.automation.jrubyscripting.internal; package org.openhab.automation.jrubyscripting.internal;
import java.io.File;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -24,13 +26,20 @@ import javax.script.ScriptException;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.AbstractScriptEngineFactory;
import org.openhab.core.automation.module.script.ScriptDependencyTracker;
import org.openhab.core.automation.module.script.ScriptEngineFactory; 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.openhab.core.config.core.ConfigurableService;
import org.osgi.framework.Constants; import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component; 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.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. * This is an implementation of a {@link ScriptEngineFactory} for Ruby.
@ -43,14 +52,14 @@ import org.osgi.service.component.annotations.Modified;
+ "=org.openhab.automation.jrubyscripting") + "=org.openhab.automation.jrubyscripting")
@ConfigurableService(category = "automation", label = "JRuby Scripting", description_uri = "automation:jruby") @ConfigurableService(category = "automation", label = "JRuby Scripting", description_uri = "automation:jruby")
public class JRubyScriptEngineFactory extends AbstractScriptEngineFactory { public class JRubyScriptEngineFactory extends AbstractScriptEngineFactory {
private final JRubyScriptEngineConfiguration configuration = new JRubyScriptEngineConfiguration(); private final JRubyScriptEngineConfiguration configuration = new JRubyScriptEngineConfiguration();
private final javax.script.ScriptEngineFactory factory = new org.jruby.embed.jsr223.JRubyEngineFactory(); private final javax.script.ScriptEngineFactory factory = new org.jruby.embed.jsr223.JRubyEngineFactory();
private final List<String> scriptTypes = Stream private final List<String> scriptTypes = Stream.concat(Objects.requireNonNull(factory.getExtensions()).stream(),
.concat(factory.getExtensions().stream(), factory.getMimeTypes().stream()) Objects.requireNonNull(factory.getMimeTypes()).stream()).collect(Collectors.toUnmodifiableList());
.collect(Collectors.toUnmodifiableList());
private JRubyDependencyTracker jrubyDependencyTracker;
// Adds $ in front of a set of variables so that Ruby recognizes them as global variables // Adds $ in front of a set of variables so that Ruby recognizes them as global variables
private static Map.Entry<String, Object> mapGlobalPresets(Map.Entry<String, Object> entry) { private static Map.Entry<String, Object> mapGlobalPresets(Map.Entry<String, Object> entry) {
@ -62,16 +71,24 @@ public class JRubyScriptEngineFactory extends AbstractScriptEngineFactory {
} }
} }
// The activate call activates the automation and sets its configuration
@Activate @Activate
protected void activate(Map<String, Object> config) { public JRubyScriptEngineFactory(Map<String, Object> config) {
configuration.update(config, factory); jrubyDependencyTracker = new JRubyDependencyTracker(this);
modified(config);
}
@Deactivate
protected void deactivate() {
jrubyDependencyTracker.deactivate();
} }
// The modified call updates configuration for the automation // The modified call updates configuration for the automation
@Modified @Modified
protected void modified(Map<String, Object> config) { protected void modified(Map<String, Object> config) {
configuration.update(config, factory); configuration.update(config, factory);
// Re-initialize the dependency tracker's watchers.
jrubyDependencyTracker.deactivate();
jrubyDependencyTracker.activate();
} }
@Override @Override
@ -99,6 +116,15 @@ public class JRubyScriptEngineFactory extends AbstractScriptEngineFactory {
importClassesToRuby(scriptEngine, partitionedMap.getOrDefault(true, new HashMap<>())); importClassesToRuby(scriptEngine, partitionedMap.getOrDefault(true, new HashMap<>()));
super.scopeValues(scriptEngine, partitionedMap.getOrDefault(false, 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 // 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. // presets, including 'ir'. We wait for the second call before running the require statements.
if (scopeValues.containsKey("ir")) { if (scopeValues.containsKey("ir")) {
@ -120,7 +146,50 @@ public class JRubyScriptEngineFactory extends AbstractScriptEngineFactory {
@Override @Override
public @Nullable ScriptEngine createScriptEngine(String scriptType) { public @Nullable ScriptEngine createScriptEngine(String scriptType) {
return scriptTypes.contains(scriptType) ? configuration.configureRubyEnvironment(factory.getScriptEngine()) if (!scriptTypes.contains(scriptType)) {
: null; 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<String> 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);
} }
} }

View File

@ -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 <K> Type of Key
* @param <V> Type of Value
*/
@NonNullByDefault
public class BidiSetBag<K, V> {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Map<K, Set<V>> keyToValues = new HashMap<>();
private final Map<V, Set<K>> 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<V> getValues(K key) {
lock.readLock().lock();
try {
Set<V> values = keyToValues.getOrDefault(key, Set.of());
return Collections.unmodifiableSet(values);
} finally {
lock.readLock().unlock();
}
}
public Set<K> getKeys(V value) {
lock.readLock().lock();
try {
Set<K> keys = valueToKeys.getOrDefault(value, Set.of());
return Collections.unmodifiableSet(keys);
} finally {
lock.readLock().unlock();
}
}
public Set<V> removeKey(K key) {
lock.writeLock().lock();
try {
Set<V> 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<K> removeValue(V value) {
lock.writeLock().lock();
try {
Set<K> 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();
}
}
}

View File

@ -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<ScriptDependencyTracker.Listener> dependencyChangeListeners = ConcurrentHashMap.newKeySet();
private final BidiSetBag<String, String> scriptToLibs = new BidiSetBag<>();
private final JRubyScriptEngineFactory scriptEngineFactory;
private final List<AbstractWatchService> 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<String> 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<String> 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);
}
}

View File

@ -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
// `<name>-*.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)));
}
}
}
}

View File

@ -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());
}
}
}

View File

@ -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 <openHAB-conf>/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);
}
}

View File

@ -48,15 +48,17 @@
<parameter name="gem_home" type="text" required="false" groupName="environment"> <parameter name="gem_home" type="text" required="false" groupName="environment">
<label>GEM_HOME</label> <label>GEM_HOME</label>
<description><![CDATA[Location Ruby Gems will be installed and loaded, directory will be created if missing and gem <description><![CDATA[Location Ruby Gems will be installed to and loaded from. Directory will be created if necessary.
installs are specified. Defaults to "<tt>OPENHAB_CONF/scripts/lib/ruby/gem_home</tt>" when not specified. You can use <tt>{RUBY_ENGINE_VERSION}</tt>, <tt>{RUBY_ENGINE}</tt> and/or <tt>{RUBY_VERSION}</tt> replacements in this value to automatically point to
a new directory when the addon is updated with a new version of JRuby.
Defaults to "<tt>OPENHAB_CONF/automation/ruby/.gem/{RUBY_ENGINE_VERSION}</tt>" when not specified.
]]></description> ]]></description>
</parameter> </parameter>
<parameter name="rubylib" type="text" required="false" groupName="environment"> <parameter name="rubylib" type="text" required="false" groupName="environment">
<label>RUBYLIB</label> <label>RUBYLIB</label>
<description><![CDATA[Search path for user libraries. Separate each path with a colon (semicolon in Windows). Defaults to <description><![CDATA[Search path for user libraries. Separate each path with a colon (semicolon in Windows). Defaults to
"<tt>OPENHAB_CONF/automation/lib/ruby</tt>" when not specified.]]></description> "<tt>OPENHAB_CONF/automation/ruby/lib</tt>" when not specified.]]></description>
</parameter> </parameter>
<parameter name="local_context" type="text" required="false" groupName="system"> <parameter name="local_context" type="text" required="false" groupName="system">

View File

@ -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.true = Check For Updates
automation.config.jruby.check_update.option.false = Do Not 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.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 "<tt>OPENHAB_CONF/scripts/lib/ruby/gem_home</tt>" 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 <tt>{RUBY_ENGINE_VERSION}</tt>, <tt>{RUBY_ENGINE}</tt> and/or <tt>{RUBY_VERSION}</tt> replacements in this value to automatically point to a new directory when the addon is updated with a new version of JRuby. Defaults to "<tt>OPENHAB_CONF/automation/ruby/.gem/{RUBY_ENGINE_VERSION}</tt>" when not specified.
automation.config.jruby.gems.label = Ruby Gems 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 <tt>=</tt> and then the standard RubyGems version constraint, such as "<tt>openhab-scripting=~>4.0</tt>". automation.config.jruby.gems.description = A comma separated list of Ruby Gems to install. Versions may be constrained by separating with an <tt>=</tt> and then the standard RubyGems version constraint, such as "<tt>openhab-scripting=~>4.0</tt>".
automation.config.jruby.group.environment.label = Ruby Environment 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.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.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.label = RUBYLIB
automation.config.jruby.rubylib.description = Search path for user libraries. Separate each path with a colon (semicolon in Windows). Defaults to "<tt>OPENHAB_CONF/automation/lib/ruby</tt>" when not specified. automation.config.jruby.rubylib.description = Search path for user libraries. Separate each path with a colon (semicolon in Windows). Defaults to "<tt>OPENHAB_CONF/automation/ruby/lib</tt>" when not specified.
# service # service