[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>pull/13845/head
parent
a0ac57cfd9
commit
2382fadaaa
|
@ -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
|
||||
|
||||
|
|
|
@ -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<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 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<String, OptionalConfigurationElement> CONFIGURATION_PARAMETERS = Map.ofEntries(
|
||||
private final Map<String, OptionalConfigurationElement> 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<OptionalConfigurationElement.Type, List<OptionalConfigurationElement>> 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<String, Object> 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 @@ public class JRubyScriptEngineConfiguration {
|
|||
* @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
|
||||
*
|
||||
* @param factory Script Engine to configure
|
||||
* Gets a single configuration element.
|
||||
*/
|
||||
void configureScriptEngine(ScriptEngineFactory factory) {
|
||||
configureSystemProperties();
|
||||
private String get(String key) {
|
||||
OptionalConfigurationElement configElement = configurationParameters.get(key);
|
||||
|
||||
ScriptEngine engine = factory.getScriptEngine();
|
||||
configureRubyEnvironment(engine);
|
||||
configureGems(engine);
|
||||
return Objects.requireNonNull(configElement).getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private void ensureGemHomeExists() {
|
||||
OptionalConfigurationElement gemHomeConfigElement = CONFIGURATION_PARAMETERS.get(GEM_HOME);
|
||||
if (gemHomeConfigElement == null) {
|
||||
return;
|
||||
}
|
||||
Optional<String> 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");
|
||||
}
|
||||
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 @@ public class JRubyScriptEngineConfiguration {
|
|||
* @param engine Engine to install gems
|
||||
*/
|
||||
private synchronized void configureGems(ScriptEngine engine) {
|
||||
ensureGemHomeExists();
|
||||
|
||||
OptionalConfigurationElement gemsConfigElement = CONFIGURATION_PARAMETERS.get(GEMS);
|
||||
if (gemsConfigElement == null || !gemsConfigElement.getValue().isPresent()) {
|
||||
String gems = get(GEMS_CONFIG_KEY);
|
||||
if (gems.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
boolean checkUpdate = true;
|
||||
OptionalConfigurationElement updateConfigElement = CONFIGURATION_PARAMETERS.get(CHECK_UPDATE);
|
||||
if (updateConfigElement != null && updateConfigElement.getValue().isPresent()) {
|
||||
checkUpdate = updateConfigElement.getValue().get().equals("true");
|
||||
|
||||
String gemHome = getSpecificGemHome();
|
||||
if (gemHome.isEmpty()) {
|
||||
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
|
||||
// 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 @@ public class JRubyScriptEngineConfiguration {
|
|||
* @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 class JRubyScriptEngineConfiguration {
|
|||
*
|
||||
* @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 class JRubyScriptEngineConfiguration {
|
|||
* @param engine Engine in which to configure environment
|
||||
*/
|
||||
private void configureRubyLib(ScriptEngine engine) {
|
||||
OptionalConfigurationElement rubyLibConfigElement = CONFIGURATION_PARAMETERS.get(RUBYLIB);
|
||||
if (rubyLibConfigElement == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<String> 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<String> 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 @@ public class JRubyScriptEngineConfiguration {
|
|||
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<OptionalConfigurationElement> getConfigurationElements(
|
||||
OptionalConfigurationElement.Type configurationType) {
|
||||
return CONFIGURATION_TYPE_MAP
|
||||
.getOrDefault(configurationType, Collections.<OptionalConfigurationElement> emptyList()).stream()
|
||||
.filter(element -> element.getValue().isPresent());
|
||||
private Stream<OptionalConfigurationElement> getConfigurationElements(OptionalConfigurationElement.Type type) {
|
||||
return configurationParameters.values().stream().filter(element -> element.type.equals(type));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -314,61 +363,42 @@ public class JRubyScriptEngineConfiguration {
|
|||
* Inner static companion class for configuration elements
|
||||
*/
|
||||
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 Type type;
|
||||
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.defaultValue = Optional.ofNullable(defaultValue);
|
||||
this.defaultValue = defaultValue;
|
||||
this.mappedTo = Optional.ofNullable(mappedTo);
|
||||
value = Optional.empty();
|
||||
}
|
||||
|
||||
private Optional<String> getValue() {
|
||||
return value.or(() -> defaultValue);
|
||||
private String getValue() {
|
||||
return value.orElse(defaultValue);
|
||||
}
|
||||
|
||||
private void setValue(String value) {
|
||||
this.value = Optional.of(value);
|
||||
}
|
||||
|
||||
private void clearValue() {
|
||||
this.value = Optional.empty();
|
||||
}
|
||||
|
||||
private Optional<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 javax.script.ScriptException;
|
|||
|
||||
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 @@ import org.osgi.service.component.annotations.Modified;
|
|||
+ "=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<String> scriptTypes = Stream
|
||||
.concat(factory.getExtensions().stream(), factory.getMimeTypes().stream())
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
private final List<String> 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<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
|
||||
protected void activate(Map<String, Object> config) {
|
||||
configuration.update(config, factory);
|
||||
public JRubyScriptEngineFactory(Map<String, Object> 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<String, Object> config) {
|
||||
configuration.update(config, factory);
|
||||
// Re-initialize the dependency tracker's watchers.
|
||||
jrubyDependencyTracker.deactivate();
|
||||
jrubyDependencyTracker.activate();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -99,6 +116,15 @@ public class JRubyScriptEngineFactory extends AbstractScriptEngineFactory {
|
|||
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 @@ public class JRubyScriptEngineFactory extends AbstractScriptEngineFactory {
|
|||
|
||||
@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<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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -48,15 +48,17 @@
|
|||
|
||||
<parameter name="gem_home" type="text" required="false" groupName="environment">
|
||||
<label>GEM_HOME</label>
|
||||
<description><![CDATA[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.
|
||||
<description><![CDATA[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.
|
||||
]]></description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="rubylib" type="text" required="false" groupName="environment">
|
||||
<label>RUBYLIB</label>
|
||||
<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 name="local_context" type="text" required="false" groupName="system">
|
||||
|
|
|
@ -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 "<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.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
|
||||
|
@ -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 "<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
|
||||
|
||||
|
|
Loading…
Reference in New Issue