aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Data/SQLite/SQLiteRegionData.cs
diff options
context:
space:
mode:
authorMelanie2010-06-07 05:23:52 +0100
committerMelanie2010-06-07 05:23:52 +0100
commitcc2622660bad93ef46844476e434b6640a3a3643 (patch)
treeaa0b0f36bcbfb9bd44fe396e1cd98f8de26f2255 /OpenSim/Data/SQLite/SQLiteRegionData.cs
parentGosh, fixing another nullref issue in cmGetAvatarList. I deserve spanks. (diff)
parentRevert "commit code which stops full updates being fired multiple times when ... (diff)
downloadopensim-SC-cc2622660bad93ef46844476e434b6640a3a3643.zip
opensim-SC-cc2622660bad93ef46844476e434b6640a3a3643.tar.gz
opensim-SC-cc2622660bad93ef46844476e434b6640a3a3643.tar.bz2
opensim-SC-cc2622660bad93ef46844476e434b6640a3a3643.tar.xz
Merge branch '0.6.9-post-fixes' into careminster
Diffstat (limited to 'OpenSim/Data/SQLite/SQLiteRegionData.cs')
-rw-r--r--OpenSim/Data/SQLite/SQLiteRegionData.cs311
1 files changed, 190 insertions, 121 deletions
diff --git a/OpenSim/Data/SQLite/SQLiteRegionData.cs b/OpenSim/Data/SQLite/SQLiteRegionData.cs
index 81c0703..8ea6bfb 100644
--- a/OpenSim/Data/SQLite/SQLiteRegionData.cs
+++ b/OpenSim/Data/SQLite/SQLiteRegionData.cs
@@ -32,7 +32,7 @@ using System.Drawing;
32using System.IO; 32using System.IO;
33using System.Reflection; 33using System.Reflection;
34using log4net; 34using log4net;
35using Mono.Data.SqliteClient; 35using Mono.Data.Sqlite;
36using OpenMetaverse; 36using OpenMetaverse;
37using OpenSim.Framework; 37using OpenSim.Framework;
38using OpenSim.Region.Framework.Interfaces; 38using OpenSim.Region.Framework.Interfaces;
@@ -87,119 +87,151 @@ namespace OpenSim.Data.SQLite
87 /// <param name="connectionString">the connection string</param> 87 /// <param name="connectionString">the connection string</param>
88 public void Initialise(string connectionString) 88 public void Initialise(string connectionString)
89 { 89 {
90 m_connectionString = connectionString; 90 try
91 {
92 m_connectionString = connectionString;
91 93
92 ds = new DataSet(); 94 ds = new DataSet("Region");
93 95
94 m_log.Info("[REGION DB]: Sqlite - connecting: " + connectionString); 96 m_log.Info("[SQLITE REGION DB]: Sqlite - connecting: " + connectionString);
95 m_conn = new SqliteConnection(m_connectionString); 97 m_conn = new SqliteConnection(m_connectionString);
96 m_conn.Open(); 98 m_conn.Open();
97 99
100 SqliteCommand primSelectCmd = new SqliteCommand(primSelect, m_conn);
101 primDa = new SqliteDataAdapter(primSelectCmd);
98 102
103 SqliteCommand shapeSelectCmd = new SqliteCommand(shapeSelect, m_conn);
104 shapeDa = new SqliteDataAdapter(shapeSelectCmd);
105 // SqliteCommandBuilder shapeCb = new SqliteCommandBuilder(shapeDa);
99 106
100 SqliteCommand primSelectCmd = new SqliteCommand(primSelect, m_conn); 107 SqliteCommand itemsSelectCmd = new SqliteCommand(itemsSelect, m_conn);
101 primDa = new SqliteDataAdapter(primSelectCmd); 108 itemsDa = new SqliteDataAdapter(itemsSelectCmd);
102 // SqliteCommandBuilder primCb = new SqliteCommandBuilder(primDa);
103 109
104 SqliteCommand shapeSelectCmd = new SqliteCommand(shapeSelect, m_conn); 110 SqliteCommand terrainSelectCmd = new SqliteCommand(terrainSelect, m_conn);
105 shapeDa = new SqliteDataAdapter(shapeSelectCmd); 111 terrainDa = new SqliteDataAdapter(terrainSelectCmd);
106 // SqliteCommandBuilder shapeCb = new SqliteCommandBuilder(shapeDa);
107 112
108 SqliteCommand itemsSelectCmd = new SqliteCommand(itemsSelect, m_conn); 113 SqliteCommand landSelectCmd = new SqliteCommand(landSelect, m_conn);
109 itemsDa = new SqliteDataAdapter(itemsSelectCmd); 114 landDa = new SqliteDataAdapter(landSelectCmd);
110 115
111 SqliteCommand terrainSelectCmd = new SqliteCommand(terrainSelect, m_conn); 116 SqliteCommand landAccessListSelectCmd = new SqliteCommand(landAccessListSelect, m_conn);
112 terrainDa = new SqliteDataAdapter(terrainSelectCmd); 117 landAccessListDa = new SqliteDataAdapter(landAccessListSelectCmd);
113 118
114 SqliteCommand landSelectCmd = new SqliteCommand(landSelect, m_conn); 119 SqliteCommand regionSettingsSelectCmd = new SqliteCommand(regionSettingsSelect, m_conn);
115 landDa = new SqliteDataAdapter(landSelectCmd); 120 regionSettingsDa = new SqliteDataAdapter(regionSettingsSelectCmd);
121 // This actually does the roll forward assembly stuff
122 Assembly assem = GetType().Assembly;
123 Migration m = new Migration(m_conn, assem, "RegionStore");
124 m.Update();
116 125
117 SqliteCommand landAccessListSelectCmd = new SqliteCommand(landAccessListSelect, m_conn); 126 lock (ds)
118 landAccessListDa = new SqliteDataAdapter(landAccessListSelectCmd); 127 {
128 ds.Tables.Add(createPrimTable());
129 setupPrimCommands(primDa, m_conn);
119 130
120 SqliteCommand regionSettingsSelectCmd = new SqliteCommand(regionSettingsSelect, m_conn); 131 ds.Tables.Add(createShapeTable());
121 regionSettingsDa = new SqliteDataAdapter(regionSettingsSelectCmd); 132 setupShapeCommands(shapeDa, m_conn);
122 // This actually does the roll forward assembly stuff
123 Assembly assem = GetType().Assembly;
124 Migration m = new Migration(m_conn, assem, "RegionStore");
125 m.Update();
126 133
127 lock (ds) 134 ds.Tables.Add(createItemsTable());
128 { 135 setupItemsCommands(itemsDa, m_conn);
129 ds.Tables.Add(createPrimTable());
130 setupPrimCommands(primDa, m_conn);
131 primDa.Fill(ds.Tables["prims"]);
132 136
133 ds.Tables.Add(createShapeTable()); 137 ds.Tables.Add(createTerrainTable());
134 setupShapeCommands(shapeDa, m_conn); 138 setupTerrainCommands(terrainDa, m_conn);
135 139
136 ds.Tables.Add(createItemsTable()); 140 ds.Tables.Add(createLandTable());
137 setupItemsCommands(itemsDa, m_conn); 141 setupLandCommands(landDa, m_conn);
138 itemsDa.Fill(ds.Tables["primitems"]);
139 142
140 ds.Tables.Add(createTerrainTable()); 143 ds.Tables.Add(createLandAccessListTable());
141 setupTerrainCommands(terrainDa, m_conn); 144 setupLandAccessCommands(landAccessListDa, m_conn);
142 145
143 ds.Tables.Add(createLandTable()); 146 ds.Tables.Add(createRegionSettingsTable());
144 setupLandCommands(landDa, m_conn); 147 setupRegionSettingsCommands(regionSettingsDa, m_conn);
145 148
146 ds.Tables.Add(createLandAccessListTable()); 149 // WORKAROUND: This is a work around for sqlite on
147 setupLandAccessCommands(landAccessListDa, m_conn); 150 // windows, which gets really unhappy with blob columns
151 // that have no sample data in them. At some point we
152 // need to actually find a proper way to handle this.
153 try
154 {
155 primDa.Fill(ds.Tables["prims"]);
156 }
157 catch (Exception)
158 {
159 m_log.Info("[SQLITE REGION DB]: Caught fill error on prims table");
160 }
148 161
149 ds.Tables.Add(createRegionSettingsTable()); 162 try
150 163 {
151 setupRegionSettingsCommands(regionSettingsDa, m_conn); 164 shapeDa.Fill(ds.Tables["primshapes"]);
165 }
166 catch (Exception)
167 {
168 m_log.Info("[SQLITE REGION DB]: Caught fill error on primshapes table");
169 }
152 170
153 // WORKAROUND: This is a work around for sqlite on 171 try
154 // windows, which gets really unhappy with blob columns 172 {
155 // that have no sample data in them. At some point we 173 itemsDa.Fill(ds.Tables["primitems"]);
156 // need to actually find a proper way to handle this. 174 }
157 try 175 catch (Exception)
158 { 176 {
159 shapeDa.Fill(ds.Tables["primshapes"]); 177 m_log.Info("[SQLITE REGION DB]: Caught fill error on primitems table");
160 } 178 }
161 catch (Exception) 179
162 { 180 try
163 m_log.Info("[REGION DB]: Caught fill error on primshapes table"); 181 {
164 } 182 terrainDa.Fill(ds.Tables["terrain"]);
183 }
184 catch (Exception)
185 {
186 m_log.Info("[SQLITE REGION DB]: Caught fill error on terrain table");
187 }
165 188
166 try 189 try
167 { 190 {
168 terrainDa.Fill(ds.Tables["terrain"]); 191 landDa.Fill(ds.Tables["land"]);
169 } 192 }
170 catch (Exception) 193 catch (Exception)
171 { 194 {
172 m_log.Info("[REGION DB]: Caught fill error on terrain table"); 195 m_log.Info("[SQLITE REGION DB]: Caught fill error on land table");
173 } 196 }
174 197
175 try 198 try
176 { 199 {
177 landDa.Fill(ds.Tables["land"]); 200 landAccessListDa.Fill(ds.Tables["landaccesslist"]);
178 } 201 }
179 catch (Exception) 202 catch (Exception)
180 { 203 {
181 m_log.Info("[REGION DB]: Caught fill error on land table"); 204 m_log.Info("[SQLITE REGION DB]: Caught fill error on landaccesslist table");
182 } 205 }
183 206
184 try 207 try
185 { 208 {
186 landAccessListDa.Fill(ds.Tables["landaccesslist"]); 209 regionSettingsDa.Fill(ds.Tables["regionsettings"]);
187 } 210 }
188 catch (Exception) 211 catch (Exception)
189 { 212 {
190 m_log.Info("[REGION DB]: Caught fill error on landaccesslist table"); 213 m_log.Info("[SQLITE REGION DB]: Caught fill error on regionsettings table");
191 } 214 }
192 215
193 try 216 // We have to create a data set mapping for every table, otherwise the IDataAdaptor.Update() will not populate rows with values!
194 { 217 // Not sure exactly why this is - this kind of thing was not necessary before - justincc 20100409
195 regionSettingsDa.Fill(ds.Tables["regionsettings"]); 218 // Possibly because we manually set up our own DataTables before connecting to the database
196 } 219 CreateDataSetMapping(primDa, "prims");
197 catch (Exception) 220 CreateDataSetMapping(shapeDa, "primshapes");
198 { 221 CreateDataSetMapping(itemsDa, "primitems");
199 m_log.Info("[REGION DB]: Caught fill error on regionsettings table"); 222 CreateDataSetMapping(terrainDa, "terrain");
223 CreateDataSetMapping(landDa, "land");
224 CreateDataSetMapping(landAccessListDa, "landaccesslist");
225 CreateDataSetMapping(regionSettingsDa, "regionsettings");
200 } 226 }
201 return;
202 } 227 }
228 catch (Exception e)
229 {
230 m_log.Error(e);
231 Environment.Exit(23);
232 }
233
234 return;
203 } 235 }
204 236
205 public void Dispose() 237 public void Dispose()
@@ -411,7 +443,7 @@ namespace OpenSim.Data.SQLite
411 lock (ds) 443 lock (ds)
412 { 444 {
413 DataRow[] primsForRegion = prims.Select(byRegion); 445 DataRow[] primsForRegion = prims.Select(byRegion);
414 m_log.Info("[REGION DB]: Loaded " + primsForRegion.Length + " prims for region: " + regionUUID); 446// m_log.Info("[SQLITE REGION DB]: Loaded " + primsForRegion.Length + " prims for region: " + regionUUID);
415 447
416 // First, create all groups 448 // First, create all groups
417 foreach (DataRow primRow in primsForRegion) 449 foreach (DataRow primRow in primsForRegion)
@@ -433,8 +465,8 @@ namespace OpenSim.Data.SQLite
433 } 465 }
434 else 466 else
435 { 467 {
436 m_log.Info( 468 m_log.Warn(
437 "[REGION DB]: No shape found for prim in storage, so setting default box shape"); 469 "[SQLITE REGION DB]: No shape found for prim in storage, so setting default box shape");
438 prim.Shape = PrimitiveBaseShape.Default; 470 prim.Shape = PrimitiveBaseShape.Default;
439 } 471 }
440 472
@@ -446,11 +478,11 @@ namespace OpenSim.Data.SQLite
446 } 478 }
447 catch (Exception e) 479 catch (Exception e)
448 { 480 {
449 m_log.Error("[REGION DB]: Failed create prim object in new group, exception and data follows"); 481 m_log.Error("[SQLITE REGION DB]: Failed create prim object in new group, exception and data follows");
450 m_log.Info("[REGION DB]: " + e.ToString()); 482 m_log.Error("[SQLITE REGION DB]: ", e);
451 foreach (DataColumn col in prims.Columns) 483 foreach (DataColumn col in prims.Columns)
452 { 484 {
453 m_log.Info("[REGION DB]: Col: " + col.ColumnName + " => " + primRow[col]); 485 m_log.Error("[SQLITE REGION DB]: Col: " + col.ColumnName + " => " + primRow[col]);
454 } 486 }
455 } 487 }
456 } 488 }
@@ -475,7 +507,7 @@ namespace OpenSim.Data.SQLite
475 else 507 else
476 { 508 {
477 m_log.Warn( 509 m_log.Warn(
478 "[REGION DB]: No shape found for prim in storage, so setting default box shape"); 510 "[SQLITE REGION DB]: No shape found for prim in storage, so setting default box shape");
479 prim.Shape = PrimitiveBaseShape.Default; 511 prim.Shape = PrimitiveBaseShape.Default;
480 } 512 }
481 513
@@ -485,11 +517,11 @@ namespace OpenSim.Data.SQLite
485 } 517 }
486 catch (Exception e) 518 catch (Exception e)
487 { 519 {
488 m_log.Error("[REGION DB]: Failed create prim object in group, exception and data follows"); 520 m_log.Error("[SQLITE REGION DB]: Failed create prim object in group, exception and data follows");
489 m_log.Info("[REGION DB]: " + e.ToString()); 521 m_log.Error("[SQLITE REGION DB]: ", e);
490 foreach (DataColumn col in prims.Columns) 522 foreach (DataColumn col in prims.Columns)
491 { 523 {
492 m_log.Info("[REGION DB]: Col: " + col.ColumnName + " => " + primRow[col]); 524 m_log.Error("[SQLITE REGION DB]: Col: " + col.ColumnName + " => " + primRow[col]);
493 } 525 }
494 } 526 }
495 } 527 }
@@ -502,20 +534,23 @@ namespace OpenSim.Data.SQLite
502 /// </summary> 534 /// </summary>
503 /// <param name="prim">the prim</param> 535 /// <param name="prim">the prim</param>
504 private void LoadItems(SceneObjectPart prim) 536 private void LoadItems(SceneObjectPart prim)
505 { 537 {
506 //m_log.DebugFormat("[DATASTORE]: Loading inventory for {0}, {1}", prim.Name, prim.UUID); 538// m_log.DebugFormat("[SQLITE REGION DB]: Loading inventory for {0} {1}", prim.Name, prim.UUID);
507 539
508 DataTable dbItems = ds.Tables["primitems"]; 540 DataTable dbItems = ds.Tables["primitems"];
509 String sql = String.Format("primID = '{0}'", prim.UUID.ToString()); 541 String sql = String.Format("primID = '{0}'", prim.UUID.ToString());
510 DataRow[] dbItemRows = dbItems.Select(sql); 542 DataRow[] dbItemRows = dbItems.Select(sql);
511 IList<TaskInventoryItem> inventory = new List<TaskInventoryItem>(); 543 IList<TaskInventoryItem> inventory = new List<TaskInventoryItem>();
512 544
545// m_log.DebugFormat(
546// "[SQLITE REGION DB]: Found {0} items for {1} {2}", dbItemRows.Length, prim.Name, prim.UUID);
547
513 foreach (DataRow row in dbItemRows) 548 foreach (DataRow row in dbItemRows)
514 { 549 {
515 TaskInventoryItem item = buildItem(row); 550 TaskInventoryItem item = buildItem(row);
516 inventory.Add(item); 551 inventory.Add(item);
517 552
518 //m_log.DebugFormat("[DATASTORE]: Restored item {0}, {1}", item.Name, item.ItemID); 553// m_log.DebugFormat("[SQLITE REGION DB]: Restored item {0} {1}", item.Name, item.ItemID);
519 } 554 }
520 555
521 prim.Inventory.RestoreInventoryItems(inventory); 556 prim.Inventory.RestoreInventoryItems(inventory);
@@ -551,7 +586,7 @@ namespace OpenSim.Data.SQLite
551 586
552 // the following is an work around for .NET. The perf 587 // the following is an work around for .NET. The perf
553 // issues associated with it aren't as bad as you think. 588 // issues associated with it aren't as bad as you think.
554 m_log.Info("[REGION DB]: Storing terrain revision r" + revision.ToString()); 589 m_log.Debug("[SQLITE REGION DB]: Storing terrain revision r" + revision.ToString());
555 String sql = "insert into terrain(RegionUUID, Revision, Heightfield)" + 590 String sql = "insert into terrain(RegionUUID, Revision, Heightfield)" +
556 " values(:RegionUUID, :Revision, :Heightfield)"; 591 " values(:RegionUUID, :Revision, :Heightfield)";
557 592
@@ -603,15 +638,15 @@ namespace OpenSim.Data.SQLite
603 } 638 }
604 } 639 }
605 } 640 }
606 rev = (int) row["Revision"]; 641 rev = Convert.ToInt32(row["Revision"]);
607 } 642 }
608 else 643 else
609 { 644 {
610 m_log.Info("[REGION DB]: No terrain found for region"); 645 m_log.Warn("[SQLITE REGION DB]: No terrain found for region");
611 return null; 646 return null;
612 } 647 }
613 648
614 m_log.Info("[REGION DB]: Loaded terrain revision r" + rev.ToString()); 649 m_log.Debug("[SQLITE REGION DB]: Loaded terrain revision r" + rev.ToString());
615 } 650 }
616 } 651 }
617 return terret; 652 return terret;
@@ -755,6 +790,7 @@ namespace OpenSim.Data.SQLite
755 /// </summary> 790 /// </summary>
756 public void Commit() 791 public void Commit()
757 { 792 {
793 //m_log.Debug("[SQLITE]: Starting commit");
758 lock (ds) 794 lock (ds)
759 { 795 {
760 primDa.Update(ds, "prims"); 796 primDa.Update(ds, "prims");
@@ -769,18 +805,11 @@ namespace OpenSim.Data.SQLite
769 { 805 {
770 regionSettingsDa.Update(ds, "regionsettings"); 806 regionSettingsDa.Update(ds, "regionsettings");
771 } 807 }
772 catch (SqliteExecutionException SqlEx) 808 catch (SqliteException SqlEx)
773 { 809 {
774 if (SqlEx.Message.Contains("logic error")) 810 throw new Exception(
775 { 811 "There was a SQL error or connection string configuration error when saving the region settings. This could be a bug, it could also happen if ConnectionString is defined in the [DatabaseService] section of StandaloneCommon.ini in the config_include folder. This could also happen if the config_include folder doesn't exist or if the OpenSim.ini [Architecture] section isn't set. If this is your first time running OpenSimulator, please restart the simulator and bug a developer to fix this!",
776 throw new Exception( 812 SqlEx);
777 "There was a SQL error or connection string configuration error when saving the region settings. This could be a bug, it could also happen if ConnectionString is defined in the [DatabaseService] section of StandaloneCommon.ini in the config_include folder. This could also happen if the config_include folder doesn't exist or if the OpenSim.ini [Architecture] section isn't set. If this is your first time running OpenSimulator, please restart the simulator and bug a developer to fix this!",
778 SqlEx);
779 }
780 else
781 {
782 throw SqlEx;
783 }
784 } 813 }
785 ds.AcceptChanges(); 814 ds.AcceptChanges();
786 } 815 }
@@ -802,6 +831,15 @@ namespace OpenSim.Data.SQLite
802 * 831 *
803 **********************************************************************/ 832 **********************************************************************/
804 833
834 protected void CreateDataSetMapping(IDataAdapter da, string tableName)
835 {
836 ITableMapping dbMapping = da.TableMappings.Add(tableName, tableName);
837 foreach (DataColumn col in ds.Tables[tableName].Columns)
838 {
839 dbMapping.ColumnMappings.Add(col.ColumnName, col.ColumnName);
840 }
841 }
842
805 /// <summary> 843 /// <summary>
806 /// 844 ///
807 /// </summary> 845 /// </summary>
@@ -1390,7 +1428,7 @@ namespace OpenSim.Data.SQLite
1390 } 1428 }
1391 catch (InvalidCastException) 1429 catch (InvalidCastException)
1392 { 1430 {
1393 m_log.ErrorFormat("[PARCEL]: unable to get parcel telehub settings for {1}", newData.Name); 1431 m_log.ErrorFormat("[SQLITE REGION DB]: unable to get parcel telehub settings for {1}", newData.Name);
1394 newData.UserLocation = Vector3.Zero; 1432 newData.UserLocation = Vector3.Zero;
1395 newData.UserLookAt = Vector3.Zero; 1433 newData.UserLookAt = Vector3.Zero;
1396 } 1434 }
@@ -1897,7 +1935,7 @@ namespace OpenSim.Data.SQLite
1897 /// <param name="items"></param> 1935 /// <param name="items"></param>
1898 public void StorePrimInventory(UUID primID, ICollection<TaskInventoryItem> items) 1936 public void StorePrimInventory(UUID primID, ICollection<TaskInventoryItem> items)
1899 { 1937 {
1900 m_log.InfoFormat("[REGION DB]: Entered StorePrimInventory with prim ID {0}", primID); 1938// m_log.DebugFormat("[SQLITE REGION DB]: Entered StorePrimInventory with prim ID {0}", primID);
1901 1939
1902 DataTable dbItems = ds.Tables["primitems"]; 1940 DataTable dbItems = ds.Tables["primitems"];
1903 1941
@@ -1964,6 +2002,7 @@ namespace OpenSim.Data.SQLite
1964 sql += ") values (:"; 2002 sql += ") values (:";
1965 sql += String.Join(", :", cols); 2003 sql += String.Join(", :", cols);
1966 sql += ")"; 2004 sql += ")";
2005 //m_log.DebugFormat("[SQLITE]: Created insert command {0}", sql);
1967 SqliteCommand cmd = new SqliteCommand(sql); 2006 SqliteCommand cmd = new SqliteCommand(sql);
1968 2007
1969 // this provides the binding for all our parameters, so 2008 // this provides the binding for all our parameters, so
@@ -2259,6 +2298,36 @@ namespace OpenSim.Data.SQLite
2259 return DbType.String; 2298 return DbType.String;
2260 } 2299 }
2261 } 2300 }
2301
2302 static void PrintDataSet(DataSet ds)
2303 {
2304 // Print out any name and extended properties.
2305 Console.WriteLine("DataSet is named: {0}", ds.DataSetName);
2306 foreach (System.Collections.DictionaryEntry de in ds.ExtendedProperties)
2307 {
2308 Console.WriteLine("Key = {0}, Value = {1}", de.Key, de.Value);
2309 }
2310 Console.WriteLine();
2311 foreach (DataTable dt in ds.Tables)
2312 {
2313 Console.WriteLine("=> {0} Table:", dt.TableName);
2314 // Print out the column names.
2315 for (int curCol = 0; curCol < dt.Columns.Count; curCol++)
2316 {
2317 Console.Write(dt.Columns[curCol].ColumnName + "\t");
2318 }
2319 Console.WriteLine("\n----------------------------------");
2320 // Print the DataTable.
2321 for (int curRow = 0; curRow < dt.Rows.Count; curRow++)
2322 {
2323 for (int curCol = 0; curCol < dt.Columns.Count; curCol++)
2324 {
2325 Console.Write(dt.Rows[curRow][curCol].ToString() + "\t");
2326 }
2327 Console.WriteLine();
2328 }
2329 }
2330 }
2262 2331
2263 } 2332 }
2264} 2333}