From 78660cc42ac6cb424ff903612000e4cbe63a97bf Mon Sep 17 00:00:00 2001 From: Manfred Aabye Date: Sat, 28 Feb 2026 17:52:22 +0100 Subject: [PATCH] 28022026 --- OpenSim/Data/MySQL/MySQLtsAssetData.cs | 670 +++++++++++++ .../MySQL/Resources/TSAssetStore.migrations | 18 + OpenSim/Data/Tests/AssetTests.cs | 323 ++++++ .../Services/AssetService/TSAssetConnector.cs | 640 ++++++++++++ bin/Robust.HG.ini.example | 938 ++++++++++++++++++ 5 files changed, 2589 insertions(+) create mode 100644 OpenSim/Data/MySQL/MySQLtsAssetData.cs create mode 100644 OpenSim/Data/MySQL/Resources/TSAssetStore.migrations create mode 100644 OpenSim/Data/Tests/AssetTests.cs create mode 100644 OpenSim/Services/AssetService/TSAssetConnector.cs create mode 100644 bin/Robust.HG.ini.example diff --git a/OpenSim/Data/MySQL/MySQLtsAssetData.cs b/OpenSim/Data/MySQL/MySQLtsAssetData.cs new file mode 100644 index 0000000..7489dd8 --- /dev/null +++ b/OpenSim/Data/MySQL/MySQLtsAssetData.cs @@ -0,0 +1,670 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSimulator Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.Reflection; +using log4net; +using MySql.Data.MySqlClient; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Data; + +namespace OpenSim.Data.MySQL +{ + /// + /// MySQL storage provider that stores assets in type-specific tables: + /// assets_{assetType}. A legacy fallback to table "assets" can be enabled explicitly. + /// + public class MySQLtsAssetData : AssetDataBase + { + private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private const string LegacyTableName = "assets"; + private const string IndexTableName = "tsassets_index"; + private const string TypedTableNameFormat = "assets_{0}"; + private const bool DefaultFallbackToLegacy = false; + + private readonly object m_tableSync = new object(); + private readonly HashSet m_initializedTables = new HashSet(StringComparer.OrdinalIgnoreCase); + + private string m_connectionString; + private bool m_fallbackToLegacy; + + protected virtual Assembly Assembly + { + get { return GetType().Assembly; } + } + + #region IPlugin Members + + public override string Version + { + get { return "1.0.0.0"; } + } + + public override string Name + { + get { return "MySQL TSAsset storage engine"; } + } + + public override void Initialise(string connect) + { + if (string.IsNullOrEmpty(connect)) + throw new ArgumentException("Connection string must not be null or empty", nameof(connect)); + + m_connectionString = connect; + m_fallbackToLegacy = TryReadBooleanSetting(connect, "TSFallbackToLegacyAssets", DefaultFallbackToLegacy); + + using (MySqlConnection dbcon = new MySqlConnection(m_connectionString)) + { + dbcon.Open(); + + Migration migration = new Migration(dbcon, Assembly, "TSAssetStore"); + migration.Update(); + + EnsureIndexTable(dbcon); + } + } + + public override void Initialise() + { + throw new NotImplementedException(); + } + + public override void Dispose() + { + } + + #endregion + + #region IAssetDataPlugin Members + + public override AssetBase GetAsset(UUID assetID) + { + try + { + using (MySqlConnection dbcon = new MySqlConnection(m_connectionString)) + { + dbcon.Open(); + + if (TryGetIndexedAssetType(dbcon, assetID, out sbyte indexedType)) + { + string typedTable = GetTypeTableName(indexedType); + + AssetBase typedAsset = GetAssetFromTable(dbcon, typedTable, assetID); + if (typedAsset != null) + return typedAsset; + + RemoveIndexRow(dbcon, assetID); + } + + if (m_fallbackToLegacy) + { + AssetBase legacyAsset = GetAssetFromTable(dbcon, LegacyTableName, assetID); + if (legacyAsset != null) + { + TryStoreTypedWithExistingConnection(dbcon, legacyAsset); + return legacyAsset; + } + } + } + } + catch (Exception e) + { + m_log.ErrorFormat("[TSASSET DB]: MySQL failure fetching asset {0}. Error: {1}", assetID, e.Message); + } + + return null; + } + + public override bool StoreAsset(AssetBase asset) + { + if (asset == null) + return false; + + try + { + using (MySqlConnection dbcon = new MySqlConnection(m_connectionString)) + { + dbcon.Open(); + return TryStoreTypedWithExistingConnection(dbcon, asset); + } + } + catch (Exception e) + { + m_log.ErrorFormat("[TSASSET DB]: MySQL failure storing asset {0}. Error: {1}", asset.FullID, e.Message); + return false; + } + } + + public override bool[] AssetsExist(UUID[] uuids) + { + if (uuids == null || uuids.Length == 0) + return new bool[0]; + + bool[] results = new bool[uuids.Length]; + Dictionary> positions = new Dictionary>(); + + for (int i = 0; i < uuids.Length; i++) + { + if (!positions.TryGetValue(uuids[i], out List indexes)) + { + indexes = new List(); + positions[uuids[i]] = indexes; + } + + indexes.Add(i); + } + + try + { + using (MySqlConnection dbcon = new MySqlConnection(m_connectionString)) + { + dbcon.Open(); + + HashSet found = QueryExistingIds(dbcon, IndexTableName, "id", uuids); + + if (m_fallbackToLegacy) + { + UUID[] missing = BuildMissingArray(uuids, found); + if (missing.Length > 0) + { + HashSet legacyFound = QueryExistingIds(dbcon, LegacyTableName, "id", missing); + foreach (UUID id in legacyFound) + found.Add(id); + } + } + + foreach (UUID id in found) + { + if (positions.TryGetValue(id, out List idxList)) + { + foreach (int idx in idxList) + results[idx] = true; + } + } + } + } + catch (Exception e) + { + m_log.ErrorFormat("[TSASSET DB]: MySQL failure checking asset existence. Error: {0}", e.Message); + } + + return results; + } + + public override List FetchAssetMetadataSet(int start, int count) + { + List result = new List(Math.Max(0, count)); + List> pending = new List>(Math.Max(0, count)); + + if (count <= 0) + return result; + + try + { + using (MySqlConnection dbcon = new MySqlConnection(m_connectionString)) + { + dbcon.Open(); + + using (MySqlCommand cmd = new MySqlCommand( + $"SELECT id, assetType FROM `{IndexTableName}` ORDER BY updated_at DESC LIMIT ?start, ?count", + dbcon)) + { + cmd.Parameters.AddWithValue("?start", start); + cmd.Parameters.AddWithValue("?count", count); + + using (MySqlDataReader dbReader = cmd.ExecuteReader()) + { + while (dbReader.Read()) + { + UUID id = DBGuid.FromDB(dbReader["id"]); + int typeInt = Convert.ToInt32(dbReader["assetType"], CultureInfo.InvariantCulture); + sbyte type = Convert.ToSByte(typeInt, CultureInfo.InvariantCulture); + pending.Add(new KeyValuePair(id, type)); + } + } + + for (int i = 0; i < pending.Count; i++) + { + AssetMetadata metadata = GetMetadataByIdAndType(dbcon, pending[i].Key, pending[i].Value); + if (metadata != null) + result.Add(metadata); + } + } + } + } + catch (Exception e) + { + m_log.ErrorFormat("[TSASSET DB]: MySQL failure fetching metadata set from {0}, count {1}. Error: {2}", start, count, e.Message); + } + + return result; + } + + public override bool Delete(string id) + { + if (!UUID.TryParse(id, out UUID assetId)) + return false; + + try + { + using (MySqlConnection dbcon = new MySqlConnection(m_connectionString)) + { + dbcon.Open(); + + using (MySqlTransaction tx = dbcon.BeginTransaction()) + { + try + { + if (TryGetIndexedAssetType(dbcon, tx, assetId, out sbyte indexedType)) + { + string typedTable = GetTypeTableName(indexedType); + + using (MySqlCommand deleteTyped = new MySqlCommand($"DELETE FROM `{typedTable}` WHERE id=?id", dbcon, tx)) + { + deleteTyped.Parameters.AddWithValue("?id", assetId.ToString()); + deleteTyped.ExecuteNonQuery(); + } + } + + using (MySqlCommand deleteIndex = new MySqlCommand($"DELETE FROM `{IndexTableName}` WHERE id=?id", dbcon, tx)) + { + deleteIndex.Parameters.AddWithValue("?id", assetId.ToString()); + deleteIndex.ExecuteNonQuery(); + } + + if (m_fallbackToLegacy) + { + using (MySqlCommand deleteLegacy = new MySqlCommand($"DELETE FROM `{LegacyTableName}` WHERE id=?id", dbcon, tx)) + { + deleteLegacy.Parameters.AddWithValue("?id", assetId.ToString()); + deleteLegacy.ExecuteNonQuery(); + } + } + + tx.Commit(); + return true; + } + catch + { + tx.Rollback(); + throw; + } + } + } + } + catch (Exception e) + { + m_log.ErrorFormat("[TSASSET DB]: MySQL failure deleting asset {0}. Error: {1}", id, e.Message); + return false; + } + } + + #endregion + + private static bool TryReadBooleanSetting(string connectionString, string key, bool defaultValue) + { + if (string.IsNullOrEmpty(connectionString) || string.IsNullOrEmpty(key)) + return defaultValue; + + string[] parts = connectionString.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + foreach (string rawPart in parts) + { + string part = rawPart.Trim(); + int eqIndex = part.IndexOf('='); + if (eqIndex <= 0 || eqIndex == part.Length - 1) + continue; + + string k = part.Substring(0, eqIndex).Trim(); + if (!k.Equals(key, StringComparison.OrdinalIgnoreCase)) + continue; + + string v = part.Substring(eqIndex + 1).Trim(); + if (v.Equals("1", StringComparison.OrdinalIgnoreCase) || v.Equals("true", StringComparison.OrdinalIgnoreCase) || v.Equals("yes", StringComparison.OrdinalIgnoreCase)) + return true; + + if (v.Equals("0", StringComparison.OrdinalIgnoreCase) || v.Equals("false", StringComparison.OrdinalIgnoreCase) || v.Equals("no", StringComparison.OrdinalIgnoreCase)) + return false; + } + + return defaultValue; + } + + private static string GetTypeTableName(sbyte assetType) + { + return string.Format(CultureInfo.InvariantCulture, TypedTableNameFormat, assetType); + } + + private void EnsureTypeTable(string tableName) + { + lock (m_tableSync) + { + if (m_initializedTables.Contains(tableName)) + return; + + using (MySqlConnection dbcon = new MySqlConnection(m_connectionString)) + { + dbcon.Open(); + CreateTypeTable(dbcon, tableName); + } + + m_initializedTables.Add(tableName); + } + } + + private void CreateTypeTable(MySqlConnection dbcon, string tableName) + { + string sql = + $"CREATE TABLE IF NOT EXISTS `{tableName}` (" + + "`name` varchar(64) NOT NULL," + + "`description` varchar(64) NOT NULL," + + "`assetType` tinyint(4) NOT NULL," + + "`local` tinyint(1) NOT NULL," + + "`temporary` tinyint(1) NOT NULL," + + "`data` longblob NOT NULL," + + "`id` char(36) NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'," + + "`create_time` int(11) DEFAULT '0'," + + "`access_time` int(11) DEFAULT '0'," + + "`asset_flags` int(11) NOT NULL DEFAULT '0'," + + "`CreatorID` varchar(128) NOT NULL DEFAULT ''," + + "PRIMARY KEY (`id`)" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8"; + + using (MySqlCommand cmd = new MySqlCommand(sql, dbcon)) + { + cmd.ExecuteNonQuery(); + } + } + + private static void EnsureIndexTable(MySqlConnection dbcon) + { + string sql = + $"CREATE TABLE IF NOT EXISTS `{IndexTableName}` (" + + "`id` char(36) NOT NULL," + + "`assetType` tinyint(4) NOT NULL," + + "`updated_at` int(11) NOT NULL DEFAULT '0'," + + "PRIMARY KEY (`id`)," + + $"INDEX `idx_{IndexTableName}_assetType` (`assetType`)" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8"; + + using (MySqlCommand cmd = new MySqlCommand(sql, dbcon)) + { + cmd.ExecuteNonQuery(); + } + } + + private bool TryStoreTypedWithExistingConnection(MySqlConnection dbcon, AssetBase asset) + { + string tableName = GetTypeTableName(asset.Type); + EnsureTypeTable(tableName); + + string assetName = asset.Name ?? string.Empty; + if (assetName.Length > AssetBase.MAX_ASSET_NAME) + assetName = assetName.Substring(0, AssetBase.MAX_ASSET_NAME); + + string assetDescription = asset.Description ?? string.Empty; + if (assetDescription.Length > AssetBase.MAX_ASSET_DESC) + assetDescription = assetDescription.Substring(0, AssetBase.MAX_ASSET_DESC); + + int now = (int)Utils.DateTimeToUnixTime(DateTime.UtcNow); + + using (MySqlTransaction tx = dbcon.BeginTransaction()) + { + try + { + string upsertAssetSql = + $"REPLACE INTO `{tableName}` " + + "(id, name, description, assetType, local, temporary, create_time, access_time, asset_flags, CreatorID, data) " + + "VALUES(?id, ?name, ?description, ?assetType, ?local, ?temporary, ?create_time, ?access_time, ?asset_flags, ?CreatorID, ?data)"; + + using (MySqlCommand assetCmd = new MySqlCommand(upsertAssetSql, dbcon, tx)) + { + assetCmd.Parameters.AddWithValue("?id", asset.ID); + assetCmd.Parameters.AddWithValue("?name", assetName); + assetCmd.Parameters.AddWithValue("?description", assetDescription); + assetCmd.Parameters.AddWithValue("?assetType", asset.Type); + assetCmd.Parameters.AddWithValue("?local", asset.Local); + assetCmd.Parameters.AddWithValue("?temporary", asset.Temporary); + assetCmd.Parameters.AddWithValue("?create_time", now); + assetCmd.Parameters.AddWithValue("?access_time", now); + assetCmd.Parameters.AddWithValue("?asset_flags", (int)asset.Flags); + assetCmd.Parameters.AddWithValue("?CreatorID", asset.Metadata.CreatorID ?? string.Empty); + assetCmd.Parameters.AddWithValue("?data", asset.Data ?? Array.Empty()); + assetCmd.ExecuteNonQuery(); + } + + using (MySqlCommand indexCmd = new MySqlCommand( + $"REPLACE INTO `{IndexTableName}` (id, assetType, updated_at) VALUES (?id, ?assetType, ?updated_at)", + dbcon, + tx)) + { + indexCmd.Parameters.AddWithValue("?id", asset.ID); + indexCmd.Parameters.AddWithValue("?assetType", asset.Type); + indexCmd.Parameters.AddWithValue("?updated_at", now); + indexCmd.ExecuteNonQuery(); + } + + tx.Commit(); + return true; + } + catch + { + tx.Rollback(); + throw; + } + } + } + + private AssetBase GetAssetFromTable(MySqlConnection dbcon, string tableName, UUID assetID) + { + string sql = + $"SELECT name, description, assetType, local, temporary, asset_flags, CreatorID, data " + + $"FROM `{tableName}` WHERE id=?id"; + + try + { + using (MySqlCommand cmd = new MySqlCommand(sql, dbcon)) + { + cmd.Parameters.AddWithValue("?id", assetID.ToString()); + + using (MySqlDataReader dbReader = cmd.ExecuteReader(CommandBehavior.SingleRow)) + { + if (!dbReader.Read()) + return null; + + AssetBase asset = new AssetBase(assetID, (string)dbReader["name"], (sbyte)dbReader["assetType"], dbReader["CreatorID"].ToString()); + asset.Description = (string)dbReader["description"]; + asset.Local = Convert.ToBoolean(dbReader["local"]); + asset.Temporary = Convert.ToBoolean(dbReader["temporary"]); + asset.Flags = (AssetFlags)Convert.ToInt32(dbReader["asset_flags"], CultureInfo.InvariantCulture); + asset.Data = (byte[])dbReader["data"]; + return asset; + } + } + } + catch (MySqlException) + { + return null; + } + } + + private AssetMetadata GetMetadataByIdAndType(MySqlConnection dbcon, UUID id, sbyte type) + { + string tableName = GetTypeTableName(type); + + string sql = + $"SELECT id, name, description, assetType, temporary, asset_flags, CreatorID " + + $"FROM `{tableName}` WHERE id=?id"; + + try + { + using (MySqlCommand cmd = new MySqlCommand(sql, dbcon)) + { + cmd.Parameters.AddWithValue("?id", id.ToString()); + using (MySqlDataReader dbReader = cmd.ExecuteReader(CommandBehavior.SingleRow)) + { + if (dbReader.Read()) + return ReadMetadata(dbReader); + } + } + } + catch (MySqlException) + { + // Type table may not yet exist if no asset of this type was stored. + } + + if (!m_fallbackToLegacy) + return null; + + using (MySqlCommand legacyCmd = new MySqlCommand( + $"SELECT id, name, description, assetType, temporary, asset_flags, CreatorID FROM `{LegacyTableName}` WHERE id=?id", + dbcon)) + { + legacyCmd.Parameters.AddWithValue("?id", id.ToString()); + using (MySqlDataReader dbReader = legacyCmd.ExecuteReader(CommandBehavior.SingleRow)) + { + if (dbReader.Read()) + return ReadMetadata(dbReader); + } + } + + return null; + } + + private static AssetMetadata ReadMetadata(MySqlDataReader dbReader) + { + AssetMetadata metadata = new AssetMetadata(); + metadata.FullID = DBGuid.FromDB(dbReader["id"]); + metadata.Name = dbReader["name"].ToString(); + metadata.Description = dbReader["description"].ToString(); + metadata.Type = Convert.ToSByte(dbReader["assetType"], CultureInfo.InvariantCulture); + metadata.Temporary = Convert.ToBoolean(dbReader["temporary"]); + metadata.Flags = (AssetFlags)Convert.ToInt32(dbReader["asset_flags"], CultureInfo.InvariantCulture); + metadata.CreatorID = dbReader["CreatorID"].ToString(); + metadata.SHA1 = Array.Empty(); + return metadata; + } + + private static UUID[] BuildMissingArray(UUID[] requested, HashSet found) + { + List missing = new List(requested.Length); + for (int i = 0; i < requested.Length; i++) + { + if (!found.Contains(requested[i])) + missing.Add(requested[i]); + } + + return missing.ToArray(); + } + + private static HashSet QueryExistingIds(MySqlConnection dbcon, string tableName, string idColumn, UUID[] uuids) + { + HashSet found = new HashSet(); + + if (uuids.Length == 0) + return found; + + const int batchSize = 200; + for (int offset = 0; offset < uuids.Length; offset += batchSize) + { + int take = Math.Min(batchSize, uuids.Length - offset); + string[] placeholders = new string[take]; + + using (MySqlCommand cmd = dbcon.CreateCommand()) + { + for (int i = 0; i < take; i++) + { + string paramName = "?id" + i.ToString(CultureInfo.InvariantCulture); + placeholders[i] = paramName; + cmd.Parameters.AddWithValue(paramName, uuids[offset + i].ToString()); + } + + cmd.CommandText = string.Format( + CultureInfo.InvariantCulture, + "SELECT {0} FROM `{1}` WHERE {0} IN ({2})", + idColumn, + tableName, + string.Join(",", placeholders)); + + using (MySqlDataReader dbReader = cmd.ExecuteReader()) + { + while (dbReader.Read()) + { + UUID id = DBGuid.FromDB(dbReader[idColumn]); + found.Add(id); + } + } + } + } + + return found; + } + + private bool TryGetIndexedAssetType(MySqlConnection dbcon, UUID assetID, out sbyte assetType) + { + return TryGetIndexedAssetType(dbcon, null, assetID, out assetType); + } + + private bool TryGetIndexedAssetType(MySqlConnection dbcon, MySqlTransaction tx, UUID assetID, out sbyte assetType) + { + using (MySqlCommand cmd = new MySqlCommand($"SELECT assetType FROM `{IndexTableName}` WHERE id=?id", dbcon, tx)) + { + cmd.Parameters.AddWithValue("?id", assetID.ToString()); + + object value = cmd.ExecuteScalar(); + if (value == null || value is DBNull) + { + assetType = 0; + return false; + } + + int typeInt = Convert.ToInt32(value, CultureInfo.InvariantCulture); + if (typeInt < sbyte.MinValue || typeInt > sbyte.MaxValue) + { + assetType = 0; + return false; + } + + assetType = (sbyte)typeInt; + return true; + } + } + + private static void RemoveIndexRow(MySqlConnection dbcon, UUID assetID) + { + using (MySqlCommand cmd = new MySqlCommand($"DELETE FROM `{IndexTableName}` WHERE id=?id", dbcon)) + { + cmd.Parameters.AddWithValue("?id", assetID.ToString()); + cmd.ExecuteNonQuery(); + } + } + } +} diff --git a/OpenSim/Data/MySQL/Resources/TSAssetStore.migrations b/OpenSim/Data/MySQL/Resources/TSAssetStore.migrations new file mode 100644 index 0000000..372b3cf --- /dev/null +++ b/OpenSim/Data/MySQL/Resources/TSAssetStore.migrations @@ -0,0 +1,18 @@ +# ----------------- +:VERSION 1 + +BEGIN; + +CREATE TABLE IF NOT EXISTS `tsassets_index` ( + `id` char(36) NOT NULL, + `assetType` tinyint(4) NOT NULL, + `updated_at` int(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + INDEX `idx_tsassets_index_assetType` (`assetType`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Type tables are created dynamically at runtime +-- using the same schema and name pattern: assets_ +-- e.g. assets_1, assets_22, assets_-1, assets_-2. + +COMMIT; diff --git a/OpenSim/Data/Tests/AssetTests.cs b/OpenSim/Data/Tests/AssetTests.cs new file mode 100644 index 0000000..65c04be --- /dev/null +++ b/OpenSim/Data/Tests/AssetTests.cs @@ -0,0 +1,323 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSimulator Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using log4net.Config; +using NUnit.Framework; +using NUnit.Framework.Constraints; +using OpenMetaverse; +using OpenSim.Framework; +using OpenSim.Tests.Common; +using System.Data.Common; +using log4net; + +// DBMS-specific: +using MySql.Data.MySqlClient; +using OpenSim.Data.MySQL; + +using Mono.Data.Sqlite; +using OpenSim.Data.SQLite; + +namespace OpenSim.Data.Tests +{ + [TestFixture(Description = "Asset store tests (SQLite)")] + public class SQLiteAssetTests : AssetTests + { + } + + [TestFixture(Description = "Asset store tests (MySQL)")] + public class MySqlAssetTests : AssetTests + { + } + + [TestFixture(Description = "Asset store tests (MySQL TSAsset)")] + public class MySqlTsAssetTests : AssetTests + { + protected override void ClearDB() + { + DropTables( + "`tsassets_index`", + "`assets_0`", + "`assets_-2`", + "`assets_49`"); + + ResetMigrations("TSAssetStore"); + } + + [Test] + public void T030_StoreReadKnownNegativeAndMeshTypes() + { + TestHelpers.InMethod(); + + UUID legacyId = UUID.Random(); + UUID meshId = UUID.Random(); + + AssetBase legacyMaterial = new AssetBase(legacyId, "legacy material", -2, UUID.Random().ToString()); + legacyMaterial.Data = new byte[] { 1, 2, 3 }; + + AssetBase mesh = new AssetBase(meshId, "mesh asset", 49, UUID.Random().ToString()); + mesh.Data = new byte[] { 4, 5, 6 }; + + Assert.IsTrue(m_db.StoreAsset(legacyMaterial), "StoreAsset should succeed for type -2"); + Assert.IsTrue(m_db.StoreAsset(mesh), "StoreAsset should succeed for type 49"); + + AssetBase legacyMaterialRead = m_db.GetAsset(legacyId); + AssetBase meshRead = m_db.GetAsset(meshId); + + Assert.That(legacyMaterialRead, Is.Not.Null); + Assert.That(meshRead, Is.Not.Null); + Assert.That(legacyMaterialRead.Type, Is.EqualTo((sbyte)-2)); + Assert.That(meshRead.Type, Is.EqualTo((sbyte)49)); + + bool[] exists = m_db.AssetsExist(new[] { legacyId, meshId }); + Assert.That(exists[0], Is.True); + Assert.That(exists[1], Is.True); + + int tableCount = 0; + ExecQuery( + "SELECT COUNT(*) AS cnt FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name IN ('assets_-2','assets_49')", + true, + reader => + { + tableCount = Convert.ToInt32(reader["cnt"]); + return false; + }); + + Assert.That(tableCount, Is.EqualTo(2), "Expected dynamic type tables assets_-2 and assets_49 to exist"); + } + + [Test] + public void T031_StoreReadBoundarySignedTinyIntAssetTypes() + { + TestHelpers.InMethod(); + + UUID minId = UUID.Random(); + UUID maxId = UUID.Random(); + + AssetBase minTypeAsset = new AssetBase(minId, "min type asset", -128, UUID.Random().ToString()); + minTypeAsset.Data = new byte[] { 10, 20, 30 }; + + AssetBase maxTypeAsset = new AssetBase(maxId, "max type asset", 127, UUID.Random().ToString()); + maxTypeAsset.Data = new byte[] { 40, 50, 60 }; + + Assert.IsTrue(m_db.StoreAsset(minTypeAsset), "StoreAsset should succeed for type -128"); + Assert.IsTrue(m_db.StoreAsset(maxTypeAsset), "StoreAsset should succeed for type 127"); + + AssetBase minTypeRead = m_db.GetAsset(minId); + AssetBase maxTypeRead = m_db.GetAsset(maxId); + + Assert.That(minTypeRead, Is.Not.Null); + Assert.That(maxTypeRead, Is.Not.Null); + Assert.That(minTypeRead.Type, Is.EqualTo((sbyte)-128)); + Assert.That(maxTypeRead.Type, Is.EqualTo((sbyte)127)); + + bool[] exists = m_db.AssetsExist(new[] { minId, maxId }); + Assert.That(exists[0], Is.True); + Assert.That(exists[1], Is.True); + + int tableCount = 0; + ExecQuery( + "SELECT COUNT(*) AS cnt FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name IN ('assets_-128','assets_127')", + true, + reader => + { + tableCount = Convert.ToInt32(reader["cnt"]); + return false; + }); + + Assert.That(tableCount, Is.EqualTo(2), "Expected dynamic type tables assets_-128 and assets_127 to exist"); + } + } + + public class AssetTests : BasicDataServiceTest + where TConn : DbConnection, new() + where TAssetData : AssetDataBase, new() + { + protected TAssetData m_db; + + public UUID uuid1 = UUID.Random(); + public UUID uuid2 = UUID.Random(); + public UUID uuid3 = UUID.Random(); + + public string critter1 = UUID.Random().ToString(); + public string critter2 = UUID.Random().ToString(); + public string critter3 = UUID.Random().ToString(); + + public byte[] data1 = new byte[100]; + + PropertyScrambler scrambler = new PropertyScrambler() + .DontScramble(x => x.ID) + .DontScramble(x => x.Type) + .DontScramble(x => x.FullID) + .DontScramble(x => x.Metadata.ID) + .DontScramble(x => x.Metadata.CreatorID) + .DontScramble(x => x.Metadata.ContentType) + .DontScramble(x => x.Metadata.FullID) + .DontScramble(x => x.Data); + + protected override void InitService(object service) + { + ClearDB(); + m_db = (TAssetData)service; + m_db.Initialise(m_connStr); + } + + protected virtual void ClearDB() + { + DropTables("assets"); + ResetMigrations("AssetStore"); + } + + + [Test] + public void T001_LoadEmpty() + { + TestHelpers.InMethod(); + + bool[] exist = m_db.AssetsExist(new[] { uuid1, uuid2, uuid3 }); + Assert.IsFalse(exist[0]); + Assert.IsFalse(exist[1]); + Assert.IsFalse(exist[2]); + } + + [Test] + public void T010_StoreReadVerifyAssets() + { + TestHelpers.InMethod(); + + AssetBase a1 = new AssetBase(uuid1, "asset one", (sbyte)AssetType.Texture, critter1.ToString()); + AssetBase a2 = new AssetBase(uuid2, "asset two", (sbyte)AssetType.Texture, critter2.ToString()); + AssetBase a3 = new AssetBase(uuid3, "asset three", (sbyte)AssetType.Texture, critter3.ToString()); + a1.Data = data1; + a2.Data = data1; + a3.Data = data1; + + scrambler.Scramble(a1); + scrambler.Scramble(a2); + scrambler.Scramble(a3); + + m_db.StoreAsset(a1); + m_db.StoreAsset(a2); + m_db.StoreAsset(a3); + a1.UploadAttempts = 0; + a2.UploadAttempts = 0; + a3.UploadAttempts = 0; + + AssetBase a1a = m_db.GetAsset(uuid1); + a1a.UploadAttempts = 0; + Assert.That(a1a, Constraints.PropertyCompareConstraint(a1)); + + AssetBase a2a = m_db.GetAsset(uuid2); + a2a.UploadAttempts = 0; + Assert.That(a2a, Constraints.PropertyCompareConstraint(a2)); + + AssetBase a3a = m_db.GetAsset(uuid3); + a3a.UploadAttempts = 0; + Assert.That(a3a, Constraints.PropertyCompareConstraint(a3)); + + scrambler.Scramble(a1a); + scrambler.Scramble(a2a); + scrambler.Scramble(a3a); + + m_db.StoreAsset(a1a); + m_db.StoreAsset(a2a); + m_db.StoreAsset(a3a); + a1a.UploadAttempts = 0; + a2a.UploadAttempts = 0; + a3a.UploadAttempts = 0; + + AssetBase a1b = m_db.GetAsset(uuid1); + a1b.UploadAttempts = 0; + Assert.That(a1b, Constraints.PropertyCompareConstraint(a1a)); + + AssetBase a2b = m_db.GetAsset(uuid2); + a2b.UploadAttempts = 0; + Assert.That(a2b, Constraints.PropertyCompareConstraint(a2a)); + + AssetBase a3b = m_db.GetAsset(uuid3); + a3b.UploadAttempts = 0; + Assert.That(a3b, Constraints.PropertyCompareConstraint(a3a)); + + bool[] exist = m_db.AssetsExist(new[] { uuid1, uuid2, uuid3 }); + Assert.IsTrue(exist[0]); + Assert.IsTrue(exist[1]); + Assert.IsTrue(exist[2]); + + List metadatas = m_db.FetchAssetMetadataSet(0, 1000); + + Assert.That(metadatas.Count >= 3, "FetchAssetMetadataSet() should have returned at least 3 assets!"); + + // It is possible that the Asset table is filled with data, in which case we don't try to find "our" + // assets there: + if (metadatas.Count < 1000) + { + AssetMetadata metadata = metadatas.Find(x => x.FullID == uuid1); + Assert.That(metadata.Name, Is.EqualTo(a1b.Name)); + Assert.That(metadata.Description, Is.EqualTo(a1b.Description)); + Assert.That(metadata.Type, Is.EqualTo(a1b.Type)); + Assert.That(metadata.Temporary, Is.EqualTo(a1b.Temporary)); + Assert.That(metadata.FullID, Is.EqualTo(a1b.FullID)); + } + } + + [Test] + public void T020_CheckForWeirdCreatorID() + { + TestHelpers.InMethod(); + + // It is expected that eventually the CreatorID might be an arbitrary string (an URI) + // rather than a valid UUID (?). This test is to make sure that the database layer does not + // attempt to convert CreatorID to GUID, but just passes it both ways as a string. + AssetBase a1 = new AssetBase(uuid1, "asset one", (sbyte)AssetType.Texture, critter1); + AssetBase a2 = new AssetBase(uuid2, "asset two", (sbyte)AssetType.Texture, "This is not a GUID!"); + AssetBase a3 = new AssetBase(uuid3, "asset three", (sbyte)AssetType.Texture, ""); + a1.Data = data1; + a2.Data = data1; + a3.Data = data1; + + m_db.StoreAsset(a1); + a1.UploadAttempts = 0; + m_db.StoreAsset(a2); + a2.UploadAttempts = 0; + m_db.StoreAsset(a3); + a3.UploadAttempts = 0; + + AssetBase a1a = m_db.GetAsset(uuid1); + a1a.UploadAttempts = 0; + Assert.That(a1a, Constraints.PropertyCompareConstraint(a1)); + + AssetBase a2a = m_db.GetAsset(uuid2); + a2a.UploadAttempts = 0; + Assert.That(a2a, Constraints.PropertyCompareConstraint(a2)); + + AssetBase a3a = m_db.GetAsset(uuid3); + a3a.UploadAttempts = 0; + Assert.That(a3a, Constraints.PropertyCompareConstraint(a3)); + } + } +} \ No newline at end of file diff --git a/OpenSim/Services/AssetService/TSAssetConnector.cs b/OpenSim/Services/AssetService/TSAssetConnector.cs new file mode 100644 index 0000000..59cafaf --- /dev/null +++ b/OpenSim/Services/AssetService/TSAssetConnector.cs @@ -0,0 +1,640 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSimulator Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Globalization; +using System.Reflection; +using System.Threading; +using log4net; +using Nini.Config; +using OpenMetaverse; +using OpenSim.Data; +using OpenSim.Framework; +using OpenSim.Services.Base; +using OpenSim.Services.Interfaces; + +namespace OpenSim.Services.AssetService +{ + public class TSAssetConnector : ServiceBase, IAssetService + { + private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private readonly Dictionary m_typeDatabases = new Dictionary(); + private readonly Dictionary m_connectionDatabases = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary m_assetLocationCache = new Dictionary(); + private readonly ConcurrentQueue m_fallbackMigrationQueue = new ConcurrentQueue(); + private readonly ConcurrentDictionary m_fallbackMigrationSet = new ConcurrentDictionary(); + private readonly object m_cacheLock = new object(); + private readonly HashSet m_allowedTypes = new HashSet(); + + private readonly List m_probeOrder = new List(); + + private IAssetDataPlugin m_defaultDatabase; + private IAssetLoader m_assetLoader; + private IAssetService m_fallbackService; + private Thread m_fallbackMigrationThread; + + private bool m_enableFallbackAutoMigration; + private bool m_enableFallbackAutoDelete; + private int m_migrationCheckIntervalMs = 60000; + private int m_migrationBatchSize = 25; + private int m_migrationLowTrafficMaxRequests = 3; + private int m_migrationQueueMax = 50000; + private int m_requestsInWindow; + + public TSAssetConnector(IConfigSource config) + : base(config) + { + IConfig assetConfig = config.Configs["AssetService"]; + if (assetConfig == null) + throw new Exception("No AssetService configuration"); + + IConfig tsConfig = config.Configs["TSAssetService"]; + IConfig dbConfig = config.Configs["DatabaseService"]; + + string storageProvider = string.Empty; + string defaultConnectionString = string.Empty; + + if (tsConfig != null) + { + storageProvider = tsConfig.GetString("StorageProvider", string.Empty); + defaultConnectionString = tsConfig.GetString("ConnectionString", string.Empty); + } + + if (string.IsNullOrEmpty(storageProvider)) + storageProvider = assetConfig.GetString("StorageProvider", string.Empty); + + if (string.IsNullOrEmpty(defaultConnectionString)) + defaultConnectionString = assetConfig.GetString("ConnectionString", string.Empty); + + if (dbConfig != null) + { + if (string.IsNullOrEmpty(storageProvider)) + storageProvider = dbConfig.GetString("StorageProvider", string.Empty); + + if (string.IsNullOrEmpty(defaultConnectionString)) + defaultConnectionString = dbConfig.GetString("ConnectionString", string.Empty); + } + + if (string.IsNullOrEmpty(storageProvider)) + throw new Exception("No StorageProvider configured"); + + if (string.IsNullOrEmpty(defaultConnectionString)) + throw new Exception("Missing database connection string"); + + ParseAllowedTypes(tsConfig); + + if (tsConfig != null) + { + m_enableFallbackAutoMigration = tsConfig.GetBoolean("EnableFallbackAutoMigration", false); + m_enableFallbackAutoDelete = tsConfig.GetBoolean("EnableFallbackAutoDelete", false); + m_migrationCheckIntervalMs = Math.Max(5000, tsConfig.GetInt("MigrationCheckIntervalSeconds", 60) * 1000); + m_migrationBatchSize = Math.Max(1, tsConfig.GetInt("MigrationBatchSize", 25)); + m_migrationLowTrafficMaxRequests = Math.Max(0, tsConfig.GetInt("MigrationLowTrafficMaxRequests", 3)); + m_migrationQueueMax = Math.Max(100, tsConfig.GetInt("MigrationQueueMax", 50000)); + } + + m_defaultDatabase = CreateAndInitDatabase(storageProvider, defaultConnectionString); + AddProbeDatabase(m_defaultDatabase); + + if (tsConfig != null) + ParseTypedDatabaseMappings(tsConfig.GetString("AssetDatabases", string.Empty), storageProvider); + + string loaderName = assetConfig.GetString("DefaultAssetLoader", string.Empty); + if (!string.IsNullOrEmpty(loaderName)) + { + m_assetLoader = LoadPlugin(loaderName); + if (m_assetLoader == null) + throw new Exception(string.Format("Asset loader could not be loaded from {0}", loaderName)); + + bool assetLoaderEnabled = assetConfig.GetBoolean("AssetLoaderEnabled", true); + if (assetLoaderEnabled) + { + string loaderArgs = assetConfig.GetString("AssetLoaderArgs", string.Empty); + m_log.InfoFormat("[TSASSET SERVICE]: Loading default asset set from {0}", loaderArgs); + + m_assetLoader.ForEachDefaultXmlAsset( + loaderArgs, + delegate(AssetBase a) + { + if (a == null) + return; + + if (Get(a.ID) == null) + Store(a); + }); + } + } + + string fallbackServiceName = assetConfig.GetString("FallbackService", string.Empty); + if (!string.IsNullOrEmpty(fallbackServiceName)) + { + object[] args = new object[] { config }; + m_fallbackService = LoadPlugin(fallbackServiceName, args); + if (m_fallbackService != null) + { + m_log.Info("[TSASSET SERVICE]: Fallback service loaded"); + + if (m_enableFallbackAutoMigration) + { + m_fallbackMigrationThread = new Thread(FallbackMigrationWorker); + m_fallbackMigrationThread.IsBackground = true; + m_fallbackMigrationThread.Name = "TSAssetFallbackMigration"; + m_fallbackMigrationThread.Start(); + m_log.Info("[TSASSET SERVICE]: Fallback auto-migration worker enabled"); + } + } + else + m_log.Error("[TSASSET SERVICE]: Failed to load fallback service"); + } + + m_log.InfoFormat("[TSASSET SERVICE]: Enabled with {0} type routes", m_typeDatabases.Count); + } + + public AssetBase Get(string id) + { + Interlocked.Increment(ref m_requestsInWindow); + + if (!UUID.TryParse(id, out UUID assetID)) + { + m_log.WarnFormat("[TSASSET SERVICE]: Could not parse requested asset id {0}", id); + return null; + } + + IAssetDataPlugin cachedDatabase = null; + lock (m_cacheLock) + { + m_assetLocationCache.TryGetValue(assetID, out cachedDatabase); + } + + if (cachedDatabase != null) + { + try + { + AssetBase cachedAsset = cachedDatabase.GetAsset(assetID); + if (cachedAsset != null) + return cachedAsset; + } + catch (Exception e) + { + m_log.WarnFormat("[TSASSET SERVICE]: Cached database lookup failed for asset {0}: {1}", assetID, e.Message); + } + } + + for (int i = 0; i < m_probeOrder.Count; i++) + { + IAssetDataPlugin db = m_probeOrder[i]; + + try + { + AssetBase asset = db.GetAsset(assetID); + if (asset != null) + { + lock (m_cacheLock) + { + m_assetLocationCache[assetID] = db; + } + return asset; + } + } + catch (Exception e) + { + m_log.WarnFormat("[TSASSET SERVICE]: Asset lookup failed for asset {0}: {1}", assetID, e.Message); + } + } + + if (m_fallbackService != null) + { + try + { + AssetBase fallbackAsset = m_fallbackService.Get(id); + if (fallbackAsset != null) + { + string storedId = Store(fallbackAsset); + bool storeOk = !storedId.Equals(UUID.Zero.ToString(), StringComparison.Ordinal); + + if (storeOk && m_enableFallbackAutoDelete) + TryDeleteFromFallback(id); + + if (m_enableFallbackAutoMigration && !storeOk) + EnqueueFallbackMigration(fallbackAsset); + + return fallbackAsset; + } + } + catch (Exception e) + { + m_log.WarnFormat("[TSASSET SERVICE]: Fallback lookup failed for asset {0}: {1}", id, e.Message); + } + } + + return null; + } + + public AssetBase Get(string id, string ForeignAssetService, bool StoreOnLocalGrid) + { + return Get(id); + } + + public AssetMetadata GetMetadata(string id) + { + AssetBase asset = Get(id); + return asset != null ? asset.Metadata : null; + } + + public byte[] GetData(string id) + { + AssetBase asset = Get(id); + return asset != null ? asset.Data : null; + } + + public AssetBase GetCached(string id) + { + return Get(id); + } + + public bool Get(string id, object sender, AssetRetrieved handler) + { + handler(id, sender, Get(id)); + return true; + } + + public void Get(string id, string ForeignAssetService, bool StoreOnLocalGrid, SimpleAssetRetrieved callBack) + { + callBack(Get(id)); + } + + public bool[] AssetsExist(string[] ids) + { + if (ids == null || ids.Length == 0) + return new bool[0]; + + bool[] results = new bool[ids.Length]; + UUID[] uuids = new UUID[ids.Length]; + bool[] valid = new bool[ids.Length]; + + for (int i = 0; i < ids.Length; i++) + { + if (UUID.TryParse(ids[i], out UUID parsed)) + { + uuids[i] = parsed; + valid[i] = true; + } + } + + for (int dbIndex = 0; dbIndex < m_probeOrder.Count; dbIndex++) + { + IAssetDataPlugin db = m_probeOrder[dbIndex]; + UUID[] query = BuildPendingUuidList(uuids, valid, results); + + if (query.Length == 0) + break; + + try + { + bool[] exists = db.AssetsExist(query); + MergeExistenceResults(query, exists, uuids, valid, results, db); + } + catch (Exception e) + { + m_log.WarnFormat("[TSASSET SERVICE]: AssetsExist failed on a database: {0}", e.Message); + } + } + + return results; + } + + public string Store(AssetBase asset) + { + if (asset == null) + return UUID.Zero.ToString(); + + IAssetDataPlugin db = ResolveDatabaseForType(asset.Type); + if (db == null) + { + m_log.WarnFormat("[TSASSET SERVICE]: No database resolved for asset {0}, type {1}", asset.FullID, asset.Type); + return UUID.Zero.ToString(); + } + + try + { + bool stored = db.StoreAsset(asset); + if (!stored) + return UUID.Zero.ToString(); + + lock (m_cacheLock) + { + m_assetLocationCache[asset.FullID] = db; + } + + return asset.ID; + } + catch (Exception e) + { + m_log.ErrorFormat("[TSASSET SERVICE]: Error storing asset {0}: {1}", asset.FullID, e.Message); + return UUID.Zero.ToString(); + } + } + + public bool UpdateContent(string id, byte[] data) + { + AssetBase asset = Get(id); + if (asset == null) + return false; + + asset.Data = data; + return Store(asset) != UUID.Zero.ToString(); + } + + public bool Delete(string id) + { + if (!UUID.TryParse(id, out UUID assetID)) + return false; + + IAssetDataPlugin cachedDatabase = null; + lock (m_cacheLock) + { + m_assetLocationCache.TryGetValue(assetID, out cachedDatabase); + } + + if (cachedDatabase != null) + { + try + { + bool deleted = cachedDatabase.Delete(id); + if (deleted) + { + lock (m_cacheLock) + { + m_assetLocationCache.Remove(assetID); + } + return true; + } + } + catch (Exception e) + { + m_log.WarnFormat("[TSASSET SERVICE]: Cached delete failed for asset {0}: {1}", assetID, e.Message); + } + } + + for (int i = 0; i < m_probeOrder.Count; i++) + { + try + { + if (m_probeOrder[i].Delete(id)) + { + lock (m_cacheLock) + { + m_assetLocationCache.Remove(assetID); + } + return true; + } + } + catch (Exception e) + { + m_log.WarnFormat("[TSASSET SERVICE]: Delete failed for asset {0}: {1}", assetID, e.Message); + } + } + + return false; + } + + private static UUID[] BuildPendingUuidList(UUID[] uuids, bool[] valid, bool[] results) + { + List pending = new List(uuids.Length); + for (int i = 0; i < uuids.Length; i++) + { + if (valid[i] && !results[i]) + pending.Add(uuids[i]); + } + + return pending.ToArray(); + } + + private void MergeExistenceResults(UUID[] query, bool[] exists, UUID[] sourceIds, bool[] valid, bool[] results, IAssetDataPlugin db) + { + if (exists == null) + return; + + int len = Math.Min(query.Length, exists.Length); + for (int i = 0; i < len; i++) + { + if (!exists[i]) + continue; + + UUID foundId = query[i]; + + for (int sourceIndex = 0; sourceIndex < sourceIds.Length; sourceIndex++) + { + if (valid[sourceIndex] && sourceIds[sourceIndex] == foundId) + { + results[sourceIndex] = true; + } + } + + lock (m_cacheLock) + { + m_assetLocationCache[foundId] = db; + } + } + } + + private void ParseAllowedTypes(IConfig tsConfig) + { + if (tsConfig == null) + return; + + string raw = tsConfig.GetString("TSAssetType", string.Empty); + if (string.IsNullOrWhiteSpace(raw)) + return; + + string[] tokens = raw.Split(new char[] { ',', ';', ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < tokens.Length; i++) + { + if (sbyte.TryParse(tokens[i], NumberStyles.Integer, CultureInfo.InvariantCulture, out sbyte type)) + m_allowedTypes.Add(type); + else + m_log.WarnFormat("[TSASSET SERVICE]: Ignoring invalid TSAssetType entry '{0}'", tokens[i]); + } + } + + private void ParseTypedDatabaseMappings(string rawMappings, string storageProvider) + { + if (string.IsNullOrWhiteSpace(rawMappings)) + return; + + string[] lines = rawMappings.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < lines.Length; i++) + { + string line = lines[i].Trim(); + if (string.IsNullOrEmpty(line)) + continue; + + line = line.Trim('"'); + if (line.EndsWith(";;", StringComparison.Ordinal)) + line = line.Substring(0, line.Length - 2); + else + line = line.TrimEnd(';'); + + int sep = line.IndexOf(':'); + if (sep <= 0 || sep == line.Length - 1) + continue; + + string typeToken = line.Substring(0, sep).Trim(); + string connectionString = line.Substring(sep + 1).Trim(); + + if (!sbyte.TryParse(typeToken, NumberStyles.Integer, CultureInfo.InvariantCulture, out sbyte assetType)) + { + m_log.WarnFormat("[TSASSET SERVICE]: Ignoring invalid asset type mapping '{0}'", typeToken); + continue; + } + + if (m_allowedTypes.Count > 0 && !m_allowedTypes.Contains(assetType)) + continue; + + if (string.IsNullOrEmpty(connectionString)) + { + m_log.WarnFormat("[TSASSET SERVICE]: Ignoring empty connection string for asset type {0}", assetType); + continue; + } + + IAssetDataPlugin db = GetOrCreateDatabase(storageProvider, connectionString); + m_typeDatabases[assetType] = db; + AddProbeDatabase(db); + } + } + + private IAssetDataPlugin ResolveDatabaseForType(sbyte type) + { + if (m_typeDatabases.TryGetValue(type, out IAssetDataPlugin db)) + return db; + + return m_defaultDatabase; + } + + private IAssetDataPlugin GetOrCreateDatabase(string storageProvider, string connectionString) + { + if (m_connectionDatabases.TryGetValue(connectionString, out IAssetDataPlugin existing)) + return existing; + + IAssetDataPlugin created = CreateAndInitDatabase(storageProvider, connectionString); + m_connectionDatabases[connectionString] = created; + return created; + } + + private IAssetDataPlugin CreateAndInitDatabase(string storageProvider, string connectionString) + { + IAssetDataPlugin database = LoadPlugin(storageProvider); + if (database == null) + throw new Exception(string.Format("Could not find a storage interface in the module {0}", storageProvider)); + + database.Initialise(connectionString); + return database; + } + + private void AddProbeDatabase(IAssetDataPlugin db) + { + for (int i = 0; i < m_probeOrder.Count; i++) + { + if (object.ReferenceEquals(m_probeOrder[i], db)) + return; + } + + m_probeOrder.Add(db); + } + + private void EnqueueFallbackMigration(AssetBase asset) + { + if (asset == null) + return; + + if (m_fallbackMigrationSet.Count >= m_migrationQueueMax) + return; + + if (m_fallbackMigrationSet.TryAdd(asset.FullID, 0)) + m_fallbackMigrationQueue.Enqueue(asset); + } + + private void FallbackMigrationWorker() + { + while (true) + { + Thread.Sleep(m_migrationCheckIntervalMs); + + int requests = Interlocked.Exchange(ref m_requestsInWindow, 0); + if (requests > m_migrationLowTrafficMaxRequests) + continue; + + int migrated = 0; + while (migrated < m_migrationBatchSize && m_fallbackMigrationQueue.TryDequeue(out AssetBase asset)) + { + try + { + string id = Store(asset); + if (!id.Equals(UUID.Zero.ToString(), StringComparison.Ordinal)) + { + if (m_enableFallbackAutoDelete) + TryDeleteFromFallback(asset.ID); + + migrated++; + } + } + catch (Exception e) + { + m_log.WarnFormat("[TSASSET SERVICE]: Auto-migration failed for asset {0}: {1}", asset != null ? asset.FullID.ToString() : "", e.Message); + } + finally + { + if (asset != null) + m_fallbackMigrationSet.TryRemove(asset.FullID, out _); + } + } + + if (migrated > 0) + m_log.InfoFormat("[TSASSET SERVICE]: Auto-migrated {0} fallback assets during low traffic", migrated); + } + } + + private void TryDeleteFromFallback(string id) + { + if (m_fallbackService == null || string.IsNullOrEmpty(id)) + return; + + try + { + if (!m_fallbackService.Delete(id)) + m_log.WarnFormat("[TSASSET SERVICE]: Fallback auto-delete failed for asset {0}", id); + } + catch (Exception e) + { + m_log.WarnFormat("[TSASSET SERVICE]: Fallback auto-delete exception for asset {0}: {1}", id, e.Message); + } + } + } +} diff --git a/bin/Robust.HG.ini.example b/bin/Robust.HG.ini.example new file mode 100644 index 0000000..40a14d9 --- /dev/null +++ b/bin/Robust.HG.ini.example @@ -0,0 +1,938 @@ +; * Run +; * $ Robust.exe -inifile Robust.HG.ini +; * + +; * Configurations for enabling HG1.5 +; * +; * HG1.5 handlers are: OpenSim.Server.Handlers.dll:GatekeeperService +; * OpenSim.Server.Handlers.dll:UserAgentService +; * Additional OpenSim.Server.Handlers.dll:AssetServiceConnector and +; * OpenSim.Server.Handlers.dll:XInventoryInConnector +; * are started in port 8002, outside the firewall +; * +; ** +; * +; * The Const section allows us to define some basic information that we +; * will use throughout our configuration. We will provide examples for +; * setting the base url of the Robust server and the public and private ports +; * it uses. Changing the values of the constants will set the operating +; * parameters thoughout the configuration. Other constants that may prove +; * to be useful may be added to the followin section. They may be +; * referenced anywhere in the configuration by using ${Const|Name}. One +; * such use is providing a base path for setting locations that Robust +; * uses to write data. +; * +[Const] + ; The domain or IP of the Robust server. + BaseHostname = "127.0.0.1" + + ; The http URL of the Robust server. + BaseURL = "http://${Const|BaseHostname}" + + ; The https URL of the Robust server. + ; Use this if you have the SSL enabled. + ; BaseURL = "https://${Const|BaseHostname}" + + ; The public port of the Robust server + PublicPort = "8002" + + ; The private port of the Robust server + PrivatePort = "8003" + +; * The startup section lists all the connectors to start up in this server +; * instance. This may be only one, or it may be the entire server suite. +; * Multiple connectors should be separated by commas. +; * +; * These are the IN connectors the server uses, the in connectors +; * read this config file and load the needed service and database connectors +; * +; * The full syntax of a connector string is: +; * [[@]/][:] +; * +[Startup] + ; Place to create a PID file + ; If no path if specified then a PID file is not created. + ; PIDFile = "/tmp/Robust.exe.pid" + + ; Plugin Registry Location + ; Set path to directory for plugin registry. Information + ; about the registered repositories and installed plugins + ; will be stored here + ; The Robust.exe process must have R/W access to the location + RegistryLocation = "." + + ; Modular configurations + ; Set path to directory for modular ini files... + ; The Robust.exe process must have R/W access to the location + ConfigDirectory = "robust-include" + + ; Console commands can be saved to a file, so the command history persists after a restart. (default is true) + ConsoleHistoryFileEnabled = true + + ; The history file can be just a filename (relative to OpenSim's bin/ directory + ; or it can be a full path to somewhere else. (default is OpenSimConsoleHistory.txt in bin/) + ConsoleHistoryFile = "RobustConsoleHistory.txt" + + ; How many lines of command history should we keep? (default is 100) + ConsoleHistoryFileLines = 100 + + ; Time stamp commands in history file (default false) + ; ConsoleHistoryTimeStamp = false + + ;; SSL selfsigned certificate settings. + ; Enable selfsigned certificate creation for local and external use. When set to true, will create a folder SSL\ and 2 sub folders SSL\ssl\ and SSL\src\. + ; Next creates and store an RSA private key in SSL\src\ and the derived selfsigned certificate in SSL\ssl\ folder. + ; Is also possible to renew the certificate on every server restart if CertRenewOnStartup is set to true. + EnableRobustSelfsignedCertSupport = false + + ;Renew the selfsigned certificate on every server startup ? + RobustCertRenewOnStartup = false + + ;; Certificate options: + ; Set the certificate file name. the output files extensions are CertFileName.p12 and RobustCertFileName.pfx. This must be different than CertFileName in OpenSim.ini + RobustCertFileName = "Robust" + + ; Set the certificate password. + RobustCertPassword = "mycertpass" + + ; The certificate host name (CN). + RobustCertHostName = ${Const|BaseHostname} + + ; The certificate host IP. + RobustCertHostIp = "127.0.0.1" + + ; peers SSL certificate validation options + ; you can allow selfsigned certificates or no official CA with next option set to true + NoVerifyCertChain = true + ; you can also bypass the hostname or domain verification + NoVerifyCertHostname = true + ; having both options true does provide encryption but with low security + ; set both true if you don't care to use SSL, they are needed to contact regions or grids that do use it. + + +[ServiceList] + AssetServiceConnector = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:AssetServiceConnector" + InventoryInConnector = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:XInventoryInConnector" + ;; Uncomment if you have set up Freeswitch (see [FreeswitchService] below) + ;VoiceConnector = "8004/OpenSim.Server.Handlers.dll:FreeswitchServerConnector" + GridServiceConnector = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:GridServiceConnector" + GridInfoServerInConnector = "${Const|PublicPort}/OpenSim.Server.Handlers.dll:GridInfoServerInConnector" + AuthenticationServiceConnector = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:AuthenticationServiceConnector" + OpenIdServerConnector = "${Const|PublicPort}/OpenSim.Server.Handlers.dll:OpenIdServerConnector" + AvatarServiceConnector = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:AvatarServiceConnector" + LLLoginServiceInConnector = "${Const|PublicPort}/OpenSim.Server.Handlers.dll:LLLoginServiceInConnector" + PresenceServiceConnector = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:PresenceServiceConnector" + UserAccountServiceConnector = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:UserAccountServiceConnector" + GridUserServiceConnector = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:GridUserServiceConnector" + AgentPreferencesServiceConnector = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:AgentPreferencesServiceConnector" + FriendsServiceConnector = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:FriendsServiceConnector" + MapAddServiceConnector = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:MapAddServiceConnector" + MapGetServiceConnector = "${Const|PublicPort}/OpenSim.Server.Handlers.dll:MapGetServiceConnector" + ;; Uncomment this if you want offline IM to work + ; OfflineIMServiceConnector = "${Const|PrivatePort}/OpenSim.Addons.OfflineIM.dll:OfflineIMServiceRobustConnector" + ;; Uncomment this if you want Groups V2 to work + ; GroupsServiceConnector = "${Const|PrivatePort}/OpenSim.Addons.Groups.dll:GroupsServiceRobustConnector" + ;; Uncomment to provide bakes caching + ; BakedTextureService = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:XBakesConnector" + + ;; Uncomment for UserProfiles see [UserProfilesService] to configure... + ; UserProfilesServiceConnector = "${Const|PublicPort}/OpenSim.Server.Handlers.dll:UserProfilesConnector" + + ;; Uncomment if you want to have centralized estate data + ; EstateDataService = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:EstateDataRobustConnector" + + MuteListConnector = "${Const|PrivatePort}/OpenSim.Server.Handlers.dll:MuteListServiceConnector" + + ;; Additions for Hypergrid + + GatekeeperServiceInConnector = "${Const|PublicPort}/OpenSim.Server.Handlers.dll:GatekeeperServiceInConnector" + UserAgentServerConnector = "${Const|PublicPort}/OpenSim.Server.Handlers.dll:UserAgentServerConnector" + HeloServiceInConnector = "${Const|PublicPort}/OpenSim.Server.Handlers.dll:HeloServiceInConnector" + HGFriendsServerConnector = "${Const|PublicPort}/OpenSim.Server.Handlers.dll:HGFriendsServerConnector" + InstantMessageServerConnector = "${Const|PublicPort}/OpenSim.Server.Handlers.dll:InstantMessageServerConnector" + HGInventoryServiceConnector = "HGInventoryService@${Const|PublicPort}/OpenSim.Server.Handlers.dll:XInventoryInConnector" + HGAssetServiceConnector = "HGAssetService@${Const|PublicPort}/OpenSim.Server.Handlers.dll:AssetServiceConnector" + ;; Uncomment this if you want Groups V2, HG to work + ; HGGroupsServiceConnector = "${Const|PublicPort}/OpenSim.Addons.Groups.dll:HGGroupsServiceRobustConnector" + +; * This is common for all services, it's the network setup for the entire +; * server instance, if none is specified above +; * +[Network] + port = ${Const|PrivatePort} + + ; HTTPS for "Out of band" management applications such as the remote admin + ; module. May specify https_main = True to make the main http server + ; use https or "False" to make the main server HTTP + ; https_main = False + ; + ; Create https_listener = "True" will create a listener on the port + ; specified. Provide the path to your server certificate along with it's + ; password + ; https_listener = False + ; + ; Set our listener to this port + ; https_port = 0 + ; + ; Path to X509 certificate + ; cert_path = "path/to/cert.p12" + ; + ; Password for cert + ; cert_pass = "password" + + ;; The follow 3 variables are for HTTP Basic Authentication for the Robust services. + ;; Use this if your central services in port ${Const|PrivatePort} need to be accessible on the Internet + ;; but you want to protect them from unauthorized access. + ; AuthType = "BasicHttpAuthentication" + ; HttpAuthUsername = "some_username" + ; HttpAuthPassword = "some_password" + ;; + ;; AuthType above can be overriden in any of the service sections below by + ; AuthType = "None" + ;; This is useful in cases where you want to protect most of the services, + ;; but unprotect individual services. Username and Password can also be + ;; overriden if you want to use different credentials for the different services. + ;; Hypergrid services are not affected by this; they are publicly available + ;; by design. + + ;; By default, scripts are not allowed to call private services via llHttpRequest() + ;; Such calls are detected by the X-SecondLife-Shared HTTP header + ;; If you allow such calls you must be sure that they are restricted to very trusted scripters + ;; (remember scripts can also be in visiting avatar attachments). + ;; This can be overriden in individual private service sections if necessary + AllowllHTTPRequestIn = false + + ; * The following are for the remote console + ; * They have no effect for the local or basic console types + ; * Leave commented to disable logins to the console + ;ConsoleUser = Test + ;ConsolePass = secret + ;ConsolePort = 0 + + +[Hypergrid] + ;# {HomeURI} {Hypergrid} {The Home URL of this grid} {} + ;; This is the address of the external robust server that + ;; runs the UserAgentsService, possibly this server. + ;; For example http://myworld.com:8002 + ;; This is a default that can be overwritten in some sections. + ; HomeURI = "${Const|BaseURL}:${Const|PublicPort}" + + ;# {GatekeeperURI} {Hypergrid} {The URL of the gatekeeper of this grid} {} + ;; This is the address of the external robust server + ;; that runs the Gatekeeper service, possibly this server. + ;; For example http://myworld.com:8002 + ;; This is a default that can be overwritten in some sections. + ; GatekeeperURI = "${Const|BaseURL}:${Const|PublicPort}" + + ;# {GatekeeperURIAlias} {Hypergrid} {alternative hostnames (FQDN), or IPs of the gatekeeper of this grid and port (default 80 or 443 if alias starts with https://)} {} + ;; comma separated list, + ;; this is to allow this grid to identify this as references to itself + ;; entries can be unsecure url (host:port) if using ssl, direct login url if different, old grid url, etc + ; GatekeeperURIAlias = "login.osgrid.org" + +[AccessControl] + ;# {AllowedClients} {} {Bar (|) separated list of allowed clients} {} + ;; Bar (|) separated list of viewers which may gain access to the regions. + ;; One can use a substring of the viewer name to enable only certain + ;; versions + ;; Example: Agent uses the viewer "Imprudence 1.3.2.0" + ;; - "Imprudence" has access + ;; - "Imprudence 1.3" has access + ;; - "Imprudence 1.3.1" has no access + ; AllowedClients = "" + + ;# {DeniedClients} {} {Bar (|) separated list of denied clients} {} + ;; Bar (|) separated list of viewers which may not gain access to the regions. + ;; One can use a Substring of the viewer name to disable only certain + ;; versions + ;; Example: Agent uses the viewer "Imprudence 1.3.2.0" + ;; - "Imprudence" has no access + ;; - "Imprudence 1.3" has no access + ;; - "Imprudence 1.3.1" has access + ; DeniedClients = "" + +[DatabaseService] + ; PGSQL + ; Uncomment these lines if you want to use PGSQL storage + ; Change the connection string to your db details + ;StorageProvider = "OpenSim.Data.PGSQL.dll" + ;ConnectionString = "Server=localhost;Database=opensim;User Id=opensim; password=***;" + + ; MySQL + ; Uncomment these lines if you want to use MySQL storage + ; Change the connection string to your db details + ; If using MySQL 8.0.4 or later, check that default-authentication-plugin=mysql_native_password + ; rather than caching_sha2_password is set in /etc/mysql/mysql.conf.d/mysqld.cnf (not applicable to MariaDB). + StorageProvider = "OpenSim.Data.MySQL.dll" + ConnectionString = "Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;" + + +; * As an example, the below configuration precisely mimicks the legacy +; * asset server. It is read by the asset IN connector (defined above) +; * and it then loads the OUT connector (a local database module). That, +; * in turn, reads the asset loader and database connection information +; * +[AssetService] + + ;; Choose an asset service (Only one option should be enabled) + LocalServiceModule = "OpenSim.Services.AssetService.dll:TSAssetConnector" + ;LocalServiceModule = "OpenSim.Services.AssetService.dll:AssetService" + ;LocalServiceModule = "OpenSim.Services.FSAssetService.dll:FSAssetConnector" + + ;; FSAssets Directories. Base directory, where final asset files are stored and Spool directory for temp files + ;; These directories must be on the same physical filesystem + ;BaseDirectory = "./fsassets/data" + ;SpoolDirectory = "./fsassets/tmp" + + ;; FSAssets only: if running several instances, only one can run some services, so others need to be set as secondary + ;; for secondary instances uncoment this line + ;SecondaryInstance = true + + ;; Optional fallback to original asset service if TS routing misses + FallbackService = "OpenSim.Services.AssetService.dll:AssetService" + + ;; How many days since last updating the access time before its updated again by FSAssets when accessing an asset + ;; Reduces DB calls if asset is requested often. Default value 0 will always update access time + ;DaysBetweenAccessTimeUpdates = 30 + + ;; Should FSAssets print read/write stats to the robust console, default is true + ;ShowConsoleStats = true + + ;; Legacy provider used by FallbackService (optional but recommended) + StorageProvider = "OpenSim.Data.MySQL.dll:MySQLAssetData" + ConnectionString = "Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;" + + ;; FSAssets uses a folder structure to reduce file access times. By default that structure is 00/00/00 + ;; Setting this to true will switch to an older 000/000 format, which can cause issues with long file lists + ;UseOsgridFormat = false + + ;; The following are common to both the default asset service and FSAsset service + + ;; Common asset service options + DefaultAssetLoader = "OpenSim.Framework.AssetLoader.Filesystem.dll" + AssetLoaderArgs = "./assets/AssetSets.xml" + + ; Allow maptile assets to remotely deleted by remote calls to the asset service. + ; There is no harm in having this as false - it just means that historical maptile assets are not deleted. + ; This only applies to maptiles served via the version 1 viewer mechanisms + ; Default is false + AllowRemoteDelete = false + + ; Allow all assets to be remotely deleted. + ; Only set this to true if you are operating a grid where you control all calls to the asset service + ; (where a necessary condition is that you control all simulators) and you need this for admin purposes. + ; If set to true, AllowRemoteDelete = true is required as well. + ; Default is false. + AllowRemoteDeleteAllTypes = false + +[TSAssetService] + + ;; TS provider (explicit class avoids wrong provider selection) + StorageProvider = "OpenSim.Data.MySQL.dll:MySQLtsAssetData" + ConnectionString = "Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;" + + ;; Optional type allowlist. Empty means all asset types. + TSAssetType = "" + + ;; Fallback hits are always copied immediately into TS storage (standard behavior) + ;; If true, failed immediate copies are queued and retried in background only when traffic is low + ;; If false, failed immediate copies are not retried by background worker + EnableFallbackAutoMigration = false + ;; If true, fallback records are deleted after successful copy into TS storage + EnableFallbackAutoDelete = false + MigrationCheckIntervalSeconds = 60 + MigrationLowTrafficMaxRequests = 3 + MigrationBatchSize = 25 + MigrationQueueMax = 50000 + + ;; Per-type DB routing. Format: AssetType:ConnectionString + ;; Everything was inserted here without any consideration. + AssetDatabases = " + -2:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 0:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 1:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 2:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 3:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 5:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 6:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 7:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 8:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 10:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 11:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 13:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 17:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 18:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 19:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 20:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 21:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 22:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 24:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 25:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 26:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 49:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 56:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + 57:Source=localhost;Database=robust;User ID=opensim;Password=opensim123;Old Guids=true;SslMode=None;; + " + +; * This configuration loads the inventory server modules. It duplicates +; * the function of the legacy inventory server +; * +[InventoryService] + LocalServiceModule = "OpenSim.Services.InventoryService.dll:XInventoryService" + + ; Will calls to purge folders (empty trash) and immediately delete/update items or folders (not move to trash first) succeed? + ; If this is set to false then some other arrangement must be made to perform these operations if necessary. + AllowDelete = true + + +; * This is the new style grid service. +; * "Realm" is the table that is used for user lookup. +; * It defaults to "regions", which uses the legacy tables +; * +[GridService] + LocalServiceModule = "OpenSim.Services.GridService.dll:GridService" + + ; Realm = "regions" + ; AllowDuplicateNames = "" + + ;; Perform distance check for the creation of a linked region + ; Check4096 = "True" + + ;; Needed to display non-default map tile images for linked regions + AssetService = "OpenSim.Services.AssetService.dll:AssetService" + + ;; Directory for map tile images of linked regions + ; MapTileDirectory = "./maptiles" + + ;; Next, we can specify properties of regions, including default and fallback regions + ;; The syntax is: Region_ = "" + ;; or: Region_ = "" + ;; where can be DefaultRegion, DefaultHGRegion, FallbackRegion, NoDirectLogin, Persistent, LockedOut, Reservation, NoMove, Authenticate + ;; + ;; DefaultRegion If a local login cannot be placed in the required region (e.g. home region does not exist, avatar is not allowed entry, etc.) + ;; then this region becomes the destination. Only the first online default region will be used. If no DefaultHGRegion + ;; is specified then this will also be used as the region for hypergrid connections that require it (commonly because they have not specified + ;; an explicit region. + ;; + ;; DefaultHGRegion If an avatar connecting via the hypergrid does not specify a region, then they are placed here. Only the first online + ;; region will be used. + ;; + ;; FallbackRegion If the DefaultRegion is not available for a local login, then any FallbackRegions are tried instead. These are tried in the + ;; order specified. This only applies to local logins at this time, not Hypergrid connections. + ;; + ;; NoDirectLogin A hypergrid user cannot directly connect to this region. This does not apply to local logins. + ;; + ;; Persistent When the simulator is shutdown, the region is signalled as offline but left registered on the grid. + ;; + ;; Example specification: + ; Region_Welcome_Area = "DefaultRegion, DefaultHGRegion" + ; (replace spaces with underscore) + + ;; Allow Hyperlinks to be created at the console + HypergridLinker = true + + ;; Allow supporting viewers to export content + ;; Set to false to prevent export + ExportSupported = true + + ;; If you have this set under [Hypergrid], no need to set it here, leave it commented + ; GatekeeperURI = "${Const|BaseURL}:${Const|PublicPort}" + + +; * This is the configuration for the freeswitch server in grid mode +[FreeswitchService] + LocalServiceModule = "OpenSim.Services.FreeswitchService.dll:FreeswitchService" + + ;; The IP address of your FreeSWITCH server. + ;; This address must be reachable by viewers. + ; ServerAddress = 127.0.0.1 + + ;; The following configuration parameters are optional + + ;; By default, this is the same as the ServerAddress + ; Realm = 127.0.0.1 + + ;; By default, this is the same as the ServerAddress on port 5060 + ; SIPProxy = 127.0.0.1:5060 + + ;; Default is 5000ms + ; DefaultTimeout = 5000 + + ;; The dial plan context. Default is "default" + ; Context = default + + ;; Currently unused + ; UserName = freeswitch + + ;; Currently unused + ; Password = password + + ;; The following parameters are for STUN = Simple Traversal of UDP through NATs + ;; See http://wiki.freeswitch.org/wiki/NAT_Traversal + ;; stun.freeswitch.org is not guaranteed to be running so use it in + ;; production at your own risk + ; EchoServer = 127.0.0.1 + ; EchoPort = 50505 + ; AttemptSTUN = false + + +; * This is the new style authentication service. Currently, only MySQL +; * is implemented. +; * +[AuthenticationService] + ; for the server connector + LocalServiceModule = "OpenSim.Services.AuthenticationService.dll:PasswordAuthenticationService" + ; Realm = "auth" + + ;; Allow the service to process HTTP getauthinfo calls. + ;; Default is false. + ; AllowGetAuthInfo = false + + ;; Allow the service to process HTTP setauthinfo calls. + ;; Default is false. + ; AllowSetAuthInfo = false + + ;; Allow the service to process HTTP setpassword calls. + ;; Default is false. + ; AllowSetPassword = false + + +[OpenIdService] + ; for the server connector + AuthenticationServiceModule = "OpenSim.Services.AuthenticationService.dll:PasswordAuthenticationService" + UserAccountServiceModule = "OpenSim.Services.UserAccountService.dll:UserAccountService" + + +; * This is the new style user service. +; * "Realm" is the table that is used for user lookup. +; * It defaults to "UserAccounts", which uses the new style. +; * Realm = "users" will use the legacy tables as an authentication source +; * +[UserAccountService] + ; for the server connector + LocalServiceModule = "OpenSim.Services.UserAccountService.dll:UserAccountService" + ; Realm = "UserAccounts" + + ; These are for creating new accounts by the service + AuthenticationService = "OpenSim.Services.AuthenticationService.dll:PasswordAuthenticationService" + PresenceService = "OpenSim.Services.PresenceService.dll:PresenceService" + GridService = "OpenSim.Services.GridService.dll:GridService" + InventoryService = "OpenSim.Services.InventoryService.dll:XInventoryService" + AvatarService = "OpenSim.Services.AvatarService.dll:AvatarService" + GridUserService = "OpenSim.Services.UserAccountService.dll:GridUserService" + + ;; This switch creates the minimum set of body parts and avatar entries for a viewer 2 + ;; to show a default "Ruth" avatar rather than a cloud for a newly created user. + ;; Default is false + CreateDefaultAvatarEntries = true + + ;; Allow the service to process HTTP createuser calls. + ;; Default is false. + ; AllowCreateUser = false + + ;; Allow the service to process HTTP setaccount calls. + ;; Default is false. + ; AllowSetAccount = false + + +[GridUserService] + ; for the server connector + LocalServiceModule = "OpenSim.Services.UserAccountService.dll:GridUserService" + + +[AgentPreferencesService] + ; for the server connector + LocalServiceModule = "OpenSim.Services.UserAccountService.dll:AgentPreferencesService" + + +[PresenceService] + ; for the server connector + LocalServiceModule = "OpenSim.Services.PresenceService.dll:PresenceService" + +[AvatarService] + ; for the server connector + LocalServiceModule = "OpenSim.Services.AvatarService.dll:AvatarService" + + +[FriendsService] + ; for the server connector + LocalServiceModule = "OpenSim.Services.FriendsService.dll:FriendsService" + +[EstateService] + LocalServiceModule = "OpenSim.Services.EstateService.dll:EstateDataService" + +[LibraryService] + LibraryName = "OpenSim Library" + DefaultLibrary = "./inventory/Libraries.xml" + + +[LoginService] + ; for the server connector + LocalServiceModule = "OpenSim.Services.LLLoginService.dll:LLLoginService" + ; for the service + UserAccountService = "OpenSim.Services.UserAccountService.dll:UserAccountService" + GridUserService = "OpenSim.Services.UserAccountService.dll:GridUserService" + AuthenticationService = "OpenSim.Services.AuthenticationService.dll:PasswordAuthenticationService" + InventoryService = "OpenSim.Services.InventoryService.dll:XInventoryService" + AvatarService = "OpenSim.Services.AvatarService.dll:AvatarService" + PresenceService = "OpenSim.Services.PresenceService.dll:PresenceService" + GridService = "OpenSim.Services.GridService.dll:GridService" + SimulationService ="OpenSim.Services.Connectors.dll:SimulationServiceConnector" + LibraryService = "OpenSim.Services.InventoryService.dll:LibraryService" + FriendsService = "OpenSim.Services.FriendsService.dll:FriendsService" + ; The minimum user level required for a user to be able to login. 0 by default + ; If you disable a particular user's account then you can set their login level below this number. + ; You can also change this level from the console though these changes will not be persisted. + ; MinLoginLevel = 0 + + ;; for hypergrid + UserAgentService = "OpenSim.Services.HypergridService.dll:UserAgentService" + + ; This inventory service will be used to initialize the user's inventory + HGInventoryServicePlugin = "HGInventoryService@OpenSim.Services.HypergridService.dll:HGSuitcaseInventoryService" + ; NOTE: HGInventoryServiceConstructorArg is deprecated. For now it will work, but see above + ; for the correct method if passing additional arguments. + ;; end hypergrid + + ; Ask co-operative viewers to use a different currency name + ;Currency = "" + + ;; Set minimum fee to publish classified + ; ClassifiedFee = 0 + + WelcomeMessage = "Welcome, Avatar!" + AllowRemoteSetLoginLevel = "false" + + ; For V2 map + MapTileURL = "${Const|BaseURL}:${Const|PublicPort}/"; + + ; Url to search service + ; SearchURL = "${Const|BaseURL}:${Const|PublicPort}/"; + + ; For V3 destination guide + ; DestinationGuide = "${Const|BaseURL}/guide" + + ; For V3 avatar picker (( work in progress )) + ; AvatarPicker = "${Const|BaseURL}/avatars" + + ; If you run this login server behind a proxy, set this to true + ; HasProxy = false + + ; Defaults for the users, if none is specified in the useraccounts table entry (ServiceURLs) + ;; If you have GatekeeperURI set under [Hypergrid], no need to set it here, leave it commented + ; GatekeeperURI = "${Const|BaseURL}:${Const|PublicPort}" + + SRV_HomeURI = "${Const|BaseURL}:${Const|PublicPort}" + SRV_InventoryServerURI = "${Const|BaseURL}:${Const|PublicPort}" + SRV_AssetServerURI = "${Const|BaseURL}:${Const|PublicPort}" + SRV_ProfileServerURI = "${Const|BaseURL}:${Const|PublicPort}" + SRV_FriendsServerURI = "${Const|BaseURL}:${Const|PublicPort}" + SRV_IMServerURI = "${Const|BaseURL}:${Const|PublicPort}" + SRV_GroupsServerURI = "${Const|BaseURL}:${Const|PublicPort}" + + ;# {DSTZone} {} {Override Daylight Saving Time rules} {* none local} "America/Los_Angeles;Pacific Standard Time" + ;; Viewers do not receive timezone information from the server - almost all (?) default to Pacific Standard Time + ;; However, they do rely on the server to tell them whether it's Daylight Saving Time or not. + ;; Hence, calculating DST based on a different timezone can result in a misleading viewer display and inconsistencies between grids. + ;; By default, this setting uses various timezone names to calculate DST with regards to the viewer's standard PST. + ;; Options are + ;; "none" no DST + ;; "local" use the server's only timezone to calculate DST. This is previous OpenSimulator behaviour. + ;; "America/Los_Angeles;Pacific Standard Time" use these timezone names to look up Daylight savings. + ;; 'America/Los_Angeles' is used on Linux/Mac systems whilst 'Pacific Standard Time' is used on Windows + DSTZone = "America/Los_Angeles;Pacific Standard Time" + + ;; If the region requested at login is not found and there are no default or fallback regions + ;; online or defined in section [GridService], try to send user to any region online + ;; this similar to legacy (was disabled on 0.9.2.0) + ;; you should set this to false and define regions with Default and possible Fallback flags + ;; With this option set to true, users maybe sent to regions they where not supposed to be, or even know about + AllowLoginFallbackToAnyRegion = false + + ;Basic Login Service Dos Protection Tweaks + ;; + ;; Some Grids/Users use a transparent proxy that makes use of the X-Forwarded-For HTTP Header, If you do, set this to true + ;; If you set this to true and you don't have a transparent proxy, it may allow attackers to put random things in the X-Forwarded-For header to + ;; get around this basic DOS protection. + ;DOSAllowXForwardedForHeader = false + ;; + ;; The protector adds up requests during this rolling period of time, default 10 seconds + ;DOSRequestTimeFrameMS = 10000 + ;; + ;; The amount of requests in the above timeframe from the same endpoint that triggers protection + ;DOSMaxRequestsInTimeFrame = 5 + ;; + ;; The amount of time that a specific endpoint is blocked. Default 2 minutes. + ;DOSForgiveClientAfterMS = 120000 + ;; + ;; To turn off basic dos protection, set the DOSMaxRequestsInTimeFrame to 0. + + ;; Allow banning via hashed MAC must be set in both [GatekeeperService] and [LoginService] + ;DeniedMacs = "YOURLONGMACTRSING ANOTHERMAC" + +[MapImageService] + LocalServiceModule = "OpenSim.Services.MapImageService.dll:MapImageService" + + ; Set this if you want to change the default + ; TilesStoragePath = "maptiles" + ; + ; If for some reason you have the AddMapTile service outside the firewall (e.g. ${Const|PublicPort}), + ; you may want to set this. Otherwise, don't set it, because it's already protected. + ; GridService = "OpenSim.Services.GridService.dll:GridService" + ; + ; Additionally, if you run this server behind a proxy, set this to true + ; HasProxy = false + + +[GridInfoService] + ; These settings are used to return information on a get_grid_info call. + ; Client launcher scripts and third-party clients make use of this to + ; autoconfigure the client and to provide a nice user experience. If you + ; want to facilitate that, you should configure the settings here according + ; to your grid or standalone setup. + ; + ; See http://opensimulator.org/wiki/GridInfo + + ; login uri: for grid this is the login server URI + login = ${Const|BaseURL}:${Const|PublicPort}/ + + ; long grid name: the long name of your grid + gridname = "the lost continent of hippo" + + ; short grid name: the short name of your grid + gridnick = "hippogrid" + + ; login page: optional: if it exists it will be used to tell the client to use + ; this as splash page + ;welcome = ${Const|BaseURL}/welcome + + ; helper uri: optional: if it exists it will be used to tell the client to use + ; this for all economy related things + ;economy = ${Const|BaseURL}/economy + + ; web page of grid: optional: page providing further information about your grid + ;about = ${Const|BaseURL}/about + + ; account creation: optional: page providing further information about obtaining + ; a user account on your grid + ;register = ${Const|BaseURL}/register + + ; help: optional: page providing further assistance for users of your grid + ;help = ${Const|BaseURL}/help + + ; password help: optional: page providing password assistance for users of your grid + ;password = ${Const|BaseURL}/password + + ; HG address of the gatekeeper, if you have one + ; this is the entry point for all the regions of the world + ; gatekeeper = ${Const|BaseURL}:${Const|PublicPort}/ + + ; a http page for grid status + ;GridStatus = ${Const|BaseURL}:${Const|PublicPort}/GridStatus + ; a RSS page for grid status + ;GridStatusRSS = ${Const|BaseURL}:${Const|PublicPort}/GridStatusRSS + + ; optional web page for profiles + ;[AGENT_NAME] will be converted to Firstname.LastName by viewers + ; web_profile_url = http://webprofilesurl:ItsPort?name=[AGENT_NAME] + + ; http REST handler /get_grid_stats may output some grid statistics This can be turned off here + ;DisableStatsEndpoint = true + +[GatekeeperService] + LocalServiceModule = "OpenSim.Services.HypergridService.dll:GatekeeperService" + ;; for the service + UserAccountService = "OpenSim.Services.UserAccountService.dll:UserAccountService" + UserAgentService = "OpenSim.Services.HypergridService.dll:UserAgentService" + PresenceService = "OpenSim.Services.PresenceService.dll:PresenceService" + GridUserService = "OpenSim.Services.UserAccountService.dll:GridUserService" + GridService = "OpenSim.Services.GridService.dll:GridService" + AuthenticationService = "OpenSim.Services.Connectors.dll:AuthenticationServicesConnector" + SimulationService ="OpenSim.Services.Connectors.dll:SimulationServiceConnector" + ; how does the outside world reach me? This acts as public key too. + ;; If you have GatekeeperURI set under [Hypergrid], no need to set it here, leave it commented + ; ExternalName = "${Const|BaseURL}:${Const|PublicPort}" + + ; Does this grid allow incoming links to any region in it? + ; If false, HG TPs happen only to the Default regions specified in [GridService] section + AllowTeleportsToAnyRegion = true + + ; If you run this gatekeeper server behind a proxy, set this to true + ; HasProxy = false + + ;; Are foreign visitors allowed? + ;ForeignAgentsAllowed = true + ;; + ;; If ForeignAgentsAllowed is true, make exceptions using AllowExcept. + ;; Leave blank or commented for no exceptions. + ; AllowExcept = "http://griefer.com:8002, http://enemy.com:8002" + ;; + ;; If ForeignAgentsAllowed is false, make exceptions using DisallowExcept + ;; Leave blank or commented for no exceptions. + ; DisallowExcept = "http://myfriendgrid.com:8002, http://myboss.com:8002" + + ;; Allow banning via hashed MAC must be set in both [GatekeeperService] and [LoginService] + ;DeniedMacs = "YOURLONGMACTRSING ANOTHERMAC" + +[UserAgentService] + LocalServiceModule = "OpenSim.Services.HypergridService.dll:UserAgentService" + ;; for the service + GridUserService = "OpenSim.Services.UserAccountService.dll:GridUserService" + GridService = "OpenSim.Services.GridService.dll:GridService" + GatekeeperService = "OpenSim.Services.HypergridService.dll:GatekeeperService" + PresenceService = "OpenSim.Services.PresenceService.dll:PresenceService" + FriendsService = "OpenSim.Services.FriendsService.dll:FriendsService" + UserAccountService = "OpenSim.Services.UserAccountService.dll:UserAccountService" + + ; If you run this user agent server behind a proxy, set this to true + ; HasProxy = false + + ;; If you separate the UserAgentService from the LoginService, set this to + ;; the IP address of the machine where your LoginService is + ;LoginServerIP = "127.0.0.1" + + ; User level required to be contacted from other grids + ;LevelOutsideContacts = 0 + + ;; Restrictions on destinations of local users. + ;; Are local users allowed to visit other grids? + ;; What user level? Use variables of this forrm: + ;; ForeignTripsAllowed_Level_ = true | false + ;; (the default is true) + ;; For example: + ; ForeignTripsAllowed_Level_0 = false + ; ForeignTripsAllowed_Level_200 = true ; true is default, no need to say it + ;; + ;; If ForeignTripsAllowed is false, make exceptions using DisallowExcept + ;; Leave blank or commented for no exceptions. + ; DisallowExcept_Level_0 = "http://myothergrid.com:8002, http://boss.com:8002" + ;; + ;; If ForeignTripsAllowed is true, make exceptions using AllowExcept. + ;; Leave blank or commented for no exceptions. + ; AllowExcept_Level_200 = "http://griefer.com:8002, http://enemy.com:8002" + + ;; This variable controls what is exposed to profiles of local users + ;; as seen from outside of this grid. Leave it uncommented for exposing + ;; UserTitle, UserFlags and the creation date. Uncomment and change to False + ;; to block this info from being exposed. + ; ShowUserDetailsInHGProfile = True + + +; * The interface that local users get when they are in other grids. +; * This restricts the inventory operations while in other grids. +; * Still not completely safe, especially if users perform inventory operations +; * while in those grids. The more the user accesses his/her inventory, the more +; * those simulators will know about the user's inventory. +; * +[HGInventoryService] + ; For the InventoryServiceInConnector + LocalServiceModule = "OpenSim.Services.HypergridService.dll:HGSuitcaseInventoryService" + ;; alternatives: + ;; HG1.5, more permissive, not recommended, but still supported + ;LocalServiceModule = "OpenSim.Services.HypergridService.dll:HGInventoryService" + ;; HG1.0, totally permissive, not recommended, but OK for grids with 100% trust + ;LocalServiceModule = "OpenSim.Services.InventoryService.dll:XInventoryService" + + UserAccountsService = "OpenSim.Services.UserAccountService.dll:UserAccountService" + AvatarService = "OpenSim.Services.AvatarService.dll:AvatarService" + + ; HGInventoryService is a public-facing inventory service that allows users to + ; interact with their suitcase folder when on a foreign grid. This reuses the general inventory service connector. + ; Hence, if the user has set up authentication in [Network] to protect their private services + ; make sure it is not set here. + AuthType = None + + ;; Can overwrite the default in [Hypergrid], but probably shouldn't + ; HomeURI = "${Const|BaseURL}:${Const|PublicPort}" + + +; * The interface that local users get when they are in other grids. +; * This restricts the access that the rest of the world has to +; * the assets of this world. +; * +[HGAssetService] + ;; Use the second option if you have FSAsset service enabled + LocalServiceModule = "OpenSim.Services.HypergridService.dll:HGAssetService" + ;LocalServiceModule = "OpenSim.Services.HypergridService.dll:HGFSAssetService" + + UserAccountsService = "OpenSim.Services.UserAccountService.dll:UserAccountService" + + ; HGAssetService is a public-facing service that allows users to + ; read and create assets when on another grid. This reuses the general asset service connector. + ; Hence, if the user has set up authentication in [Network] to protect their private services + ; make sure it is overriden for this public service. + AuthType = None + + ;; Can overwrite the default in [Hypergrid], but probably shouldn't + ; HomeURI = "${Const|BaseURL}:${Const|PublicPort}" + + ;; The asset types that this grid can export to / import from other grids. + ;; Comma separated. + ;; Valid values are all the asset types in OpenMetaverse.AssetType, namely: + ;; Unknown, Texture, Sound, CallingCard, Landmark, Clothing, Object, Notecard, LSLText, + ;; LSLBytecode, TextureTGA, Bodypart, SoundWAV, ImageTGA, ImageJPEG, Animation, Gesture, Mesh + ;; + ;; Leave blank or commented if you don't want to apply any restrictions. + ;; A more strict, but still reasonable, policy may be to disallow the exchange + ;; of scripts, like so: + ; DisallowExport ="LSLText" + ; DisallowImport ="LSLBytecode" + + +[HGFriendsService] + LocalServiceModule = "OpenSim.Services.HypergridService.dll:HGFriendsService" + UserAgentService = "OpenSim.Services.HypergridService.dll:UserAgentService" + FriendsService = "OpenSim.Services.FriendsService.dll:FriendsService" + UserAccountService = "OpenSim.Services.UserAccountService.dll:UserAccountService" + GridService = "OpenSim.Services.GridService.dll:GridService" + PresenceService = "OpenSim.Services.PresenceService.dll:PresenceService" + + +[HGInstantMessageService] + LocalServiceModule = "OpenSim.Services.HypergridService.dll:HGInstantMessageService" + GridService = "OpenSim.Services.GridService.dll:GridService" + PresenceService = "OpenSim.Services.PresenceService.dll:PresenceService" + UserAgentService = "OpenSim.Services.HypergridService.dll:UserAgentService" + ; This should always be true in the Robust config + InGatekeeper = True + + +[Messaging] + ; OfflineIM + OfflineIMService = "OpenSim.Addons.OfflineIM.dll:OfflineIMService" + ; maximum number of offline ims per user, default 25 + ;MaxOfflineIMs = 25; + + +[Groups] + ;; for the HG Groups service + OfflineIMService = "OpenSim.Addons.OfflineIM.dll:OfflineIMService" + UserAccountService = "OpenSim.Services.UserAccountService.dll:UserAccountService" + + ;; What is the HomeURI of users associated with this grid? + ;; Can overwrite the default in [Hypergrid], but probably shouldn't + ; HomeURI = "${Const|BaseURL}:${Const|PublicPort}" + ;; end hypergrid + + ;; Sets the maximum number of groups an agent may join + ; MaxAgentGroups = 42 + + +[UserProfilesService] + LocalServiceModule = "OpenSim.Services.UserProfilesService.dll:UserProfilesService" + Enabled = false + ;; Configure this for separate profiles database + ;; ConnectionString = "Data Source=localhost;Database=opensim;User ID=opensim;Password=*****;Old Guids=true;" + ;; Realm = UserProfiles + UserAccountService = OpenSim.Services.UserAccountService.dll:UserAccountService + AuthenticationServiceModule = "OpenSim.Services.AuthenticationService.dll:PasswordAuthenticationService" + + +[BakedTextureService] + LocalServiceModule = "OpenSim.Server.Handlers.dll:XBakes" + ;; This directory must be writable by the user ROBUST runs as. It will be created automatically. + BaseDirectory = "./bakes" + +[MuteListService] + LocalServiceModule = "OpenSim.Services.MuteListService.dll:MuteListService"