aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Framework/Servers
diff options
context:
space:
mode:
authorDiva Canto2012-11-27 14:43:01 -0800
committerDiva Canto2012-11-27 14:43:01 -0800
commita82f699f4348ea1ab139ab338973c9cee04df712 (patch)
treed1bf4948670e1d207fe4f6c60f6b7771e1625839 /OpenSim/Framework/Servers
parentPrevent the core Groups module from being enabled when its name doesn't match... (diff)
parentBulletSim: reorganize linear movement routine into separate subroutines enabl... (diff)
downloadopensim-SC-a82f699f4348ea1ab139ab338973c9cee04df712.zip
opensim-SC-a82f699f4348ea1ab139ab338973c9cee04df712.tar.gz
opensim-SC-a82f699f4348ea1ab139ab338973c9cee04df712.tar.bz2
opensim-SC-a82f699f4348ea1ab139ab338973c9cee04df712.tar.xz
Merge branch 'master' of ssh://opensimulator.org/var/git/opensim
Diffstat (limited to 'OpenSim/Framework/Servers')
-rw-r--r--OpenSim/Framework/Servers/BaseOpenSimServer.cs466
-rw-r--r--OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs60
-rw-r--r--OpenSim/Framework/Servers/ServerBase.cs677
-rw-r--r--OpenSim/Framework/Servers/Tests/OSHttpTests.cs3
-rw-r--r--OpenSim/Framework/Servers/Tests/VersionInfoTests.cs3
5 files changed, 702 insertions, 507 deletions
diff --git a/OpenSim/Framework/Servers/BaseOpenSimServer.cs b/OpenSim/Framework/Servers/BaseOpenSimServer.cs
index 5b2d7dc..c0dc907 100644
--- a/OpenSim/Framework/Servers/BaseOpenSimServer.cs
+++ b/OpenSim/Framework/Servers/BaseOpenSimServer.cs
@@ -27,7 +27,6 @@
27 27
28using System; 28using System;
29using System.Collections.Generic; 29using System.Collections.Generic;
30using System.Diagnostics;
31using System.IO; 30using System.IO;
32using System.Reflection; 31using System.Reflection;
33using System.Text; 32using System.Text;
@@ -38,6 +37,8 @@ using log4net;
38using log4net.Appender; 37using log4net.Appender;
39using log4net.Core; 38using log4net.Core;
40using log4net.Repository; 39using log4net.Repository;
40using OpenMetaverse;
41using OpenMetaverse.StructuredData;
41using OpenSim.Framework; 42using OpenSim.Framework;
42using OpenSim.Framework.Console; 43using OpenSim.Framework.Console;
43using OpenSim.Framework.Monitoring; 44using OpenSim.Framework.Monitoring;
@@ -45,16 +46,12 @@ using OpenSim.Framework.Servers;
45using OpenSim.Framework.Servers.HttpServer; 46using OpenSim.Framework.Servers.HttpServer;
46using Timer=System.Timers.Timer; 47using Timer=System.Timers.Timer;
47 48
48using OpenMetaverse;
49using OpenMetaverse.StructuredData;
50
51
52namespace OpenSim.Framework.Servers 49namespace OpenSim.Framework.Servers
53{ 50{
54 /// <summary> 51 /// <summary>
55 /// Common base for the main OpenSimServers (user, grid, inventory, region, etc) 52 /// Common base for the main OpenSimServers (user, grid, inventory, region, etc)
56 /// </summary> 53 /// </summary>
57 public abstract class BaseOpenSimServer 54 public abstract class BaseOpenSimServer : ServerBase
58 { 55 {
59 private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); 56 private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
60 57
@@ -63,27 +60,6 @@ namespace OpenSim.Framework.Servers
63 /// server. 60 /// server.
64 /// </summary> 61 /// </summary>
65 private Timer m_periodicDiagnosticsTimer = new Timer(60 * 60 * 1000); 62 private Timer m_periodicDiagnosticsTimer = new Timer(60 * 60 * 1000);
66
67 protected CommandConsole m_console;
68 protected OpenSimAppender m_consoleAppender;
69 protected IAppender m_logFileAppender = null;
70
71 /// <summary>
72 /// Time at which this server was started
73 /// </summary>
74 protected DateTime m_startuptime;
75
76 /// <summary>
77 /// Record the initial startup directory for info purposes
78 /// </summary>
79 protected string m_startupDirectory = Environment.CurrentDirectory;
80
81 /// <summary>
82 /// Server version information. Usually VersionInfo + information about git commit, operating system, etc.
83 /// </summary>
84 protected string m_version;
85
86 protected string m_pidFile = String.Empty;
87 63
88 /// <summary> 64 /// <summary>
89 /// Random uuid for private data 65 /// Random uuid for private data
@@ -96,30 +72,13 @@ namespace OpenSim.Framework.Servers
96 get { return m_httpServer; } 72 get { return m_httpServer; }
97 } 73 }
98 74
99 public BaseOpenSimServer() 75 public BaseOpenSimServer() : base()
100 { 76 {
101 m_startuptime = DateTime.Now;
102 m_version = VersionInfo.Version;
103
104 // Random uuid for private data 77 // Random uuid for private data
105 m_osSecret = UUID.Random().ToString(); 78 m_osSecret = UUID.Random().ToString();
106 79
107 m_periodicDiagnosticsTimer.Elapsed += new ElapsedEventHandler(LogDiagnostics); 80 m_periodicDiagnosticsTimer.Elapsed += new ElapsedEventHandler(LogDiagnostics);
108 m_periodicDiagnosticsTimer.Enabled = true; 81 m_periodicDiagnosticsTimer.Enabled = true;
109
110 // This thread will go on to become the console listening thread
111 Thread.CurrentThread.Name = "ConsoleThread";
112
113 ILoggerRepository repository = LogManager.GetRepository();
114 IAppender[] appenders = repository.GetAppenders();
115
116 foreach (IAppender appender in appenders)
117 {
118 if (appender.Name == "LogFileAppender")
119 {
120 m_logFileAppender = appender;
121 }
122 }
123 } 82 }
124 83
125 /// <summary> 84 /// <summary>
@@ -127,83 +86,18 @@ namespace OpenSim.Framework.Servers
127 /// </summary> 86 /// </summary>
128 protected virtual void StartupSpecific() 87 protected virtual void StartupSpecific()
129 { 88 {
130 if (m_console != null) 89 if (m_console == null)
131 { 90 return;
132 ILoggerRepository repository = LogManager.GetRepository();
133 IAppender[] appenders = repository.GetAppenders();
134
135 foreach (IAppender appender in appenders)
136 {
137 if (appender.Name == "Console")
138 {
139 m_consoleAppender = (OpenSimAppender)appender;
140 break;
141 }
142 }
143
144 if (null == m_consoleAppender)
145 {
146 Notice("No appender named Console found (see the log4net config file for this executable)!");
147 }
148 else
149 {
150 m_consoleAppender.Console = m_console;
151
152 // If there is no threshold set then the threshold is effectively everything.
153 if (null == m_consoleAppender.Threshold)
154 m_consoleAppender.Threshold = Level.All;
155
156 Notice(String.Format("Console log level is {0}", m_consoleAppender.Threshold));
157 }
158
159 m_console.Commands.AddCommand("General", false, "quit",
160 "quit",
161 "Quit the application", HandleQuit);
162
163 m_console.Commands.AddCommand("General", false, "shutdown",
164 "shutdown",
165 "Quit the application", HandleQuit);
166
167 m_console.Commands.AddCommand("General", false, "set log level",
168 "set log level <level>",
169 "Set the console logging level", HandleLogLevel);
170
171 m_console.Commands.AddCommand("General", false, "show info",
172 "show info",
173 "Show general information about the server", HandleShow);
174
175 m_console.Commands.AddCommand("General", false, "show threads",
176 "show threads",
177 "Show thread status", HandleShow);
178
179 m_console.Commands.AddCommand("General", false, "show uptime",
180 "show uptime",
181 "Show server uptime", HandleShow);
182
183 m_console.Commands.AddCommand("General", false, "show version",
184 "show version",
185 "Show server version", HandleShow);
186
187 m_console.Commands.AddCommand("General", false, "threads abort",
188 "threads abort <thread-id>",
189 "Abort a managed thread. Use \"show threads\" to find possible threads.", HandleThreadsAbort);
190
191 m_console.Commands.AddCommand("General", false, "threads show",
192 "threads show",
193 "Show thread status. Synonym for \"show threads\"",
194 (string module, string[] args) => Notice(GetThreadsReport()));
195 91
196 m_console.Commands.AddCommand("General", false, "force gc", 92 RegisterCommonCommands();
197 "force gc", 93
198 "Manually invoke runtime garbage collection. For debugging purposes", 94 m_console.Commands.AddCommand("General", false, "quit",
199 HandleForceGc); 95 "quit",
200 } 96 "Quit the application", HandleQuit);
201 }
202 97
203 private void HandleForceGc(string module, string[] args) 98 m_console.Commands.AddCommand("General", false, "shutdown",
204 { 99 "shutdown",
205 MainConsole.Instance.Output("Manually invoking runtime garbage collection"); 100 "Quit the application", HandleQuit);
206 GC.Collect();
207 } 101 }
208 102
209 /// <summary> 103 /// <summary>
@@ -236,74 +130,11 @@ namespace OpenSim.Framework.Servers
236 } 130 }
237 131
238 /// <summary> 132 /// <summary>
239 /// Get a report about the registered threads in this server.
240 /// </summary>
241 protected string GetThreadsReport()
242 {
243 // This should be a constant field.
244 string reportFormat = "{0,6} {1,35} {2,16} {3,13} {4,10} {5,30}";
245
246 StringBuilder sb = new StringBuilder();
247 Watchdog.ThreadWatchdogInfo[] threads = Watchdog.GetThreadsInfo();
248
249 sb.Append(threads.Length + " threads are being tracked:" + Environment.NewLine);
250
251 int timeNow = Environment.TickCount & Int32.MaxValue;
252
253 sb.AppendFormat(reportFormat, "ID", "NAME", "LAST UPDATE (MS)", "LIFETIME (MS)", "PRIORITY", "STATE");
254 sb.Append(Environment.NewLine);
255
256 foreach (Watchdog.ThreadWatchdogInfo twi in threads)
257 {
258 Thread t = twi.Thread;
259
260 sb.AppendFormat(
261 reportFormat,
262 t.ManagedThreadId,
263 t.Name,
264 timeNow - twi.LastTick,
265 timeNow - twi.FirstTick,
266 t.Priority,
267 t.ThreadState);
268
269 sb.Append("\n");
270 }
271
272 sb.Append("\n");
273
274 // For some reason mono 2.6.7 returns an empty threads set! Not going to confuse people by reporting
275 // zero active threads.
276 int totalThreads = Process.GetCurrentProcess().Threads.Count;
277 if (totalThreads > 0)
278 sb.AppendFormat("Total threads active: {0}\n\n", totalThreads);
279
280 sb.Append("Main threadpool (excluding script engine pools)\n");
281 sb.Append(Util.GetThreadPoolReport());
282
283 return sb.ToString();
284 }
285
286 /// <summary>
287 /// Return a report about the uptime of this server
288 /// </summary>
289 /// <returns></returns>
290 protected string GetUptimeReport()
291 {
292 StringBuilder sb = new StringBuilder(String.Format("Time now is {0}\n", DateTime.Now));
293 sb.Append(String.Format("Server has been running since {0}, {1}\n", m_startuptime.DayOfWeek, m_startuptime));
294 sb.Append(String.Format("That is an elapsed time of {0}\n", DateTime.Now - m_startuptime));
295
296 return sb.ToString();
297 }
298
299 /// <summary>
300 /// Performs initialisation of the scene, such as loading configuration from disk. 133 /// Performs initialisation of the scene, such as loading configuration from disk.
301 /// </summary> 134 /// </summary>
302 public virtual void Startup() 135 public virtual void Startup()
303 { 136 {
304 m_log.Info("[STARTUP]: Beginning startup processing"); 137 m_log.Info("[STARTUP]: Beginning startup processing");
305
306 EnhanceVersionInformation();
307 138
308 m_log.Info("[STARTUP]: OpenSimulator version: " + m_version + Environment.NewLine); 139 m_log.Info("[STARTUP]: OpenSimulator version: " + m_version + Environment.NewLine);
309 // clr version potentially is more confusing than helpful, since it doesn't tell us if we're running under Mono/MS .NET and 140 // clr version potentially is more confusing than helpful, since it doesn't tell us if we're running under Mono/MS .NET and
@@ -338,257 +169,7 @@ namespace OpenSim.Framework.Servers
338 private void HandleQuit(string module, string[] args) 169 private void HandleQuit(string module, string[] args)
339 { 170 {
340 Shutdown(); 171 Shutdown();
341 } 172 }
342
343 private void HandleLogLevel(string module, string[] cmd)
344 {
345 if (null == m_consoleAppender)
346 {
347 Notice("No appender named Console found (see the log4net config file for this executable)!");
348 return;
349 }
350
351 if (cmd.Length > 3)
352 {
353 string rawLevel = cmd[3];
354
355 ILoggerRepository repository = LogManager.GetRepository();
356 Level consoleLevel = repository.LevelMap[rawLevel];
357
358 if (consoleLevel != null)
359 m_consoleAppender.Threshold = consoleLevel;
360 else
361 Notice(
362 String.Format(
363 "{0} is not a valid logging level. Valid logging levels are ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF",
364 rawLevel));
365 }
366
367 Notice(String.Format("Console log level is {0}", m_consoleAppender.Threshold));
368 }
369
370 /// <summary>
371 /// Show help information
372 /// </summary>
373 /// <param name="helpArgs"></param>
374 protected virtual void ShowHelp(string[] helpArgs)
375 {
376 Notice("");
377
378 if (helpArgs.Length == 0)
379 {
380 Notice("set log level [level] - change the console logging level only. For example, off or debug.");
381 Notice("show info - show server information (e.g. startup path).");
382 Notice("show threads - list tracked threads");
383 Notice("show uptime - show server startup time and uptime.");
384 Notice("show version - show server version.");
385 Notice("");
386
387 return;
388 }
389 }
390
391 public virtual void HandleShow(string module, string[] cmd)
392 {
393 List<string> args = new List<string>(cmd);
394
395 args.RemoveAt(0);
396
397 string[] showParams = args.ToArray();
398
399 switch (showParams[0])
400 {
401 case "info":
402 ShowInfo();
403 break;
404
405 case "threads":
406 Notice(GetThreadsReport());
407 break;
408
409 case "uptime":
410 Notice(GetUptimeReport());
411 break;
412
413 case "version":
414 Notice(GetVersionText());
415 break;
416 }
417 }
418
419 public virtual void HandleThreadsAbort(string module, string[] cmd)
420 {
421 if (cmd.Length != 3)
422 {
423 MainConsole.Instance.Output("Usage: threads abort <thread-id>");
424 return;
425 }
426
427 int threadId;
428 if (!int.TryParse(cmd[2], out threadId))
429 {
430 MainConsole.Instance.Output("ERROR: Thread id must be an integer");
431 return;
432 }
433
434 if (Watchdog.AbortThread(threadId))
435 MainConsole.Instance.OutputFormat("Aborted thread with id {0}", threadId);
436 else
437 MainConsole.Instance.OutputFormat("ERROR - Thread with id {0} not found in managed threads", threadId);
438 }
439
440 protected void ShowInfo()
441 {
442 Notice(GetVersionText());
443 Notice("Startup directory: " + m_startupDirectory);
444 if (null != m_consoleAppender)
445 Notice(String.Format("Console log level: {0}", m_consoleAppender.Threshold));
446 }
447
448 protected string GetVersionText()
449 {
450 return String.Format("Version: {0} (interface version {1})", m_version, VersionInfo.MajorInterfaceVersion);
451 }
452
453 /// <summary>
454 /// Console output is only possible if a console has been established.
455 /// That is something that cannot be determined within this class. So
456 /// all attempts to use the console MUST be verified.
457 /// </summary>
458 /// <param name="msg"></param>
459 protected void Notice(string msg)
460 {
461 if (m_console != null)
462 {
463 m_console.Output(msg);
464 }
465 }
466
467 /// <summary>
468 /// Console output is only possible if a console has been established.
469 /// That is something that cannot be determined within this class. So
470 /// all attempts to use the console MUST be verified.
471 /// </summary>
472 /// <param name="format"></param>
473 /// <param name="components"></param>
474 protected void Notice(string format, params string[] components)
475 {
476 if (m_console != null)
477 m_console.OutputFormat(format, components);
478 }
479
480 /// <summary>
481 /// Enhance the version string with extra information if it's available.
482 /// </summary>
483 protected void EnhanceVersionInformation()
484 {
485 string buildVersion = string.Empty;
486
487 // The subversion information is deprecated and will be removed at a later date
488 // Add subversion revision information if available
489 // Try file "svn_revision" in the current directory first, then the .svn info.
490 // This allows to make the revision available in simulators not running from the source tree.
491 // FIXME: Making an assumption about the directory we're currently in - we do this all over the place
492 // elsewhere as well
493 string gitDir = "../.git/";
494 string gitRefPointerPath = gitDir + "HEAD";
495
496 string svnRevisionFileName = "svn_revision";
497 string svnFileName = ".svn/entries";
498 string manualVersionFileName = ".version";
499 string inputLine;
500 int strcmp;
501
502 if (File.Exists(manualVersionFileName))
503 {
504 using (StreamReader CommitFile = File.OpenText(manualVersionFileName))
505 buildVersion = CommitFile.ReadLine();
506
507 m_version += buildVersion ?? "";
508 }
509 else if (File.Exists(gitRefPointerPath))
510 {
511// m_log.DebugFormat("[OPENSIM]: Found {0}", gitRefPointerPath);
512
513 string rawPointer = "";
514
515 using (StreamReader pointerFile = File.OpenText(gitRefPointerPath))
516 rawPointer = pointerFile.ReadLine();
517
518// m_log.DebugFormat("[OPENSIM]: rawPointer [{0}]", rawPointer);
519
520 Match m = Regex.Match(rawPointer, "^ref: (.+)$");
521
522 if (m.Success)
523 {
524// m_log.DebugFormat("[OPENSIM]: Matched [{0}]", m.Groups[1].Value);
525
526 string gitRef = m.Groups[1].Value;
527 string gitRefPath = gitDir + gitRef;
528 if (File.Exists(gitRefPath))
529 {
530// m_log.DebugFormat("[OPENSIM]: Found gitRefPath [{0}]", gitRefPath);
531
532 using (StreamReader refFile = File.OpenText(gitRefPath))
533 {
534 string gitHash = refFile.ReadLine();
535 m_version += gitHash.Substring(0, 7);
536 }
537 }
538 }
539 }
540 else
541 {
542 // Remove the else logic when subversion mirror is no longer used
543 if (File.Exists(svnRevisionFileName))
544 {
545 StreamReader RevisionFile = File.OpenText(svnRevisionFileName);
546 buildVersion = RevisionFile.ReadLine();
547 buildVersion.Trim();
548 RevisionFile.Close();
549 }
550
551 if (string.IsNullOrEmpty(buildVersion) && File.Exists(svnFileName))
552 {
553 StreamReader EntriesFile = File.OpenText(svnFileName);
554 inputLine = EntriesFile.ReadLine();
555 while (inputLine != null)
556 {
557 // using the dir svn revision at the top of entries file
558 strcmp = String.Compare(inputLine, "dir");
559 if (strcmp == 0)
560 {
561 buildVersion = EntriesFile.ReadLine();
562 break;
563 }
564 else
565 {
566 inputLine = EntriesFile.ReadLine();
567 }
568 }
569 EntriesFile.Close();
570 }
571
572 m_version += string.IsNullOrEmpty(buildVersion) ? " " : ("." + buildVersion + " ").Substring(0, 6);
573 }
574 }
575
576 protected void CreatePIDFile(string path)
577 {
578 try
579 {
580 string pidstring = System.Diagnostics.Process.GetCurrentProcess().Id.ToString();
581 FileStream fs = File.Create(path);
582
583 Byte[] buf = Encoding.ASCII.GetBytes(pidstring);
584 fs.Write(buf, 0, buf.Length);
585 fs.Close();
586 m_pidFile = path;
587 }
588 catch (Exception)
589 {
590 }
591 }
592 173
593 public string osSecret { 174 public string osSecret {
594 // Secret uuid for the simulator 175 // Secret uuid for the simulator
@@ -607,20 +188,5 @@ namespace OpenSim.Framework.Servers
607 return StatsManager.SimExtraStats.XReport((DateTime.Now - m_startuptime).ToString() , m_version); 188 return StatsManager.SimExtraStats.XReport((DateTime.Now - m_startuptime).ToString() , m_version);
608 } 189 }
609 } 190 }
610
611 protected void RemovePIDFile()
612 {
613 if (m_pidFile != String.Empty)
614 {
615 try
616 {
617 File.Delete(m_pidFile);
618 m_pidFile = String.Empty;
619 }
620 catch (Exception)
621 {
622 }
623 }
624 }
625 } 191 }
626} 192} \ No newline at end of file
diff --git a/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs b/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs
index 410a76a..2cd626f 100644
--- a/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs
+++ b/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs
@@ -719,8 +719,11 @@ namespace OpenSim.Framework.Servers.HttpServer
719 if (DebugLevel == 5) 719 if (DebugLevel == 5)
720 { 720 {
721 const int sampleLength = 80; 721 const int sampleLength = 80;
722 char[] sampleChars = new char[sampleLength]; 722 char[] sampleChars = new char[sampleLength + 3];
723 reader.Read(sampleChars, 0, sampleLength); 723 reader.Read(sampleChars, 0, sampleLength);
724 sampleChars[80] = '.';
725 sampleChars[81] = '.';
726 sampleChars[82] = '.';
724 output = new string(sampleChars); 727 output = new string(sampleChars);
725 } 728 }
726 else 729 else
@@ -728,7 +731,7 @@ namespace OpenSim.Framework.Servers.HttpServer
728 output = reader.ReadToEnd(); 731 output = reader.ReadToEnd();
729 } 732 }
730 733
731 m_log.DebugFormat("[BASE HTTP SERVER]: {0}...", output.Replace("\n", @"\n")); 734 m_log.DebugFormat("[BASE HTTP SERVER]: {0}", output.Replace("\n", @"\n"));
732 } 735 }
733 } 736 }
734 737
@@ -1279,59 +1282,6 @@ namespace OpenSim.Framework.Servers.HttpServer
1279 map["login"] = OSD.FromString("false"); 1282 map["login"] = OSD.FromString("false");
1280 return map; 1283 return map;
1281 } 1284 }
1282 /// <summary>
1283 /// A specific agent handler was provided. Such a handler is expecetd to have an
1284 /// intimate, and highly specific relationship with the client. Consequently,
1285 /// nothing is done here.
1286 /// </summary>
1287 /// <param name="handler"></param>
1288 /// <param name="request"></param>
1289 /// <param name="response"></param>
1290
1291 private bool HandleAgentRequest(IHttpAgentHandler handler, OSHttpRequest request, OSHttpResponse response)
1292 {
1293 // In the case of REST, then handler is responsible for ALL aspects of
1294 // the request/response handling. Nothing is done here, not even encoding.
1295
1296 try
1297 {
1298 return handler.Handle(request, response);
1299 }
1300 catch (Exception e)
1301 {
1302 // If the handler did in fact close the stream, then this will blow
1303 // chunks. So that that doesn't disturb anybody we throw away any
1304 // and all exceptions raised. We've done our best to release the
1305 // client.
1306 try
1307 {
1308 m_log.Warn("[HTTP-AGENT]: Error - " + e.Message);
1309 response.SendChunked = false;
1310 response.KeepAlive = true;
1311 response.StatusCode = (int)OSHttpStatusCode.ServerErrorInternalError;
1312 //response.OutputStream.Close();
1313 try
1314 {
1315 response.Send();
1316 //response.FreeContext();
1317 }
1318 catch (SocketException f)
1319 {
1320 // This has to be here to prevent a Linux/Mono crash
1321 m_log.Warn(
1322 String.Format("[BASE HTTP SERVER]: XmlRpcRequest issue {0}.\nNOTE: this may be spurious on Linux. ", f.Message), f);
1323 }
1324 }
1325 catch(Exception)
1326 {
1327 }
1328 }
1329
1330 // Indicate that the request has been "handled"
1331
1332 return true;
1333
1334 }
1335 1285
1336 public byte[] HandleHTTPRequest(OSHttpRequest request, OSHttpResponse response) 1286 public byte[] HandleHTTPRequest(OSHttpRequest request, OSHttpResponse response)
1337 { 1287 {
diff --git a/OpenSim/Framework/Servers/ServerBase.cs b/OpenSim/Framework/Servers/ServerBase.cs
new file mode 100644
index 0000000..47baac8
--- /dev/null
+++ b/OpenSim/Framework/Servers/ServerBase.cs
@@ -0,0 +1,677 @@
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.Text;
34using System.Text.RegularExpressions;
35using System.Threading;
36using log4net;
37using log4net.Appender;
38using log4net.Core;
39using log4net.Repository;
40using Nini.Config;
41using OpenSim.Framework.Console;
42using OpenSim.Framework.Monitoring;
43
44namespace OpenSim.Framework.Servers
45{
46 public class ServerBase
47 {
48 private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
49
50 public IConfigSource Config { get; protected set; }
51
52 /// <summary>
53 /// Console to be used for any command line output. Can be null, in which case there should be no output.
54 /// </summary>
55 protected ICommandConsole m_console;
56
57 protected OpenSimAppender m_consoleAppender;
58 protected FileAppender m_logFileAppender;
59
60 protected DateTime m_startuptime;
61 protected string m_startupDirectory = Environment.CurrentDirectory;
62
63 protected string m_pidFile = String.Empty;
64
65 /// <summary>
66 /// Server version information. Usually VersionInfo + information about git commit, operating system, etc.
67 /// </summary>
68 protected string m_version;
69
70 public ServerBase()
71 {
72 m_startuptime = DateTime.Now;
73 m_version = VersionInfo.Version;
74 EnhanceVersionInformation();
75 }
76
77 protected void CreatePIDFile(string path)
78 {
79 try
80 {
81 string pidstring = System.Diagnostics.Process.GetCurrentProcess().Id.ToString();
82
83 using (FileStream fs = File.Create(path))
84 {
85 Byte[] buf = Encoding.ASCII.GetBytes(pidstring);
86 fs.Write(buf, 0, buf.Length);
87 }
88
89 m_pidFile = path;
90
91 m_log.InfoFormat("[SERVER BASE]: Created pid file {0}", m_pidFile);
92 }
93 catch (Exception e)
94 {
95 m_log.Warn(string.Format("[SERVER BASE]: Could not create PID file at {0} ", path), e);
96 }
97 }
98
99 protected void RemovePIDFile()
100 {
101 if (m_pidFile != String.Empty)
102 {
103 try
104 {
105 File.Delete(m_pidFile);
106 }
107 catch (Exception e)
108 {
109 m_log.Error(string.Format("[SERVER BASE]: Error whilst removing {0} ", m_pidFile), e);
110 }
111
112 m_pidFile = String.Empty;
113 }
114 }
115
116 public void RegisterCommonAppenders(IConfig startupConfig)
117 {
118 ILoggerRepository repository = LogManager.GetRepository();
119 IAppender[] appenders = repository.GetAppenders();
120
121 foreach (IAppender appender in appenders)
122 {
123 if (appender.Name == "Console")
124 {
125 m_consoleAppender = (OpenSimAppender)appender;
126 }
127 else if (appender.Name == "LogFileAppender")
128 {
129 m_logFileAppender = (FileAppender)appender;
130 }
131 }
132
133 if (null == m_consoleAppender)
134 {
135 Notice("No appender named Console found (see the log4net config file for this executable)!");
136 }
137 else
138 {
139 // FIXME: This should be done through an interface rather than casting.
140 m_consoleAppender.Console = (ConsoleBase)m_console;
141
142 // If there is no threshold set then the threshold is effectively everything.
143 if (null == m_consoleAppender.Threshold)
144 m_consoleAppender.Threshold = Level.All;
145
146 Notice(String.Format("Console log level is {0}", m_consoleAppender.Threshold));
147 }
148
149 if (m_logFileAppender != null && startupConfig != null)
150 {
151 string cfgFileName = startupConfig.GetString("LogFile", null);
152 if (cfgFileName != null)
153 {
154 m_logFileAppender.File = cfgFileName;
155 m_logFileAppender.ActivateOptions();
156 }
157
158 m_log.InfoFormat("[SERVER BASE]: Logging started to file {0}", m_logFileAppender.File);
159 }
160 }
161
162 /// <summary>
163 /// Register common commands once m_console has been set if it is going to be set
164 /// </summary>
165 public void RegisterCommonCommands()
166 {
167 if (m_console == null)
168 return;
169
170 m_console.Commands.AddCommand(
171 "General", false, "show info", "show info", "Show general information about the server", HandleShow);
172
173 m_console.Commands.AddCommand(
174 "General", false, "show version", "show version", "Show server version", HandleShow);
175
176 m_console.Commands.AddCommand(
177 "General", false, "show uptime", "show uptime", "Show server uptime", HandleShow);
178
179 m_console.Commands.AddCommand(
180 "General", false, "get log level", "get log level", "Get the current console logging level",
181 (mod, cmd) => ShowLogLevel());
182
183 m_console.Commands.AddCommand(
184 "General", false, "set log level", "set log level <level>",
185 "Set the console logging level for this session.", HandleSetLogLevel);
186
187 m_console.Commands.AddCommand(
188 "General", false, "config set",
189 "config set <section> <key> <value>",
190 "Set a config option. In most cases this is not useful since changed parameters are not dynamically reloaded. Neither do changed parameters persist - you will have to change a config file manually and restart.", HandleConfig);
191
192 m_console.Commands.AddCommand(
193 "General", false, "config get",
194 "config get [<section>] [<key>]",
195 "Synonym for config show",
196 HandleConfig);
197
198 m_console.Commands.AddCommand(
199 "General", false, "config show",
200 "config show [<section>] [<key>]",
201 "Show config information",
202 "If neither section nor field are specified, then the whole current configuration is printed." + Environment.NewLine
203 + "If a section is given but not a field, then all fields in that section are printed.",
204 HandleConfig);
205
206 m_console.Commands.AddCommand(
207 "General", false, "config save",
208 "config save <path>",
209 "Save current configuration to a file at the given path", HandleConfig);
210
211 m_console.Commands.AddCommand(
212 "General", false, "command-script",
213 "command-script <script>",
214 "Run a command script from file", HandleScript);
215
216 m_console.Commands.AddCommand(
217 "General", false, "show threads",
218 "show threads",
219 "Show thread status", HandleShow);
220
221 m_console.Commands.AddCommand(
222 "General", false, "threads abort",
223 "threads abort <thread-id>",
224 "Abort a managed thread. Use \"show threads\" to find possible threads.", HandleThreadsAbort);
225
226 m_console.Commands.AddCommand(
227 "General", false, "threads show",
228 "threads show",
229 "Show thread status. Synonym for \"show threads\"",
230 (string module, string[] args) => Notice(GetThreadsReport()));
231
232 m_console.Commands.AddCommand(
233 "General", false, "force gc",
234 "force gc",
235 "Manually invoke runtime garbage collection. For debugging purposes",
236 HandleForceGc);
237 }
238
239 private void HandleForceGc(string module, string[] args)
240 {
241 Notice("Manually invoking runtime garbage collection");
242 GC.Collect();
243 }
244
245 public virtual void HandleShow(string module, string[] cmd)
246 {
247 List<string> args = new List<string>(cmd);
248
249 args.RemoveAt(0);
250
251 string[] showParams = args.ToArray();
252
253 switch (showParams[0])
254 {
255 case "info":
256 ShowInfo();
257 break;
258
259 case "version":
260 Notice(GetVersionText());
261 break;
262
263 case "uptime":
264 Notice(GetUptimeReport());
265 break;
266
267 case "threads":
268 Notice(GetThreadsReport());
269 break;
270 }
271 }
272
273 /// <summary>
274 /// Change and load configuration file data.
275 /// </summary>
276 /// <param name="module"></param>
277 /// <param name="cmd"></param>
278 private void HandleConfig(string module, string[] cmd)
279 {
280 List<string> args = new List<string>(cmd);
281 args.RemoveAt(0);
282 string[] cmdparams = args.ToArray();
283
284 if (cmdparams.Length > 0)
285 {
286 string firstParam = cmdparams[0].ToLower();
287
288 switch (firstParam)
289 {
290 case "set":
291 if (cmdparams.Length < 4)
292 {
293 Notice("Syntax: config set <section> <key> <value>");
294 Notice("Example: config set ScriptEngine.DotNetEngine NumberOfScriptThreads 5");
295 }
296 else
297 {
298 IConfig c;
299 IConfigSource source = new IniConfigSource();
300 c = source.AddConfig(cmdparams[1]);
301 if (c != null)
302 {
303 string _value = String.Join(" ", cmdparams, 3, cmdparams.Length - 3);
304 c.Set(cmdparams[2], _value);
305 Config.Merge(source);
306
307 Notice("In section [{0}], set {1} = {2}", c.Name, cmdparams[2], _value);
308 }
309 }
310 break;
311
312 case "get":
313 case "show":
314 if (cmdparams.Length == 1)
315 {
316 foreach (IConfig config in Config.Configs)
317 {
318 Notice("[{0}]", config.Name);
319 string[] keys = config.GetKeys();
320 foreach (string key in keys)
321 Notice(" {0} = {1}", key, config.GetString(key));
322 }
323 }
324 else if (cmdparams.Length == 2 || cmdparams.Length == 3)
325 {
326 IConfig config = Config.Configs[cmdparams[1]];
327 if (config == null)
328 {
329 Notice("Section \"{0}\" does not exist.",cmdparams[1]);
330 break;
331 }
332 else
333 {
334 if (cmdparams.Length == 2)
335 {
336 Notice("[{0}]", config.Name);
337 foreach (string key in config.GetKeys())
338 Notice(" {0} = {1}", key, config.GetString(key));
339 }
340 else
341 {
342 Notice(
343 "config get {0} {1} : {2}",
344 cmdparams[1], cmdparams[2], config.GetString(cmdparams[2]));
345 }
346 }
347 }
348 else
349 {
350 Notice("Syntax: config {0} [<section>] [<key>]", firstParam);
351 Notice("Example: config {0} ScriptEngine.DotNetEngine NumberOfScriptThreads", firstParam);
352 }
353
354 break;
355
356 case "save":
357 if (cmdparams.Length < 2)
358 {
359 Notice("Syntax: config save <path>");
360 return;
361 }
362
363 string path = cmdparams[1];
364 Notice("Saving configuration file: {0}", path);
365
366 if (Config is IniConfigSource)
367 {
368 IniConfigSource iniCon = (IniConfigSource)Config;
369 iniCon.Save(path);
370 }
371 else if (Config is XmlConfigSource)
372 {
373 XmlConfigSource xmlCon = (XmlConfigSource)Config;
374 xmlCon.Save(path);
375 }
376
377 break;
378 }
379 }
380 }
381
382 private void HandleSetLogLevel(string module, string[] cmd)
383 {
384 if (cmd.Length != 4)
385 {
386 Notice("Usage: set log level <level>");
387 return;
388 }
389
390 if (null == m_consoleAppender)
391 {
392 Notice("No appender named Console found (see the log4net config file for this executable)!");
393 return;
394 }
395
396 string rawLevel = cmd[3];
397
398 ILoggerRepository repository = LogManager.GetRepository();
399 Level consoleLevel = repository.LevelMap[rawLevel];
400
401 if (consoleLevel != null)
402 m_consoleAppender.Threshold = consoleLevel;
403 else
404 Notice(
405 "{0} is not a valid logging level. Valid logging levels are ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF",
406 rawLevel);
407
408 ShowLogLevel();
409 }
410
411 private void ShowLogLevel()
412 {
413 Notice("Console log level is {0}", m_consoleAppender.Threshold);
414 }
415
416 protected virtual void HandleScript(string module, string[] parms)
417 {
418 if (parms.Length != 2)
419 {
420 Notice("Usage: command-script <path-to-script");
421 return;
422 }
423
424 RunCommandScript(parms[1]);
425 }
426
427 /// <summary>
428 /// Run an optional startup list of commands
429 /// </summary>
430 /// <param name="fileName"></param>
431 protected void RunCommandScript(string fileName)
432 {
433 if (m_console == null)
434 return;
435
436 if (File.Exists(fileName))
437 {
438 m_log.Info("[SERVER BASE]: Running " + fileName);
439
440 using (StreamReader readFile = File.OpenText(fileName))
441 {
442 string currentCommand;
443 while ((currentCommand = readFile.ReadLine()) != null)
444 {
445 currentCommand = currentCommand.Trim();
446 if (!(currentCommand == ""
447 || currentCommand.StartsWith(";")
448 || currentCommand.StartsWith("//")
449 || currentCommand.StartsWith("#")))
450 {
451 m_log.Info("[SERVER BASE]: Running '" + currentCommand + "'");
452 m_console.RunCommand(currentCommand);
453 }
454 }
455 }
456 }
457 }
458
459 /// <summary>
460 /// Return a report about the uptime of this server
461 /// </summary>
462 /// <returns></returns>
463 protected string GetUptimeReport()
464 {
465 StringBuilder sb = new StringBuilder(String.Format("Time now is {0}\n", DateTime.Now));
466 sb.Append(String.Format("Server has been running since {0}, {1}\n", m_startuptime.DayOfWeek, m_startuptime));
467 sb.Append(String.Format("That is an elapsed time of {0}\n", DateTime.Now - m_startuptime));
468
469 return sb.ToString();
470 }
471
472 protected void ShowInfo()
473 {
474 Notice(GetVersionText());
475 Notice("Startup directory: " + m_startupDirectory);
476 if (null != m_consoleAppender)
477 Notice(String.Format("Console log level: {0}", m_consoleAppender.Threshold));
478 }
479
480 /// <summary>
481 /// Enhance the version string with extra information if it's available.
482 /// </summary>
483 protected void EnhanceVersionInformation()
484 {
485 string buildVersion = string.Empty;
486
487 // The subversion information is deprecated and will be removed at a later date
488 // Add subversion revision information if available
489 // Try file "svn_revision" in the current directory first, then the .svn info.
490 // This allows to make the revision available in simulators not running from the source tree.
491 // FIXME: Making an assumption about the directory we're currently in - we do this all over the place
492 // elsewhere as well
493 string gitDir = "../.git/";
494 string gitRefPointerPath = gitDir + "HEAD";
495
496 string svnRevisionFileName = "svn_revision";
497 string svnFileName = ".svn/entries";
498 string manualVersionFileName = ".version";
499 string inputLine;
500 int strcmp;
501
502 if (File.Exists(manualVersionFileName))
503 {
504 using (StreamReader CommitFile = File.OpenText(manualVersionFileName))
505 buildVersion = CommitFile.ReadLine();
506
507 m_version += buildVersion ?? "";
508 }
509 else if (File.Exists(gitRefPointerPath))
510 {
511// m_log.DebugFormat("[SERVER BASE]: Found {0}", gitRefPointerPath);
512
513 string rawPointer = "";
514
515 using (StreamReader pointerFile = File.OpenText(gitRefPointerPath))
516 rawPointer = pointerFile.ReadLine();
517
518// m_log.DebugFormat("[SERVER BASE]: rawPointer [{0}]", rawPointer);
519
520 Match m = Regex.Match(rawPointer, "^ref: (.+)$");
521
522 if (m.Success)
523 {
524// m_log.DebugFormat("[SERVER BASE]: Matched [{0}]", m.Groups[1].Value);
525
526 string gitRef = m.Groups[1].Value;
527 string gitRefPath = gitDir + gitRef;
528 if (File.Exists(gitRefPath))
529 {
530// m_log.DebugFormat("[SERVER BASE]: Found gitRefPath [{0}]", gitRefPath);
531
532 using (StreamReader refFile = File.OpenText(gitRefPath))
533 {
534 string gitHash = refFile.ReadLine();
535 m_version += gitHash.Substring(0, 7);
536 }
537 }
538 }
539 }
540 else
541 {
542 // Remove the else logic when subversion mirror is no longer used
543 if (File.Exists(svnRevisionFileName))
544 {
545 StreamReader RevisionFile = File.OpenText(svnRevisionFileName);
546 buildVersion = RevisionFile.ReadLine();
547 buildVersion.Trim();
548 RevisionFile.Close();
549 }
550
551 if (string.IsNullOrEmpty(buildVersion) && File.Exists(svnFileName))
552 {
553 StreamReader EntriesFile = File.OpenText(svnFileName);
554 inputLine = EntriesFile.ReadLine();
555 while (inputLine != null)
556 {
557 // using the dir svn revision at the top of entries file
558 strcmp = String.Compare(inputLine, "dir");
559 if (strcmp == 0)
560 {
561 buildVersion = EntriesFile.ReadLine();
562 break;
563 }
564 else
565 {
566 inputLine = EntriesFile.ReadLine();
567 }
568 }
569 EntriesFile.Close();
570 }
571
572 m_version += string.IsNullOrEmpty(buildVersion) ? " " : ("." + buildVersion + " ").Substring(0, 6);
573 }
574 }
575
576 protected string GetVersionText()
577 {
578 return String.Format("Version: {0} (interface version {1})", m_version, VersionInfo.MajorInterfaceVersion);
579 }
580
581 /// <summary>
582 /// Get a report about the registered threads in this server.
583 /// </summary>
584 protected string GetThreadsReport()
585 {
586 // This should be a constant field.
587 string reportFormat = "{0,6} {1,35} {2,16} {3,13} {4,10} {5,30}";
588
589 StringBuilder sb = new StringBuilder();
590 Watchdog.ThreadWatchdogInfo[] threads = Watchdog.GetThreadsInfo();
591
592 sb.Append(threads.Length + " threads are being tracked:" + Environment.NewLine);
593
594 int timeNow = Environment.TickCount & Int32.MaxValue;
595
596 sb.AppendFormat(reportFormat, "ID", "NAME", "LAST UPDATE (MS)", "LIFETIME (MS)", "PRIORITY", "STATE");
597 sb.Append(Environment.NewLine);
598
599 foreach (Watchdog.ThreadWatchdogInfo twi in threads)
600 {
601 Thread t = twi.Thread;
602
603 sb.AppendFormat(
604 reportFormat,
605 t.ManagedThreadId,
606 t.Name,
607 timeNow - twi.LastTick,
608 timeNow - twi.FirstTick,
609 t.Priority,
610 t.ThreadState);
611
612 sb.Append("\n");
613 }
614
615 sb.Append("\n");
616
617 // For some reason mono 2.6.7 returns an empty threads set! Not going to confuse people by reporting
618 // zero active threads.
619 int totalThreads = Process.GetCurrentProcess().Threads.Count;
620 if (totalThreads > 0)
621 sb.AppendFormat("Total threads active: {0}\n\n", totalThreads);
622
623 sb.Append("Main threadpool (excluding script engine pools)\n");
624 sb.Append(Util.GetThreadPoolReport());
625
626 return sb.ToString();
627 }
628
629 public virtual void HandleThreadsAbort(string module, string[] cmd)
630 {
631 if (cmd.Length != 3)
632 {
633 MainConsole.Instance.Output("Usage: threads abort <thread-id>");
634 return;
635 }
636
637 int threadId;
638 if (!int.TryParse(cmd[2], out threadId))
639 {
640 MainConsole.Instance.Output("ERROR: Thread id must be an integer");
641 return;
642 }
643
644 if (Watchdog.AbortThread(threadId))
645 MainConsole.Instance.OutputFormat("Aborted thread with id {0}", threadId);
646 else
647 MainConsole.Instance.OutputFormat("ERROR - Thread with id {0} not found in managed threads", threadId);
648 }
649
650 /// <summary>
651 /// Console output is only possible if a console has been established.
652 /// That is something that cannot be determined within this class. So
653 /// all attempts to use the console MUST be verified.
654 /// </summary>
655 /// <param name="msg"></param>
656 protected void Notice(string msg)
657 {
658 if (m_console != null)
659 {
660 m_console.Output(msg);
661 }
662 }
663
664 /// <summary>
665 /// Console output is only possible if a console has been established.
666 /// That is something that cannot be determined within this class. So
667 /// all attempts to use the console MUST be verified.
668 /// </summary>
669 /// <param name="format"></param>
670 /// <param name="components"></param>
671 protected void Notice(string format, params object[] components)
672 {
673 if (m_console != null)
674 m_console.OutputFormat(format, components);
675 }
676 }
677} \ No newline at end of file
diff --git a/OpenSim/Framework/Servers/Tests/OSHttpTests.cs b/OpenSim/Framework/Servers/Tests/OSHttpTests.cs
index dc4eb8f..3412e0f 100644
--- a/OpenSim/Framework/Servers/Tests/OSHttpTests.cs
+++ b/OpenSim/Framework/Servers/Tests/OSHttpTests.cs
@@ -35,11 +35,12 @@ using HttpServer;
35using HttpServer.FormDecoders; 35using HttpServer.FormDecoders;
36using NUnit.Framework; 36using NUnit.Framework;
37using OpenSim.Framework.Servers.HttpServer; 37using OpenSim.Framework.Servers.HttpServer;
38using OpenSim.Tests.Common;
38 39
39namespace OpenSim.Framework.Servers.Tests 40namespace OpenSim.Framework.Servers.Tests
40{ 41{
41 [TestFixture] 42 [TestFixture]
42 public class OSHttpTests 43 public class OSHttpTests : OpenSimTestCase
43 { 44 {
44 // we need an IHttpClientContext for our tests 45 // we need an IHttpClientContext for our tests
45 public class TestHttpClientContext: IHttpClientContext 46 public class TestHttpClientContext: IHttpClientContext
diff --git a/OpenSim/Framework/Servers/Tests/VersionInfoTests.cs b/OpenSim/Framework/Servers/Tests/VersionInfoTests.cs
index 49e5061..480f2bb 100644
--- a/OpenSim/Framework/Servers/Tests/VersionInfoTests.cs
+++ b/OpenSim/Framework/Servers/Tests/VersionInfoTests.cs
@@ -29,11 +29,12 @@ using System;
29using System.Collections.Generic; 29using System.Collections.Generic;
30using System.Text; 30using System.Text;
31using NUnit.Framework; 31using NUnit.Framework;
32using OpenSim.Tests.Common;
32 33
33namespace OpenSim.Framework.Servers.Tests 34namespace OpenSim.Framework.Servers.Tests
34{ 35{
35 [TestFixture] 36 [TestFixture]
36 public class VersionInfoTests 37 public class VersionInfoTests : OpenSimTestCase
37 { 38 {
38 [Test] 39 [Test]
39 public void TestVersionLength() 40 public void TestVersionLength()