Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 64 additions & 21 deletions src/ScriptEngine/Machine/Contexts/AttachedScriptsFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This Source Code Form is subject to the terms of the
at http://mozilla.org/MPL/2.0/.
----------------------------------------------------------*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text;
using OneScript.Sources;
Expand All @@ -22,15 +23,18 @@ namespace ScriptEngine.Machine.Contexts
{
public class AttachedScriptsFactory
{
private readonly Dictionary<string, IExecutableModule> _loadedModules;
private readonly Dictionary<string, string> _fileHashes;
// Сценарии подключаются и из фоновых заданий: регистрация под блокировкой,
// модули читаются без нее при каждом создании объекта
private readonly ConcurrentDictionary<string, IExecutableModule> _loadedModules;
private readonly ConcurrentDictionary<string, string> _fileHashes;
private readonly object _registrationLock = new object();

private readonly ScriptingEngine _engine;

internal AttachedScriptsFactory(ScriptingEngine engine)
{
_loadedModules = new Dictionary<string, IExecutableModule>(StringComparer.InvariantCultureIgnoreCase);
_fileHashes = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
_loadedModules = new ConcurrentDictionary<string, IExecutableModule>(StringComparer.InvariantCultureIgnoreCase);
_fileHashes = new ConcurrentDictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
_engine = engine;
}

Expand Down Expand Up @@ -106,10 +110,10 @@ private void ThrowIfTypeExist(string typeName, SourceCode code)
{
string moduleCode = code.GetSourceCode();
string hash = GetMd5Hash(md5Hash, moduleCode);
string storedHash = _fileHashes[typeName];

// Хеша нет у классов библиотек и у модуля, регистрацию которого откатили
StringComparer comparer = StringComparer.OrdinalIgnoreCase;
if(comparer.Compare(hash, storedHash) != 0)
if(!_fileHashes.TryGetValue(typeName, out var storedHash) || comparer.Compare(hash, storedHash) != 0)
throw new RuntimeException("Type «" + typeName + "» already registered");
}

Expand All @@ -119,38 +123,77 @@ private void ThrowIfTypeExist(string typeName, SourceCode code)

private void CompileAndRegister(Type type, ICompilerFrontend compiler, string typeName, SourceCode code, IBslProcess process)
{
if(_loadedModules.ContainsKey(typeName))
// Под блокировкой: регистрация из другого потока могла еще не закончиться или откатиться
lock (_registrationLock)
{
return;
if(_loadedModules.ContainsKey(typeName))
{
return;
}
}

var module = CompileModuleFromSource(compiler, code, null, process);
_loadedModules.Add(typeName, module);
string hash;
using(var md5Hash = MD5.Create())
{
var hash = GetMd5Hash(md5Hash, code.GetSourceCode());
_fileHashes.Add(typeName, hash);
hash = GetMd5Hash(md5Hash, code.GetSourceCode());
}

TypeManager.RegisterType(typeName, default, type);
lock (_registrationLock)
{
// Пока модуль компилировался, этот же тип могли подключить из другого потока
if (_loadedModules.ContainsKey(typeName))
{
if (!_fileHashes.TryGetValue(typeName, out var storedHash)
|| !StringComparer.OrdinalIgnoreCase.Equals(hash, storedHash))
throw new RuntimeException("Type «" + typeName + "» already registered");

return;
}

// Хеш раньше модуля: ThrowIfTypeExist читает его, когда модуль уже виден
_fileHashes[typeName] = hash;
_loadedModules[typeName] = module;
try
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
TypeManager.RegisterType(typeName, default, type);
}
catch
{
// Имя занято другим типом: без отката повторное подключение молча «удалось» бы
_loadedModules.TryRemove(typeName, out _);
_fileHashes.TryRemove(typeName, out _);
throw;
}
}
}

public void RegisterTypeModule(string typeName, IExecutableModule module)
{
if (_loadedModules.ContainsKey(typeName))
lock (_registrationLock)
{
var alreadyLoadedSrc = (_loadedModules[typeName]).Source.Location;
var currentSrc = module.Source.Location;
if (_loadedModules.TryGetValue(typeName, out var loadedModule))
{
var alreadyLoadedSrc = loadedModule.Source.Location;
var currentSrc = module.Source.Location;

if(alreadyLoadedSrc != currentSrc)
throw new RuntimeException("Type «" + typeName + "» already registered");
if(alreadyLoadedSrc != currentSrc)
throw new RuntimeException("Type «" + typeName + "» already registered");

return;
}

return;
_loadedModules[typeName] = module;
try
{
_engine.TypeManager.RegisterType(typeName, default, typeof(AttachedScriptsFactory));
}
catch
{
_loadedModules.TryRemove(typeName, out _);
throw;
}
}

_loadedModules.Add(typeName, module);
_engine.TypeManager.RegisterType(typeName, default, typeof(AttachedScriptsFactory));
}

private UserScriptContextInstance LoadAndCreate(ICompilerFrontend compiler, SourceCode code,
Expand Down
65 changes: 37 additions & 28 deletions src/ScriptEngine/Machine/DefaultTypeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This Source Code Form is subject to the terms of the
----------------------------------------------------------*/

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using OneScript.Contexts;
Expand All @@ -18,8 +19,12 @@ namespace ScriptEngine.Machine
{
public class DefaultTypeManager : ITypeManager
{
private readonly Dictionary<string, int> _knownTypesIndexes = new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
private readonly List<TypeDescriptor> _knownTypes = new List<TypeDescriptor>();
// Типы регистрируются и из фоновых заданий (ПодключитьСценарий, внешние компоненты).
// Регистрация идет под блокировкой, чтение — без нее: словарь конкурентный,
// а список типов при регистрации заменяется новым массивом.
private readonly object _registrationLock = new object();
private readonly ConcurrentDictionary<string, TypeDescriptor> _knownTypesByName = new ConcurrentDictionary<string, TypeDescriptor>(StringComparer.InvariantCultureIgnoreCase);
private volatile TypeDescriptor[] _knownTypes = Array.Empty<TypeDescriptor>();
private readonly TypeFactoryCache _factoryCache = new TypeFactoryCache();
private readonly ILazyTypeResolver[] _resolvers;

Expand All @@ -42,9 +47,9 @@ public DefaultTypeManager(IEnumerable<ILazyTypeResolver> resolvers = null)

public TypeDescriptor GetTypeByName(string name)
{
if (_knownTypesIndexes.TryGetValue(name, out var index))
if (_knownTypesByName.TryGetValue(name, out var knownType))
{
return _knownTypes[index];
return knownType;
}

if (TryResolveLazily(name, out var resolvedType))
Expand All @@ -70,9 +75,8 @@ public bool TryGetType(Type frameworkType, out TypeDescriptor type)

public bool TryGetType(string name, out TypeDescriptor type)
{
if (_knownTypesIndexes.TryGetValue(name, out var index))
if (_knownTypesByName.TryGetValue(name, out type))
{
type = _knownTypes[index];
return true;
}

Expand All @@ -87,37 +91,38 @@ public bool TryGetType(string name, out TypeDescriptor type)

public TypeDescriptor RegisterType(string name, string alias, Type implementingClass)
{
if (_knownTypesIndexes.ContainsKey(name))
lock (_registrationLock)
{
var td = GetTypeByName(name);
if (td.ImplementingClass != implementingClass)
if (_knownTypesByName.TryGetValue(name, out var td))
{
throw new InvalidOperationException($"Name `{name}` is already registered");
if (td.ImplementingClass != implementingClass)
{
throw new InvalidOperationException($"Name `{name}` is already registered");
}

return td;
}

return td;
}
else
{
var typeDesc = new TypeDescriptor(implementingClass, name, alias);
RegisterTypeInternal(typeDesc);
return typeDesc;
}

}

public void RegisterType(TypeDescriptor typeDescriptor)
{
if (_knownTypesIndexes.TryGetValue(typeDescriptor.Name, out var index))
lock (_registrationLock)
{
var knownType = _knownTypes[index];
if (knownType != typeDescriptor)
throw new InvalidOperationException($"Type {typeDescriptor} already registered");

return;
if (_knownTypesByName.TryGetValue(typeDescriptor.Name, out var knownType))
{
if (knownType != typeDescriptor)
throw new InvalidOperationException($"Type {typeDescriptor} already registered");

return;
}

RegisterTypeInternal(typeDescriptor);
}

RegisterTypeInternal(typeDescriptor);
}

public ITypeFactory GetFactoryFor(TypeDescriptor type)
Expand All @@ -127,12 +132,16 @@ public ITypeFactory GetFactoryFor(TypeDescriptor type)

private void RegisterTypeInternal(TypeDescriptor td)
{
var nextListId = _knownTypes.Count;
_knownTypesIndexes.Add(td.Name, nextListId);
// Сначала список: тип, найденный по имени, уже есть и в нем
var knownTypes = _knownTypes;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Просьба пояснить, зачем делается именно так

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Список типов читают без блокировки: TryGetType(Type), GetTypeByFrameworkType и IsKnownType перебирают его, а RegisteredTypes отдает наружу — Рефлектор.ИзвестныеТипы перебирает его, пока другое задание может подключать сценарий. List.Add параллельно с перебором либо падает с «Collection was modified», либо отдает null: размер увеличивается раньше, чем записан элемент, а при расширении подменяется внутренний массив.

Поэтому при регистрации собирается новый массив и подменяется ссылка: читатель один раз берет ссылку и перебирает неизменный снимок. С блокировкой на чтение RegisteredTypes все равно пришлось бы копировать на каждый вызов, а так копирование происходит только при регистрации. Регистрация редкая — на старте около 280 типов плюс подключаемые сценарии, так что копирование незаметно.

Массив обновляется раньше словаря имен, чтобы тип, найденный по имени, уже был и в списке.

var newKnownTypes = new TypeDescriptor[knownTypes.Length + 1];
knownTypes.CopyTo(newKnownTypes, 0);
newKnownTypes[knownTypes.Length] = td;
_knownTypes = newKnownTypes;

_knownTypesByName[td.Name] = td;
if (!string.IsNullOrWhiteSpace(td.Alias) && td.Alias != td.Name)
_knownTypesIndexes[td.Alias] = nextListId;

_knownTypes.Add(td);
_knownTypesByName[td.Alias] = td;
}

private bool TryResolveLazily(string name, out TypeDescriptor type)
Expand Down
37 changes: 36 additions & 1 deletion src/Tests/OneScript.Core.Tests/TestTypes_Registration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ This Source Code Form is subject to the terms of the
using FluentAssertions;
using Moq;
using OneScript.Contexts;
using OneScript.Exceptions;
using OneScript.StandardLibrary.Collections;
using OneScript.Types;
using ScriptEngine;
using ScriptEngine.Hosting;
using ScriptEngine.Machine;
using ScriptEngine.Machine.Contexts;
using ScriptEngine.Types;
Expand Down Expand Up @@ -98,6 +100,39 @@ public void TypeEqualityForNull()
Assert.True(type1 == type2); // operator==
Assert.False(type1 != type2); // operator !=
}


[Fact]
public void AttachingScriptUnderBuiltInTypeNameFailsEveryTime()
{
var engine = DefaultEngineBuilder.Create()
.SetDefaultOptions()
.Build();
engine.Initialize();

for (var i = 0; i < 2; i++)
{
Action attach = () => engine.AttachedScriptsFactory.AttachFromString(
engine.GetCompilerService(), "Перем А;", "Строка", engine.NewProcess());
attach.Should().Throw<InvalidOperationException>();
}
}

[Fact]
public void AttachingScriptUnderLibraryClassNameIsRejected()
{
var engine = DefaultEngineBuilder.Create()
.SetDefaultOptions()
.Build();
engine.Initialize();
var libraryClass = engine.AttachedScriptsFactory.CompileModuleFromSource(
engine.GetCompilerService(), engine.Loader.FromString("Перем А;"), null, engine.NewProcess());
engine.AttachedScriptsFactory.RegisterTypeModule("КлассБиблиотеки", libraryClass);

Action attach = () => engine.AttachedScriptsFactory.AttachFromString(
engine.GetCompilerService(), "Перем Б;", "КлассБиблиотеки", engine.NewProcess());

attach.Should().Throw<RuntimeException>().WithMessage("*already registered*");
}

}
}
Loading