Skip to content

Commit

Permalink
Support for timeout on user input added. (ZeraGmbH#24)
Browse files Browse the repository at this point in the history
* Introduced model cache helper.

* Use automatic model dependency resolving to simplify customization.

* Review and cleanups.

* Added EnumBlock.md.

* ModelBlock.md added.

* Added BlocklyExtensions.md.

* When main script dies all child scripts have to be stopped as well.

* Added evaluation interceptions to allow implmenting a debugger.

* User input can now have a timeout. (ZeraGmbH/websam#629)
  • Loading branch information
JMS-1 authored Aug 15, 2024
1 parent 640887a commit 277a978
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 26 deletions.
9 changes: 6 additions & 3 deletions Library/Core/Model/Block.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ public abstract class Block : IFragment
public IList<Comment> Comments { get; } = [];

/// <inheritdoc/>
public virtual Task<object?> Evaluate(Context context)
public virtual async Task<object?> Evaluate(Context context)
{
/* Wait for debugger to allow execution. */
await context.Engine.SingleStep(this);

/* Always check for cancel before proceeding with the execution of the next block in chain. */
context.Cancellation.ThrowIfCancellationRequested();

Expand All @@ -60,8 +63,8 @@ public abstract class Block : IFragment

/* Run the next block if we are not forcefully exiting a loop. */
if (Next != null && context.EscapeMode == EscapeMode.None)
return Next.Evaluate(context);
return await Next.Evaluate(context);

return Task.FromResult((object?)null);
return null;
}
}
56 changes: 52 additions & 4 deletions Library/Extensions/RequestUserInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace BlocklyNet.Extensions;
"request_user_input",
"Scripts",
@"{
""message0"": ""RequestUserInput %1 %2 %3 %4 %5"",
""message0"": ""RequestUserInput %1 %2 %3 %4 %5 %6 %7 %8 %9"",
""args0"": [
{
""type"": ""input_dummy""
Expand All @@ -34,6 +34,26 @@ namespace BlocklyNet.Extensions;
{
""type"": ""input_value"",
""name"": ""TYPE""
},
{
""type"": ""field_label_serializable"",
""name"": ""DELAY"",
""text"": ""Auto close after (s)""
},
{
""type"": ""input_value"",
""name"": ""DELAY"",
""check"": ""Number""
},
{
""type"": ""field_label_serializable"",
""name"": ""THROWMESSAGE"",
""text"": ""Exception on auto close""
},
{
""type"": ""input_value"",
""name"": ""THROWMESSAGE"",
""check"": ""String""
}
],
""output"": null,
Expand Down Expand Up @@ -67,8 +87,36 @@ public class RequestUserInput : Block
/// <inheritdoc/>
public override async Task<object?> Evaluate(Context context)
{
return await context.Engine.GetUserInput<object>(
await Values.Evaluate<string>("KEY", context),
await Values.Evaluate<string>("TYPE", context, false));
var key = await Values.Evaluate<string>("KEY", context);
var type = await Values.Evaluate<string>("TYPE", context, false);
var delay = await Values.Evaluate<double?>("DELAY", context, false);
var secs = delay.GetValueOrDefault(0);

/* No delay necessary - just wait for the reply to be available. */
if (secs <= 0) return await context.Engine.GetUserInput<object>(key, type);

var cancel = new CancellationTokenSource();
var delayTask = Task.Delay(TimeSpan.FromSeconds(secs), cancel.Token);
var inputTask = context.Engine.GetUserInput<object>(key, type, delay);

/* User has terminated the task. */
if (Task.WaitAny(inputTask, delayTask) == 0)
{
/* Cancel timer. */
cancel.Cancel();

/* Report result. */
return inputTask.Result;
}

/* Simulate user input. */
context.Engine.Engine.SetUserInput(null);

/* May want to throw an exception. */
var message = await Values.Evaluate<string?>("THROWMESSAGE", context, false);

if (message != null) throw new TimeoutException(message);

return null;
}
}
2 changes: 1 addition & 1 deletion Library/Scripting/Engine/IScriptEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public interface IScriptEngine
/// A client reports a requested value.
/// </summary>
/// <param name="value">Information on the request and the value.</param>
void SetUserInput(UserInputResponse value);
void SetUserInput(UserInputResponse? value);

/// <summary>
/// Report the script parsing engine to use.
Expand Down
15 changes: 14 additions & 1 deletion Library/Scripting/Engine/IScriptSite.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using BlocklyNet.Core.Model;
using Microsoft.Extensions.Logging;

namespace BlocklyNet.Scripting.Engine;
Expand All @@ -7,6 +8,11 @@ namespace BlocklyNet.Scripting.Engine;
/// </summary>
public interface IScriptSite
{
/// <summary>
/// The controlling engine.
/// </summary>
IScriptEngine Engine { get; }

/// <summary>
/// Report the current logging helper of the script engine.
/// </summary>
Expand Down Expand Up @@ -62,8 +68,15 @@ public interface IScriptSite
/// </summary>
/// <param name="key"></param>
/// <param name="type"></param>
/// <param name="delay"></param>
/// <typeparam name="T">Expected type of the response.</typeparam>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
Task<T?> GetUserInput<T>(string key, string? type = null);
Task<T?> GetUserInput<T>(string key, string? type = null, double? delay = null);

/// <summary>
/// Call just before a block is executed.
/// </summary>
/// <param name="block">The block to execute.</param>
Task SingleStep(Block block);
}
40 changes: 34 additions & 6 deletions Library/Scripting/Engine/ScriptEngine.Input.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,44 @@ public partial class ScriptEngine
/// </summary>
private TaskCompletionSource<UserInputResponse>? _inputResponse;

/// <summary>
/// Exact time the input was requested.
/// </summary>
private DateTime _inputStarted = DateTime.MinValue;

/// <summary>
/// Optional planned seconds to auto-close the input request.
/// </summary>
private double? _inputDelay = null;

/// <inheritdoc/>
public void SetUserInput(UserInputResponse response) => SetUserInput(response, true);
public void SetUserInput(UserInputResponse? response) => SetUserInput(response, true);

private void SetUserInput(UserInputResponse response, bool mustLock)
private void SetUserInput(UserInputResponse? response, bool mustLock)
{
TaskCompletionSource<UserInputResponse>? inputResponse;

using (mustLock ? Lock.Wait() : null)
{
/* The script requesting the input must still be the active one. */
if (_active == null || _active.JobId != response.JobId)
if (_active == null || (response != null && _active.JobId != response.JobId))
throw new ArgumentException("jobId");

/* See if there is anyone wating on the response and clear the pending request. */
/* See if there is anyone wating on the response. */
inputResponse = _inputResponse;

if (inputResponse == null)
return;

/* Copy from request. */
response ??= new UserInputResponse
{
JobId = _active.JobId,
Key = _inputRequest?.Key ?? string.Empty,
ValueType = _inputRequest?.ValueType
};

/* Clear the pending request. */
_inputRequest = null;
_inputResponse = null;
}
Expand All @@ -47,7 +66,7 @@ private void SetUserInput(UserInputResponse response, bool mustLock)
}

/// <inheritdoc/>
public Task<T?> GetUserInput<T>(string key, string? type = null)
public Task<T?> GetUserInput<T>(string key, string? type = null, double? delay = null)
{
using (Lock.Wait())
{
Expand All @@ -60,9 +79,18 @@ private void SetUserInput(UserInputResponse response, bool mustLock)
{
/* Create a new response handler. */
_inputResponse = new TaskCompletionSource<UserInputResponse>();
_inputDelay = delay;
_inputStarted = DateTime.UtcNow;

/* Tell our clients that we would like to get some input. */
var inputRequest = new UserInputRequest { JobId = _active.JobId, Key = key, ValueType = type };
var inputRequest = new UserInputRequest
{
JobId = _active.JobId,
Key = key,
SecondsToAutoClose = _inputDelay,
StartedAt = _inputStarted,
ValueType = type,
};

context?
.Send(ScriptEngineNotifyMethods.InputRequest, _inputRequest = inputRequest)
Expand Down
10 changes: 9 additions & 1 deletion Library/Scripting/Engine/ScriptEngine.Nested.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reflection;
using BlocklyNet.Core.Model;
using Microsoft.Extensions.Logging;

namespace BlocklyNet.Scripting.Engine;
Expand All @@ -13,6 +14,9 @@ public partial class ScriptEngine
/// <param name="depth">Nestring depth of the script, at least 1.</param>
protected class ScriptSite(ScriptEngine engine, IScript? parent, int depth) : IScriptSite
{
/// <inheritdoc/>
public IScriptEngine Engine => _engine;

/// <summary>
/// Last progress information of this child script.
/// </summary>
Expand Down Expand Up @@ -73,7 +77,8 @@ public Task<TResult> Run<TResult>(StartScript request, StartScriptOptions? optio
=> _engine.StartChild<TResult>(request, CurrentScript, options, depth);

/// <inheritdoc/>
public Task<T?> GetUserInput<T>(string key, string? type = null) => _engine.GetUserInput<T>(key, type);
public Task<T?> GetUserInput<T>(string key, string? type = null, double? delay = null)
=> _engine.GetUserInput<T>(key, type, delay);

/// <inheritdoc/>
public void ReportProgress(object info, double? progress, string? name)
Expand Down Expand Up @@ -176,6 +181,9 @@ private void RunScript(object? state)
}
}
}

/// <inheritdoc/>
public Task SingleStep(Block block) => Task.CompletedTask;
}

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions Library/Scripting/Engine/ScriptEngine.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reflection;
using BlocklyNet.Core.Model;
using BlocklyNet.Scripting.Parsing;
using BlocklyNet.User;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -16,6 +17,9 @@ namespace BlocklyNet.Scripting.Engine;
/// <param name="parser">Script parser to use.</param>
public partial class ScriptEngine(IServiceProvider _rootProvider, IScriptParser parser, ILogger<ScriptEngine> logger, IScriptEngineNotifySink? context = null) : IScriptEngine, IScriptSite, IDisposable
{
/// <inheritdoc/>
public IScriptEngine Engine => this;

/// <inheritdoc/>
public ILogger Logger => logger;

Expand Down Expand Up @@ -353,4 +357,7 @@ protected virtual ScriptError CreateErrorNotification(IScriptInstance script, Ex
/// <returns></returns>
protected virtual ScriptFinished CreateFinishNotification(IScriptInstance script)
=> new() { JobId = script.JobId, ModelType = script.GetRequest().ModelType, Name = script.GetRequest().Name };

/// <inheritdoc/>
public Task SingleStep(Block block) => Task.CompletedTask;
}
20 changes: 19 additions & 1 deletion Library/Scripting/Engine/UserInputRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace BlocklyNet.Scripting.Engine;
/// A script requires some user input - currently only
/// numbers are supported.
/// </summary>
public class UserInputRequest
public class UserInputRequestBase
{
/// <summary>
/// The unique identifier of the active script.
Expand All @@ -28,3 +28,21 @@ public class UserInputRequest
/// </summary>
public string? ValueType { get; set; }
}

/// <summary>
/// A script requires some user input - currently only
/// numbers are supported.
/// </summary>
public class UserInputRequest : UserInputRequestBase
{
/// <summary>
/// Exact time the request was started.
/// </summary>
[Required, NotNull]
public DateTime StartedAt { get; set; }

/// <summary>
/// Optional delay to auto-close the request.
/// </summary>
public double? SecondsToAutoClose { get; set; }
}
2 changes: 1 addition & 1 deletion Library/Scripting/Engine/UserInputResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace BlocklyNet.Scripting.Engine;
/// Reports the requested input - currently only numbers
/// are supported.
/// </summary>
public class UserInputResponse : UserInputRequest
public class UserInputResponse : UserInputRequestBase
{
/// <summary>
/// The input provided by the user.
Expand Down
Loading

0 comments on commit 277a978

Please sign in to comment.