diff --git a/OpenSim/Region/OptionalModules/World/AutoBackup/AutoBackupModule.cs b/OpenSim/Region/OptionalModules/World/AutoBackup/AutoBackupModule.cs index 364697b3f1..a4dbea406a 100644 --- a/OpenSim/Region/OptionalModules/World/AutoBackup/AutoBackupModule.cs +++ b/OpenSim/Region/OptionalModules/World/AutoBackup/AutoBackupModule.cs @@ -1,764 +1,745 @@ -/* - * 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.IO; -using System.Timers; -using System.Diagnostics; -using System.Reflection; -using System.Collections.Generic; -using log4net; -using Nini; -using Nini.Config; -using OpenSim.Framework; -using OpenSim.Framework.Statistics; -using OpenSim.Region.Framework.Interfaces; -using OpenSim.Region.Framework.Scenes; - - -/* - * Config Settings Documentation. - * At the TOP LEVEL, e.g. in OpenSim.ini, we have the following options: - * In the [Modules] section: - * AutoBackupModule: True/False. Default: False. If True, use the auto backup module. Otherwise it will be disabled regardless of what settings are in Regions.ini! - * EACH REGION, in OpenSim.ini, can have the following settings under the [AutoBackupModule] section. - * VERY IMPORTANT: You must create the key name as follows: . - * Example: My region is named Foo. - * If I wanted to specify the "AutoBackupInterval" key below, 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. - * 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, the auto-backup module becomes a no-op for the region, and all other AutoBackup* settings are ignored. - * 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. - * */ - -namespace OpenSim.Region.OptionalModules.World.AutoBackup -{ - - public enum NamingType - { - TIME, - SEQUENTIAL, - OVERWRITE - } - - public class AutoBackupModule : ISharedRegionModule, IRegionModuleBase - { - - private static readonly ILog m_log = LogManager.GetLogger (MethodBase.GetCurrentMethod ().DeclaringType); - - //AutoBackupModuleState: Auto-Backup state for one region (scene). - public class AutoBackupModuleState - { - private bool m_enabled = false; - private NamingType m_naming = NamingType.TIME; - private Timer m_timer = null; - private bool m_busycheck = true; - private string m_script = null; - private string m_dir = "."; - - public AutoBackupModuleState () - { - - } - - public void SetEnabled (bool b) - { - m_enabled = b; - } - - public bool GetEnabled () - { - return m_enabled; - } - - public Timer GetTimer () - { - return m_timer; - } - - public double GetIntervalMinutes () - { - if(m_timer == null) - { - return -1.0; - } - else - { - return m_timer.Interval / 60000.0; - } - } - - public void SetTimer (Timer t) - { - m_timer = t; - } - - public bool GetBusyCheck () - { - return m_busycheck; - } - - public void SetBusyCheck (bool b) - { - m_busycheck = b; - } - - - public string GetScript () - { - return m_script; - } - - public void SetScript (string s) - { - m_script = s; - } - - public string GetBackupDir () - { - return m_dir; - } - - public void SetBackupDir (string s) - { - m_dir = s; - } - - public NamingType GetNamingType () - { - return m_naming; - } - - public void SetNamingType (NamingType n) - { - m_naming = n; - } - - public string ToString() - { - string retval = ""; - - retval += "[AUTO BACKUP]: AutoBackup: " + (GetEnabled() ? "ENABLED" : "DISABLED") + "\n"; - retval += "[AUTO BACKUP]: Interval: " + GetIntervalMinutes() + " minutes" + "\n"; - retval += "[AUTO BACKUP]: Do Busy Check: " + (GetBusyCheck() ? "Yes" : "No") + "\n"; - retval += "[AUTO BACKUP]: Naming Type: " + GetNamingType().ToString() + "\n"; - retval += "[AUTO BACKUP]: Backup Dir: " + GetBackupDir() + "\n"; - retval += "[AUTO BACKUP]: Script: " + GetScript() + "\n"; - return retval; - } - } - - //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 - readonly Dictionary states = new Dictionary (4); - readonly Dictionary timers = new Dictionary (1); - readonly Dictionary> timerMap = new Dictionary> (1); - private IConfigSource m_configSource = null; - private bool m_Enabled = false; - //Whether the shared module should be enabled at all. NOT the same as m_Enabled in AutoBackupModuleState! - private bool m_closed = false; - //True means IRegionModuleBase.Close() was called on us, and we should stop operation ASAP. - //Used to prevent elapsing timers after Close() is called from trying to start an autobackup while the sim is shutting down. - readonly AutoBackupModuleState defaultState = new AutoBackupModuleState(); - - public AutoBackupModule () - { - - } - - #region IRegionModuleBase implementation - void IRegionModuleBase.Initialise (Nini.Config.IConfigSource source) - { - //Determine if we have been enabled at all in OpenSim.ini -- this is part and parcel of being an optional module - m_configSource = source; - IConfig moduleConfig = source.Configs["Modules"]; - if (moduleConfig != null) { - m_Enabled = moduleConfig.GetBoolean ("AutoBackupModule", false); - if (m_Enabled) { - m_log.Info ("[AUTO BACKUP]: AutoBackupModule enabled"); - } - else { - m_log.Info ("[AUTO BACKUP]: AutoBackupModule disabled"); - return; - } - } - - Timer defTimer = new Timer(720 * 60000); - defaultState.SetTimer(defTimer); - timers.Add (720*60000, defTimer); - defTimer.Elapsed += HandleElapsed; - defTimer.AutoReset = true; - defTimer.Start (); - - AutoBackupModuleState abms = ParseConfig(null, true); - m_log.Debug("[AUTO BACKUP]: Config for default"); - m_log.Debug(abms.ToString()); - } - - void IRegionModuleBase.Close () - { - if (!m_Enabled) - return; - - //We don't want any timers firing while the sim's coming down; strange things may happen. - StopAllTimers (); - } - - void IRegionModuleBase.AddRegion (Framework.Scenes.Scene scene) - { - //NO-OP. Wait for the region to be loaded. - } - - void IRegionModuleBase.RemoveRegion (Framework.Scenes.Scene scene) - { - if (!m_Enabled) - return; - - if(states.ContainsKey(scene)) - { - AutoBackupModuleState abms = states[scene]; - - //Remove this scene out of the timer map list - Timer timer = abms.GetTimer (); - List list = timerMap[timer]; - list.Remove (scene); - - //Shut down the timer if this was the last scene for the timer - if (list.Count == 0) { - timerMap.Remove (timer); - timers.Remove (timer.Interval); - timer.Close (); - } - states.Remove(scene); - } - } - - void IRegionModuleBase.RegionLoaded (Framework.Scenes.Scene scene) - { - if (!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 = ParseConfig(scene, false); - m_log.Debug("[AUTO BACKUP]: Config for " + scene.RegionInfo.RegionName); - m_log.Debug((abms == null ? "DEFAULT" : abms.ToString())); - } - - AutoBackupModuleState ParseConfig (IScene scene, bool parseDefault) - { - string sRegionName; - string sRegionLabel; - string prepend; - AutoBackupModuleState state; - - if(parseDefault) - { - sRegionName = null; - sRegionLabel = "DEFAULT"; - prepend = ""; - state = defaultState; - } - else - { - sRegionName = scene.RegionInfo.RegionName; - sRegionLabel = sRegionName; - prepend = sRegionName + "."; - state = null; - } - - //Read the config settings and set variables. - IConfig config = m_configSource.Configs["AutoBackupModule"]; - if (config == null) { - state = defaultState; //defaultState would be disabled too if the section doesn't exist. - m_log.Info ("[AUTO BACKUP]: Region " + sRegionLabel + " is NOT AutoBackup enabled."); - return state; - } - - bool tmpEnabled = config.GetBoolean (prepend + "AutoBackup", defaultState.GetEnabled()); - if(state == null && tmpEnabled != defaultState.GetEnabled()) //Varies from default state - { - state = new AutoBackupModuleState(); - } - - if(state != null) - { - state.SetEnabled (tmpEnabled); - } - - //If you don't want AutoBackup, we stop. - if ((state == null && !defaultState.GetEnabled()) || (state != null && !state.GetEnabled ())) { - m_log.Info ("[AUTO BACKUP]: Region " + sRegionLabel + " is NOT AutoBackup 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 = config.GetDouble (prepend + "AutoBackupInterval", defaultState.GetIntervalMinutes()) * 60000.0; - if(state == null && interval != defaultState.GetIntervalMinutes() * 60000.0) - { - state = new AutoBackupModuleState(); - } - - if (timers.ContainsKey (interval)) { - if(state != null) - state.SetTimer (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.SetEnabled (false); - return state; - } - if(state == null) - state = new AutoBackupModuleState(); - Timer tim = new Timer (interval); - state.SetTimer (tim); - //Milliseconds -> minutes - timers.Add (interval, tim); - tim.Elapsed += HandleElapsed; - tim.AutoReset = true; - tim.Start (); - //m_log.Debug("[AUTO BACKUP]: New timer for " + interval + " msec for region " + sRegionName); - } - - //Add the current region to the list of regions tied to this timer. - if(scene != null) - { - if(state != null) - { - if (timerMap.ContainsKey (state.GetTimer ())) { - timerMap[state.GetTimer ()].Add (scene); - } else { - List scns = new List (1); - scns.Add (scene); - timerMap.Add (state.GetTimer (), scns); - } - } - else - { - if(timerMap.ContainsKey(defaultState.GetTimer())) { - timerMap[defaultState.GetTimer()].Add(scene); - } else { - List scns = new List (1); - scns.Add(scene); - timerMap.Add(defaultState.GetTimer(), scns); - } - } - } - - bool tmpBusyCheck = config.GetBoolean (prepend + "AutoBackupBusyCheck", defaultState.GetBusyCheck()); - if(state == null && tmpBusyCheck != defaultState.GetBusyCheck()) - { - state = new AutoBackupModuleState(); - } - - if(state != null) - { - state.SetBusyCheck (tmpBusyCheck); - } - - //Set file naming algorithm - string stmpNamingType = config.GetString (prepend + "AutoBackupNaming", defaultState.GetNamingType().ToString()); - 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 != defaultState.GetNamingType()) - { - state = new AutoBackupModuleState(); - } - - if(state != null) - { - state.SetNamingType(tmpNamingType); - } - - string tmpScript = config.GetString (prepend + "AutoBackupScript", defaultState.GetScript()); - if(state == null && tmpScript != defaultState.GetScript()) - { - state = new AutoBackupModuleState(); - } - - if(state != null) - { - state.SetScript (tmpScript); - } - - string tmpBackupDir = config.GetString (prepend + "AutoBackupDir", "."); - if(state == null && tmpBackupDir != defaultState.GetBackupDir()) - { - state = new AutoBackupModuleState(); - } - - if(state != null) - { - state.SetBackupDir (tmpBackupDir); - //Let's give the user *one* convenience and auto-mkdir - if (state.GetBackupDir () != ".") { - try { - DirectoryInfo dirinfo = new DirectoryInfo (state.GetBackupDir ()); - if (!dirinfo.Exists) { - dirinfo.Create (); - } - } catch (Exception e) { - m_log.Warn ("BAD NEWS. You won't be able to save backups to directory " + state.GetBackupDir () + " because it doesn't exist or there's a permissions issue with it. Here's the exception.", e); - } - } - } - - return state; - } - - 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... - if (m_closed) - return; - bool heuristicsRun = false; - bool heuristicsPassed = false; - if (!timerMap.ContainsKey ((Timer)sender)) { - m_log.Debug ("Code-up error: timerMap doesn't contain timer " + sender.ToString ()); - } - - List tmap = timerMap[(Timer)sender]; - if(tmap != null && tmap.Count > 0) - foreach (IScene scene in tmap) { - AutoBackupModuleState state = states[scene]; - bool heuristics = state.GetBusyCheck (); - - //Fast path: heuristics are on; already ran em; and sim is fine; OR, no heuristics for the region. - if ((heuristics && heuristicsRun && heuristicsPassed) || !heuristics) { - doRegionBackup (scene); - //Heuristics are on; ran but we're too busy -- keep going. Maybe another region will have heuristics off! - } else if (heuristics && heuristicsRun && !heuristicsPassed) { - 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 = RunHeuristics (scene); - heuristicsRun = true; - if (!heuristicsPassed) { - m_log.Info ("[AUTO BACKUP]: Heuristics: too busy to backup " + scene.RegionInfo.RegionName + " right now."); - continue; - } - doRegionBackup (scene); - } - } - } - - 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.ToString ()); - return; - } - - AutoBackupModuleState state = states[scene]; - IRegionArchiverModule iram = scene.RequestModuleInterface (); - string savePath = BuildOarPath (scene.RegionInfo.RegionName, state.GetBackupDir (), state.GetNamingType ()); - //m_log.Debug("[AUTO BACKUP]: savePath = " + savePath); - if (savePath == null) { - m_log.Warn ("[AUTO BACKUP]: savePath is null in HandleElapsed"); - return; - } - iram.ArchiveRegion (savePath, null); - ExecuteScript (state.GetScript (), savePath); - } - - string IRegionModuleBase.Name { - get { return "AutoBackupModule"; } - } - - Type IRegionModuleBase.ReplaceableInterface { - get { return null; } - } - - #endregion - #region ISharedRegionModule implementation - void ISharedRegionModule.PostInitialise () - { - //I don't care right now. - } - - #endregion - - //Is this even needed? - public bool IsSharedModule { - get { return true; } - } - - private string BuildOarPath (string regionName, string baseDir, NamingType naming) - { - FileInfo path = null; - switch (naming) { - case NamingType.OVERWRITE: - path = new FileInfo (baseDir + Path.DirectorySeparatorChar + regionName); - return path.FullName; - case NamingType.TIME: - path = new FileInfo (baseDir + Path.DirectorySeparatorChar + regionName + GetTimeString () + ".oar"); - return path.FullName; - case NamingType.SEQUENTIAL: - path = new FileInfo (GetNextFile (baseDir, regionName)); - return path.FullName; - default: - m_log.Warn ("VERY BAD: Unhandled case element " + naming.ToString ()); - break; - } - - return path.FullName; - } - - //Welcome to the TIME STRING. 4 CORNER INTEGERS, CUBES 4 QUAD MEMORY -- No 1 Integer God. - //(Terrible reference to ) - //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? Ugh. Almost makes TOO much sense. - private 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; - } - - //Get the next logical file name - //I really shouldn't put fields here, but for now.... ;) - private string m_dirName = null; - private string m_regionName = null; - private string GetNextFile (string dirName, string regionName) - { - FileInfo uniqueFile = null; - m_dirName = dirName; - m_regionName = regionName; - long biggestExistingFile = HalfIntervalMaximize (1, FileExistsTest); - biggestExistingFile++; - //We don't want to overwrite the biggest existing file; we want to write to the NEXT biggest. - uniqueFile = new FileInfo (m_dirName + Path.DirectorySeparatorChar + m_regionName + "_" + biggestExistingFile + ".oar"); - if (uniqueFile.Exists) { - //Congratulations, your strange deletion patterns fooled my half-interval search into picking an existing file! - //Now you get to pay the performance cost :) - uniqueFile = UniqueFileSearchLinear (biggestExistingFile); - } - - return uniqueFile.FullName; - } - - /* - * 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 RunTimeDilationHeuristic (region) && 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". - * Return value of "true" ==> not too busy. Return value of "false" ==> too busy! - * */ - private bool RunTimeDilationHeuristic (IScene region) - { - string regionName = region.RegionInfo.RegionName; - return region.TimeDilation >= 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". - * Return value of "true" ==> not too busy. Return value of "false" ==> too busy! - * */ - 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 () <= 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!"); - return true; - //Non-obstructionist safest answer... - } - } - - private void ExecuteScript (string scriptName, string savePath) - { - //Fast path out - 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); - } - } - - void HandleProcErrorDataReceived (object sender, DataReceivedEventArgs e) - { - m_log.Warn ("ExecuteScript hook " + ((Process)sender).ProcessName + " is yacking on stderr: " + e.Data); - } - - private void StopAllTimers () - { - foreach (Timer t in timerMap.Keys) { - t.Close (); - } - m_closed = true; - } - - /* Find the largest value for which the predicate returns true. - * We use a bisection algorithm (half interval) to make the algorithm scalable. - * The worst-case complexity is about O(log(n)^2) in practice. - * Only for extremely small values (under 10) do you notice it taking more iterations than a linear search. - * The number of predicate invocations only hits a few hundred when the maximized value - * is in the tens of millions, so prepare for the predicate to be invoked between 10 and 100 times. - * And of course it is fantastic with powers of 2, which are densely packed in values under 100 anyway. - * The Predicate parameter must be a function that accepts a long and returns a bool. - * */ - public long HalfIntervalMaximize (long start, Predicate pred) - { - long prev = start, curr = start, biggest = 0; - - if (start < 0) - throw new IndexOutOfRangeException ("Start value for HalfIntervalMaximize must be non-negative"); - - do { - if (pred (curr)) { - if (curr > biggest) { - biggest = curr; - } - prev = curr; - if (curr == 0) { - //Special case because 0 * 2 = 0 :) - curr = 1; - } else { - //Look deeper - curr *= 2; - } - } else { - // We went too far, back off halfway - curr = (curr + prev) / 2; - } - } while (curr - prev > 0); - - return biggest; - } - - public bool FileExistsTest (long num) - { - FileInfo test = new FileInfo (m_dirName + Path.DirectorySeparatorChar + m_regionName + "_" + num + ".oar"); - return test.Exists; - } - - - //Very slow, hence why we try the HalfIntervalMaximize first! - public FileInfo UniqueFileSearchLinear (long start) - { - long l = start; - FileInfo retval = null; - do { - retval = new FileInfo (m_dirName + Path.DirectorySeparatorChar + m_regionName + "_" + (l++) + ".oar"); - } while (retval.Exists); - - return retval; - } - } - -} - +#pragma warning disable 1587 +/// +/// 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.Globalization; +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; + +/// +/// 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: . +/// 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. +/// + +namespace OpenSim.Region.OptionalModules.World.AutoBackup +{ + public enum NamingType + { + Time, + Sequential, + Overwrite + } + + public class AutoBackupModule : ISharedRegionModule + { + private static readonly ILog m_log = + LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + /// True means IRegionModuleBase.Close() was called on us, and we should stop operation ASAP. + /// Used to prevent elapsing timers after Close() is called from trying to start an autobackup while the sim is shutting down. + private readonly AutoBackupModuleState m_defaultState = new AutoBackupModuleState(); + + /// 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 + 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 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; + + public bool IsSharedModule + { + get { return true; } + } + + #region ISharedRegionModule Members + + string IRegionModuleBase.Name + { + get { return "AutoBackupModule"; } + } + + Type IRegionModuleBase.ReplaceableInterface + { + get { return null; } + } + + 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()); + } + + 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(); + } + + void IRegionModuleBase.AddRegion(Scene scene) + { + /// NO-OP. Wait for the region to be loaded. + } + + 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); + } + } + + 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())); + } + + void ISharedRegionModule.PostInitialise() + { + /// I don't care right now. + } + + #endregion + + 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 config = this.m_configSource.Configs["AutoBackupModule"]; + if (config == null) + { + /// defaultState would be disabled too if the section doesn't exist. + state = this.m_defaultState; + m_log.Info("[AUTO BACKUP]: Region " + sRegionLabel + " is NOT AutoBackup enabled."); + return state; + } + + bool tmpEnabled = config.GetBoolean(prepend + "AutoBackup", this.m_defaultState.Enabled); + 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)) + { + m_log.Info("[AUTO BACKUP]: Region " + sRegionLabel + " is NOT AutoBackup 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 = + config.GetDouble(prepend + "AutoBackupInterval", this.m_defaultState.IntervalMinutes)* + 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 = config.GetBoolean(prepend + "AutoBackupBusyCheck", + this.m_defaultState.BusyCheck); + if (state == null && tmpBusyCheck != this.m_defaultState.BusyCheck) + { + state = new AutoBackupModuleState(); + } + + if (state != null) + { + state.BusyCheck = tmpBusyCheck; + } + + /// Set file naming algorithm + string stmpNamingType = config.GetString(prepend + "AutoBackupNaming", + this.m_defaultState.NamingType.ToString()); + 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 = config.GetString(prepend + "AutoBackupScript", + this.m_defaultState.Script); + if (state == null && tmpScript != this.m_defaultState.Script) + { + state = new AutoBackupModuleState(); + } + + if (state != null) + { + state.Script = tmpScript; + } + + string tmpBackupDir = config.GetString(prepend + "AutoBackupDir", "."); + if (state == null && tmpBackupDir != this.m_defaultState.BackupDir) + { + state = new AutoBackupModuleState(); + } + + if (state != null) + { + state.BackupDir = tmpBackupDir; + /// Let's give the user *one* 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; + } + + 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... + 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); + } + } + } + } + + 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); + /// m_log.Debug("[AUTO BACKUP]: savePath = " + savePath); + if (savePath == null) + { + m_log.Warn("[AUTO BACKUP]: savePath is null in HandleElapsed"); + return; + } + iram.ArchiveRegion(savePath, Guid.NewGuid(), null); + ExecuteScript(state.Script, savePath); + } + + /// 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? Ugh. Almost makes TOO much sense. + 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". + /// Return value of "true" ==> not too busy. Return value of "false" ==> too busy! + /// + 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". + /// Return value of "true" ==> not too busy. Return value of "false" ==> too busy! + /// + 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... + } + } + + private static void ExecuteScript(string scriptName, string savePath) + { + //Fast path out + 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); + } + } + + private static void HandleProcErrorDataReceived(object sender, DataReceivedEventArgs e) + { + m_log.Warn("ExecuteScript hook " + ((Process) sender).ProcessName + + " is yacking on stderr: " + e.Data); + } + + private void StopAllTimers() + { + foreach (Timer t in this.m_timerMap.Keys) + { + t.Close(); + } + this.m_closed = true; + } + + 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; + } + + 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; + } + + 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..1b348af5eb --- /dev/null +++ b/OpenSim/Region/OptionalModules/World/AutoBackup/AutoBackupModuleState.cs @@ -0,0 +1,109 @@ +#pragma warning disable 1587 +/// +/// 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. +/// + +namespace OpenSim.Region.OptionalModules.World.AutoBackup +{ + /// AutoBackupModuleState: Auto-Backup state for one region (scene). + public class AutoBackupModuleState + { + public AutoBackupModuleState() + { + this.Enabled = false; + this.BackupDir = "."; + this.BusyCheck = true; + this.Timer = null; + this.NamingType = NamingType.Time; + this.Script = null; + } + + 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 96ffb7ea6b..db4bb6ae8f 100644 --- a/bin/OpenSimDefaults.ini +++ b/bin/OpenSimDefaults.ini @@ -1277,6 +1277,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"