diff --git a/OpenSim/Region/OptionalModules/Resources/OptionalModules.addin.xml b/OpenSim/Region/OptionalModules/Resources/OptionalModules.addin.xml index 5eea286fd7..869134354f 100644 --- a/OpenSim/Region/OptionalModules/Resources/OptionalModules.addin.xml +++ b/OpenSim/Region/OptionalModules/Resources/OptionalModules.addin.xml @@ -13,5 +13,6 @@ + diff --git a/OpenSim/Region/OptionalModules/World/AutoBackup/AutoBackupModule.cs b/OpenSim/Region/OptionalModules/World/AutoBackup/AutoBackupModule.cs new file mode 100644 index 0000000000..ce9a4481ea --- /dev/null +++ b/OpenSim/Region/OptionalModules/World/AutoBackup/AutoBackupModule.cs @@ -0,0 +1,929 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSimulator Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Timers; +using System.Text.RegularExpressions; +using log4net; +using Nini.Config; +using OpenSim.Framework; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; + +namespace OpenSim.Region.OptionalModules.World.AutoBackup +{ + /// + /// Choose between ways of naming the backup files that are generated. + /// + /// Time: OARs are named by a timestamp. + /// Sequential: OARs are named by counting (Region_1.oar, Region_2.oar, etc.) + /// Overwrite: Only one file per region is created; it's overwritten each time a backup is made. + public enum NamingType + { + Time, + Sequential, + Overwrite + } + + /// + /// AutoBackupModule: save OAR region backups to disk periodically + /// + /// + /// Config Settings Documentation. + /// At the TOP LEVEL, e.g. in OpenSim.ini, we have the following options: + /// EACH REGION, in OpenSim.ini, can have the following settings under the [AutoBackupModule] section. + /// IMPORTANT: You may optionally specify the key name as follows for a per-region key: [Region Name].[Key Name] + /// Example: My region is named Foo. + /// If I wanted to specify the "AutoBackupInterval" key just for this region, I would name my key "Foo.AutoBackupInterval", under the [AutoBackupModule] section of OpenSim.ini. + /// Instead of specifying them on a per-region basis, you can also omit the region name to specify the default setting for all regions. + /// Region-specific settings take precedence. + /// + /// AutoBackupModuleEnabled: True/False. Default: False. If True, use the auto backup module. This setting does not support per-region basis. + /// All other settings under [AutoBackupModule] are ignored if AutoBackupModuleEnabled is false, even per-region settings! + /// AutoBackup: True/False. Default: False. If True, activate auto backup functionality. + /// This is the only required option for enabling auto-backup; the other options have sane defaults. + /// If False for a particular region, the auto-backup module becomes a no-op for the region, and all other AutoBackup* settings are ignored. + /// If False globally (the default), only regions that specifically override this with "FooRegion.AutoBackup = true" will get AutoBackup functionality. + /// AutoBackupInterval: Double, non-negative value. Default: 720 (12 hours). + /// The number of minutes between each backup attempt. + /// If a negative or zero value is given, it is equivalent to setting AutoBackup = False. + /// AutoBackupBusyCheck: True/False. Default: True. + /// If True, we will only take an auto-backup if a set of conditions are met. + /// These conditions are heuristics to try and avoid taking a backup when the sim is busy. + /// AutoBackupScript: String. Default: not specified (disabled). + /// File path to an executable script or binary to run when an automatic backup is taken. + /// The file should really be (Windows) an .exe or .bat, or (Linux/Mac) a shell script or binary. + /// Trying to "run" directories, or things with weird file associations on Win32, might cause unexpected results! + /// argv[1] of the executed file/script will be the file name of the generated OAR. + /// If the process can't be spawned for some reason (file not found, no execute permission, etc), write a warning to the console. + /// AutoBackupNaming: string. Default: Time. + /// One of three strings (case insensitive): + /// "Time": Current timestamp is appended to file name. An existing file will never be overwritten. + /// "Sequential": A number is appended to the file name. So if RegionName_x.oar exists, we'll save to RegionName_{x+1}.oar next. An existing file will never be overwritten. + /// "Overwrite": Always save to file named "${AutoBackupDir}/RegionName.oar", even if we have to overwrite an existing file. + /// AutoBackupDir: String. Default: "." (the current directory). + /// A directory (absolute or relative) where backups should be saved. + /// AutoBackupDilationThreshold: float. Default: 0.5. Lower bound on time dilation required for BusyCheck heuristics to pass. + /// If the time dilation is below this value, don't take a backup right now. + /// AutoBackupAgentThreshold: int. Default: 10. Upper bound on # of agents in region required for BusyCheck heuristics to pass. + /// If the number of agents is greater than this value, don't take a backup right now + /// Save memory by setting low initial capacities. Minimizes impact in common cases of all regions using same interval, and instances hosting 1 ~ 4 regions. + /// Also helps if you don't want AutoBackup at all. + /// + public class AutoBackupModule : ISharedRegionModule + { + private static readonly ILog m_log = + LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private readonly Dictionary m_pendingSaves = new Dictionary(1); + private readonly AutoBackupModuleState m_defaultState = new AutoBackupModuleState(); + private readonly Dictionary m_states = + new Dictionary(1); + private readonly Dictionary> m_timerMap = + new Dictionary>(1); + private readonly Dictionary m_timers = new Dictionary(1); + + private delegate T DefaultGetter(string settingName, T defaultValue); + private bool m_enabled; + + /// + /// Whether the shared module should be enabled at all. NOT the same as m_Enabled in AutoBackupModuleState! + /// + private bool m_closed; + + private IConfigSource m_configSource; + + /// + /// Required by framework. + /// + public bool IsSharedModule + { + get { return true; } + } + + #region ISharedRegionModule Members + + /// + /// Identifies the module to the system. + /// + string IRegionModuleBase.Name + { + get { return "AutoBackupModule"; } + } + + /// + /// We don't implement an interface, this is a single-use module. + /// + Type IRegionModuleBase.ReplaceableInterface + { + get { return null; } + } + + /// + /// Called once in the lifetime of the module at startup. + /// + /// The input config source for OpenSim.ini. + void IRegionModuleBase.Initialise(IConfigSource source) + { + // Determine if we have been enabled at all in OpenSim.ini -- this is part and parcel of being an optional module + this.m_configSource = source; + IConfig moduleConfig = source.Configs["AutoBackupModule"]; + if (moduleConfig == null) + { + this.m_enabled = false; + return; + } + else + { + this.m_enabled = moduleConfig.GetBoolean("AutoBackupModuleEnabled", false); + if (this.m_enabled) + { + m_log.Info("[AUTO BACKUP]: AutoBackupModule enabled"); + } + else + { + return; + } + } + + Timer defTimer = new Timer(43200000); + this.m_defaultState.Timer = defTimer; + this.m_timers.Add(43200000, defTimer); + defTimer.Elapsed += this.HandleElapsed; + defTimer.AutoReset = true; + defTimer.Start(); + + AutoBackupModuleState abms = this.ParseConfig(null, true); + m_log.Debug("[AUTO BACKUP]: Here is the default config:"); + m_log.Debug(abms.ToString()); + } + + /// + /// Called once at de-init (sim shutting down). + /// + void IRegionModuleBase.Close() + { + if (!this.m_enabled) + { + return; + } + + // We don't want any timers firing while the sim's coming down; strange things may happen. + this.StopAllTimers(); + } + + /// + /// Currently a no-op for AutoBackup because we have to wait for region to be fully loaded. + /// + /// + void IRegionModuleBase.AddRegion(Scene scene) + { + } + + /// + /// Here we just clean up some resources and stop the OAR backup (if any) for the given scene. + /// + /// The scene (region) to stop performing AutoBackup on. + void IRegionModuleBase.RemoveRegion(Scene scene) + { + if (!this.m_enabled) + { + return; + } + + if (this.m_states.ContainsKey(scene)) + { + AutoBackupModuleState abms = this.m_states[scene]; + + // Remove this scene out of the timer map list + Timer timer = abms.Timer; + List list = this.m_timerMap[timer]; + list.Remove(scene); + + // Shut down the timer if this was the last scene for the timer + if (list.Count == 0) + { + this.m_timerMap.Remove(timer); + this.m_timers.Remove(timer.Interval); + timer.Close(); + } + this.m_states.Remove(scene); + } + } + + /// + /// Most interesting/complex code paths in AutoBackup begin here. + /// We read lots of Nini config, maybe set a timer, add members to state tracking Dictionaries, etc. + /// + /// The scene to (possibly) perform AutoBackup on. + void IRegionModuleBase.RegionLoaded(Scene scene) + { + if (!this.m_enabled) + { + return; + } + + // This really ought not to happen, but just in case, let's pretend it didn't... + if (scene == null) + { + return; + } + + AutoBackupModuleState abms = this.ParseConfig(scene, false); + m_log.Debug("[AUTO BACKUP]: Config for " + scene.RegionInfo.RegionName); + m_log.Debug((abms == null ? "DEFAULT" : abms.ToString())); + } + + /// + /// Currently a no-op. + /// + void ISharedRegionModule.PostInitialise() + { + } + + #endregion + + /// + /// Set up internal state for a given scene. Fairly complex code. + /// When this method returns, we've started auto-backup timers, put members in Dictionaries, and created a State object for this scene. + /// + /// The scene to look at. + /// Whether this call is intended to figure out what we consider the "default" config (applied to all regions unless overridden by per-region settings). + /// An AutoBackupModuleState contains most information you should need to know relevant to auto-backup, as applicable to a single region. + private AutoBackupModuleState ParseConfig(IScene scene, bool parseDefault) + { + string sRegionName; + string sRegionLabel; + string prepend; + AutoBackupModuleState state; + + if (parseDefault) + { + sRegionName = null; + sRegionLabel = "DEFAULT"; + prepend = ""; + state = this.m_defaultState; + } + else + { + sRegionName = scene.RegionInfo.RegionName; + sRegionLabel = sRegionName; + prepend = sRegionName + "."; + state = null; + } + + // Read the config settings and set variables. + IConfig regionConfig = (scene != null ? scene.Config.Configs[sRegionName] : null); + IConfig config = this.m_configSource.Configs["AutoBackupModule"]; + if (config == null) + { + // defaultState would be disabled too if the section doesn't exist. + state = this.m_defaultState; + return state; + } + + bool tmpEnabled = ResolveBoolean("AutoBackup", this.m_defaultState.Enabled, config, regionConfig); + if (state == null && tmpEnabled != this.m_defaultState.Enabled) + //Varies from default state + { + state = new AutoBackupModuleState(); + } + + if (state != null) + { + state.Enabled = tmpEnabled; + } + + // If you don't want AutoBackup, we stop. + if ((state == null && !this.m_defaultState.Enabled) || (state != null && !state.Enabled)) + { + return state; + } + else + { + m_log.Info("[AUTO BACKUP]: Region " + sRegionLabel + " is AutoBackup ENABLED."); + } + + // Borrow an existing timer if one exists for the same interval; otherwise, make a new one. + double interval = + this.ResolveDouble("AutoBackupInterval", this.m_defaultState.IntervalMinutes, + config, regionConfig) * 60000.0; + if (state == null && interval != this.m_defaultState.IntervalMinutes*60000.0) + { + state = new AutoBackupModuleState(); + } + + if (this.m_timers.ContainsKey(interval)) + { + if (state != null) + { + state.Timer = this.m_timers[interval]; + } + m_log.Debug("[AUTO BACKUP]: Reusing timer for " + interval + " msec for region " + + sRegionLabel); + } + else + { + // 0 or negative interval == do nothing. + if (interval <= 0.0 && state != null) + { + state.Enabled = false; + return state; + } + if (state == null) + { + state = new AutoBackupModuleState(); + } + Timer tim = new Timer(interval); + state.Timer = tim; + //Milliseconds -> minutes + this.m_timers.Add(interval, tim); + tim.Elapsed += this.HandleElapsed; + tim.AutoReset = true; + tim.Start(); + } + + // Add the current region to the list of regions tied to this timer. + if (scene != null) + { + if (state != null) + { + if (this.m_timerMap.ContainsKey(state.Timer)) + { + this.m_timerMap[state.Timer].Add(scene); + } + else + { + List scns = new List(1); + scns.Add(scene); + this.m_timerMap.Add(state.Timer, scns); + } + } + else + { + if (this.m_timerMap.ContainsKey(this.m_defaultState.Timer)) + { + this.m_timerMap[this.m_defaultState.Timer].Add(scene); + } + else + { + List scns = new List(1); + scns.Add(scene); + this.m_timerMap.Add(this.m_defaultState.Timer, scns); + } + } + } + + bool tmpBusyCheck = ResolveBoolean("AutoBackupBusyCheck", + this.m_defaultState.BusyCheck, config, regionConfig); + if (state == null && tmpBusyCheck != this.m_defaultState.BusyCheck) + { + state = new AutoBackupModuleState(); + } + + if (state != null) + { + state.BusyCheck = tmpBusyCheck; + } + + // Set file naming algorithm + string stmpNamingType = ResolveString("AutoBackupNaming", + this.m_defaultState.NamingType.ToString(), config, regionConfig); + NamingType tmpNamingType; + if (stmpNamingType.Equals("Time", StringComparison.CurrentCultureIgnoreCase)) + { + tmpNamingType = NamingType.Time; + } + else if (stmpNamingType.Equals("Sequential", StringComparison.CurrentCultureIgnoreCase)) + { + tmpNamingType = NamingType.Sequential; + } + else if (stmpNamingType.Equals("Overwrite", StringComparison.CurrentCultureIgnoreCase)) + { + tmpNamingType = NamingType.Overwrite; + } + else + { + m_log.Warn("Unknown naming type specified for region " + sRegionLabel + ": " + + stmpNamingType); + tmpNamingType = NamingType.Time; + } + + if (state == null && tmpNamingType != this.m_defaultState.NamingType) + { + state = new AutoBackupModuleState(); + } + + if (state != null) + { + state.NamingType = tmpNamingType; + } + + string tmpScript = ResolveString("AutoBackupScript", + this.m_defaultState.Script, config, regionConfig); + if (state == null && tmpScript != this.m_defaultState.Script) + { + state = new AutoBackupModuleState(); + } + + if (state != null) + { + state.Script = tmpScript; + } + + string tmpBackupDir = ResolveString("AutoBackupDir", ".", config, regionConfig); + if (state == null && tmpBackupDir != this.m_defaultState.BackupDir) + { + state = new AutoBackupModuleState(); + } + + if (state != null) + { + state.BackupDir = tmpBackupDir; + // Let's give the user some convenience and auto-mkdir + if (state.BackupDir != ".") + { + try + { + DirectoryInfo dirinfo = new DirectoryInfo(state.BackupDir); + if (!dirinfo.Exists) + { + dirinfo.Create(); + } + } + catch (Exception e) + { + m_log.Warn( + "BAD NEWS. You won't be able to save backups to directory " + + state.BackupDir + + " because it doesn't exist or there's a permissions issue with it. Here's the exception.", + e); + } + } + } + + return state; + } + + /// + /// Helper function for ParseConfig. + /// + /// + /// + /// + /// + /// + private bool ResolveBoolean(string settingName, bool defaultValue, IConfig global, IConfig local) + { + if(local != null) + { + return local.GetBoolean(settingName, global.GetBoolean(settingName, defaultValue)); + } + else + { + return global.GetBoolean(settingName, defaultValue); + } + } + + /// + /// Helper function for ParseConfig. + /// + /// + /// + /// + /// + /// + private double ResolveDouble(string settingName, double defaultValue, IConfig global, IConfig local) + { + if (local != null) + { + return local.GetDouble(settingName, global.GetDouble(settingName, defaultValue)); + } + else + { + return global.GetDouble(settingName, defaultValue); + } + } + + /// + /// Helper function for ParseConfig. + /// + /// + /// + /// + /// + /// + private int ResolveInt(string settingName, int defaultValue, IConfig global, IConfig local) + { + if (local != null) + { + return local.GetInt(settingName, global.GetInt(settingName, defaultValue)); + } + else + { + return global.GetInt(settingName, defaultValue); + } + } + + /// + /// Helper function for ParseConfig. + /// + /// + /// + /// + /// + /// + private string ResolveString(string settingName, string defaultValue, IConfig global, IConfig local) + { + if (local != null) + { + return local.GetString(settingName, global.GetString(settingName, defaultValue)); + } + else + { + return global.GetString(settingName, defaultValue); + } + } + + /// + /// Called when any auto-backup timer expires. This starts the code path for actually performing a backup. + /// + /// + /// + private void HandleElapsed(object sender, ElapsedEventArgs e) + { + // TODO: heuristic thresholds are per-region, so we should probably run heuristics once per region + // XXX: Running heuristics once per region could add undue performance penalty for something that's supposed to + // check whether the region is too busy! Especially on sims with LOTS of regions. + // Alternative: make heuristics thresholds global to the module rather than per-region. Less flexible, + // but would allow us to be semantically correct while being easier on perf. + // Alternative 2: Run heuristics once per unique set of heuristics threshold parameters! Ay yi yi... + // Alternative 3: Don't support per-region heuristics at all; just accept them as a global only parameter. + // Since this is pretty experimental, I haven't decided which alternative makes the most sense. + if (this.m_closed) + { + return; + } + bool heuristicsRun = false; + bool heuristicsPassed = false; + if (!this.m_timerMap.ContainsKey((Timer) sender)) + { + m_log.Debug("Code-up error: timerMap doesn't contain timer " + sender); + } + + List tmap = this.m_timerMap[(Timer) sender]; + if (tmap != null && tmap.Count > 0) + { + foreach (IScene scene in tmap) + { + AutoBackupModuleState state = this.m_states[scene]; + bool heuristics = state.BusyCheck; + + // Fast path: heuristics are on; already ran em; and sim is fine; OR, no heuristics for the region. + if ((heuristics && heuristicsRun && heuristicsPassed) || !heuristics) + { + this.DoRegionBackup(scene); + // Heuristics are on; ran but we're too busy -- keep going. Maybe another region will have heuristics off! + } + else if (heuristicsRun) + { + m_log.Info("[AUTO BACKUP]: Heuristics: too busy to backup " + + scene.RegionInfo.RegionName + " right now."); + continue; + // Logical Deduction: heuristics are on but haven't been run + } + else + { + heuristicsPassed = this.RunHeuristics(scene); + heuristicsRun = true; + if (!heuristicsPassed) + { + m_log.Info("[AUTO BACKUP]: Heuristics: too busy to backup " + + scene.RegionInfo.RegionName + " right now."); + continue; + } + this.DoRegionBackup(scene); + } + } + } + } + + /// + /// Save an OAR, register for the callback for when it's done, then call the AutoBackupScript (if applicable). + /// + /// + private void DoRegionBackup(IScene scene) + { + if (scene.RegionStatus != RegionStatus.Up) + { + // We won't backup a region that isn't operating normally. + m_log.Warn("[AUTO BACKUP]: Not backing up region " + scene.RegionInfo.RegionName + + " because its status is " + scene.RegionStatus); + return; + } + + AutoBackupModuleState state = this.m_states[scene]; + IRegionArchiverModule iram = scene.RequestModuleInterface(); + string savePath = BuildOarPath(scene.RegionInfo.RegionName, + state.BackupDir, + state.NamingType); + if (savePath == null) + { + m_log.Warn("[AUTO BACKUP]: savePath is null in HandleElapsed"); + return; + } + Guid guid = Guid.NewGuid(); + m_pendingSaves.Add(guid, scene); + state.LiveRequests.Add(guid, savePath); + ((Scene) scene).EventManager.OnOarFileSaved += new EventManager.OarFileSaved(EventManager_OnOarFileSaved); + iram.ArchiveRegion(savePath, guid, null); + } + + /// + /// Called by the Event Manager when the OnOarFileSaved event is fired. + /// + /// + /// + void EventManager_OnOarFileSaved(Guid guid, string message) + { + // Ignore if the OAR save is being done by some other part of the system + if (m_pendingSaves.ContainsKey(guid)) + { + AutoBackupModuleState abms = m_states[(m_pendingSaves[guid])]; + ExecuteScript(abms.Script, abms.LiveRequests[guid]); + m_pendingSaves.Remove(guid); + abms.LiveRequests.Remove(guid); + } + } + + /// This format may turn out to be too unwieldy to keep... + /// Besides, that's what ctimes are for. But then how do I name each file uniquely without using a GUID? + /// Sequential numbers, right? We support those, too! + private static string GetTimeString() + { + StringWriter sw = new StringWriter(); + sw.Write("_"); + DateTime now = DateTime.Now; + sw.Write(now.Year); + sw.Write("y_"); + sw.Write(now.Month); + sw.Write("M_"); + sw.Write(now.Day); + sw.Write("d_"); + sw.Write(now.Hour); + sw.Write("h_"); + sw.Write(now.Minute); + sw.Write("m_"); + sw.Write(now.Second); + sw.Write("s"); + sw.Flush(); + string output = sw.ToString(); + sw.Close(); + return output; + } + + /// Return value of true ==> not too busy; false ==> too busy to backup an OAR right now, or error. + private bool RunHeuristics(IScene region) + { + try + { + return this.RunTimeDilationHeuristic(region) && this.RunAgentLimitHeuristic(region); + } + catch (Exception e) + { + m_log.Warn("[AUTO BACKUP]: Exception in RunHeuristics", e); + return false; + } + } + + /// + /// If the time dilation right at this instant is less than the threshold specified in AutoBackupDilationThreshold (default 0.5), + /// then we return false and trip the busy heuristic's "too busy" path (i.e. don't save an OAR). + /// AutoBackupDilationThreshold is a _LOWER BOUND_. Lower Time Dilation is bad, so if you go lower than our threshold, it's "too busy". + /// + /// + /// Returns true if we're not too busy; false means we've got worse time dilation than the threshold. + private bool RunTimeDilationHeuristic(IScene region) + { + string regionName = region.RegionInfo.RegionName; + return region.TimeDilation >= + this.m_configSource.Configs["AutoBackupModule"].GetFloat( + regionName + ".AutoBackupDilationThreshold", 0.5f); + } + + /// + /// If the root agent count right at this instant is less than the threshold specified in AutoBackupAgentThreshold (default 10), + /// then we return false and trip the busy heuristic's "too busy" path (i.e., don't save an OAR). + /// AutoBackupAgentThreshold is an _UPPER BOUND_. Higher Agent Count is bad, so if you go higher than our threshold, it's "too busy". + /// + /// + /// Returns true if we're not too busy; false means we've got more agents on the sim than the threshold. + private bool RunAgentLimitHeuristic(IScene region) + { + string regionName = region.RegionInfo.RegionName; + try + { + Scene scene = (Scene) region; + // TODO: Why isn't GetRootAgentCount() a method in the IScene interface? Seems generally useful... + return scene.GetRootAgentCount() <= + this.m_configSource.Configs["AutoBackupModule"].GetInt( + regionName + ".AutoBackupAgentThreshold", 10); + } + catch (InvalidCastException ice) + { + m_log.Debug( + "[AUTO BACKUP]: I NEED MAINTENANCE: IScene is not a Scene; can't get root agent count!", + ice); + return true; + // Non-obstructionist safest answer... + } + } + + /// + /// Run the script or executable specified by the "AutoBackupScript" config setting. + /// Of course this is a security risk if you let anyone modify OpenSim.ini and they want to run some nasty bash script. + /// But there are plenty of other nasty things that can be done with an untrusted OpenSim.ini, such as running high threat level scripting functions. + /// + /// + /// + private static void ExecuteScript(string scriptName, string savePath) + { + // Do nothing if there's no script. + if (scriptName == null || scriptName.Length <= 0) + { + return; + } + + try + { + FileInfo fi = new FileInfo(scriptName); + if (fi.Exists) + { + ProcessStartInfo psi = new ProcessStartInfo(scriptName); + psi.Arguments = savePath; + psi.CreateNoWindow = true; + Process proc = Process.Start(psi); + proc.ErrorDataReceived += HandleProcErrorDataReceived; + } + } + catch (Exception e) + { + m_log.Warn( + "Exception encountered when trying to run script for oar backup " + savePath, e); + } + } + + /// + /// Called if a running script process writes to stderr. + /// + /// + /// + private static void HandleProcErrorDataReceived(object sender, DataReceivedEventArgs e) + { + m_log.Warn("ExecuteScript hook " + ((Process) sender).ProcessName + + " is yacking on stderr: " + e.Data); + } + + /// + /// Quickly stop all timers from firing. + /// + private void StopAllTimers() + { + foreach (Timer t in this.m_timerMap.Keys) + { + t.Close(); + } + this.m_closed = true; + } + + /// + /// Determine the next unique filename by number, for "Sequential" AutoBackupNamingType. + /// + /// + /// + /// + private static string GetNextFile(string dirName, string regionName) + { + FileInfo uniqueFile = null; + long biggestExistingFile = GetNextOarFileNumber(dirName, regionName); + biggestExistingFile++; + // We don't want to overwrite the biggest existing file; we want to write to the NEXT biggest. + uniqueFile = + new FileInfo(dirName + Path.DirectorySeparatorChar + regionName + "_" + + biggestExistingFile + ".oar"); + return uniqueFile.FullName; + } + + /// + /// Top-level method for creating an absolute path to an OAR backup file based on what naming scheme the user wants. + /// + /// Name of the region to save. + /// Absolute or relative path to the directory where the file should reside. + /// The naming scheme for the file name. + /// + private static string BuildOarPath(string regionName, string baseDir, NamingType naming) + { + FileInfo path = null; + switch (naming) + { + case NamingType.Overwrite: + path = new FileInfo(baseDir + Path.DirectorySeparatorChar + regionName + ".oar"); + return path.FullName; + case NamingType.Time: + path = + new FileInfo(baseDir + Path.DirectorySeparatorChar + regionName + + GetTimeString() + ".oar"); + return path.FullName; + case NamingType.Sequential: + // All codepaths in GetNextFile should return a file name ending in .oar + path = new FileInfo(GetNextFile(baseDir, regionName)); + return path.FullName; + default: + m_log.Warn("VERY BAD: Unhandled case element " + naming); + break; + } + + return null; + } + + /// + /// Helper function for Sequential file naming type (see BuildOarPath and GetNextFile). + /// + /// + /// + /// + private static long GetNextOarFileNumber(string dirName, string regionName) + { + long retval = 1; + + DirectoryInfo di = new DirectoryInfo(dirName); + FileInfo[] fi = di.GetFiles(regionName, SearchOption.TopDirectoryOnly); + Array.Sort(fi, (f1, f2) => StringComparer.CurrentCultureIgnoreCase.Compare(f1.Name, f2.Name)); + + if (fi.LongLength > 0) + { + long subtract = 1L; + bool worked = false; + Regex reg = new Regex(regionName + "_([0-9])+" + ".oar"); + + while (!worked && subtract <= fi.LongLength) + { + // Pick the file with the last natural ordering + string biggestFileName = fi[fi.LongLength - subtract].Name; + MatchCollection matches = reg.Matches(biggestFileName); + long l = 1; + if (matches.Count > 0 && matches[0].Groups.Count > 0) + { + try + { + long.TryParse(matches[0].Groups[1].Value, out l); + retval = l; + worked = true; + } + catch (FormatException fe) + { + m_log.Warn( + "[AUTO BACKUP]: Error: Can't parse long value from file name to determine next OAR backup file number!", + fe); + subtract++; + } + } + else + { + subtract++; + } + } + } + return retval; + } + } +} + + diff --git a/OpenSim/Region/OptionalModules/World/AutoBackup/AutoBackupModuleState.cs b/OpenSim/Region/OptionalModules/World/AutoBackup/AutoBackupModuleState.cs new file mode 100644 index 0000000000..2db718c413 --- /dev/null +++ b/OpenSim/Region/OptionalModules/World/AutoBackup/AutoBackupModuleState.cs @@ -0,0 +1,126 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSimulator Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; + + +namespace OpenSim.Region.OptionalModules.World.AutoBackup +{ + /// AutoBackupModuleState: Auto-Backup state for one region (scene). + /// If you use this class in any way outside of AutoBackupModule, you should treat the class as opaque. + /// Since it is not part of the framework, you really should not rely upon it outside of the AutoBackupModule implementation. + /// + /// + public class AutoBackupModuleState + { + private Dictionary m_liveRequests = null; + + public AutoBackupModuleState() + { + this.Enabled = false; + this.BackupDir = "."; + this.BusyCheck = true; + this.Timer = null; + this.NamingType = NamingType.Time; + this.Script = null; + } + + public Dictionary LiveRequests + { + get { + return this.m_liveRequests ?? + (this.m_liveRequests = new Dictionary(1)); + } + } + + public bool Enabled + { + get; + set; + } + + public System.Timers.Timer Timer + { + get; + set; + } + + public double IntervalMinutes + { + get + { + if (this.Timer == null) + { + return -1.0; + } + else + { + return this.Timer.Interval / 60000.0; + } + } + } + + public bool BusyCheck + { + get; + set; + } + + public string Script + { + get; + set; + } + + public string BackupDir + { + get; + set; + } + + public NamingType NamingType + { + get; + set; + } + + public new string ToString() + { + string retval = ""; + + retval += "[AUTO BACKUP]: AutoBackup: " + (Enabled ? "ENABLED" : "DISABLED") + "\n"; + retval += "[AUTO BACKUP]: Interval: " + IntervalMinutes + " minutes" + "\n"; + retval += "[AUTO BACKUP]: Do Busy Check: " + (BusyCheck ? "Yes" : "No") + "\n"; + retval += "[AUTO BACKUP]: Naming Type: " + NamingType.ToString() + "\n"; + retval += "[AUTO BACKUP]: Backup Dir: " + BackupDir + "\n"; + retval += "[AUTO BACKUP]: Script: " + Script + "\n"; + return retval; + } + } +} + diff --git a/bin/OpenSimDefaults.ini b/bin/OpenSimDefaults.ini index 2e192f1178..b32d198343 100644 --- a/bin/OpenSimDefaults.ini +++ b/bin/OpenSimDefaults.ini @@ -1366,6 +1366,10 @@ [GridService] ;; default standalone, overridable in StandaloneCommon.ini StorageProvider = "OpenSim.Data.Null.dll:NullRegionData" + +[AutoBackupModule] + ;; default is module is disabled at the top level + AutoBackupModuleEnabled = false [Modules] Include-modules = "addon-modules/*/config/*.ini"