PluginUpdater.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. using MediaBrowser.Common.IO;
  2. using MediaBrowser.Model.Logging;
  3. using MediaBrowser.Model.Net;
  4. using MediaBrowser.Model.Plugins;
  5. using System;
  6. using System.Collections.Generic;
  7. using System.Diagnostics;
  8. using System.IO;
  9. using System.Linq;
  10. using System.Threading.Tasks;
  11. namespace MediaBrowser.UI.Controller
  12. {
  13. /// <summary>
  14. /// This keeps ui plugin assemblies in sync with plugins installed on the server
  15. /// </summary>
  16. public class PluginUpdater
  17. {
  18. private readonly ILogger _logger;
  19. public PluginUpdater(ILogger logger)
  20. {
  21. _logger = logger;
  22. }
  23. /// <summary>
  24. /// Updates the plugins.
  25. /// </summary>
  26. /// <returns>Task{PluginUpdateResult}.</returns>
  27. public async Task<PluginUpdateResult> UpdatePlugins()
  28. {
  29. _logger.Info("Downloading list of installed plugins");
  30. var allInstalledPlugins = await UIKernel.Instance.ApiClient.GetInstalledPluginsAsync().ConfigureAwait(false);
  31. var uiPlugins = allInstalledPlugins.Where(p => p.DownloadToUI).ToList();
  32. var result = new PluginUpdateResult { };
  33. result.DeletedPlugins = DeleteUninstalledPlugins(uiPlugins);
  34. await DownloadPluginAssemblies(uiPlugins, result).ConfigureAwait(false);
  35. result.UpdatedConfigurations = await DownloadPluginConfigurations(uiPlugins).ConfigureAwait(false);
  36. return result;
  37. }
  38. /// <summary>
  39. /// Downloads plugin assemblies from the server, if they need to be installed or updated.
  40. /// </summary>
  41. /// <param name="uiPlugins">The UI plugins.</param>
  42. /// <param name="result">The result.</param>
  43. /// <returns>Task.</returns>
  44. private async Task DownloadPluginAssemblies(IEnumerable<PluginInfo> uiPlugins, PluginUpdateResult result)
  45. {
  46. var newlyInstalledPlugins = new List<PluginInfo>();
  47. var updatedPlugins = new List<PluginInfo>();
  48. // Loop through the list of plugins that are on the server
  49. foreach (var pluginInfo in uiPlugins)
  50. {
  51. // See if it is already installed in the UI
  52. var currentAssemblyPath = Path.Combine(UIKernel.Instance.ApplicationPaths.PluginsPath, pluginInfo.AssemblyFileName);
  53. var isPluginInstalled = File.Exists(currentAssemblyPath);
  54. // Download the plugin if it is not present, or if the current version is out of date
  55. bool downloadPlugin;
  56. if (!isPluginInstalled)
  57. {
  58. downloadPlugin = true;
  59. _logger.Info("{0} is not installed and needs to be downloaded.", pluginInfo.Name);
  60. }
  61. else
  62. {
  63. var serverVersion = Version.Parse(pluginInfo.Version);
  64. var fileVersion = FileVersionInfo.GetVersionInfo(currentAssemblyPath).FileVersion ?? string.Empty;
  65. downloadPlugin = string.IsNullOrEmpty(fileVersion) || Version.Parse(fileVersion) < serverVersion;
  66. if (downloadPlugin)
  67. {
  68. _logger.Info("{0} has an updated version on the server and needs to be downloaded. Server version: {1}, UI version: {2}", pluginInfo.Name, serverVersion, fileVersion);
  69. }
  70. }
  71. if (downloadPlugin)
  72. {
  73. if (UIKernel.Instance.ApplicationVersion < Version.Parse(pluginInfo.MinimumRequiredUIVersion))
  74. {
  75. _logger.Warn("Can't download new version of {0} because the application needs to be updated first.", pluginInfo.Name);
  76. continue;
  77. }
  78. try
  79. {
  80. await DownloadPlugin(pluginInfo).ConfigureAwait(false);
  81. if (isPluginInstalled)
  82. {
  83. updatedPlugins.Add(pluginInfo);
  84. }
  85. else
  86. {
  87. newlyInstalledPlugins.Add(pluginInfo);
  88. }
  89. }
  90. catch (HttpException ex)
  91. {
  92. _logger.ErrorException("Error downloading {0} configuration", ex, pluginInfo.Name);
  93. }
  94. catch (IOException ex)
  95. {
  96. _logger.ErrorException("Error saving plugin assembly for {0}", ex, pluginInfo.Name);
  97. }
  98. }
  99. }
  100. result.NewlyInstalledPlugins = newlyInstalledPlugins;
  101. result.UpdatedPlugins = updatedPlugins;
  102. }
  103. /// <summary>
  104. /// Downloads plugin configurations from the server.
  105. /// </summary>
  106. /// <param name="uiPlugins">The UI plugins.</param>
  107. /// <returns>Task{List{PluginInfo}}.</returns>
  108. private async Task<List<PluginInfo>> DownloadPluginConfigurations(IEnumerable<PluginInfo> uiPlugins)
  109. {
  110. var updatedPlugins = new List<PluginInfo>();
  111. // Loop through the list of plugins that are on the server
  112. foreach (var pluginInfo in uiPlugins
  113. .Where(p => UIKernel.Instance.ApplicationVersion >= Version.Parse(p.MinimumRequiredUIVersion)))
  114. {
  115. // See if it is already installed in the UI
  116. var path = Path.Combine(UIKernel.Instance.ApplicationPaths.PluginConfigurationsPath, pluginInfo.ConfigurationFileName);
  117. var download = false;
  118. if (!File.Exists(path))
  119. {
  120. download = true;
  121. _logger.Info("{0} configuration was not found needs to be downloaded.", pluginInfo.Name);
  122. }
  123. else if (File.GetLastWriteTimeUtc(path) < pluginInfo.ConfigurationDateLastModified)
  124. {
  125. download = true;
  126. _logger.Info("{0} has an updated configuration on the server and needs to be downloaded.", pluginInfo.Name);
  127. }
  128. if (download)
  129. {
  130. if (UIKernel.Instance.ApplicationVersion < Version.Parse(pluginInfo.MinimumRequiredUIVersion))
  131. {
  132. _logger.Warn("Can't download updated configuration of {0} because the application needs to be updated first.", pluginInfo.Name);
  133. continue;
  134. }
  135. try
  136. {
  137. await DownloadPluginConfiguration(pluginInfo, path).ConfigureAwait(false);
  138. updatedPlugins.Add(pluginInfo);
  139. }
  140. catch (HttpException ex)
  141. {
  142. _logger.ErrorException("Error downloading {0} configuration", ex, pluginInfo.Name);
  143. }
  144. catch (IOException ex)
  145. {
  146. _logger.ErrorException("Error saving plugin configuration to {0}", ex, path);
  147. }
  148. }
  149. }
  150. return updatedPlugins;
  151. }
  152. /// <summary>
  153. /// Downloads a plugin assembly from the server
  154. /// </summary>
  155. /// <param name="plugin">The plugin.</param>
  156. /// <returns>Task.</returns>
  157. private async Task DownloadPlugin(PluginInfo plugin)
  158. {
  159. _logger.Info("Downloading {0} Plugin", plugin.Name);
  160. var path = Path.Combine(UIKernel.Instance.ApplicationPaths.PluginsPath, plugin.AssemblyFileName);
  161. // First download to a MemoryStream. This way if the download is cut off, we won't be left with a partial file
  162. using (var memoryStream = new MemoryStream())
  163. {
  164. var assemblyStream = await UIKernel.Instance.ApiClient.GetPluginAssemblyAsync(plugin).ConfigureAwait(false);
  165. await assemblyStream.CopyToAsync(memoryStream).ConfigureAwait(false);
  166. memoryStream.Position = 0;
  167. using (var fileStream = new FileStream(path, FileMode.Create))
  168. {
  169. await memoryStream.CopyToAsync(fileStream).ConfigureAwait(false);
  170. }
  171. }
  172. }
  173. /// <summary>
  174. /// Downloads the latest configuration for a plugin
  175. /// </summary>
  176. /// <param name="pluginInfo">The plugin info.</param>
  177. /// <param name="path">The path.</param>
  178. /// <returns>Task.</returns>
  179. private async Task DownloadPluginConfiguration(PluginInfo pluginInfo, string path)
  180. {
  181. _logger.Info("Downloading {0} Configuration", pluginInfo.Name);
  182. // First download to a MemoryStream. This way if the download is cut off, we won't be left with a partial file
  183. using (var stream = await UIKernel.Instance.ApiClient.GetPluginConfigurationFileAsync(pluginInfo.Id).ConfigureAwait(false))
  184. {
  185. using (var memoryStream = new MemoryStream())
  186. {
  187. await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
  188. memoryStream.Position = 0;
  189. using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, true))
  190. {
  191. await memoryStream.CopyToAsync(fs).ConfigureAwait(false);
  192. }
  193. }
  194. }
  195. File.SetLastWriteTimeUtc(path, pluginInfo.ConfigurationDateLastModified);
  196. }
  197. /// <summary>
  198. /// Deletes any plugins that have been uninstalled from the server
  199. /// </summary>
  200. /// <param name="uiPlugins">The UI plugins.</param>
  201. /// <returns>IEnumerable{System.String}.</returns>
  202. private IEnumerable<string> DeleteUninstalledPlugins(IEnumerable<PluginInfo> uiPlugins)
  203. {
  204. var deletedPlugins = new List<string>();
  205. foreach (var plugin in Directory.EnumerateFiles(UIKernel.Instance.ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly)
  206. .Select(Path.GetFileName)
  207. .ToList())
  208. {
  209. var serverPlugin = uiPlugins.FirstOrDefault(p => p.AssemblyFileName.Equals(plugin, StringComparison.OrdinalIgnoreCase));
  210. if (serverPlugin == null)
  211. {
  212. try
  213. {
  214. DeletePlugin(plugin);
  215. deletedPlugins.Add(plugin);
  216. }
  217. catch (IOException ex)
  218. {
  219. _logger.ErrorException("Error deleting plugin assembly {0}", ex, plugin);
  220. }
  221. }
  222. }
  223. return deletedPlugins;
  224. }
  225. /// <summary>
  226. /// Deletes an installed ui plugin.
  227. /// Leaves config and data behind in the event it is later re-installed
  228. /// </summary>
  229. /// <param name="plugin">The plugin.</param>
  230. private void DeletePlugin(string plugin)
  231. {
  232. _logger.Info("Deleting {0} Plugin", plugin);
  233. if (File.Exists(plugin))
  234. {
  235. File.Delete(plugin);
  236. }
  237. }
  238. }
  239. /// <summary>
  240. /// Class PluginUpdateResult
  241. /// </summary>
  242. public class PluginUpdateResult
  243. {
  244. /// <summary>
  245. /// Gets or sets the deleted plugins.
  246. /// </summary>
  247. /// <value>The deleted plugins.</value>
  248. public IEnumerable<string> DeletedPlugins { get; set; }
  249. /// <summary>
  250. /// Gets or sets the newly installed plugins.
  251. /// </summary>
  252. /// <value>The newly installed plugins.</value>
  253. public IEnumerable<PluginInfo> NewlyInstalledPlugins { get; set; }
  254. /// <summary>
  255. /// Gets or sets the updated plugins.
  256. /// </summary>
  257. /// <value>The updated plugins.</value>
  258. public IEnumerable<PluginInfo> UpdatedPlugins { get; set; }
  259. /// <summary>
  260. /// Gets or sets the updated configurations.
  261. /// </summary>
  262. /// <value>The updated configurations.</value>
  263. public IEnumerable<PluginInfo> UpdatedConfigurations { get; set; }
  264. }
  265. }