You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

204 lines
6.8 KiB

  1. using Microsoft.Extensions.Logging;
  2. using Microsoft.Scripting;
  3. using Microsoft.Scripting.Hosting;
  4. using MQTTnet.Server.Configuration;
  5. using System;
  6. using System.Collections.Generic;
  7. using System.Dynamic;
  8. using System.IO;
  9. using System.Linq;
  10. using System.Text;
  11. using System.Threading;
  12. using System.Threading.Tasks;
  13. namespace MQTTnet.Server.Scripting
  14. {
  15. public class PythonScriptHostService
  16. {
  17. readonly IDictionary<string, object> _proxyObjects = new ExpandoObject();
  18. readonly List<PythonScriptInstance> _scriptInstances = new List<PythonScriptInstance>();
  19. readonly string _scriptsPath;
  20. readonly ScriptingSettingsModel _scriptingSettings;
  21. readonly ILogger<PythonScriptHostService> _logger;
  22. readonly ScriptEngine _scriptEngine;
  23. public PythonScriptHostService(ScriptingSettingsModel scriptingSettings, PythonIOStream pythonIOStream, ILogger<PythonScriptHostService> logger)
  24. {
  25. _scriptingSettings = scriptingSettings ?? throw new ArgumentNullException(nameof(scriptingSettings));
  26. _logger = logger ?? throw new ArgumentNullException(nameof(logger));
  27. _scriptEngine = IronPython.Hosting.Python.CreateEngine();
  28. _scriptEngine.Runtime.IO.SetOutput(pythonIOStream, Encoding.UTF8);
  29. _scriptsPath = PathHelper.ExpandPath(scriptingSettings.ScriptsPath);
  30. }
  31. public void Configure()
  32. {
  33. AddSearchPaths(_scriptEngine);
  34. TryInitializeScriptsAsync().GetAwaiter().GetResult();
  35. }
  36. public void RegisterProxyObject(string name, object @object)
  37. {
  38. if (name == null) throw new ArgumentNullException(nameof(name));
  39. if (@object == null) throw new ArgumentNullException(nameof(@object));
  40. _proxyObjects.Add(name, @object);
  41. }
  42. public void InvokeOptionalFunction(string name, object parameters)
  43. {
  44. if (name == null) throw new ArgumentNullException(nameof(name));
  45. lock (_scriptInstances)
  46. {
  47. foreach (var pythonScriptInstance in _scriptInstances)
  48. {
  49. try
  50. {
  51. pythonScriptInstance.InvokeOptionalFunction(name, parameters);
  52. }
  53. catch (Exception exception)
  54. {
  55. _logger.LogError(exception, $"Error while invoking function '{name}' at script '{pythonScriptInstance.Uid}'.");
  56. }
  57. }
  58. }
  59. }
  60. public List<string> GetScriptUids()
  61. {
  62. lock (_scriptInstances)
  63. {
  64. return _scriptInstances.Select(si => si.Uid).ToList();
  65. }
  66. }
  67. public Task<string> ReadScriptAsync(string uid, CancellationToken cancellationToken)
  68. {
  69. if (uid == null) throw new ArgumentNullException(nameof(uid));
  70. string path;
  71. lock (_scriptInstances)
  72. {
  73. path = _scriptInstances.FirstOrDefault(si => si.Uid == uid)?.Path;
  74. }
  75. if (path == null || !File.Exists(path))
  76. {
  77. return null;
  78. }
  79. return File.ReadAllTextAsync(path, Encoding.UTF8, cancellationToken);
  80. }
  81. public async Task WriteScriptAsync(string uid, string code, CancellationToken cancellationToken)
  82. {
  83. var path = Path.Combine(_scriptsPath, uid + ".py");
  84. await File.WriteAllTextAsync(path, code, Encoding.UTF8, cancellationToken).ConfigureAwait(false);
  85. await TryInitializeScriptsAsync().ConfigureAwait(false);
  86. }
  87. public async Task DeleteScriptAsync(string uid)
  88. {
  89. var path = Path.Combine(_scriptsPath, uid + ".py");
  90. if (File.Exists(path))
  91. {
  92. File.Delete(path);
  93. await TryInitializeScriptsAsync().ConfigureAwait(false);
  94. }
  95. }
  96. public async Task TryInitializeScriptsAsync()
  97. {
  98. lock (_scriptInstances)
  99. {
  100. foreach (var scriptInstance in _scriptInstances)
  101. {
  102. try
  103. {
  104. scriptInstance.InvokeOptionalFunction("destroy");
  105. }
  106. catch (Exception exception)
  107. {
  108. _logger.LogWarning(exception, $"Error while unloading script '{scriptInstance.Uid}'.");
  109. }
  110. }
  111. _scriptInstances.Clear();
  112. }
  113. foreach (var path in Directory.GetFiles(_scriptsPath, "*.py", SearchOption.AllDirectories).OrderBy(file => file))
  114. {
  115. await TryInitializeScriptAsync(path).ConfigureAwait(false);
  116. }
  117. }
  118. async Task TryInitializeScriptAsync(string path)
  119. {
  120. var uid = new FileInfo(path).Name.Replace(".py", string.Empty, StringComparison.OrdinalIgnoreCase);
  121. try
  122. {
  123. _logger.LogTrace($"Initializing Python script '{uid}'...");
  124. var code = await File.ReadAllTextAsync(path).ConfigureAwait(false);
  125. var scriptInstance = CreateScriptInstance(uid, path, code);
  126. scriptInstance.InvokeOptionalFunction("initialize");
  127. lock (_scriptInstances)
  128. {
  129. _scriptInstances.Add(scriptInstance);
  130. }
  131. _logger.LogInformation($"Initialized script '{uid}'.");
  132. }
  133. catch (Exception exception)
  134. {
  135. _logger.LogError(exception, $"Error while initializing script '{uid}'.");
  136. }
  137. }
  138. PythonScriptInstance CreateScriptInstance(string uid, string path, string code)
  139. {
  140. var scriptScope = _scriptEngine.CreateScope();
  141. var source = scriptScope.Engine.CreateScriptSourceFromString(code, SourceCodeKind.File);
  142. var compiledCode = source.Compile();
  143. scriptScope.SetVariable("mqtt_net_server", _proxyObjects);
  144. compiledCode.Execute(scriptScope);
  145. return new PythonScriptInstance(uid, path, scriptScope);
  146. }
  147. void AddSearchPaths(ScriptEngine scriptEngine)
  148. {
  149. if (_scriptingSettings.IncludePaths?.Any() != true)
  150. {
  151. return;
  152. }
  153. var searchPaths = scriptEngine.GetSearchPaths();
  154. foreach (var path in _scriptingSettings.IncludePaths)
  155. {
  156. var effectivePath = PathHelper.ExpandPath(path);
  157. if (Directory.Exists(effectivePath))
  158. {
  159. searchPaths.Add(effectivePath);
  160. _logger.LogInformation($"Added Python lib path: {effectivePath}");
  161. }
  162. }
  163. scriptEngine.SetSearchPaths(searchPaths);
  164. }
  165. }
  166. }