AutoBackupModule.cs 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000
  1. /*
  2. * Copyright (c) Contributors, http://opensimulator.org/
  3. * See CONTRIBUTORS.TXT for a full list of copyright holders.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. * * Redistributions of source code must retain the above copyright
  8. * notice, this list of conditions and the following disclaimer.
  9. * * Redistributions in binary form must reproduce the above copyright
  10. * notice, this list of conditions and the following disclaimer in the
  11. * documentation and/or other materials provided with the distribution.
  12. * * Neither the name of the OpenSimulator Project nor the
  13. * names of its contributors may be used to endorse or promote products
  14. * derived from this software without specific prior written permission.
  15. *
  16. * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
  17. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19. * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
  20. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. */
  27. using System;
  28. using System.Collections.Generic;
  29. using System.Diagnostics;
  30. using System.IO;
  31. using System.Reflection;
  32. using System.Timers;
  33. using System.Text.RegularExpressions;
  34. using log4net;
  35. using Mono.Addins;
  36. using Nini.Config;
  37. using OpenSim.Framework;
  38. using OpenSim.Region.Framework.Interfaces;
  39. using OpenSim.Region.Framework.Scenes;
  40. namespace OpenSim.Region.OptionalModules.World.AutoBackup
  41. {
  42. /// <summary>
  43. /// Choose between ways of naming the backup files that are generated.
  44. /// </summary>
  45. /// <remarks>Time: OARs are named by a timestamp.
  46. /// Sequential: OARs are named by counting (Region_1.oar, Region_2.oar, etc.)
  47. /// Overwrite: Only one file per region is created; it's overwritten each time a backup is made.</remarks>
  48. public enum NamingType
  49. {
  50. Time,
  51. Sequential,
  52. Overwrite
  53. }
  54. ///<summary>
  55. /// AutoBackupModule: save OAR region backups to disk periodically
  56. /// </summary>
  57. /// <remarks>
  58. /// Config Settings Documentation.
  59. /// Each configuration setting can be specified in two places: OpenSim.ini or Regions.ini.
  60. /// If specified in Regions.ini, the settings should be within the region's section name.
  61. /// If specified in OpenSim.ini, the settings should be within the [AutoBackupModule] section.
  62. /// Region-specific settings take precedence.
  63. ///
  64. /// AutoBackupModuleEnabled: True/False. Default: False. If True, use the auto backup module. This setting does not support per-region basis.
  65. /// All other settings under [AutoBackupModule] are ignored if AutoBackupModuleEnabled is false, even per-region settings!
  66. /// AutoBackup: True/False. Default: False. If True, activate auto backup functionality.
  67. /// This is the only required option for enabling auto-backup; the other options have sane defaults.
  68. /// If False for a particular region, the auto-backup module becomes a no-op for the region, and all other AutoBackup* settings are ignored.
  69. /// If False globally (the default), only regions that specifically override it in Regions.ini will get AutoBackup functionality.
  70. /// AutoBackupInterval: Double, non-negative value. Default: 720 (12 hours).
  71. /// The number of minutes between each backup attempt.
  72. /// If a negative or zero value is given, it is equivalent to setting AutoBackup = False.
  73. /// AutoBackupBusyCheck: True/False. Default: True.
  74. /// If True, we will only take an auto-backup if a set of conditions are met.
  75. /// These conditions are heuristics to try and avoid taking a backup when the sim is busy.
  76. /// AutoBackupSkipAssets
  77. /// If true, assets are not saved to the oar file. Considerably reduces impact on simulator when backing up. Intended for when assets db is backed up separately
  78. /// AutoBackupKeepFilesForDays
  79. /// Backup files older than this value (in days) are deleted during the current backup process, 0 will disable this and keep all backup files indefinitely
  80. /// AutoBackupScript: String. Default: not specified (disabled).
  81. /// File path to an executable script or binary to run when an automatic backup is taken.
  82. /// The file should really be (Windows) an .exe or .bat, or (Linux/Mac) a shell script or binary.
  83. /// Trying to "run" directories, or things with weird file associations on Win32, might cause unexpected results!
  84. /// argv[1] of the executed file/script will be the file name of the generated OAR.
  85. /// If the process can't be spawned for some reason (file not found, no execute permission, etc), write a warning to the console.
  86. /// AutoBackupNaming: string. Default: Time.
  87. /// One of three strings (case insensitive):
  88. /// "Time": Current timestamp is appended to file name. An existing file will never be overwritten.
  89. /// "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.
  90. /// "Overwrite": Always save to file named "${AutoBackupDir}/RegionName.oar", even if we have to overwrite an existing file.
  91. /// AutoBackupDir: String. Default: "." (the current directory).
  92. /// A directory (absolute or relative) where backups should be saved.
  93. /// AutoBackupDilationThreshold: float. Default: 0.5. Lower bound on time dilation required for BusyCheck heuristics to pass.
  94. /// If the time dilation is below this value, don't take a backup right now.
  95. /// AutoBackupAgentThreshold: int. Default: 10. Upper bound on # of agents in region required for BusyCheck heuristics to pass.
  96. /// If the number of agents is greater than this value, don't take a backup right now
  97. /// Save memory by setting low initial capacities. Minimizes impact in common cases of all regions using same interval, and instances hosting 1 ~ 4 regions.
  98. /// Also helps if you don't want AutoBackup at all.
  99. /// </remarks>
  100. [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "AutoBackupModule")]
  101. public class AutoBackupModule : ISharedRegionModule
  102. {
  103. private static readonly ILog m_log =
  104. LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
  105. private readonly Dictionary<Guid, IScene> m_pendingSaves = new Dictionary<Guid, IScene>(1);
  106. private readonly AutoBackupModuleState m_defaultState = new AutoBackupModuleState();
  107. private readonly Dictionary<IScene, AutoBackupModuleState> m_states =
  108. new Dictionary<IScene, AutoBackupModuleState>(1);
  109. private readonly Dictionary<Timer, List<IScene>> m_timerMap =
  110. new Dictionary<Timer, List<IScene>>(1);
  111. private readonly Dictionary<double, Timer> m_timers = new Dictionary<double, Timer>(1);
  112. private delegate T DefaultGetter<T>(string settingName, T defaultValue);
  113. private bool m_enabled;
  114. /// <summary>
  115. /// Whether the shared module should be enabled at all. NOT the same as m_Enabled in AutoBackupModuleState!
  116. /// </summary>
  117. private bool m_closed;
  118. private IConfigSource m_configSource;
  119. /// <summary>
  120. /// Required by framework.
  121. /// </summary>
  122. public bool IsSharedModule
  123. {
  124. get { return true; }
  125. }
  126. #region ISharedRegionModule Members
  127. /// <summary>
  128. /// Identifies the module to the system.
  129. /// </summary>
  130. string IRegionModuleBase.Name
  131. {
  132. get { return "AutoBackupModule"; }
  133. }
  134. /// <summary>
  135. /// We don't implement an interface, this is a single-use module.
  136. /// </summary>
  137. Type IRegionModuleBase.ReplaceableInterface
  138. {
  139. get { return null; }
  140. }
  141. /// <summary>
  142. /// Called once in the lifetime of the module at startup.
  143. /// </summary>
  144. /// <param name="source">The input config source for OpenSim.ini.</param>
  145. void IRegionModuleBase.Initialise(IConfigSource source)
  146. {
  147. // Determine if we have been enabled at all in OpenSim.ini -- this is part and parcel of being an optional module
  148. this.m_configSource = source;
  149. IConfig moduleConfig = source.Configs["AutoBackupModule"];
  150. if (moduleConfig == null)
  151. {
  152. this.m_enabled = false;
  153. return;
  154. }
  155. else
  156. {
  157. this.m_enabled = moduleConfig.GetBoolean("AutoBackupModuleEnabled", false);
  158. if (this.m_enabled)
  159. {
  160. m_log.Info("[AUTO BACKUP]: AutoBackupModule enabled");
  161. }
  162. else
  163. {
  164. return;
  165. }
  166. }
  167. Timer defTimer = new Timer(43200000);
  168. this.m_defaultState.Timer = defTimer;
  169. this.m_timers.Add(43200000, defTimer);
  170. defTimer.Elapsed += this.HandleElapsed;
  171. defTimer.AutoReset = true;
  172. defTimer.Start();
  173. AutoBackupModuleState abms = this.ParseConfig(null, true);
  174. m_log.Debug("[AUTO BACKUP]: Here is the default config:");
  175. m_log.Debug(abms.ToString());
  176. }
  177. /// <summary>
  178. /// Called once at de-init (sim shutting down).
  179. /// </summary>
  180. void IRegionModuleBase.Close()
  181. {
  182. if (!this.m_enabled)
  183. {
  184. return;
  185. }
  186. // We don't want any timers firing while the sim's coming down; strange things may happen.
  187. this.StopAllTimers();
  188. }
  189. /// <summary>
  190. /// Currently a no-op for AutoBackup because we have to wait for region to be fully loaded.
  191. /// </summary>
  192. /// <param name="scene"></param>
  193. void IRegionModuleBase.AddRegion(Scene scene)
  194. {
  195. }
  196. /// <summary>
  197. /// Here we just clean up some resources and stop the OAR backup (if any) for the given scene.
  198. /// </summary>
  199. /// <param name="scene">The scene (region) to stop performing AutoBackup on.</param>
  200. void IRegionModuleBase.RemoveRegion(Scene scene)
  201. {
  202. if (!this.m_enabled)
  203. {
  204. return;
  205. }
  206. if (this.m_states.ContainsKey(scene))
  207. {
  208. AutoBackupModuleState abms = this.m_states[scene];
  209. // Remove this scene out of the timer map list
  210. Timer timer = abms.Timer;
  211. List<IScene> list = this.m_timerMap[timer];
  212. list.Remove(scene);
  213. // Shut down the timer if this was the last scene for the timer
  214. if (list.Count == 0)
  215. {
  216. this.m_timerMap.Remove(timer);
  217. this.m_timers.Remove(timer.Interval);
  218. timer.Close();
  219. }
  220. this.m_states.Remove(scene);
  221. }
  222. }
  223. /// <summary>
  224. /// Most interesting/complex code paths in AutoBackup begin here.
  225. /// We read lots of Nini config, maybe set a timer, add members to state tracking Dictionaries, etc.
  226. /// </summary>
  227. /// <param name="scene">The scene to (possibly) perform AutoBackup on.</param>
  228. void IRegionModuleBase.RegionLoaded(Scene scene)
  229. {
  230. if (!this.m_enabled)
  231. {
  232. return;
  233. }
  234. // This really ought not to happen, but just in case, let's pretend it didn't...
  235. if (scene == null)
  236. {
  237. return;
  238. }
  239. AutoBackupModuleState abms = this.ParseConfig(scene, false);
  240. m_log.Debug("[AUTO BACKUP]: Config for " + scene.RegionInfo.RegionName);
  241. m_log.Debug((abms == null ? "DEFAULT" : abms.ToString()));
  242. m_states.Add(scene, abms);
  243. }
  244. /// <summary>
  245. /// Currently a no-op.
  246. /// </summary>
  247. void ISharedRegionModule.PostInitialise()
  248. {
  249. }
  250. #endregion
  251. /// <summary>
  252. /// Set up internal state for a given scene. Fairly complex code.
  253. /// When this method returns, we've started auto-backup timers, put members in Dictionaries, and created a State object for this scene.
  254. /// </summary>
  255. /// <param name="scene">The scene to look at.</param>
  256. /// <param name="parseDefault">Whether this call is intended to figure out what we consider the "default" config (applied to all regions unless overridden by per-region settings).</param>
  257. /// <returns>An AutoBackupModuleState contains most information you should need to know relevant to auto-backup, as applicable to a single region.</returns>
  258. private AutoBackupModuleState ParseConfig(IScene scene, bool parseDefault)
  259. {
  260. string sRegionName;
  261. string sRegionLabel;
  262. // string prepend;
  263. AutoBackupModuleState state;
  264. if (parseDefault)
  265. {
  266. sRegionName = null;
  267. sRegionLabel = "DEFAULT";
  268. // prepend = "";
  269. state = this.m_defaultState;
  270. }
  271. else
  272. {
  273. sRegionName = scene.RegionInfo.RegionName;
  274. sRegionLabel = sRegionName;
  275. // prepend = sRegionName + ".";
  276. state = null;
  277. }
  278. // Read the config settings and set variables.
  279. IConfig regionConfig = (scene != null ? scene.Config.Configs[sRegionName] : null);
  280. IConfig config = this.m_configSource.Configs["AutoBackupModule"];
  281. if (config == null)
  282. {
  283. // defaultState would be disabled too if the section doesn't exist.
  284. state = this.m_defaultState;
  285. return state;
  286. }
  287. bool tmpEnabled = ResolveBoolean("AutoBackup", this.m_defaultState.Enabled, config, regionConfig);
  288. if (state == null && tmpEnabled != this.m_defaultState.Enabled)
  289. //Varies from default state
  290. {
  291. state = new AutoBackupModuleState();
  292. }
  293. if (state != null)
  294. {
  295. state.Enabled = tmpEnabled;
  296. }
  297. // If you don't want AutoBackup, we stop.
  298. if ((state == null && !this.m_defaultState.Enabled) || (state != null && !state.Enabled))
  299. {
  300. return state;
  301. }
  302. else
  303. {
  304. m_log.Info("[AUTO BACKUP]: Region " + sRegionLabel + " is AutoBackup ENABLED.");
  305. }
  306. // Borrow an existing timer if one exists for the same interval; otherwise, make a new one.
  307. double interval =
  308. this.ResolveDouble("AutoBackupInterval", this.m_defaultState.IntervalMinutes,
  309. config, regionConfig) * 60000.0;
  310. if (state == null && interval != this.m_defaultState.IntervalMinutes * 60000.0)
  311. {
  312. state = new AutoBackupModuleState();
  313. }
  314. if (this.m_timers.ContainsKey(interval))
  315. {
  316. if (state != null)
  317. {
  318. state.Timer = this.m_timers[interval];
  319. }
  320. m_log.Debug("[AUTO BACKUP]: Reusing timer for " + interval + " msec for region " +
  321. sRegionLabel);
  322. }
  323. else
  324. {
  325. // 0 or negative interval == do nothing.
  326. if (interval <= 0.0 && state != null)
  327. {
  328. state.Enabled = false;
  329. return state;
  330. }
  331. if (state == null)
  332. {
  333. state = new AutoBackupModuleState();
  334. }
  335. Timer tim = new Timer(interval);
  336. state.Timer = tim;
  337. //Milliseconds -> minutes
  338. this.m_timers.Add(interval, tim);
  339. tim.Elapsed += this.HandleElapsed;
  340. tim.AutoReset = true;
  341. tim.Start();
  342. }
  343. // Add the current region to the list of regions tied to this timer.
  344. if (scene != null)
  345. {
  346. if (state != null)
  347. {
  348. if (this.m_timerMap.ContainsKey(state.Timer))
  349. {
  350. this.m_timerMap[state.Timer].Add(scene);
  351. }
  352. else
  353. {
  354. List<IScene> scns = new List<IScene>(1);
  355. scns.Add(scene);
  356. this.m_timerMap.Add(state.Timer, scns);
  357. }
  358. }
  359. else
  360. {
  361. if (this.m_timerMap.ContainsKey(this.m_defaultState.Timer))
  362. {
  363. this.m_timerMap[this.m_defaultState.Timer].Add(scene);
  364. }
  365. else
  366. {
  367. List<IScene> scns = new List<IScene>(1);
  368. scns.Add(scene);
  369. this.m_timerMap.Add(this.m_defaultState.Timer, scns);
  370. }
  371. }
  372. }
  373. bool tmpBusyCheck = ResolveBoolean("AutoBackupBusyCheck",
  374. this.m_defaultState.BusyCheck, config, regionConfig);
  375. if (state == null && tmpBusyCheck != this.m_defaultState.BusyCheck)
  376. {
  377. state = new AutoBackupModuleState();
  378. }
  379. if (state != null)
  380. {
  381. state.BusyCheck = tmpBusyCheck;
  382. }
  383. // Included Option To Skip Assets
  384. bool tmpSkipAssets = ResolveBoolean("AutoBackupSkipAssets",
  385. this.m_defaultState.SkipAssets, config, regionConfig);
  386. if (state == null && tmpSkipAssets != this.m_defaultState.SkipAssets)
  387. {
  388. state = new AutoBackupModuleState();
  389. }
  390. if (state != null)
  391. {
  392. state.SkipAssets = tmpSkipAssets;
  393. }
  394. // How long to keep backup files in days, 0 Disables this feature
  395. int tmpKeepFilesForDays = ResolveInt("AutoBackupKeepFilesForDays",
  396. this.m_defaultState.KeepFilesForDays, config, regionConfig);
  397. if (state == null && tmpKeepFilesForDays != this.m_defaultState.KeepFilesForDays)
  398. {
  399. state = new AutoBackupModuleState();
  400. }
  401. if (state != null)
  402. {
  403. state.KeepFilesForDays = tmpKeepFilesForDays;
  404. }
  405. // Set file naming algorithm
  406. string stmpNamingType = ResolveString("AutoBackupNaming",
  407. this.m_defaultState.NamingType.ToString(), config, regionConfig);
  408. NamingType tmpNamingType;
  409. if (stmpNamingType.Equals("Time", StringComparison.CurrentCultureIgnoreCase))
  410. {
  411. tmpNamingType = NamingType.Time;
  412. }
  413. else if (stmpNamingType.Equals("Sequential", StringComparison.CurrentCultureIgnoreCase))
  414. {
  415. tmpNamingType = NamingType.Sequential;
  416. }
  417. else if (stmpNamingType.Equals("Overwrite", StringComparison.CurrentCultureIgnoreCase))
  418. {
  419. tmpNamingType = NamingType.Overwrite;
  420. }
  421. else
  422. {
  423. m_log.Warn("Unknown naming type specified for region " + sRegionLabel + ": " +
  424. stmpNamingType);
  425. tmpNamingType = NamingType.Time;
  426. }
  427. if (state == null && tmpNamingType != this.m_defaultState.NamingType)
  428. {
  429. state = new AutoBackupModuleState();
  430. }
  431. if (state != null)
  432. {
  433. state.NamingType = tmpNamingType;
  434. }
  435. string tmpScript = ResolveString("AutoBackupScript",
  436. this.m_defaultState.Script, config, regionConfig);
  437. if (state == null && tmpScript != this.m_defaultState.Script)
  438. {
  439. state = new AutoBackupModuleState();
  440. }
  441. if (state != null)
  442. {
  443. state.Script = tmpScript;
  444. }
  445. string tmpBackupDir = ResolveString("AutoBackupDir", ".", config, regionConfig);
  446. if (state == null && tmpBackupDir != this.m_defaultState.BackupDir)
  447. {
  448. state = new AutoBackupModuleState();
  449. }
  450. if (state != null)
  451. {
  452. state.BackupDir = tmpBackupDir;
  453. // Let's give the user some convenience and auto-mkdir
  454. if (state.BackupDir != ".")
  455. {
  456. try
  457. {
  458. DirectoryInfo dirinfo = new DirectoryInfo(state.BackupDir);
  459. if (!dirinfo.Exists)
  460. {
  461. dirinfo.Create();
  462. }
  463. }
  464. catch (Exception e)
  465. {
  466. m_log.Warn(
  467. "[AUTO BACKUP]: BAD NEWS. You won't be able to save backups to directory " +
  468. state.BackupDir +
  469. " because it doesn't exist or there's a permissions issue with it. Here's the exception.",
  470. e);
  471. }
  472. }
  473. }
  474. if(state == null)
  475. return m_defaultState;
  476. return state;
  477. }
  478. /// <summary>
  479. /// Helper function for ParseConfig.
  480. /// </summary>
  481. /// <param name="settingName"></param>
  482. /// <param name="defaultValue"></param>
  483. /// <param name="global"></param>
  484. /// <param name="local"></param>
  485. /// <returns></returns>
  486. private bool ResolveBoolean(string settingName, bool defaultValue, IConfig global, IConfig local)
  487. {
  488. if(local != null)
  489. {
  490. return local.GetBoolean(settingName, global.GetBoolean(settingName, defaultValue));
  491. }
  492. else
  493. {
  494. return global.GetBoolean(settingName, defaultValue);
  495. }
  496. }
  497. /// <summary>
  498. /// Helper function for ParseConfig.
  499. /// </summary>
  500. /// <param name="settingName"></param>
  501. /// <param name="defaultValue"></param>
  502. /// <param name="global"></param>
  503. /// <param name="local"></param>
  504. /// <returns></returns>
  505. private double ResolveDouble(string settingName, double defaultValue, IConfig global, IConfig local)
  506. {
  507. if (local != null)
  508. {
  509. return local.GetDouble(settingName, global.GetDouble(settingName, defaultValue));
  510. }
  511. else
  512. {
  513. return global.GetDouble(settingName, defaultValue);
  514. }
  515. }
  516. /// <summary>
  517. /// Helper function for ParseConfig.
  518. /// </summary>
  519. /// <param name="settingName"></param>
  520. /// <param name="defaultValue"></param>
  521. /// <param name="global"></param>
  522. /// <param name="local"></param>
  523. /// <returns></returns>
  524. private int ResolveInt(string settingName, int defaultValue, IConfig global, IConfig local)
  525. {
  526. if (local != null)
  527. {
  528. return local.GetInt(settingName, global.GetInt(settingName, defaultValue));
  529. }
  530. else
  531. {
  532. return global.GetInt(settingName, defaultValue);
  533. }
  534. }
  535. /// <summary>
  536. /// Helper function for ParseConfig.
  537. /// </summary>
  538. /// <param name="settingName"></param>
  539. /// <param name="defaultValue"></param>
  540. /// <param name="global"></param>
  541. /// <param name="local"></param>
  542. /// <returns></returns>
  543. private string ResolveString(string settingName, string defaultValue, IConfig global, IConfig local)
  544. {
  545. if (local != null)
  546. {
  547. return local.GetString(settingName, global.GetString(settingName, defaultValue));
  548. }
  549. else
  550. {
  551. return global.GetString(settingName, defaultValue);
  552. }
  553. }
  554. /// <summary>
  555. /// Called when any auto-backup timer expires. This starts the code path for actually performing a backup.
  556. /// </summary>
  557. /// <param name="sender"></param>
  558. /// <param name="e"></param>
  559. private void HandleElapsed(object sender, ElapsedEventArgs e)
  560. {
  561. // TODO: heuristic thresholds are per-region, so we should probably run heuristics once per region
  562. // XXX: Running heuristics once per region could add undue performance penalty for something that's supposed to
  563. // check whether the region is too busy! Especially on sims with LOTS of regions.
  564. // Alternative: make heuristics thresholds global to the module rather than per-region. Less flexible,
  565. // but would allow us to be semantically correct while being easier on perf.
  566. // Alternative 2: Run heuristics once per unique set of heuristics threshold parameters! Ay yi yi...
  567. // Alternative 3: Don't support per-region heuristics at all; just accept them as a global only parameter.
  568. // Since this is pretty experimental, I haven't decided which alternative makes the most sense.
  569. if (this.m_closed)
  570. {
  571. return;
  572. }
  573. bool heuristicsRun = false;
  574. bool heuristicsPassed = false;
  575. if (!this.m_timerMap.ContainsKey((Timer) sender))
  576. {
  577. m_log.Debug("[AUTO BACKUP]: Code-up error: timerMap doesn't contain timer " + sender);
  578. }
  579. List<IScene> tmap = this.m_timerMap[(Timer) sender];
  580. if (tmap != null && tmap.Count > 0)
  581. {
  582. foreach (IScene scene in tmap)
  583. {
  584. AutoBackupModuleState state = this.m_states[scene];
  585. bool heuristics = state.BusyCheck;
  586. // Fast path: heuristics are on; already ran em; and sim is fine; OR, no heuristics for the region.
  587. if ((heuristics && heuristicsRun && heuristicsPassed) || !heuristics)
  588. {
  589. this.DoRegionBackup(scene);
  590. // Heuristics are on; ran but we're too busy -- keep going. Maybe another region will have heuristics off!
  591. }
  592. else if (heuristicsRun)
  593. {
  594. m_log.Info("[AUTO BACKUP]: Heuristics: too busy to backup " +
  595. scene.RegionInfo.RegionName + " right now.");
  596. continue;
  597. // Logical Deduction: heuristics are on but haven't been run
  598. }
  599. else
  600. {
  601. heuristicsPassed = this.RunHeuristics(scene);
  602. heuristicsRun = true;
  603. if (!heuristicsPassed)
  604. {
  605. m_log.Info("[AUTO BACKUP]: Heuristics: too busy to backup " +
  606. scene.RegionInfo.RegionName + " right now.");
  607. continue;
  608. }
  609. this.DoRegionBackup(scene);
  610. }
  611. // Remove Old Backups
  612. this.RemoveOldFiles(state);
  613. }
  614. }
  615. }
  616. /// <summary>
  617. /// Save an OAR, register for the callback for when it's done, then call the AutoBackupScript (if applicable).
  618. /// </summary>
  619. /// <param name="scene"></param>
  620. private void DoRegionBackup(IScene scene)
  621. {
  622. if (!scene.Ready)
  623. {
  624. // We won't backup a region that isn't operating normally.
  625. m_log.Warn("[AUTO BACKUP]: Not backing up region " + scene.RegionInfo.RegionName +
  626. " because its status is " + scene.RegionStatus);
  627. return;
  628. }
  629. AutoBackupModuleState state = this.m_states[scene];
  630. IRegionArchiverModule iram = scene.RequestModuleInterface<IRegionArchiverModule>();
  631. string savePath = BuildOarPath(scene.RegionInfo.RegionName,
  632. state.BackupDir,
  633. state.NamingType);
  634. if (savePath == null)
  635. {
  636. m_log.Warn("[AUTO BACKUP]: savePath is null in HandleElapsed");
  637. return;
  638. }
  639. Guid guid = Guid.NewGuid();
  640. m_pendingSaves.Add(guid, scene);
  641. state.LiveRequests.Add(guid, savePath);
  642. ((Scene) scene).EventManager.OnOarFileSaved += new EventManager.OarFileSaved(EventManager_OnOarFileSaved);
  643. m_log.Info("[AUTO BACKUP]: Backing up region " + scene.RegionInfo.RegionName);
  644. // Must pass options, even if dictionary is empty!
  645. Dictionary<string, object> options = new Dictionary<string, object>();
  646. if (state.SkipAssets)
  647. options["noassets"] = true;
  648. iram.ArchiveRegion(savePath, guid, options);
  649. }
  650. // For the given state, remove backup files older than the states KeepFilesForDays property
  651. private void RemoveOldFiles(AutoBackupModuleState state)
  652. {
  653. // 0 Means Disabled, Keep Files Indefinitely
  654. if (state.KeepFilesForDays > 0)
  655. {
  656. string[] files = Directory.GetFiles(state.BackupDir, "*.oar");
  657. DateTime CuttOffDate = DateTime.Now.AddDays(0 - state.KeepFilesForDays);
  658. foreach (string file in files)
  659. {
  660. try
  661. {
  662. FileInfo fi = new FileInfo(file);
  663. if (fi.CreationTime < CuttOffDate)
  664. fi.Delete();
  665. }
  666. catch (Exception Ex)
  667. {
  668. m_log.Error("[AUTO BACKUP]: Error deleting old backup file '" + file + "': " + Ex.Message);
  669. }
  670. }
  671. }
  672. }
  673. /// <summary>
  674. /// Called by the Event Manager when the OnOarFileSaved event is fired.
  675. /// </summary>
  676. /// <param name="guid"></param>
  677. /// <param name="message"></param>
  678. void EventManager_OnOarFileSaved(Guid guid, string message)
  679. {
  680. // Ignore if the OAR save is being done by some other part of the system
  681. if (m_pendingSaves.ContainsKey(guid))
  682. {
  683. AutoBackupModuleState abms = m_states[(m_pendingSaves[guid])];
  684. ExecuteScript(abms.Script, abms.LiveRequests[guid]);
  685. m_pendingSaves.Remove(guid);
  686. abms.LiveRequests.Remove(guid);
  687. }
  688. }
  689. /// <summary>This format may turn out to be too unwieldy to keep...
  690. /// Besides, that's what ctimes are for. But then how do I name each file uniquely without using a GUID?
  691. /// Sequential numbers, right? We support those, too!</summary>
  692. private static string GetTimeString()
  693. {
  694. StringWriter sw = new StringWriter();
  695. sw.Write("_");
  696. DateTime now = DateTime.Now;
  697. sw.Write(now.Year);
  698. sw.Write("y_");
  699. sw.Write(now.Month);
  700. sw.Write("M_");
  701. sw.Write(now.Day);
  702. sw.Write("d_");
  703. sw.Write(now.Hour);
  704. sw.Write("h_");
  705. sw.Write(now.Minute);
  706. sw.Write("m_");
  707. sw.Write(now.Second);
  708. sw.Write("s");
  709. sw.Flush();
  710. string output = sw.ToString();
  711. sw.Close();
  712. return output;
  713. }
  714. /// <summary>Return value of true ==> not too busy; false ==> too busy to backup an OAR right now, or error.</summary>
  715. private bool RunHeuristics(IScene region)
  716. {
  717. try
  718. {
  719. return this.RunTimeDilationHeuristic(region) && this.RunAgentLimitHeuristic(region);
  720. }
  721. catch (Exception e)
  722. {
  723. m_log.Warn("[AUTO BACKUP]: Exception in RunHeuristics", e);
  724. return false;
  725. }
  726. }
  727. /// <summary>
  728. /// If the time dilation right at this instant is less than the threshold specified in AutoBackupDilationThreshold (default 0.5),
  729. /// then we return false and trip the busy heuristic's "too busy" path (i.e. don't save an OAR).
  730. /// AutoBackupDilationThreshold is a _LOWER BOUND_. Lower Time Dilation is bad, so if you go lower than our threshold, it's "too busy".
  731. /// </summary>
  732. /// <param name="region"></param>
  733. /// <returns>Returns true if we're not too busy; false means we've got worse time dilation than the threshold.</returns>
  734. private bool RunTimeDilationHeuristic(IScene region)
  735. {
  736. string regionName = region.RegionInfo.RegionName;
  737. return region.TimeDilation >=
  738. this.m_configSource.Configs["AutoBackupModule"].GetFloat(
  739. regionName + ".AutoBackupDilationThreshold", 0.5f);
  740. }
  741. /// <summary>
  742. /// If the root agent count right at this instant is less than the threshold specified in AutoBackupAgentThreshold (default 10),
  743. /// then we return false and trip the busy heuristic's "too busy" path (i.e., don't save an OAR).
  744. /// AutoBackupAgentThreshold is an _UPPER BOUND_. Higher Agent Count is bad, so if you go higher than our threshold, it's "too busy".
  745. /// </summary>
  746. /// <param name="region"></param>
  747. /// <returns>Returns true if we're not too busy; false means we've got more agents on the sim than the threshold.</returns>
  748. private bool RunAgentLimitHeuristic(IScene region)
  749. {
  750. string regionName = region.RegionInfo.RegionName;
  751. try
  752. {
  753. Scene scene = (Scene) region;
  754. // TODO: Why isn't GetRootAgentCount() a method in the IScene interface? Seems generally useful...
  755. return scene.GetRootAgentCount() <=
  756. this.m_configSource.Configs["AutoBackupModule"].GetInt(
  757. regionName + ".AutoBackupAgentThreshold", 10);
  758. }
  759. catch (InvalidCastException ice)
  760. {
  761. m_log.Debug(
  762. "[AUTO BACKUP]: I NEED MAINTENANCE: IScene is not a Scene; can't get root agent count!",
  763. ice);
  764. return true;
  765. // Non-obstructionist safest answer...
  766. }
  767. }
  768. /// <summary>
  769. /// Run the script or executable specified by the "AutoBackupScript" config setting.
  770. /// Of course this is a security risk if you let anyone modify OpenSim.ini and they want to run some nasty bash script.
  771. /// 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.
  772. /// </summary>
  773. /// <param name="scriptName"></param>
  774. /// <param name="savePath"></param>
  775. private static void ExecuteScript(string scriptName, string savePath)
  776. {
  777. // Do nothing if there's no script.
  778. if (scriptName == null || scriptName.Length <= 0)
  779. {
  780. return;
  781. }
  782. try
  783. {
  784. FileInfo fi = new FileInfo(scriptName);
  785. if (fi.Exists)
  786. {
  787. ProcessStartInfo psi = new ProcessStartInfo(scriptName);
  788. psi.Arguments = savePath;
  789. psi.CreateNoWindow = true;
  790. Process proc = Process.Start(psi);
  791. proc.ErrorDataReceived += HandleProcErrorDataReceived;
  792. }
  793. }
  794. catch (Exception e)
  795. {
  796. m_log.Warn(
  797. "Exception encountered when trying to run script for oar backup " + savePath, e);
  798. }
  799. }
  800. /// <summary>
  801. /// Called if a running script process writes to stderr.
  802. /// </summary>
  803. /// <param name="sender"></param>
  804. /// <param name="e"></param>
  805. private static void HandleProcErrorDataReceived(object sender, DataReceivedEventArgs e)
  806. {
  807. m_log.Warn("ExecuteScript hook " + ((Process) sender).ProcessName +
  808. " is yacking on stderr: " + e.Data);
  809. }
  810. /// <summary>
  811. /// Quickly stop all timers from firing.
  812. /// </summary>
  813. private void StopAllTimers()
  814. {
  815. foreach (Timer t in this.m_timerMap.Keys)
  816. {
  817. t.Close();
  818. }
  819. this.m_closed = true;
  820. }
  821. /// <summary>
  822. /// Determine the next unique filename by number, for "Sequential" AutoBackupNamingType.
  823. /// </summary>
  824. /// <param name="dirName"></param>
  825. /// <param name="regionName"></param>
  826. /// <returns></returns>
  827. private static string GetNextFile(string dirName, string regionName)
  828. {
  829. FileInfo uniqueFile = null;
  830. long biggestExistingFile = GetNextOarFileNumber(dirName, regionName);
  831. biggestExistingFile++;
  832. // We don't want to overwrite the biggest existing file; we want to write to the NEXT biggest.
  833. uniqueFile =
  834. new FileInfo(dirName + Path.DirectorySeparatorChar + regionName + "_" +
  835. biggestExistingFile + ".oar");
  836. return uniqueFile.FullName;
  837. }
  838. /// <summary>
  839. /// Top-level method for creating an absolute path to an OAR backup file based on what naming scheme the user wants.
  840. /// </summary>
  841. /// <param name="regionName">Name of the region to save.</param>
  842. /// <param name="baseDir">Absolute or relative path to the directory where the file should reside.</param>
  843. /// <param name="naming">The naming scheme for the file name.</param>
  844. /// <returns></returns>
  845. private static string BuildOarPath(string regionName, string baseDir, NamingType naming)
  846. {
  847. FileInfo path = null;
  848. switch (naming)
  849. {
  850. case NamingType.Overwrite:
  851. path = new FileInfo(baseDir + Path.DirectorySeparatorChar + regionName + ".oar");
  852. return path.FullName;
  853. case NamingType.Time:
  854. path =
  855. new FileInfo(baseDir + Path.DirectorySeparatorChar + regionName +
  856. GetTimeString() + ".oar");
  857. return path.FullName;
  858. case NamingType.Sequential:
  859. // All codepaths in GetNextFile should return a file name ending in .oar
  860. path = new FileInfo(GetNextFile(baseDir, regionName));
  861. return path.FullName;
  862. default:
  863. m_log.Warn("VERY BAD: Unhandled case element " + naming);
  864. break;
  865. }
  866. return null;
  867. }
  868. /// <summary>
  869. /// Helper function for Sequential file naming type (see BuildOarPath and GetNextFile).
  870. /// </summary>
  871. /// <param name="dirName"></param>
  872. /// <param name="regionName"></param>
  873. /// <returns></returns>
  874. private static long GetNextOarFileNumber(string dirName, string regionName)
  875. {
  876. long retval = 1;
  877. DirectoryInfo di = new DirectoryInfo(dirName);
  878. FileInfo[] fi = di.GetFiles(regionName, SearchOption.TopDirectoryOnly);
  879. Array.Sort(fi, (f1, f2) => StringComparer.CurrentCultureIgnoreCase.Compare(f1.Name, f2.Name));
  880. if (fi.LongLength > 0)
  881. {
  882. long subtract = 1L;
  883. bool worked = false;
  884. Regex reg = new Regex(regionName + "_([0-9])+" + ".oar");
  885. while (!worked && subtract <= fi.LongLength)
  886. {
  887. // Pick the file with the last natural ordering
  888. string biggestFileName = fi[fi.LongLength - subtract].Name;
  889. MatchCollection matches = reg.Matches(biggestFileName);
  890. long l = 1;
  891. if (matches.Count > 0 && matches[0].Groups.Count > 0)
  892. {
  893. try
  894. {
  895. long.TryParse(matches[0].Groups[1].Value, out l);
  896. retval = l;
  897. worked = true;
  898. }
  899. catch (FormatException fe)
  900. {
  901. m_log.Warn(
  902. "[AUTO BACKUP]: Error: Can't parse long value from file name to determine next OAR backup file number!",
  903. fe);
  904. subtract++;
  905. }
  906. }
  907. else
  908. {
  909. subtract++;
  910. }
  911. }
  912. }
  913. return retval;
  914. }
  915. }
  916. }