aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Region/OptionalModules/World
diff options
context:
space:
mode:
Diffstat (limited to 'OpenSim/Region/OptionalModules/World')
-rw-r--r--OpenSim/Region/OptionalModules/World/AutoBackup/AutoBackupModule.cs540
1 files changed, 540 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..ed21e41
--- /dev/null
+++ b/OpenSim/Region/OptionalModules/World/AutoBackup/AutoBackupModule.cs
@@ -0,0 +1,540 @@
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.IO;
30using System.Timers;
31using System.Diagnostics;
32using System.Reflection;
33using System.Collections.Generic;
34using log4net;
35using Nini;
36using Nini.Config;
37using OpenSim.Framework;
38using OpenSim.Region.Framework.Interfaces;
39
40
41/*
42 * Config Settings Documentation.
43 * EACH REGION in e.g. Regions/Regions.ini can have the following options:
44 * AutoBackup: True/False. Default: False. If True, activate auto backup functionality.
45 * This is the only required option for enabling auto-backup; the other options have sane defaults.
46 * If False, the auto-backup module becomes a no-op for the region, and all other AutoBackup* settings are ignored.
47 * AutoBackupInterval: Double, non-negative value. Default: 720 (12 hours).
48 * The number of minutes between each backup attempt.
49 * If a negative or zero value is given, it is equivalent to setting AutoBackup = False.
50 * AutoBackupBusyCheck: True/False. Default: True.
51 * If True, we will only take an auto-backup if a set of conditions are met.
52 * These conditions are heuristics to try and avoid taking a backup when the sim is busy.
53 * AutoBackupScript: String. Default: not specified (disabled).
54 * File path to an executable script or binary to run when an automatic backup is taken.
55 * The file should really be (Windows) an .exe or .bat, or (Linux/Mac) a shell script or binary.
56 * Trying to "run" directories, or things with weird file associations on Win32, might cause unexpected results!
57 * argv[1] of the executed file/script will be the file name of the generated OAR.
58 * If the process can't be spawned for some reason (file not found, no execute permission, etc), write a warning to the console.
59 * AutoBackupNaming: string. Default: Time.
60 * One of three strings (case insensitive):
61 * "Time": Current timestamp is appended to file name. An existing file will never be overwritten.
62 * "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.
63 * "Overwrite": Always save to file named "${AutoBackupDir}/RegionName.oar", even if we have to overwrite an existing file.
64 * AutoBackupDir: String. Default: "." (the current directory).
65 * A directory (absolute or relative) where backups should be saved.
66 * */
67
68namespace OpenSim.Region.OptionalModules.World.AutoBackup
69{
70
71 public enum NamingType
72 {
73 TIME,
74 SEQUENTIAL,
75 OVERWRITE
76 };
77
78 public class AutoBackupModule : ISharedRegionModule, IRegionModuleBase
79 {
80
81 private static readonly ILog m_log =
82 LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
83
84 //AutoBackupModuleState: Auto-Backup state for one region (scene).
85 public class AutoBackupModuleState
86 {
87 private readonly IScene m_scene;
88 private bool m_enabled = false;
89 private NamingType m_naming = NamingType.TIME;
90 private Timer m_timer = null;
91 private bool m_busycheck = true;
92 private string m_script = null;
93 private string m_dir = ".";
94
95 public AutoBackupModuleState(IScene scene)
96 {
97 m_scene = scene;
98 if(scene == null)
99 throw new NullReferenceException("Required parameter missing for AutoBackupModuleState constructor");
100 }
101
102 public void SetEnabled(bool b)
103 {
104 m_enabled = b;
105 }
106
107 public bool GetEnabled()
108 {
109 return m_enabled;
110 }
111
112 public Timer GetTimer()
113 {
114 return m_timer;
115 }
116
117 public void SetTimer(Timer t)
118 {
119 m_timer = t;
120 }
121
122 public bool GetBusyCheck()
123 {
124 return m_busycheck;
125 }
126
127 public void SetBusyCheck(bool b)
128 {
129 m_busycheck = b;
130 }
131
132
133 public string GetScript()
134 {
135 return m_script;
136 }
137
138 public void SetScript(string s)
139 {
140 m_script = s;
141 }
142
143 public string GetBackupDir()
144 {
145 return m_dir;
146 }
147
148 public void SetBackupDir(string s)
149 {
150 m_dir = s;
151 }
152
153 public NamingType GetNamingType()
154 {
155 return m_naming;
156 }
157
158 public void SetNamingType(NamingType n)
159 {
160 m_naming = n;
161 }
162 }
163
164 //Save memory by setting low initial capacities. Minimizes impact in common cases of all regions using same interval, and instances hosting 1 ~ 4 regions.
165 //Also helps if you don't want AutoBackup at all
166 readonly Dictionary<IScene, AutoBackupModuleState> states = new Dictionary<IScene, AutoBackupModuleState>(4);
167 readonly Dictionary<double, Timer> timers = new Dictionary<double, Timer>(1);
168 readonly Dictionary<Timer, List<IScene>> timerMap = new Dictionary<Timer, List<IScene>>(1);
169
170 public AutoBackupModule ()
171 {
172
173 }
174
175 #region IRegionModuleBase implementation
176 void IRegionModuleBase.Initialise (Nini.Config.IConfigSource source)
177 {
178 //I have no overall config settings to care about.
179 }
180
181 void IRegionModuleBase.Close ()
182 {
183 //We don't want any timers firing while the sim's coming down; strange things may happen.
184 StopAllTimers();
185 }
186
187 void IRegionModuleBase.AddRegion (Framework.Scenes.Scene scene)
188 {
189 //NO-OP. Wait for the region to be loaded.
190 }
191
192 void IRegionModuleBase.RemoveRegion (Framework.Scenes.Scene scene)
193 {
194 AutoBackupModuleState abms = states[scene];
195 Timer timer = abms.GetTimer();
196 List<IScene> list = timerMap[timer];
197 list.Remove(scene);
198 if(list.Count == 0)
199 {
200 timerMap.Remove(timer);
201 timers.Remove(timer.Interval);
202 timer.Close();
203 }
204 }
205
206 void IRegionModuleBase.RegionLoaded (Framework.Scenes.Scene scene)
207 {
208 //This really ought not to happen, but just in case, let's pretend it didn't...
209 if(scene == null)
210 return;
211
212 AutoBackupModuleState st = new AutoBackupModuleState(scene);
213 states.Add(scene, st);
214
215 //Read the config settings and set variables.
216 IConfig config = scene.Config.Configs[scene.RegionInfo.RegionName];
217 st.SetEnabled(config.GetBoolean("AutoBackup", false));
218 if(!st.GetEnabled()) //If you don't want AutoBackup, we stop.
219 return;
220
221 //Borrow an existing timer if one exists for the same interval; otherwise, make a new one.
222 double interval = config.GetDouble("AutoBackupInterval", 720);
223 if(timers.ContainsKey(interval))
224 {
225 st.SetTimer(timers[interval]);
226 }
227 else
228 {
229 st.SetTimer(new Timer(interval));
230 timers.Add(interval, st.GetTimer());
231 st.GetTimer().Elapsed += HandleElapsed;
232 }
233
234 //Add the current region to the list of regions tied to this timer.
235 if(timerMap.ContainsKey(st.GetTimer()))
236 {
237 timerMap[st.GetTimer()].Add(scene);
238 }
239 else
240 {
241 List<IScene> scns = new List<IScene>(1);
242 timerMap.Add(st.GetTimer(), scns);
243 }
244
245 st.SetBusyCheck(config.GetBoolean("AutoBackupBusyCheck", true));
246
247 //Set file naming algorithm
248 string namingtype = config.GetString("AutoBackupNaming", "Time");
249 if(namingtype.Equals("Time", StringComparison.CurrentCultureIgnoreCase))
250 {
251 st.SetNamingType(NamingType.TIME);
252 }
253 else if(namingtype.Equals("Sequential", StringComparison.CurrentCultureIgnoreCase))
254 {
255 st.SetNamingType(NamingType.SEQUENTIAL);
256 }
257 else if(namingtype.Equals("Overwrite", StringComparison.CurrentCultureIgnoreCase))
258 {
259 st.SetNamingType(NamingType.OVERWRITE);
260 }
261 else
262 {
263 m_log.Warn("Unknown naming type specified for region " + scene.RegionInfo.RegionName + ": " + namingtype);
264 st.SetNamingType(NamingType.TIME);
265 }
266
267 st.SetScript(config.GetString("AutoBackupScript", null));
268 st.SetBackupDir(config.GetString("AutoBackupDir", "."));
269
270 //Let's give the user *one* convenience and auto-mkdir
271 if(st.GetBackupDir() != ".")
272 {
273 try
274 {
275 DirectoryInfo dirinfo = new DirectoryInfo(st.GetBackupDir());
276 if(!dirinfo.Exists)
277 {
278 dirinfo.Create();
279 }
280 }
281 catch(Exception e)
282 {
283 m_log.Warn("BAD NEWS. You won't be able to save backups to directory " + st.GetBackupDir() +
284 " because it doesn't exist or there's a permissions issue with it. Here's the exception.", e);
285 }
286 }
287 }
288
289 void HandleElapsed (object sender, ElapsedEventArgs e)
290 {
291 bool heuristicsRun = false;
292 bool heuristicsPassed = false;
293 foreach(IScene scene in timerMap[(Timer)sender])
294 {
295 AutoBackupModuleState state = states[scene];
296 bool heuristics = state.GetBusyCheck();
297
298 //Fast path: heuristics are on; already ran em; and sim is fine; OR, no heuristics for the region.
299 if((heuristics && heuristicsRun && heuristicsPassed)
300 || !heuristics)
301 {
302 IRegionArchiverModule iram = scene.RequestModuleInterface<IRegionArchiverModule>();
303 string savePath = BuildOarPath(scene.RegionInfo.RegionName, state.GetBackupDir(), state.GetNamingType());
304 if(savePath == null)
305 {
306 m_log.Warn("savePath is null in HandleElapsed");
307 continue;
308 }
309 iram.ArchiveRegion(savePath, null);
310 ExecuteScript(state.GetScript(), savePath);
311 }
312 //Heuristics are on; ran but we're too busy -- keep going. Maybe another region will have heuristics off!
313 else if(heuristics && heuristicsRun && !heuristicsPassed)
314 {
315 continue;
316 }
317 //Logical Deduction: heuristics are on but haven't been run
318 else
319 {
320 heuristicsPassed = RunHeuristics();
321 heuristicsRun = true;
322 if(!heuristicsPassed)
323 continue;
324 }
325 }
326 }
327
328 string IRegionModuleBase.Name {
329 get {
330 return "AutoBackupModule";
331 }
332 }
333
334 Type IRegionModuleBase.ReplaceableInterface {
335 get {
336 return null;
337 }
338 }
339
340 #endregion
341 #region ISharedRegionModule implementation
342 void ISharedRegionModule.PostInitialise ()
343 {
344 //I don't care right now.
345 }
346
347 #endregion
348
349 //Is this even needed?
350 public bool IsSharedModule
351 {
352 get { return true; }
353 }
354
355 private string BuildOarPath(string regionName, string baseDir, NamingType naming)
356 {
357 FileInfo path = null;
358 switch(naming)
359 {
360 case NamingType.OVERWRITE:
361 path = new FileInfo(baseDir + Path.DirectorySeparatorChar + regionName);
362 return path.FullName;
363 case NamingType.TIME:
364 path = new FileInfo(baseDir + Path.DirectorySeparatorChar + regionName + GetTimeString() + ".oar");
365 return path.FullName;
366 case NamingType.SEQUENTIAL:
367 path = new FileInfo(baseDir + Path.DirectorySeparatorChar + regionName + "_" + GetNextFile(baseDir, regionName) + ".oar");
368 return path.FullName;
369 default:
370 m_log.Warn("VERY BAD: Unhandled case element " + naming.ToString());
371 break;
372 }
373
374 return path.FullName;
375 }
376
377 //Welcome to the TIME STRING. 4 CORNER INTEGERS, CUBES 4 QUAD MEMORY -- No 1 Integer God.
378 //(Terrible reference to <timecube.com>)
379 //This format may turn out to be too unwieldy to keep...
380 //Besides, that's what ctimes are for. But then how do I name each file uniquely without using a GUID?
381 //Sequential numbers, right? Ugh. Almost makes TOO much sense.
382 private string GetTimeString()
383 {
384 StringWriter sw = new StringWriter();
385 sw.Write("_");
386 DateTime now = DateTime.Now;
387 sw.Write(now.Year);
388 sw.Write("y_");
389 sw.Write(now.Month);
390 sw.Write("M_");
391 sw.Write(now.Day);
392 sw.Write("d_");
393 sw.Write(now.Hour);
394 sw.Write("h_");
395 sw.Write(now.Minute);
396 sw.Write("m_");
397 sw.Write(now.Second);
398 sw.Write("s");
399 sw.Flush();
400 string output = sw.ToString();
401 sw.Close();
402 return output;
403 }
404
405 //Get the next logical file name
406 //I really shouldn't put fields here, but for now.... ;)
407 private string m_dirName = null;
408 private string m_regionName = null;
409 private string GetNextFile(string dirName, string regionName)
410 {
411 FileInfo uniqueFile = null;
412 m_dirName = dirName;
413 m_regionName = regionName;
414 long biggestExistingFile = HalfIntervalMaximize(1, FileExistsTest);
415 biggestExistingFile++; //We don't want to overwrite the biggest existing file; we want to write to the NEXT biggest.
416
417 uniqueFile = new FileInfo(m_dirName + Path.DirectorySeparatorChar + m_regionName + "_" + biggestExistingFile + ".oar");
418 if(uniqueFile.Exists)
419 {
420 //Congratulations, your strange deletion patterns fooled my half-interval search into picking an existing file!
421 //Now you get to pay the performance cost :)
422 uniqueFile = UniqueFileSearchLinear(biggestExistingFile);
423 }
424
425 return uniqueFile.FullName;
426 }
427
428 private bool RunHeuristics()
429 {
430 return true;
431 }
432
433 private void ExecuteScript(string scriptName, string savePath)
434 {
435 //Fast path out
436 if(scriptName == null || scriptName.Length <= 0)
437 return;
438
439 try
440 {
441 FileInfo fi = new FileInfo(scriptName);
442 if(fi.Exists)
443 {
444 ProcessStartInfo psi = new ProcessStartInfo(scriptName);
445 psi.Arguments = savePath;
446 psi.CreateNoWindow = true;
447 Process proc = Process.Start(psi);
448 proc.ErrorDataReceived += HandleProcErrorDataReceived;
449 }
450 }
451 catch(Exception e)
452 {
453 m_log.Warn("Exception encountered when trying to run script for oar backup " + savePath, e);
454 }
455 }
456
457 void HandleProcErrorDataReceived (object sender, DataReceivedEventArgs e)
458 {
459 m_log.Warn("ExecuteScript hook " + ((Process)sender).ProcessName + " is yacking on stderr: " + e.Data);
460 }
461
462 private void StopAllTimers()
463 {
464 foreach(Timer t in timerMap.Keys)
465 {
466 t.Close();
467 }
468 }
469
470 /* Find the largest value for which the predicate returns true.
471 * We use a bisection algorithm (half interval) to make the algorithm scalable.
472 * The worst-case complexity is about O(log(n)^2) in practice.
473 * Only for extremely small values (under 10) do you notice it taking more iterations than a linear search.
474 * The number of predicate invocations only hits a few hundred when the maximized value
475 * is in the tens of millions, so prepare for the predicate to be invoked between 10 and 100 times.
476 * And of course it is fantastic with powers of 2, which are densely packed in values under 100 anyway.
477 * The Predicate<long> parameter must be a function that accepts a long and returns a bool.
478 * */
479 public long HalfIntervalMaximize(long start, Predicate<long> pred)
480 {
481 long prev = start, curr = start, biggest = 0;
482
483 if(start < 0)
484 throw new IndexOutOfRangeException("Start value for HalfIntervalMaximize must be non-negative");
485
486 do
487 {
488 if(pred(curr))
489 {
490 if(curr > biggest)
491 {
492 biggest = curr;
493 }
494 prev = curr;
495 if(curr == 0)
496 {
497 //Special case because 0 * 2 = 0 :)
498 curr = 1;
499 }
500 else
501 {
502 //Look deeper
503 curr *= 2;
504 }
505 }
506 else
507 {
508 // We went too far, back off halfway
509 curr = (curr + prev) / 2;
510 }
511 }
512 while(curr - prev > 0);
513
514 return biggest;
515 }
516
517 public bool FileExistsTest(long num)
518 {
519 FileInfo test = new FileInfo(m_dirName + Path.DirectorySeparatorChar + m_regionName + "_" + num + ".oar");
520 return test.Exists;
521 }
522
523
524 //Very slow, hence why we try the HalfIntervalMaximize first!
525 public FileInfo UniqueFileSearchLinear(long start)
526 {
527 long l = start;
528 FileInfo retval = null;
529 do
530 {
531 retval = new FileInfo(m_dirName + Path.DirectorySeparatorChar + m_regionName + "_" + (l++) + ".oar");
532 }
533 while(retval.Exists);
534
535 return retval;
536 }
537}
538
539}
540