clockwerk-opensim – Blame information for rev 1

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