#region License Information
/* HeuristicLab
* Copyright (C) 2002-2018 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
*
* This file is part of HeuristicLab.
*
* HeuristicLab is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* HeuristicLab is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with HeuristicLab. If not, see .
*/
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;
using HeuristicLab.Core;
using CoreProperties = HeuristicLab.Clients.Hive.SlaveCore.Properties;
namespace HeuristicLab.Clients.Hive.SlaveCore {
public class PluginManager {
private static object locker = new object();
private string lastUsedFileName = CoreProperties.Settings.Default.LastUsedFileName;
//maximum number of days after which a plugin gets deleted if not used
private int pluginLifetime = CoreProperties.Settings.Default.PluginLifetime;
private string PluginCacheDir { get; set; }
public string PluginTempBaseDir { get; set; }
private ILog log;
private IPluginProvider pluginService;
private List cachedPluginsGuids = new List();
public PluginManager(IPluginProvider pluginService, ILog log) {
this.pluginService = pluginService;
this.log = log;
CheckWorkingDirectories();
PluginCacheDir = CoreProperties.Settings.Default.PluginCacheDir;
PluginTempBaseDir = CoreProperties.Settings.Default.PluginTempBaseDir;
DoUpdateRun();
}
///
/// Normally the configuration file just contains the folder names of the PluginCache and the AppDomain working directory.
/// This means that these folders are created in the current directory which is ok for the console client and the windows service.
/// For the HL client we can't do that because the plugin infrastructure gets confused when starting HeuristicLab.
/// Therefore if there is only a relative path in the config, we change that to the temp path.
///
private void CheckWorkingDirectories() {
if (!Path.IsPathRooted(CoreProperties.Settings.Default.PluginCacheDir)) {
CoreProperties.Settings.Default.PluginCacheDir = Path.Combine(Path.GetTempPath(), CoreProperties.Settings.Default.PluginCacheDir);
CoreProperties.Settings.Default.Save();
}
if (!Path.IsPathRooted(CoreProperties.Settings.Default.PluginTempBaseDir)) {
CoreProperties.Settings.Default.PluginTempBaseDir = Path.Combine(Path.GetTempPath(), CoreProperties.Settings.Default.PluginTempBaseDir);
CoreProperties.Settings.Default.Save();
}
}
///
/// Returns the last directory of a path
///
private string GetFilenameFromPath(string path) {
string[] dirParts = path.Split(Path.DirectorySeparatorChar);
if (dirParts.Length > 0) {
string fileGuid = dirParts[dirParts.Length - 1];
return fileGuid;
} else
return "";
}
private void DoUpdateRun() {
SafelyCreateDirectory(PluginCacheDir);
lock (cachedPluginsGuids) {
cachedPluginsGuids.Clear();
foreach (string dir in Directory.EnumerateDirectories(PluginCacheDir)) {
cachedPluginsGuids.Add(Guid.Parse(GetFilenameFromPath(dir)));
}
}
}
public void CopyPluginsForJob(List requests, Guid jobId, out string configFileName) {
configFileName = string.Empty;
String targetDir = Path.Combine(PluginTempBaseDir, jobId.ToString());
RecreateDirectory(targetDir);
foreach (Plugin requestedPlugin in requests) {
var filePaths = GetPluginFilePaths(requestedPlugin.Id);
foreach (string filePath in filePaths) {
File.Copy(filePath, Path.Combine(targetDir, Path.GetFileName(filePath)));
}
if (requestedPlugin.Name == CoreProperties.Settings.Default.ConfigurationName) {
// configuration plugin consists only of 1 file (usually the "HeuristicLab X.X.exe.config")
configFileName = Path.Combine(targetDir, Path.GetFileName(filePaths.SingleOrDefault()));
}
}
// copy files from PluginInfrastructure (which are not declared in any plugins)
string baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
CopyFile(baseDir, targetDir, CoreProperties.Settings.Default.PluginInfrastructureDll);
// copy slave plugins, otherwise its not possible to register the UnhandledException handler to the appdomain
CopyFile(baseDir, targetDir, CoreProperties.Settings.Default.ClientsHiveSlaveCoreDll);
CopyFile(baseDir, targetDir, CoreProperties.Settings.Default.ClientsHiveDll);
CopyFile(baseDir, targetDir, CoreProperties.Settings.Default.HiveDll);
CopyFile(baseDir, targetDir, CoreProperties.Settings.Default.ClientsCommonDll);
}
private static DirectoryInfo RecreateDirectory(String targetDir) {
var di = new DirectoryInfo(targetDir);
if (di.Exists) Directory.Delete(targetDir, true);
di.Refresh();
while (di.Exists) {
Thread.Sleep(CoreProperties.Settings.Default.DirOpSleepTime);
di.Refresh();
}
return SafelyCreateDirectory(targetDir);
}
private static DirectoryInfo SafelyCreateDirectory(String targetDir) {
var di = new DirectoryInfo(targetDir);
if (!di.Exists) {
di = Directory.CreateDirectory(targetDir);
while (!di.Exists) {
Thread.Sleep(CoreProperties.Settings.Default.DirOpSleepTime);
di.Refresh();
}
}
return di;
}
private void CopyFile(string baseDir, string targetDir, string fileName) {
if (!File.Exists(Path.Combine(targetDir, fileName))) File.Copy(Path.Combine(baseDir, fileName), Path.Combine(targetDir, fileName));
}
///
/// Updates the plugin cache with missing plugins and
/// then copies the required plugins for the task.
///
public void PreparePlugins(Task task, out string configFileName) {
lock (locker) {
log.LogMessage("Fetching plugins for task " + task.Id);
List missingGuids = new List();
List neededPlugins = new List();
lock (cachedPluginsGuids) {
foreach (Guid pluginId in task.PluginsNeededIds) {
Plugin plugin = pluginService.GetPlugin(pluginId);
if (plugin != null) {
neededPlugins.Add(plugin);
}
if (!cachedPluginsGuids.Contains(pluginId)) {
missingGuids.Add(pluginId);
}
}
}
IEnumerable pluginDatas = pluginService.GetPluginDatas(missingGuids);
if (pluginDatas != null) {
foreach (PluginData pluginData in pluginDatas) {
string pluginDir = Path.Combine(PluginCacheDir, pluginData.PluginId.ToString());
//put all files belonging to a plugin in the same directory
SafelyCreateDirectory(pluginDir);
File.WriteAllBytes(Path.Combine(pluginDir, Path.GetFileName(pluginData.FileName)), pluginData.Data);
}
if (missingGuids.Count > 0) {
DoUpdateRun();
}
CopyPluginsForJob(neededPlugins, task.Id, out configFileName);
} else {
configFileName = "";
}
log.LogMessage(string.Format("Fetched {0} plugins for task {1}", missingGuids.Count, task.Id));
}
}
///
/// Returns a list of files which belong to a plugin from the plugincache
///
private IEnumerable GetPluginFilePaths(Guid pluginId) {
string pluginPath = Path.Combine(PluginCacheDir, pluginId.ToString());
if (Directory.Exists(pluginPath)) {
WriteDateLastUsed(pluginPath);
foreach (string filePath in Directory.GetFiles(pluginPath)) {
string fn = Path.GetFileName(filePath);
if (fn != lastUsedFileName)
yield return filePath;
}
}
}
///
/// creates a file in path with the current date;
/// this can later be used to find plugins which are outdated
///
private void WriteDateLastUsed(string path) {
FileStream fs = null;
try {
fs = new FileStream(Path.Combine(path, lastUsedFileName), FileMode.Create);
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(fs, DateTime.Now);
}
catch (IOException) {
log.LogMessage(string.Format("No used date written in path {0}.", path));
}
catch (SerializationException) {
//rethrow...
throw;
}
finally {
if (fs != null) {
fs.Close();
}
}
}
///
/// Checks the PluginTemp directory for orphaned directories and deletes them.
/// This should be only called if no jobs are currently running.
///
public void CleanPluginTemp() {
if (Directory.Exists(PluginTempBaseDir)) {
foreach (string dir in Directory.EnumerateDirectories(PluginTempBaseDir)) {
try {
log.LogMessage("Deleting orphaned directory " + dir);
Directory.Delete(dir, true);
}
catch (Exception ex) {
log.LogMessage("Error cleaning up PluginTemp directory " + dir + ": " + ex.ToString());
}
}
}
}
///
/// checks the pluginCacheDirectory and deletes plugin folders which are not used anymore
///
private void CleanPluginCache() {
FileStream fs = null;
DateTime luDate;
bool changed = false;
if (Directory.Exists(PluginCacheDir)) {
lock (locker) {
foreach (string dir in Directory.EnumerateDirectories(PluginCacheDir)) {
try {
fs = new FileStream(Path.Combine(dir, lastUsedFileName), FileMode.Open);
BinaryFormatter formatter = new BinaryFormatter();
luDate = (DateTime)formatter.Deserialize(fs);
fs.Close();
if (luDate.AddDays(pluginLifetime) < DateTime.Now) {
Directory.Delete(dir, true);
changed = true;
}
}
catch (FileNotFoundException) {
//nerver used
Directory.Delete(dir, true);
changed = true;
}
catch (Exception ex) {
if (fs != null) {
fs.Close();
}
log.LogMessage(string.Format("CleanPluginCache threw exception: {0}", ex.ToString()));
}
}
if (changed)
DoUpdateRun();
}
}
}
public void DeletePluginsForJob(Guid id) {
try {
log.LogMessage("Deleting plugins...");
int tries = CoreProperties.Settings.Default.PluginDeletionRetries;
string path = Path.Combine(PluginTempBaseDir, id.ToString());
while (tries > 0) {
try {
if (Directory.Exists(path)) Directory.Delete(path, true);
tries = 0;
}
catch (Exception) {
Thread.Sleep(CoreProperties.Settings.Default.PluginDeletionTimeout);
tries--;
if (tries == 0) throw;
}
}
}
catch (Exception ex) {
log.LogMessage("failed while unloading " + id + " with exception " + ex);
}
CleanPluginCache();
}
}
}