Browse Source

* Fix highlight pipe

* Fix concurrency issues in scripts.
pull/971/head
Sebastian 4 years ago
parent
commit
6009acd468
  1. 50
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintExtensions.cs
  2. 134
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs
  3. 1
      backend/src/Squidex.Web/ApiPermissionAttribute.cs
  4. 17
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs
  5. 8
      frontend/src/app/framework/angular/pipes/highlight.pipe.ts
  6. 4
      frontend/src/app/framework/angular/pipes/keys.pipe.spec.ts

50
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/JintExtensions.cs

@ -34,4 +34,54 @@ public static class JintExtensions
return ids;
}
internal static ScriptExecutionContext<T> ExtendAsync<T>(this ScriptExecutionContext<T> context,
IEnumerable<IJintExtension> extensions)
{
foreach (var extension in extensions)
{
extension.ExtendAsync(context);
}
return context;
}
internal static ScriptExecutionContext<T> Extend<T>(this ScriptExecutionContext<T> context,
IEnumerable<IJintExtension> extensions)
{
foreach (var extension in extensions)
{
extension.Extend(context);
}
return context;
}
internal static ScriptExecutionContext<T> Extend<T>(this ScriptExecutionContext<T> context,
ScriptVars vars,
ScriptOptions options)
{
var engine = context.Engine;
context.CopyFrom(vars);
if (options.AsContext)
{
var contextInstance = new WritableContext(engine, vars);
engine.SetValue("ctx", contextInstance);
engine.SetValue("context", contextInstance);
}
else
{
foreach (var (key, item) in vars)
{
engine.SetValue(key, item.Value!);
}
}
engine.SetValue("async", true);
return context;
}
}

134
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptExecutionContext.cs

@ -7,6 +7,7 @@
using Jint;
using Squidex.Infrastructure.Tasks;
using System.Diagnostics;
namespace Squidex.Domain.Apps.Core.Scripting;
@ -22,20 +23,12 @@ public abstract class ScriptExecutionContext : ScriptContext
public abstract void Schedule(Func<IScheduler, CancellationToken, Task> action);
}
#pragma warning disable MA0048 // File name must match type name
public interface IScheduler
#pragma warning restore MA0048 // File name must match type name
{
void Run(Action? action);
void Run<T>(Action<T>? action, T argument);
}
public sealed class ScriptExecutionContext<T> : ScriptExecutionContext, IScheduler
{
private readonly TaskCompletionSource<T?> tcs = new TaskCompletionSource<T?>();
private readonly TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
private readonly CancellationToken cancellationToken;
private int pendingTasks;
private readonly ReaderWriterLockSlim slimLock = new ReaderWriterLockSlim();
private int pendingTasks = 1;
public bool IsCompleted
{
@ -48,12 +41,9 @@ public sealed class ScriptExecutionContext<T> : ScriptExecutionContext, ISchedul
this.cancellationToken = cancellationToken;
}
public Task<T?> CompleteAsync()
public Task<T> CompleteAsync()
{
if (pendingTasks <= 0)
{
tcs.TrySetResult(default);
}
TryComplete(default!);
return tcs.Task.WithCancellation(cancellationToken);
}
@ -74,95 +64,103 @@ public sealed class ScriptExecutionContext<T> : ScriptExecutionContext, ISchedul
{
try
{
Interlocked.Increment(ref pendingTasks);
TryStart();
await action(this, cancellationToken);
if (Interlocked.Decrement(ref pendingTasks) <= 0)
{
tcs.TrySetResult(default);
}
TryComplete(default!);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
TryFail(ex);
}
}
ScheduleAsync().Forget();
}
public ScriptExecutionContext<T> ExtendAsync(IEnumerable<IJintExtension> extensions)
void IScheduler.Run(Action? action)
{
foreach (var extension in extensions)
if (IsCompleted || action == null)
{
extension.ExtendAsync(this);
return;
}
return this;
}
slimLock.EnterWriteLock();
try
{
TryStart();
public ScriptExecutionContext<T> Extend(IEnumerable<IJintExtension> extensions)
{
foreach (var extension in extensions)
Engine.ResetConstraints();
action();
TryComplete(default!);
}
catch (Exception ex)
{
extension.Extend(this);
TryFail(ex);
}
finally
{
slimLock.ExitWriteLock();
}
return this;
}
public ScriptExecutionContext<T> Extend(ScriptVars vars, ScriptOptions options)
void IScheduler.Run<TArg>(Action<TArg>? action, TArg argument)
{
var engine = Engine;
CopyFrom(vars);
if (IsCompleted || action == null)
{
return;
}
if (options.AsContext)
slimLock.EnterWriteLock();
try
{
var contextInstance = new WritableContext(engine, vars);
TryStart();
Engine.ResetConstraints();
action(argument);
engine.SetValue("ctx", contextInstance);
engine.SetValue("context", contextInstance);
TryComplete(default!);
}
else
catch (Exception ex)
{
foreach (var (key, item) in vars)
{
engine.SetValue(key, item.Value!);
}
TryFail(ex);
}
finally
{
slimLock.ExitWriteLock();
}
}
engine.SetValue("async", true);
return this;
private void TryFail(Exception exception)
{
tcs.TrySetException(exception);
}
void IScheduler.Run(Action? action)
private void TryStart()
{
lock (Engine)
{
if (IsCompleted || action == null)
{
return;
}
Interlocked.Increment(ref pendingTasks);
Engine.ResetConstraints();
action();
}
Debug.WriteLine(pendingTasks);
}
void IScheduler.Run<TArg>(Action<TArg>? action, TArg argument)
private void TryComplete(T result)
{
lock (Engine)
if (Interlocked.Decrement(ref pendingTasks) <= 0)
{
if (IsCompleted || action == null)
{
return;
}
Engine.ResetConstraints();
action(argument);
tcs.TrySetResult(result);
}
Debug.WriteLine(pendingTasks);
}
}
#pragma warning disable MA0048 // File name must match type name
public interface IScheduler
#pragma warning restore MA0048 // File name must match type name
{
void Run(Action? action);
void Run<T>(Action<T>? action, T argument);
}

1
backend/src/Squidex.Web/ApiPermissionAttribute.cs

@ -73,7 +73,6 @@ public class ApiPermissionAttribute : AuthorizeAttribute, IAsyncActionFilter
if (!hasPermission)
{
context.Result = new StatusCodeResult(403);
return Task.CompletedTask;
}
}

17
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs

@ -73,7 +73,7 @@ public class JintScriptEngineTests : IClassFixture<TranslationsFixture>
{
context.Schedule(async (scheduler, ct) =>
{
await Task.Delay(5, ct);
await Task.Delay(1, ct);
scheduler.Run(callback);
});
}));
@ -621,12 +621,14 @@ public class JintScriptEngineTests : IClassFixture<TranslationsFixture>
[Fact]
public async Task Should_not_run_callbacks_in_parallel()
{
var vars = new DataScriptVars
for (var i = 0; i < 10; i++)
{
["value"] = 13
};
var vars = new DataScriptVars
{
["value"] = 13
};
const string script1 = @"
const string script1 = @"
var x = ctx.value;
for (var i = 0; i < 100; i++) {
setTimeout(function () {
@ -636,8 +638,9 @@ public class JintScriptEngineTests : IClassFixture<TranslationsFixture>
}
";
await sut.ExecuteAsync(vars, script1, new ScriptOptions { AsContext = true });
await sut.ExecuteAsync(vars, script1, new ScriptOptions { AsContext = true });
Assert.Equal(113.0, vars["shared"]);
Assert.Equal(113.0, vars["shared"]);
}
}
}

8
frontend/src/app/framework/angular/pipes/highlight.pipe.ts

@ -6,7 +6,7 @@
*/
import { Pipe, PipeTransform } from '@angular/core';
import { Types } from '@app/framework/internal';
import { escapeHTML, Types } from '@app/framework/internal';
@Pipe({
name: 'sqxHighlight',
@ -14,6 +14,8 @@ import { Types } from '@app/framework/internal';
})
export class HighlightPipe implements PipeTransform {
public transform(text: string, highlight: string | RegExp | undefined | null): string {
text = escapeHTML(text);
if (!highlight) {
return text;
}
@ -22,6 +24,8 @@ export class HighlightPipe implements PipeTransform {
highlight = new RegExp(highlight, 'i');
}
return text.replace(highlight, s => `<b>${s}</b>`);
const result = text.replace(highlight, s => `<b>${s}</b>`);
return result;
}
}

4
frontend/src/app/framework/angular/pipes/keys.pipe.spec.ts

@ -8,14 +8,14 @@
import { KeysPipe } from './keys.pipe';
describe('KeysPipe', () => {
const pipe = new KeysPipe();
it('should return keys', () => {
const value = {
key1: 1,
key2: 2,
};
const pipe = new KeysPipe();
const actual = pipe.transform(value);
const expected = ['key1', 'key2'];

Loading…
Cancel
Save