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
25 changes: 25 additions & 0 deletions src/OneScript.Core/Execution/BslProcessExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*----------------------------------------------------------
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;

namespace OneScript.Execution
{
public static class BslProcessExtensions
{
/// <summary>
/// Исключение прерывает процесс по запросу отмены.
/// Такое исключение не должно перехватываться bsl-кодом и превращаться в ScriptException.
/// </summary>
public static bool IsCancellationOf(this Exception exception, IBslProcess process)
{
return exception is OperationCanceledException canceled
&& canceled.CancellationToken.IsCancellationRequested
&& canceled.CancellationToken == process.CancellationToken;
}
}
}
7 changes: 7 additions & 0 deletions src/OneScript.Core/Execution/IBslProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This Source Code Form is subject to the terms of the
#nullable enable

using System;
using System.Threading;
using OneScript.Contexts;
using OneScript.DependencyInjection;
using OneScript.Values;
Expand Down Expand Up @@ -40,6 +41,12 @@ public interface IBslProcess

public int VirtualThreadId { get; }

/// <summary>
/// Токен отмены исполнения. При запросе отмены исполнитель прерывает bsl-код
/// исключением <see cref="OperationCanceledException"/> с этим токеном.
/// </summary>
public CancellationToken CancellationToken => CancellationToken.None;

/**
* Обертка текущего процесса в терминах BSL.
* Может отсутствовать, полностью определяется хост-приложением процесса.
Expand Down
6 changes: 6 additions & 0 deletions src/OneScript.Core/Execution/IBslProcessFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ This Source Code Form is subject to the terms of the
at http://mozilla.org/MPL/2.0/.
----------------------------------------------------------*/

using System.Threading;

namespace OneScript.Execution
{
/// <summary>
Expand All @@ -14,5 +16,9 @@ public interface IBslProcessFactory
{
// Создать новый bsl-процесс с пустым стеком вызовов
IBslProcess NewProcess();

// Создать новый bsl-процесс, исполнение которого можно отменить через токен.
// Фабрики, которые не поддерживают отмену, создают обычный процесс
IBslProcess NewProcess(CancellationToken cancellationToken) => NewProcess();
}
}
21 changes: 21 additions & 0 deletions src/OneScript.Native/Compiler/ExpressionHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ This Source Code Form is subject to the terms of the
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.CSharp.RuntimeBinder;
using OneScript.Contexts;
using OneScript.Execution;
using OneScript.Language.LexicalAnalysis;
using OneScript.Localization;
using OneScript.Native.Runtime;
Expand Down Expand Up @@ -679,6 +681,25 @@ public static Expression InvokeBslNativeMethod(BslNativeMethodInfo nativeMethod,
PackArgsToArgsArray(args));
}

public static Expression GetCancellationToken(ParameterExpression process)
{
return Expression.Property(process, nameof(IBslProcess.CancellationToken));
}

public static Expression ThrowIfCancellationRequested(Expression cancellationToken)
{
return Expression.Call(cancellationToken, nameof(CancellationToken.ThrowIfCancellationRequested), Type.EmptyTypes);
}

public static Expression IsCancellationOf(Expression exception, ParameterExpression process)
{
var method = OperationsCache.GetOrAdd(
typeof(BslProcessExtensions),
nameof(BslProcessExtensions.IsCancellationOf));

return Expression.Call(method, exception, process);
}

private static Expression PackArgsToArgsArray(IEnumerable<Expression> args)
{
return Expression.NewArrayInit(typeof(BslValue), args.Select(ConvertToBslValue));
Expand Down
27 changes: 23 additions & 4 deletions src/OneScript.Native/Compiler/MethodCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This Source Code Form is subject to the terms of the
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using Microsoft.CSharp.RuntimeBinder;
using OneScript.Commons;
using OneScript.Compilation.Binding;
Expand Down Expand Up @@ -824,15 +825,23 @@ protected override void VisitWhileNode(WhileLoopNode node)

var block = _blocks.LeaveBlock();

var cancellationToken = Expression.Variable(typeof(CancellationToken));
var result = new List<Expression>();
result.Add(ExpressionHelpers.ThrowIfCancellationRequested(cancellationToken));
result.Add(Expression.IfThen(
Expression.Not(block.BuildStack.Pop()),
Expression.Break(block.LoopBreak)));

result.AddRange(block.GetStatements());

var loop = Expression.Loop(Expression.Block(result), block.LoopBreak, block.LoopContinue);
_blocks.Add(loop);
_blocks.Add(Expression.Block(new[] {cancellationToken}, new Expression[] {ReadCancellationToken(cancellationToken), loop}));
}

// Токен читается один раз перед циклом, а не у процесса на каждой итерации
private BinaryExpression ReadCancellationToken(ParameterExpression cancellationToken)
{
return Expression.Assign(cancellationToken, ExpressionHelpers.GetCancellationToken(_processParameter));
}

protected override void VisitWhileCondition(BslSyntaxNode node)
Expand Down Expand Up @@ -871,12 +880,15 @@ protected override void VisitForLoopNode(ForLoopNode node)
var initialValue = block.BuildStack.Pop();
var counterVar = block.BuildStack.Pop();

var cancellationToken = Expression.Variable(typeof(CancellationToken));
var result = new List<Expression>();
result.Add(ReadCancellationToken(cancellationToken));
result.Add(Expression.Assign(counterVar, ExpressionHelpers.CreateAssignmentSource(initialValue, counterVar.Type)));
var finalVar = Expression.Variable(typeof(decimal)); // TODO: BslNumericValue ?
result.Add(Expression.Assign(finalVar, upperLimit));

var loop = new List<Expression>();
loop.Add(ExpressionHelpers.ThrowIfCancellationRequested(cancellationToken));
loop.Add(Expression.IfThen(
Expression.GreaterThan(ExpressionHelpers.ToNumber(counterVar), finalVar),
Expression.Break(block.LoopBreak)));
Expand All @@ -888,7 +900,7 @@ protected override void VisitForLoopNode(ForLoopNode node)

result.Add(Expression.Loop(Expression.Block(loop), block.LoopBreak));

_blocks.Add(Expression.Block(new[] {finalVar}, result));
_blocks.Add(Expression.Block(new[] {finalVar, cancellationToken}, result));
}

protected override void VisitForInitializer(BslSyntaxNode node)
Expand Down Expand Up @@ -950,7 +962,9 @@ protected override void VisitForEachLoopNode(ForEachLoopNode node)
var getEnumeratorInvoke = Expression.Call(collectionCast, getEnumeratorMethod);
var enumeratorVar = Expression.Variable(typeof(IEnumerator));

var cancellationToken = Expression.Variable(typeof(CancellationToken));
var result = new List<Expression>();
result.Add(ReadCancellationToken(cancellationToken));
result.Add(Expression.Assign(enumeratorVar, getEnumeratorInvoke));


Expand All @@ -963,6 +977,7 @@ protected override void VisitForEachLoopNode(ForEachLoopNode node)
typeof(BslValue))
);

loop.Add(ExpressionHelpers.ThrowIfCancellationRequested(cancellationToken));
loop.Add(assignCurrent);
loop.AddRange(block.GetStatements());

Expand All @@ -975,7 +990,7 @@ protected override void VisitForEachLoopNode(ForEachLoopNode node)

result.Add(finalLoop);

_blocks.Add(Expression.Block(new[] {enumeratorVar}, result));
_blocks.Add(Expression.Block(new[] {enumeratorVar, cancellationToken}, result));
}

protected override void VisitIteratorLoopVariable(TerminalNode node)
Expand Down Expand Up @@ -1012,8 +1027,12 @@ protected override void VisitTryExceptNode(TryExceptNode node)
var except = block.BuildStack.Pop();
var tryBlock = block.BuildStack.Pop();

// Отмена процесса не должна перехватываться Попыткой
var notCancellation = Expression.Not(
ExpressionHelpers.IsCancellationOf(block.CurrentException, _processParameter));

_blocks.Add(Expression.TryCatch(tryBlock,
Expression.Catch(block.CurrentException, except))
Expression.Catch(block.CurrentException, except, notCancellation))
);
}

Expand Down
3 changes: 3 additions & 0 deletions src/OneScript.Native/Runtime/CallableMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public BslValue Invoke(IBslProcess process, object target, BslValue[] args)
throw new InvalidOperationException($"Method {_method} was not compiled");
}

// Проверка на входе в метод прерывает и рекурсию без циклов
process.CancellationToken.ThrowIfCancellationRequested();

var callableWrapper = GetCallableWrapper(target);
return _delegate.Invoke(callableWrapper, args, process);
}
Expand Down
14 changes: 12 additions & 2 deletions src/OneScript.StandardLibrary/StandardGlobalContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,22 @@

/// <summary>
/// Приостанавливает выполнение скрипта.
/// Ожидание прерывается, если фоновое задание, в котором оно выполняется, отменено.
/// </summary>
/// <param name="process">Текущий процесс, в котором вызван данный метод</param>
/// <param name="delay">Время приостановки в миллисекундах</param>
[ContextMethod("Приостановить", "Sleep")]
public void Sleep(int delay)
public void Sleep(IBslProcess process, int delay)

Check notice on line 107 in src/OneScript.StandardLibrary/StandardGlobalContext.cs

View check run for this annotation

sonar.openbsl.ru qa-bot / SonarQube Code Analysis

src/OneScript.StandardLibrary/StandardGlobalContext.cs#L107

Member 'Sleep' does not access instance data and can be marked as static
{
System.Threading.Thread.Sleep(delay);
var cancellationToken = process.CancellationToken;

// WaitHandle дороже Thread.Sleep и создает событие ядра, а Sleep(0) еще и уступает квант времени
if (delay == 0 || !cancellationToken.CanBeCanceled)
System.Threading.Thread.Sleep(delay);
else
cancellationToken.WaitHandle.WaitOne(delay);

cancellationToken.ThrowIfCancellationRequested();
}

[ContextMethod("КраткоеПредставлениеОшибки", "BriefErrorDescription")]
Expand Down
85 changes: 83 additions & 2 deletions src/OneScript.StandardLibrary/Tasks/BackgroundTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This Source Code Form is subject to the terms of the

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using OneScript.Commons;
using OneScript.Contexts;
Expand All @@ -24,6 +25,9 @@ public class BackgroundTask : AutoContext<BackgroundTask>
{
private readonly BslMethodInfo _method;
private readonly int _methIndex;
private readonly CancellationTokenSource _cancellation = new CancellationTokenSource();
private readonly object _cancellationLock = new object();
private bool _cancellationReleased;
private Task _workerTask;
private int _taskId;

Expand Down Expand Up @@ -52,6 +56,24 @@ public Task WorkerTask

public int TaskId => _taskId;

/// <summary>
/// Задание, из которого запущено это, или null, если запущено не из фонового задания
/// </summary>
internal BackgroundTask Parent { get; set; }

internal bool IsDescendantOf(BackgroundTask ancestor)
{
for (var task = Parent; task != null; task = task.Parent)
{
if (ReferenceEquals(task, ancestor))
return true;
}

return false;
}

public CancellationToken CancellationToken => _cancellation.Token;

[ContextProperty("УникальныйИдентификатор","UUID")]
public GuidWrapper Identifier { get; private set; }

Expand All @@ -76,21 +98,69 @@ public Task WorkerTask
/// <summary>
/// Ждать завершения задания указанное число миллисекунд
/// </summary>
/// <param name="process">Текущий процесс, в котором вызван данный метод</param>
/// <param name="timeout">Таймаут в миллисекундах. Если ноль - ждать вечно</param>
/// <returns>Истина - дождались завершения. Ложь - сработал таймаут</returns>
[ContextMethod("ОжидатьЗавершения", "Wait")]
public bool Wait(int timeout = 0)
public bool Wait(IBslProcess process, int timeout = 0)
{
timeout = BackgroundTasksManager.ConvertTimeout(timeout);

return WorkerTask.Wait(timeout);
return WorkerTask.Wait(timeout, process.CancellationToken);
}

/// <summary>
/// Отменяет выполнение задания. Код задания прерывается перед выполнением очередной строки
/// или во время Приостановить(), перехватить отмену через Попытка нельзя.
/// Обработчики завершения потока исполнения при этом отрабатывают.
/// Метод не дожидается остановки задания, для этого используйте ОжидатьЗавершения().
/// Отмена завершенного задания ничего не делает.
/// </summary>
[ContextMethod("Отменить", "Cancel")]
public void Cancel()
{
lock (_cancellationLock)
{
// Источник отмены освобождается, когда задание завершилось
if (!_cancellationReleased)
_cancellation.Cancel();
}
}

public void ExecuteOnCurrentThread(IBslProcess process)
{
try
{
Execute(process);
}
finally
{
ReleaseCancellation();
}
}

// Ожидание в Приостановить() создает у источника отмены событие ядра,
// а задание остается в списке менеджера до Очистить()
private void ReleaseCancellation()
{
lock (_cancellationLock)
{
_cancellationReleased = true;
_cancellation.Dispose();
}
}

private void Execute(IBslProcess process)
{
if (State != TaskStateEnum.NotRunned)
throw new RuntimeException(Locale.NStr("ru = 'Неверное состояние задачи';en = 'Incorrect task status'"));

if (_cancellation.IsCancellationRequested)
{
State = TaskStateEnum.Canceled;
return;
}

var parameters = Parameters is ArrayImpl array ?
array.ToArray() : Array.Empty<IValue>();

Expand All @@ -117,6 +187,17 @@ public void ExecuteOnCurrentThread(IBslProcess process)

ExceptionInfo = new ExceptionInfoContext(exception);
}
catch (OperationCanceledException exception) when (exception.CancellationToken == _cancellation.Token)
{
State = TaskStateEnum.Canceled;
}
catch (Exception exception)
{
// Метод встроенного объекта бросает исключения .NET, а не ScriptException:
// без этого задание осталось бы «Активно» без информации об ошибке
State = TaskStateEnum.CompletedWithErrors;
ExceptionInfo = new ExceptionInfoContext(new ExternalSystemException(exception));
}
}
}
}
Loading
Loading