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
47 changes: 40 additions & 7 deletions src/ScriptEngine/Libraries/LibraryManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ This Source Code Form is subject to the terms of the
at http://mozilla.org/MPL/2.0/.
----------------------------------------------------------*/

using System;
using System.Collections.Generic;
using System.Runtime.ExceptionServices;
using OneScript.Commons;
using OneScript.Contexts;
using ScriptEngine.Machine.Contexts;
using ScriptEngine.Machine;
using OneScript.Execution;

namespace ScriptEngine.Libraries
Expand All @@ -17,27 +19,58 @@ namespace ScriptEngine.Libraries
/// </summary>
internal class LibraryManager : ILibraryManager
{
private readonly IRuntimeContextInstance _contextOfGlobalSymbols;
private readonly LazyPropertyBag _contextOfGlobalSymbols;

public LibraryManager(IRuntimeContextInstance contextOfGlobalSymbols)
public LibraryManager(LazyPropertyBag contextOfGlobalSymbols)
{
_contextOfGlobalSymbols = contextOfGlobalSymbols;
}

public void InitExternalLibrary(ScriptingEngine runtime, ExternalLibraryInfo library, IBslProcess process)
{
var loadedObjects = new ScriptDrivenObject[library.Modules.Count];
var propIds = new int[library.Modules.Count];

// Ошибку модуля, инициализированного при обращении, могло поймать тело другого модуля.
// Она поднимается в очередь самого модуля, и загрузка библиотеки падает, как раньше
var failures = new Dictionary<int, ExceptionDispatchInfo>();
int i = 0;
foreach (var module in library.Modules)
{
var instance = runtime.CreateUninitializedSDO(module.Module);

var propId = _contextOfGlobalSymbols.GetPropertyNumber(module.Symbol);
_contextOfGlobalSymbols.SetPropValue(propId, instance);
loadedObjects[i++] = instance;

// Если тело другого модуля обратится к этому раньше его очереди, модуль инициализируется при обращении
_contextOfGlobalSymbols.SetInitializer(propId, () =>
{
try
{
runtime.InitializeSDO(instance, process);
}
catch (Exception e)
{
failures[propId] = ExceptionDispatchInfo.Capture(e);
throw;
}
});
propIds[i++] = propId;
}

loadedObjects.ForEach(sdo => runtime.InitializeSDO(sdo, process));
try
{
foreach (var propId in propIds)
{
_contextOfGlobalSymbols.RunInitializer(propId);
if (failures.TryGetValue(propId, out var failure))
failure.Throw();
}
}
finally
{
// Тело модуля упало - остальные модули остаются без инициализации, как и раньше
propIds.ForEach(_contextOfGlobalSymbols.RemoveInitializer);
}
}
}
}
78 changes: 78 additions & 0 deletions src/ScriptEngine/Machine/LazyPropertyBag.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*----------------------------------------------------------
This Source Code Form is subject to the terms of the
Mozilla Public License, v.2.0. If a copy of the MPL
was not distributed with this file, You can obtain one
at http://mozilla.org/MPL/2.0/.
----------------------------------------------------------*/

using System;
using System.Collections.Generic;

namespace ScriptEngine.Machine
{
/// <summary>
/// Глобальные свойства окружения, у которых может быть действие перед первым чтением.
/// Через него модуль библиотеки инициализируется, если тело другого модуля обратилось к нему раньше его очереди.
/// </summary>
internal class LazyPropertyBag : PropertyBag
{
// Действия перед первым чтением свойства и поток, задавший каждое из них
private readonly Dictionary<int, (Action Initializer, int ThreadId)> _initializers = new Dictionary<int, (Action, int)>();
private volatile int _initializersCount;

public override IValue GetPropValue(int propNum)
{
if (_initializersCount != 0)
{
RunInitializer(propNum);
}

return base.GetPropValue(propNum);
}

/// <summary>
/// Задает действие, которое выполнится перед первым чтением свойства.
/// Выполняет его только поток, который его задал, остальные получают значение как есть.
/// </summary>
public void SetInitializer(int propNum, Action initializer)
{
lock (_initializers)
{
_initializers[propNum] = (initializer, Environment.CurrentManagedThreadId);
_initializersCount = _initializers.Count;
}
}

/// <summary>
/// Выполняет действие свойства, если оно задано и еще не выполнялось.
/// </summary>
public void RunInitializer(int propNum)
{
Action initializer;
lock (_initializers)
{
if (!_initializers.TryGetValue(propNum, out var pending)
|| pending.ThreadId != Environment.CurrentManagedThreadId)
{
return;
}

// Убираем до вызова: при круговом обращении второй получит значение без инициализации
_initializers.Remove(propNum);
_initializersCount = _initializers.Count;
initializer = pending.Initializer;
}

initializer();
}

public void RemoveInitializer(int propNum)
{
lock (_initializers)
{
_initializers.Remove(propNum);
_initializersCount = _initializers.Count;
}
}
}
}
4 changes: 2 additions & 2 deletions src/ScriptEngine/RuntimeEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ public class RuntimeEnvironment : IRuntimeEnvironment, ILibraryManager
private readonly SymbolTable _symbols = new SymbolTable();
private SymbolScope _scopeOfGlobalProperties;

private readonly PropertyBag _injectedProperties;
private readonly LazyPropertyBag _injectedProperties;

private readonly List<IAttachableContext> _contexts = new List<IAttachableContext>();

private readonly ILibraryManager _libraryManager;

public RuntimeEnvironment()
{
_injectedProperties = new PropertyBag();
_injectedProperties = new LazyPropertyBag();
_libraryManager = new LibraryManager(_injectedProperties);
}

Expand Down
70 changes: 70 additions & 0 deletions tests/library-init-order.os
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
Перем юТест;

Функция ПолучитьСписокТестов(ЮнитТестирование) Экспорт

юТест = ЮнитТестирование;

ВсеТесты = Новый Массив;
ВсеТесты.Добавить("ТестДолжен_ИнициализироватьМодульБиблиотекиПриОбращенииИзТелаДругогоМодуля");
ВсеТесты.Добавить("ТестДолжен_ОтдатьМодульБезИнициализацииПриКруговомОбращении");
ВсеТесты.Добавить("ТестДолжен_ПрерватьЗагрузкуБиблиотекиПриОшибкеВТелеМодуля");
ВсеТесты.Добавить("ТестДолжен_ПрерватьЗагрузкуБиблиотекиПриОшибкеМодуляПойманнойДругимМодулем");

Возврат ВсеТесты;

КонецФункции

Функция ЗагрузитьФикстуру(Знач Имя)
Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "library-init-order", Имя + ".os"));
КонецФункции

// https://github.com/EvilBeaver/OneScript/issues/1000
Процедура ТестДолжен_ИнициализироватьМодульБиблиотекиПриОбращенииИзТелаДругогоМодуля() Экспорт

Результат = ЗагрузитьФикстуру("order").Результат();

юТест.ПроверитьРавенство("значение Б", Результат.ИзТела, "Чтение переменной модуля");
юТест.ПроверитьРавенство("значение Б", Результат.ЧерезВычислить, "Чтение через Вычислить");
юТест.ПроверитьРавенство("значение Б", Результат.ЧерезМетод, "Вызов метода модуля");
юТест.ПроверитьРавенство(1, Результат.ЧислоЗапусков, "Тело модуля выполняется один раз");
Comment thread
coderabbitai[bot] marked this conversation as resolved.

КонецПроцедуры

Процедура ТестДолжен_ОтдатьМодульБезИнициализацииПриКруговомОбращении() Экспорт

Результат = ЗагрузитьФикстуру("cycle").Результат();

юТест.ПроверитьРавенство("значение В", Результат.ЗначениеВ);
юТест.ПроверитьРавенство("значение Г", Результат.ЗначениеГ);

// В идет первым и видит Г, а Г, инициализированный из тела В, видит В еще без инициализации
юТест.ПроверитьРавенство("значение Г", Результат.СоседВ);
юТест.ПроверитьРавенство(Неопределено, Результат.СоседГ);

КонецПроцедуры

Процедура ТестДолжен_ПрерватьЗагрузкуБиблиотекиПриОшибкеВТелеМодуля() Экспорт

Попытка
ЗагрузитьФикстуру("error");
Исключение
юТест.ПроверитьВхождение(ОписаниеОшибки(), "Ошибка в теле модуля Е");
Возврат;
КонецПопытки;

юТест.ТестПровален("Ошибка в теле модуля библиотеки должна прерывать загрузку");

КонецПроцедуры

Процедура ТестДолжен_ПрерватьЗагрузкуБиблиотекиПриОшибкеМодуляПойманнойДругимМодулем() Экспорт

Попытка
ЗагрузитьФикстуру("caught");
Исключение
юТест.ПроверитьВхождение(ОписаниеОшибки(), "Ошибка в теле модуля З");
Возврат;
КонецПопытки;

юТест.ТестПровален("Ошибка в теле модуля библиотеки должна прерывать загрузку, даже если ее поймал другой модуль");

КонецПроцедуры
1 change: 1 addition & 0 deletions tests/library-init-order/caught.os
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#Использовать "caught"
8 changes: 8 additions & 0 deletions tests/library-init-order/caught/package-loader.os
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Порядок файлов на диске зависит от файловой системы, а тесту нужно, чтобы Ж шел раньше З
Процедура ПриЗагрузкеБиблиотеки(Знач Путь, СтандартнаяОбработка, Отказ)

СтандартнаяОбработка = Ложь;
ДобавитьМодуль(ОбъединитьПути(Путь, "ПорядокИнициализацииЖ.os"), "ПорядокИнициализацииЖ");
ДобавитьМодуль(ОбъединитьПути(Путь, "ПорядокИнициализацииЗ.os"), "ПорядокИнициализацииЗ");

КонецПроцедуры
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Перем Значение Экспорт;

Попытка
Значение = ПорядокИнициализацииЗ.Значение;
Исключение
Значение = Неопределено;
КонецПопытки;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Перем Значение Экспорт;

ВызватьИсключение "Ошибка в теле модуля З";
12 changes: 12 additions & 0 deletions tests/library-init-order/cycle.os
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#Использовать "cycle"

Функция Результат() Экспорт

Результат = Новый Структура;
Результат.Вставить("ЗначениеВ", ПорядокИнициализацииВ.Значение);
Результат.Вставить("ЗначениеГ", ПорядокИнициализацииГ.Значение);
Результат.Вставить("СоседВ", ПорядокИнициализацииВ.ЗначениеСоседа);
Результат.Вставить("СоседГ", ПорядокИнициализацииГ.ЗначениеСоседа);
Возврат Результат;

КонецФункции
8 changes: 8 additions & 0 deletions tests/library-init-order/cycle/package-loader.os
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Порядок файлов на диске зависит от файловой системы, а тесту нужно, чтобы В шел раньше Г
Процедура ПриЗагрузкеБиблиотеки(Знач Путь, СтандартнаяОбработка, Отказ)

СтандартнаяОбработка = Ложь;
ДобавитьМодуль(ОбъединитьПути(Путь, "ПорядокИнициализацииВ.os"), "ПорядокИнициализацииВ");
ДобавитьМодуль(ОбъединитьПути(Путь, "ПорядокИнициализацииГ.os"), "ПорядокИнициализацииГ");

КонецПроцедуры
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Перем Значение Экспорт;
Перем ЗначениеСоседа Экспорт;

ЗначениеСоседа = ПорядокИнициализацииГ.Значение;
Значение = "значение В";
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Перем Значение Экспорт;
Перем ЗначениеСоседа Экспорт;

ЗначениеСоседа = ПорядокИнициализацииВ.Значение;
Значение = "значение Г";
1 change: 1 addition & 0 deletions tests/library-init-order/error.os
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#Использовать "error"
8 changes: 8 additions & 0 deletions tests/library-init-order/error/package-loader.os
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Порядок файлов на диске зависит от файловой системы, а тесту нужно, чтобы Д шел раньше Е
Процедура ПриЗагрузкеБиблиотеки(Знач Путь, СтандартнаяОбработка, Отказ)

СтандартнаяОбработка = Ложь;
ДобавитьМодуль(ОбъединитьПути(Путь, "ПорядокИнициализацииД.os"), "ПорядокИнициализацииД");
ДобавитьМодуль(ОбъединитьПути(Путь, "ПорядокИнициализацииЕ.os"), "ПорядокИнициализацииЕ");

КонецПроцедуры
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Перем Значение Экспорт;

Значение = ПорядокИнициализацииЕ.Значение;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Перем Значение Экспорт;

ВызватьИсключение "Ошибка в теле модуля Е";
12 changes: 12 additions & 0 deletions tests/library-init-order/order.os
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#Использовать "order"

Функция Результат() Экспорт

Результат = Новый Структура;
Результат.Вставить("ИзТела", ПорядокИнициализацииА.ИзТела);
Результат.Вставить("ЧерезВычислить", ПорядокИнициализацииА.ЧерезВычислить);
Результат.Вставить("ЧерезМетод", ПорядокИнициализацииА.ЧерезМетод);
Результат.Вставить("ЧислоЗапусков", ПорядокИнициализацииБ.ЧислоЗапусков);
Возврат Результат;

КонецФункции
8 changes: 8 additions & 0 deletions tests/library-init-order/order/package-loader.os
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Порядок файлов на диске зависит от файловой системы, а тесту нужно, чтобы А шел раньше Б
Процедура ПриЗагрузкеБиблиотеки(Знач Путь, СтандартнаяОбработка, Отказ)

СтандартнаяОбработка = Ложь;
ДобавитьМодуль(ОбъединитьПути(Путь, "ПорядокИнициализацииА.os"), "ПорядокИнициализацииА");
ДобавитьМодуль(ОбъединитьПути(Путь, "ПорядокИнициализацииБ.os"), "ПорядокИнициализацииБ");

КонецПроцедуры
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Перем ИзТела Экспорт;
Перем ЧерезВычислить Экспорт;
Перем ЧерезМетод Экспорт;

// Загрузчик библиотеки добавляет этот модуль раньше модуля Б
ИзТела = ПорядокИнициализацииБ.Значение;
ЧерезВычислить = Вычислить("ПорядокИнициализацииБ.Значение");
ЧерезМетод = ПорядокИнициализацииБ.ПолучитьЗначение();
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Перем Значение Экспорт;
Перем ЧислоЗапусков Экспорт;

Функция ПолучитьЗначение() Экспорт
Возврат Значение;
КонецФункции

ЧислоЗапусков = ?(ЧислоЗапусков = Неопределено, 1, ЧислоЗапусков + 1);
Значение = "значение Б";
Loading