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
62 changes: 45 additions & 17 deletions src/OneScript.StandardLibrary/Text/ConsoleContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using OneScript.Exceptions;
using OneScript.Execution;
using OneScript.StandardLibrary.Binary;
using OneScript.Types;
using OneScript.Values;
using ScriptEngine.Machine;
using ScriptEngine.Machine.Contexts;
Expand Down Expand Up @@ -181,7 +182,7 @@
}
set
{
Console.OutputEncoding = TextEncodingEnum.GetEncoding(value);
ConsoleWriterRouter.SetOutputEncoding(TextEncodingEnum.GetEncoding(value));
}
}

Expand Down Expand Up @@ -229,34 +230,61 @@
}

/// <summary>
/// Глобально переопределяет стандартный вывод и направляет в другой поток
/// Переопределяет стандартный вывод и направляет в другой поток.
/// Действует в текущем потоке выполнения и в фоновых заданиях, запущенных из него после вызова.
/// </summary>
/// <param name="target">Поток назначения</param>
/// <param name="target">Поток назначения. Неопределено - вернуть стандартный вывод</param>
[ContextMethod("УстановитьПотокВывода", "SetOutput")]
public void SetOutput(IValue target)
{
if (!(target.AsObject() is IStreamWrapper stream))
throw RuntimeException.InvalidArgumentType(nameof(target));

var writer = new StreamWriter(stream.GetUnderlyingStream(), Console.OutputEncoding)
{
AutoFlush = true,
};
Console.SetOut(writer);
ConsoleWriterRouter.SetOut(CreateWriter(target), target);
}

/// <summary>
/// Глобально переопределяет стандартный поток ошибок и направляет в другой поток
/// Возвращает поток, в который перенаправлен стандартный вывод текущего потока выполнения,
/// в том числе унаследованный фоновым заданием от запустившего его кода.
/// </summary>
/// <param name="target">Поток назначения</param>
/// <returns>Поток, Неопределено - вывод не перенаправлен</returns>
[ContextMethod("ПолучитьПотокВывода", "GetOutput")]
public IValue GetOutput()

Check notice on line 249 in src/OneScript.StandardLibrary/Text/ConsoleContext.cs

View check run for this annotation

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

src/OneScript.StandardLibrary/Text/ConsoleContext.cs#L249

Member 'GetOutput' does not access instance data and can be marked as static
{
return ConsoleWriterRouter.GetOutSource() ?? BslUndefinedValue.Instance;
}

/// <summary>
/// Переопределяет стандартный поток ошибок и направляет в другой поток.
/// Действует в текущем потоке выполнения и в фоновых заданиях, запущенных из него после вызова.
/// </summary>
/// <param name="target">Поток назначения. Неопределено - вернуть стандартный поток ошибок</param>
[ContextMethod("УстановитьПотокОшибок", "SetError")]
public void SetError(IValue target)
{
ConsoleWriterRouter.SetError(CreateWriter(target), target);
}

/// <summary>
/// Возвращает поток, в который перенаправлен поток ошибок текущего потока выполнения,
/// в том числе унаследованный фоновым заданием от запустившего его кода.
/// </summary>
/// <returns>Поток, Неопределено - поток ошибок не перенаправлен</returns>
[ContextMethod("ПолучитьПотокОшибок", "GetError")]
public IValue GetError()

Check notice on line 271 in src/OneScript.StandardLibrary/Text/ConsoleContext.cs

View check run for this annotation

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

src/OneScript.StandardLibrary/Text/ConsoleContext.cs#L271

Member 'GetError' does not access instance data and can be marked as static
{
return ConsoleWriterRouter.GetErrorSource() ?? BslUndefinedValue.Instance;
}

private static StreamWriter CreateWriter(IValue target)
{
if (target.SystemType == BasicTypes.Undefined)
return null;

if (!(target.AsObject() is IStreamWrapper stream))
throw RuntimeException.InvalidArgumentType(nameof(target));

var writer = new StreamWriter(stream.GetUnderlyingStream());
Console.SetError(writer);

return new StreamWriter(stream.GetUnderlyingStream(), Console.OutputEncoding)
{
AutoFlush = true,
};
}

public const string ConsoleCancelKeyEvent = "CancelKeyPressed";
Expand Down
35 changes: 35 additions & 0 deletions src/OneScript.StandardLibrary/Text/ConsoleOutputTargets.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*----------------------------------------------------------
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/.
----------------------------------------------------------*/

namespace OneScript.StandardLibrary.Text
{
/// <summary>
/// Куда текущий поток выполнения выводит консоль (Консоль.УстановитьПотокВывода/УстановитьПотокОшибок).
/// Фоновые задания наследуют это сами, а там, куда контекст выполнения не передается
/// (обработчики запросов веб-сервера), цели нужно перенести явно.
/// </summary>
public sealed class ConsoleOutputTargets
{
private readonly (object Out, object Error) _targets;

private ConsoleOutputTargets((object Out, object Error) targets)
{
_targets = targets;
}

/// <summary>
/// Запоминает цели вывода текущего потока выполнения
/// </summary>
public static ConsoleOutputTargets Capture() => new ConsoleOutputTargets(ConsoleWriterRouter.CaptureTargets());

/// <summary>
/// Устанавливает запомненные цели в текущем потоке выполнения.
/// Вызванный внутри async-метода, действует только до его завершения.
/// </summary>
public void Apply() => ConsoleWriterRouter.ApplyTargets(_targets);
}
}
230 changes: 230 additions & 0 deletions src/OneScript.StandardLibrary/Text/ConsoleWriterRouter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/*----------------------------------------------------------
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.IO;
using System.Text;
using System.Threading;
using ScriptEngine.Machine;

namespace OneScript.StandardLibrary.Text
{
/// <summary>
/// Стандартный вывод или поток ошибок консоли со своей целью у каждого потока выполнения.
/// Цель хранится в AsyncLocal: фоновые задания наследуют ее от запустившего их кода,
/// а переопределение внутри задания не затрагивает остальных.
/// </summary>
internal sealed class ConsoleWriterRouter : TextWriter
{
private static readonly object InstallLock = new object();
private static ConsoleWriterRouter _out;
private static ConsoleWriterRouter _error;

private readonly AsyncLocal<Target> _current = new AsyncLocal<Target>();
// Открывает стандартный поток, если вывод по умолчанию - собственный вывод консоли, а не заданный хостом
private readonly Func<Stream> _openConsoleStream;
private volatile TextWriter _default;
private TextWriter _installed;

private ConsoleWriterRouter(TextWriter defaultWriter, Func<Stream> openConsoleStream)
{
_default = defaultWriter;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
_openConsoleStream = openConsoleStream;
}

/// <summary>
/// Направляет стандартный вывод текущего потока выполнения в writer; null возвращает вывод по умолчанию.
/// </summary>
public static void SetOut(TextWriter writer, IValue source)
{
lock (InstallLock)
{
// Console.Out мог заменить хост: тогда он становится выводом по умолчанию
if (!IsInstalled(_out, Console.Out))
{
if (writer == null)
return;

var isConsoleWriter = IsConsoleWriter(() => Console.Out);
_out = new ConsoleWriterRouter(Console.Out, isConsoleWriter ? Console.OpenStandardOutput : null);
Console.SetOut(_out);
_out._installed = Console.Out;
}

_out.SetCurrent(writer, source);
}
}

/// <summary>
/// Направляет поток ошибок текущего потока выполнения в writer; null возвращает поток ошибок по умолчанию.
/// </summary>
public static void SetError(TextWriter writer, IValue source)
{
lock (InstallLock)
{
if (!IsInstalled(_error, Console.Error))
{
if (writer == null)
return;

var isConsoleWriter = IsConsoleWriter(() => Console.Error);
_error = new ConsoleWriterRouter(Console.Error, isConsoleWriter ? Console.OpenStandardError : null);
Console.SetError(_error);
_error._installed = Console.Error;
}

_error.SetCurrent(writer, source);
}
}

/// <summary>
/// Меняет кодировку вывода консоли, в том числе вывода по умолчанию у установленных роутеров.
/// </summary>
public static void SetOutputEncoding(Encoding encoding)
{
lock (InstallLock)
{
Console.OutputEncoding = encoding;

// Console пересоздает свой вывод с новой кодировкой, только пока его не подменили через SetOut
_out?.RecreateConsoleWriter();
_error?.RecreateConsoleWriter();
}
}

// Цели текущего потока выполнения - для мест, куда контекст выполнения не передается сам
internal static (object Out, object Error) CaptureTargets()
{
lock (InstallLock)
{
return (_out?._current.Value, _error?._current.Value);
}
}

internal static void ApplyTargets((object Out, object Error) targets)
{
lock (InstallLock)
{
if (_out != null)
_out._current.Value = (Target)targets.Out;
if (_error != null)
_error._current.Value = (Target)targets.Error;
}
}

/// <summary>
/// Поток, в который перенаправлен стандартный вывод текущего потока выполнения, или null.
/// </summary>
public static IValue GetOutSource() => GetSource(_out, Console.Out);

/// <summary>
/// Поток, в который перенаправлен поток ошибок текущего потока выполнения, или null.
/// </summary>
public static IValue GetErrorSource() => GetSource(_error, Console.Error);

private static bool IsInstalled(ConsoleWriterRouter router, TextWriter console)
=> router != null && ReferenceEquals(console, router._installed);

private static IValue GetSource(ConsoleWriterRouter router, TextWriter console)
=> IsInstalled(router, console) ? router._current.Value?.Source : null;

// Публично не узнать, подменял ли хост вывод консоли. Но при смене кодировки Console пересоздает
// только свой вывод: присваиваем ту же кодировку и смотрим, сменился ли объект
private static bool IsConsoleWriter(Func<TextWriter> consoleWriter)
{
var writer = consoleWriter();
try
{
var encoding = Console.OutputEncoding;
Console.OutputEncoding = encoding;
}
catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is PlatformNotSupportedException)
{
// Без консоли кодировку не сменить, и пересоздавать вывод не придется
return false;
}

return !ReferenceEquals(writer, consoleWriter());
}

private void RecreateConsoleWriter()
{
if (_openConsoleStream == null)
return;

var previous = _default;
_default = CreateConsoleWriter(_openConsoleStream());
previous.Flush();
}

// Как свой вывод создает Console: без BOM и со сбросом после каждой записи
private static TextWriter CreateConsoleWriter(Stream stream)
{
var writer = new StreamWriter(stream, WithoutPreamble(Console.OutputEncoding), 256, leaveOpen: true)
{
AutoFlush = true
};
return TextWriter.Synchronized(writer);
}

private static Encoding WithoutPreamble(Encoding encoding)
{
if (encoding.GetPreamble().Length == 0)
return encoding;

return encoding.CodePage switch
{
65001 => new UTF8Encoding(false),
1200 => new UnicodeEncoding(false, false),
1201 => new UnicodeEncoding(true, false),
12000 => new UTF32Encoding(false, false),
12001 => new UTF32Encoding(true, false),
_ => encoding
};
}

private void SetCurrent(TextWriter writer, IValue source)
{
_current.Value = writer == null ? null : new Target(writer, source);
}

private TextWriter Writer => _current.Value?.Writer ?? _default;

public override Encoding Encoding => Writer.Encoding;

public override IFormatProvider FormatProvider => Writer.FormatProvider;

public override void Write(char value) => Writer.Write(value);

public override void Write(char[] buffer, int index, int count) => Writer.Write(buffer, index, count);

public override void Write(ReadOnlySpan<char> buffer) => Writer.Write(buffer);

public override void Write(string value) => Writer.Write(value);

public override void WriteLine() => Writer.WriteLine();

public override void WriteLine(ReadOnlySpan<char> buffer) => Writer.WriteLine(buffer);

public override void WriteLine(string value) => Writer.WriteLine(value);

public override void Flush() => Writer.Flush();

private sealed class Target
{
public Target(TextWriter writer, IValue source)
{
Writer = writer;
Source = source;
}

public TextWriter Writer { get; }

public IValue Source { get; }
}
}
}
11 changes: 11 additions & 0 deletions src/OneScript.Web.Server/WebServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This Source Code Form is subject to the terms of the
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using OneScript.Contexts;
using OneScript.StandardLibrary.Text;
using OneScript.Types;
using ScriptEngine.Machine;
using ScriptEngine.Machine.Contexts;
Expand Down Expand Up @@ -123,6 +124,16 @@ private void ConfigureApp()

_app = builder.Build();

// Обработчики запросов, как и фоновые задания, выводят консоль туда же, куда тот,
// кто запустил сервер. Переносятся только цели вывода, остальной контекст запроса
// (трассировка и т.п.) остается свой; async - чтобы цели не вышли за пределы запроса
var consoleTargets = ConsoleOutputTargets.Capture();
_app.Use(async (context, next) =>
{
consoleTargets.Apply();
await next();
});

if (_useStaticFiles)
_app.UseStaticFiles();

Expand Down
Loading
Loading