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