diff options
Diffstat (limited to '')
-rw-r--r-- | OpenSim/Framework/Servers/BaseOpenSimServer.cs | 455 |
1 files changed, 16 insertions, 439 deletions
diff --git a/OpenSim/Framework/Servers/BaseOpenSimServer.cs b/OpenSim/Framework/Servers/BaseOpenSimServer.cs index aac9c45..c0dc907 100644 --- a/OpenSim/Framework/Servers/BaseOpenSimServer.cs +++ b/OpenSim/Framework/Servers/BaseOpenSimServer.cs | |||
@@ -27,7 +27,6 @@ | |||
27 | 27 | ||
28 | using System; | 28 | using System; |
29 | using System.Collections.Generic; | 29 | using System.Collections.Generic; |
30 | using System.Diagnostics; | ||
31 | using System.IO; | 30 | using System.IO; |
32 | using System.Reflection; | 31 | using System.Reflection; |
33 | using System.Text; | 32 | using System.Text; |
@@ -38,6 +37,8 @@ using log4net; | |||
38 | using log4net.Appender; | 37 | using log4net.Appender; |
39 | using log4net.Core; | 38 | using log4net.Core; |
40 | using log4net.Repository; | 39 | using log4net.Repository; |
40 | using OpenMetaverse; | ||
41 | using OpenMetaverse.StructuredData; | ||
41 | using OpenSim.Framework; | 42 | using OpenSim.Framework; |
42 | using OpenSim.Framework.Console; | 43 | using OpenSim.Framework.Console; |
43 | using OpenSim.Framework.Monitoring; | 44 | using OpenSim.Framework.Monitoring; |
@@ -45,16 +46,12 @@ using OpenSim.Framework.Servers; | |||
45 | using OpenSim.Framework.Servers.HttpServer; | 46 | using OpenSim.Framework.Servers.HttpServer; |
46 | using Timer=System.Timers.Timer; | 47 | using Timer=System.Timers.Timer; |
47 | 48 | ||
48 | using OpenMetaverse; | ||
49 | using OpenMetaverse.StructuredData; | ||
50 | |||
51 | |||
52 | namespace OpenSim.Framework.Servers | 49 | namespace 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,72 +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 | 91 | ||
187 | m_console.Commands.AddCommand("General", false, "threads abort", | 92 | RegisterCommonCommands(); |
188 | "threads abort <thread-id>", | 93 | |
189 | "Abort a managed thread. Use \"show threads\" to find possible threads.", HandleThreadsAbort); | 94 | m_console.Commands.AddCommand("General", false, "quit", |
95 | "quit", | ||
96 | "Quit the application", HandleQuit); | ||
190 | 97 | ||
191 | m_console.Commands.AddCommand("General", false, "threads show", | 98 | m_console.Commands.AddCommand("General", false, "shutdown", |
192 | "threads show", | 99 | "shutdown", |
193 | "Show thread status. Synonym for \"show threads\"", | 100 | "Quit the application", HandleQuit); |
194 | (string module, string[] args) => Notice(GetThreadsReport())); | ||
195 | } | ||
196 | } | 101 | } |
197 | 102 | ||
198 | /// <summary> | 103 | /// <summary> |
@@ -225,74 +130,11 @@ namespace OpenSim.Framework.Servers | |||
225 | } | 130 | } |
226 | 131 | ||
227 | /// <summary> | 132 | /// <summary> |
228 | /// Get a report about the registered threads in this server. | ||
229 | /// </summary> | ||
230 | protected string GetThreadsReport() | ||
231 | { | ||
232 | // This should be a constant field. | ||
233 | string reportFormat = "{0,6} {1,35} {2,16} {3,13} {4,10} {5,30}"; | ||
234 | |||
235 | StringBuilder sb = new StringBuilder(); | ||
236 | Watchdog.ThreadWatchdogInfo[] threads = Watchdog.GetThreadsInfo(); | ||
237 | |||
238 | sb.Append(threads.Length + " threads are being tracked:" + Environment.NewLine); | ||
239 | |||
240 | int timeNow = Environment.TickCount & Int32.MaxValue; | ||
241 | |||
242 | sb.AppendFormat(reportFormat, "ID", "NAME", "LAST UPDATE (MS)", "LIFETIME (MS)", "PRIORITY", "STATE"); | ||
243 | sb.Append(Environment.NewLine); | ||
244 | |||
245 | foreach (Watchdog.ThreadWatchdogInfo twi in threads) | ||
246 | { | ||
247 | Thread t = twi.Thread; | ||
248 | |||
249 | sb.AppendFormat( | ||
250 | reportFormat, | ||
251 | t.ManagedThreadId, | ||
252 | t.Name, | ||
253 | timeNow - twi.LastTick, | ||
254 | timeNow - twi.FirstTick, | ||
255 | t.Priority, | ||
256 | t.ThreadState); | ||
257 | |||
258 | sb.Append("\n"); | ||
259 | } | ||
260 | |||
261 | sb.Append("\n"); | ||
262 | |||
263 | // For some reason mono 2.6.7 returns an empty threads set! Not going to confuse people by reporting | ||
264 | // zero active threads. | ||
265 | int totalThreads = Process.GetCurrentProcess().Threads.Count; | ||
266 | if (totalThreads > 0) | ||
267 | sb.AppendFormat("Total threads active: {0}\n\n", totalThreads); | ||
268 | |||
269 | sb.Append("Main threadpool (excluding script engine pools)\n"); | ||
270 | sb.Append(Util.GetThreadPoolReport()); | ||
271 | |||
272 | return sb.ToString(); | ||
273 | } | ||
274 | |||
275 | /// <summary> | ||
276 | /// Return a report about the uptime of this server | ||
277 | /// </summary> | ||
278 | /// <returns></returns> | ||
279 | protected string GetUptimeReport() | ||
280 | { | ||
281 | StringBuilder sb = new StringBuilder(String.Format("Time now is {0}\n", DateTime.Now)); | ||
282 | sb.Append(String.Format("Server has been running since {0}, {1}\n", m_startuptime.DayOfWeek, m_startuptime)); | ||
283 | sb.Append(String.Format("That is an elapsed time of {0}\n", DateTime.Now - m_startuptime)); | ||
284 | |||
285 | return sb.ToString(); | ||
286 | } | ||
287 | |||
288 | /// <summary> | ||
289 | /// Performs initialisation of the scene, such as loading configuration from disk. | 133 | /// Performs initialisation of the scene, such as loading configuration from disk. |
290 | /// </summary> | 134 | /// </summary> |
291 | public virtual void Startup() | 135 | public virtual void Startup() |
292 | { | 136 | { |
293 | m_log.Info("[STARTUP]: Beginning startup processing"); | 137 | m_log.Info("[STARTUP]: Beginning startup processing"); |
294 | |||
295 | EnhanceVersionInformation(); | ||
296 | 138 | ||
297 | m_log.Info("[STARTUP]: OpenSimulator version: " + m_version + Environment.NewLine); | 139 | m_log.Info("[STARTUP]: OpenSimulator version: " + m_version + Environment.NewLine); |
298 | // 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 |
@@ -327,257 +169,7 @@ namespace OpenSim.Framework.Servers | |||
327 | private void HandleQuit(string module, string[] args) | 169 | private void HandleQuit(string module, string[] args) |
328 | { | 170 | { |
329 | Shutdown(); | 171 | Shutdown(); |
330 | } | 172 | } |
331 | |||
332 | private void HandleLogLevel(string module, string[] cmd) | ||
333 | { | ||
334 | if (null == m_consoleAppender) | ||
335 | { | ||
336 | Notice("No appender named Console found (see the log4net config file for this executable)!"); | ||
337 | return; | ||
338 | } | ||
339 | |||
340 | if (cmd.Length > 3) | ||
341 | { | ||
342 | string rawLevel = cmd[3]; | ||
343 | |||
344 | ILoggerRepository repository = LogManager.GetRepository(); | ||
345 | Level consoleLevel = repository.LevelMap[rawLevel]; | ||
346 | |||
347 | if (consoleLevel != null) | ||
348 | m_consoleAppender.Threshold = consoleLevel; | ||
349 | else | ||
350 | Notice( | ||
351 | String.Format( | ||
352 | "{0} is not a valid logging level. Valid logging levels are ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF", | ||
353 | rawLevel)); | ||
354 | } | ||
355 | |||
356 | Notice(String.Format("Console log level is {0}", m_consoleAppender.Threshold)); | ||
357 | } | ||
358 | |||
359 | /// <summary> | ||
360 | /// Show help information | ||
361 | /// </summary> | ||
362 | /// <param name="helpArgs"></param> | ||
363 | protected virtual void ShowHelp(string[] helpArgs) | ||
364 | { | ||
365 | Notice(""); | ||
366 | |||
367 | if (helpArgs.Length == 0) | ||
368 | { | ||
369 | Notice("set log level [level] - change the console logging level only. For example, off or debug."); | ||
370 | Notice("show info - show server information (e.g. startup path)."); | ||
371 | Notice("show threads - list tracked threads"); | ||
372 | Notice("show uptime - show server startup time and uptime."); | ||
373 | Notice("show version - show server version."); | ||
374 | Notice(""); | ||
375 | |||
376 | return; | ||
377 | } | ||
378 | } | ||
379 | |||
380 | public virtual void HandleShow(string module, string[] cmd) | ||
381 | { | ||
382 | List<string> args = new List<string>(cmd); | ||
383 | |||
384 | args.RemoveAt(0); | ||
385 | |||
386 | string[] showParams = args.ToArray(); | ||
387 | |||
388 | switch (showParams[0]) | ||
389 | { | ||
390 | case "info": | ||
391 | ShowInfo(); | ||
392 | break; | ||
393 | |||
394 | case "threads": | ||
395 | Notice(GetThreadsReport()); | ||
396 | break; | ||
397 | |||
398 | case "uptime": | ||
399 | Notice(GetUptimeReport()); | ||
400 | break; | ||
401 | |||
402 | case "version": | ||
403 | Notice(GetVersionText()); | ||
404 | break; | ||
405 | } | ||
406 | } | ||
407 | |||
408 | public virtual void HandleThreadsAbort(string module, string[] cmd) | ||
409 | { | ||
410 | if (cmd.Length != 3) | ||
411 | { | ||
412 | MainConsole.Instance.Output("Usage: threads abort <thread-id>"); | ||
413 | return; | ||
414 | } | ||
415 | |||
416 | int threadId; | ||
417 | if (!int.TryParse(cmd[2], out threadId)) | ||
418 | { | ||
419 | MainConsole.Instance.Output("ERROR: Thread id must be an integer"); | ||
420 | return; | ||
421 | } | ||
422 | |||
423 | if (Watchdog.AbortThread(threadId)) | ||
424 | MainConsole.Instance.OutputFormat("Aborted thread with id {0}", threadId); | ||
425 | else | ||
426 | MainConsole.Instance.OutputFormat("ERROR - Thread with id {0} not found in managed threads", threadId); | ||
427 | } | ||
428 | |||
429 | protected void ShowInfo() | ||
430 | { | ||
431 | Notice(GetVersionText()); | ||
432 | Notice("Startup directory: " + m_startupDirectory); | ||
433 | if (null != m_consoleAppender) | ||
434 | Notice(String.Format("Console log level: {0}", m_consoleAppender.Threshold)); | ||
435 | } | ||
436 | |||
437 | protected string GetVersionText() | ||
438 | { | ||
439 | return String.Format("Version: {0} (interface version {1})", m_version, VersionInfo.MajorInterfaceVersion); | ||
440 | } | ||
441 | |||
442 | /// <summary> | ||
443 | /// Console output is only possible if a console has been established. | ||
444 | /// That is something that cannot be determined within this class. So | ||
445 | /// all attempts to use the console MUST be verified. | ||
446 | /// </summary> | ||
447 | /// <param name="msg"></param> | ||
448 | protected void Notice(string msg) | ||
449 | { | ||
450 | if (m_console != null) | ||
451 | { | ||
452 | m_console.Output(msg); | ||
453 | } | ||
454 | } | ||
455 | |||
456 | /// <summary> | ||
457 | /// Console output is only possible if a console has been established. | ||
458 | /// That is something that cannot be determined within this class. So | ||
459 | /// all attempts to use the console MUST be verified. | ||
460 | /// </summary> | ||
461 | /// <param name="format"></param> | ||
462 | /// <param name="components"></param> | ||
463 | protected void Notice(string format, params string[] components) | ||
464 | { | ||
465 | if (m_console != null) | ||
466 | m_console.OutputFormat(format, components); | ||
467 | } | ||
468 | |||
469 | /// <summary> | ||
470 | /// Enhance the version string with extra information if it's available. | ||
471 | /// </summary> | ||
472 | protected void EnhanceVersionInformation() | ||
473 | { | ||
474 | string buildVersion = string.Empty; | ||
475 | |||
476 | // The subversion information is deprecated and will be removed at a later date | ||
477 | // Add subversion revision information if available | ||
478 | // Try file "svn_revision" in the current directory first, then the .svn info. | ||
479 | // This allows to make the revision available in simulators not running from the source tree. | ||
480 | // FIXME: Making an assumption about the directory we're currently in - we do this all over the place | ||
481 | // elsewhere as well | ||
482 | string gitDir = "../.git/"; | ||
483 | string gitRefPointerPath = gitDir + "HEAD"; | ||
484 | |||
485 | string svnRevisionFileName = "svn_revision"; | ||
486 | string svnFileName = ".svn/entries"; | ||
487 | string manualVersionFileName = ".version"; | ||
488 | string inputLine; | ||
489 | int strcmp; | ||
490 | |||
491 | if (File.Exists(manualVersionFileName)) | ||
492 | { | ||
493 | using (StreamReader CommitFile = File.OpenText(manualVersionFileName)) | ||
494 | buildVersion = CommitFile.ReadLine(); | ||
495 | |||
496 | m_version += buildVersion ?? ""; | ||
497 | } | ||
498 | else if (File.Exists(gitRefPointerPath)) | ||
499 | { | ||
500 | // m_log.DebugFormat("[OPENSIM]: Found {0}", gitRefPointerPath); | ||
501 | |||
502 | string rawPointer = ""; | ||
503 | |||
504 | using (StreamReader pointerFile = File.OpenText(gitRefPointerPath)) | ||
505 | rawPointer = pointerFile.ReadLine(); | ||
506 | |||
507 | // m_log.DebugFormat("[OPENSIM]: rawPointer [{0}]", rawPointer); | ||
508 | |||
509 | Match m = Regex.Match(rawPointer, "^ref: (.+)$"); | ||
510 | |||
511 | if (m.Success) | ||
512 | { | ||
513 | // m_log.DebugFormat("[OPENSIM]: Matched [{0}]", m.Groups[1].Value); | ||
514 | |||
515 | string gitRef = m.Groups[1].Value; | ||
516 | string gitRefPath = gitDir + gitRef; | ||
517 | if (File.Exists(gitRefPath)) | ||
518 | { | ||
519 | // m_log.DebugFormat("[OPENSIM]: Found gitRefPath [{0}]", gitRefPath); | ||
520 | |||
521 | using (StreamReader refFile = File.OpenText(gitRefPath)) | ||
522 | { | ||
523 | string gitHash = refFile.ReadLine(); | ||
524 | m_version += gitHash.Substring(0, 7); | ||
525 | } | ||
526 | } | ||
527 | } | ||
528 | } | ||
529 | else | ||
530 | { | ||
531 | // Remove the else logic when subversion mirror is no longer used | ||
532 | if (File.Exists(svnRevisionFileName)) | ||
533 | { | ||
534 | StreamReader RevisionFile = File.OpenText(svnRevisionFileName); | ||
535 | buildVersion = RevisionFile.ReadLine(); | ||
536 | buildVersion.Trim(); | ||
537 | RevisionFile.Close(); | ||
538 | } | ||
539 | |||
540 | if (string.IsNullOrEmpty(buildVersion) && File.Exists(svnFileName)) | ||
541 | { | ||
542 | StreamReader EntriesFile = File.OpenText(svnFileName); | ||
543 | inputLine = EntriesFile.ReadLine(); | ||
544 | while (inputLine != null) | ||
545 | { | ||
546 | // using the dir svn revision at the top of entries file | ||
547 | strcmp = String.Compare(inputLine, "dir"); | ||
548 | if (strcmp == 0) | ||
549 | { | ||
550 | buildVersion = EntriesFile.ReadLine(); | ||
551 | break; | ||
552 | } | ||
553 | else | ||
554 | { | ||
555 | inputLine = EntriesFile.ReadLine(); | ||
556 | } | ||
557 | } | ||
558 | EntriesFile.Close(); | ||
559 | } | ||
560 | |||
561 | m_version += string.IsNullOrEmpty(buildVersion) ? " " : ("." + buildVersion + " ").Substring(0, 6); | ||
562 | } | ||
563 | } | ||
564 | |||
565 | protected void CreatePIDFile(string path) | ||
566 | { | ||
567 | try | ||
568 | { | ||
569 | string pidstring = System.Diagnostics.Process.GetCurrentProcess().Id.ToString(); | ||
570 | FileStream fs = File.Create(path); | ||
571 | |||
572 | Byte[] buf = Encoding.ASCII.GetBytes(pidstring); | ||
573 | fs.Write(buf, 0, buf.Length); | ||
574 | fs.Close(); | ||
575 | m_pidFile = path; | ||
576 | } | ||
577 | catch (Exception) | ||
578 | { | ||
579 | } | ||
580 | } | ||
581 | 173 | ||
582 | public string osSecret { | 174 | public string osSecret { |
583 | // Secret uuid for the simulator | 175 | // Secret uuid for the simulator |
@@ -596,20 +188,5 @@ namespace OpenSim.Framework.Servers | |||
596 | return StatsManager.SimExtraStats.XReport((DateTime.Now - m_startuptime).ToString() , m_version); | 188 | return StatsManager.SimExtraStats.XReport((DateTime.Now - m_startuptime).ToString() , m_version); |
597 | } | 189 | } |
598 | } | 190 | } |
599 | |||
600 | protected void RemovePIDFile() | ||
601 | { | ||
602 | if (m_pidFile != String.Empty) | ||
603 | { | ||
604 | try | ||
605 | { | ||
606 | File.Delete(m_pidFile); | ||
607 | m_pidFile = String.Empty; | ||
608 | } | ||
609 | catch (Exception) | ||
610 | { | ||
611 | } | ||
612 | } | ||
613 | } | ||
614 | } | 191 | } |
615 | } | 192 | } \ No newline at end of file |