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
139 changes: 104 additions & 35 deletions src/core/Statiq.App/Commands/InteractiveCommand.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
Expand Down Expand Up @@ -54,14 +55,37 @@ protected override async Task<int> ExecuteEngineAsync(

ExitCode exitCode = ExitCode.Normal;

// Start the console listener
ConsoleListener consoleListener = new ConsoleListener(
() =>
// Determine if we should run in interactive mode
bool interactiveMode = !commandSettings.NoInteractive && IsConsoleAvailable();

// Start the console listener only if interactive mode is enabled
ConsoleListener consoleListener = null;
if (interactiveMode)
{
consoleListener = new ConsoleListener(
() =>
{
TriggerExit();
return Task.CompletedTask;
},
input => EvaluateScriptAsync(input, commandContext, commandSettings, engineManager));
}
else
{
// In non-interactive mode, set up console cancel key press handler if possible
try
{
TriggerExit();
return Task.CompletedTask;
},
input => EvaluateScriptAsync(input, commandContext, commandSettings, engineManager));
Console.CancelKeyPress += (sender, e) =>
{
e.Cancel = true;
TriggerExit();
};
}
catch
{
// Console may not be available, cancellation will rely on the token
}
}

// Execute the engine for the first time
exitCode = await engineManager.ExecuteAsync(_cancellationTokenSource);
Expand All @@ -71,41 +95,64 @@ protected override async Task<int> ExecuteEngineAsync(
{
await AfterInitialExecutionAsync(commandContext, commandSettings, engineManager, _cancellationTokenSource);

// Log that we're ready and start waiting on input
const string prompt = "Type Ctrl-C or \"Exit()\" to exit and \"Help()\" for global methods and properties";
logger.LogInformation(prompt);
ConsoleLoggerProvider.FlushAndWait();
consoleListener.StartReadingLines();

// Wait for activity
while (true)
if (interactiveMode)
{
// Blocks the current thread until a signal
// This will also reset the event (since it's an AutoResetEvent) so any triggering will cause a following execution
_triggerExecutionEvent.WaitOne();

// Stop listening while we run again
consoleListener.StopReadingLines();
// Interactive mode: use REPL
const string prompt = "Type Ctrl-C or \"Exit()\" to exit and \"Help()\" for global methods and properties";
logger.LogInformation(prompt);
ConsoleLoggerProvider.FlushAndWait();
consoleListener.StartReadingLines();

// Break here before running if we're exiting
if (_exit)
// Wait for activity
while (true)
{
break;
}
// Blocks the current thread until a signal
// This will also reset the event (since it's an AutoResetEvent) so any triggering will cause a following execution
_triggerExecutionEvent.WaitOne();

// Execute
exitCode = await ExecutionTriggeredAsync(commandContext, commandSettings, engineManager, exitCode, _cancellationTokenSource);
// Stop listening while we run again
consoleListener.StopReadingLines();

// Check one more time for exit
if (_exit)
{
break;
}
// Break here before running if we're exiting
if (_exit)
{
break;
}

// Log that we're ready and start waiting on input (again)
logger.LogInformation(prompt);
// Execute
exitCode = await ExecutionTriggeredAsync(commandContext, commandSettings, engineManager, exitCode, _cancellationTokenSource);

// Check one more time for exit
if (_exit)
{
break;
}

// Log that we're ready and start waiting on input (again)
logger.LogInformation(prompt);
ConsoleLoggerProvider.FlushAndWait();
consoleListener.StartReadingLines();
}
}
else
{
// Non-interactive mode: wait for cancellation or file changes
logger.LogInformation("Running in non-interactive mode. Press Ctrl+C to exit.");
ConsoleLoggerProvider.FlushAndWait();
consoleListener.StartReadingLines();

// Wait for activity (file changes will trigger execution, Ctrl+C will trigger exit)
while (!_exit)
{
_triggerExecutionEvent.WaitOne();

if (_exit)
{
break;
}

// Execute
exitCode = await ExecutionTriggeredAsync(commandContext, commandSettings, engineManager, exitCode, _cancellationTokenSource);
}
}
}

Expand All @@ -117,6 +164,28 @@ protected override async Task<int> ExecuteEngineAsync(
}
}

/// <summary>
/// Checks if a console is available for interactive input/output.
/// </summary>
private static bool IsConsoleAvailable()
{
try
{
// Try to access console properties that require a valid handle
// This will throw if no console is available
_ = Console.KeyAvailable;
return true;
}
catch (IOException)
{
return false;
}
catch (InvalidOperationException)
{
return false;
}
}

public void TriggerExecution() => _triggerExecutionEvent.Set();

public void TriggerExit()
Expand Down
4 changes: 4 additions & 0 deletions src/core/Statiq.App/Commands/PipelinesCommandSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ public class PipelinesCommandSettings : EngineCommandSettings
[Description("Executes normal pipelines as well as those specified.")]
public bool NormalPipelines { get; set; }

[CommandOption("--no-interactive")]
[Description("Disables the interactive console/REPL. Useful for CI/CD, containers, background processes, or environments without a console.")]
public bool NoInteractive { get; set; }

[CommandArgument(0, "[pipelines]")]
[Description("The pipeline(s) to execute.")]
public string[] Pipelines { get; set; }
Expand Down