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