Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace RobotNet10.ScriptEngine.Hubs;
[Authorize]
public class ConsoleHub : Hub
{
public Task RegisterTask(string name) => Groups.AddToGroupAsync(Context.ConnectionId, $"task-{name}");
public Task UnregisterTask(string name) => Groups.RemoveFromGroupAsync(Context.ConnectionId, $"task-{name}");
public Task RegisterMission(Guid missionId) => Groups.AddToGroupAsync(Context.ConnectionId, $"mission-{missionId}");
public Task UnregisterMission(Guid missionId) => Groups.RemoveFromGroupAsync(Context.ConnectionId, $"mission-{missionId}");
public Task RegisterAll() => Groups.AddToGroupAsync(Context.ConnectionId, "alls");
public Task UnregisterAll() => Groups.RemoveFromGroupAsync(Context.ConnectionId, "alls");
}

View File

@@ -0,0 +1,302 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using RobotNet10.ScriptEngine;
using RobotNet10.ScriptEngine.HubContexts;
using RobotNet10.ScriptEngine.Shared;
namespace RobotNet10.ScriptEngine.Hubs;
/// <summary>
/// SignalR Hub for managing script files.
/// </summary>
[Authorize]
public class FileManagerHub(FileManager _fileManager, ConsoleHubContext _consoleHubContext) : Hub
{
/// <summary>
/// Called when a client disconnects.
/// </summary>
public override Task OnDisconnectedAsync(Exception? exception)
{
// Revoke edit permission if this connection had it
_fileManager.RevokeEditPermission(Context.ConnectionId);
return base.OnDisconnectedAsync(exception);
}
/// <summary>
/// Gets the root folder structure with all files and folders.
/// </summary>
public async Task<ScriptFolderDto> GetRootFolder()
{
return await _fileManager.GetRootFolderAsync();
}
/// <summary>
/// Gets the current state of the FileManager.
/// </summary>
public Task<ScriptEngineState> GetState()
{
return Task.FromResult(_fileManager.State);
}
/// <summary>
/// Requests edit permission. If another connection has permission, it will be revoked and notified.
/// </summary>
public Task<bool> RequestEditPermission()
{
var previousConnectionId = _fileManager.RequestEditPermission(Context.ConnectionId);
if (previousConnectionId != null && previousConnectionId != Context.ConnectionId)
{
// Notify the previous connection that their permission was revoked
// Only notify if it's a different connection (not the same one reconnecting)
try
{
_ = Clients.Client(previousConnectionId).SendAsync("EditPermissionRevoked", Context.UserIdentifier);
}
catch
{
// Ignore if connection is already disconnected
}
}
return Task.FromResult(true);
}
/// <summary>
/// Revokes edit permission for the current connection.
/// </summary>
public Task RevokeEditPermission()
{
_fileManager.RevokeEditPermission(Context.ConnectionId);
return Task.CompletedTask;
}
/// <summary>
/// Checks if the current connection has edit permission.
/// </summary>
public Task<bool> HasEditPermission()
{
return Task.FromResult(_fileManager.HasEditPermission(Context.ConnectionId));
}
/// <summary>
/// Saves file content.
/// </summary>
public async Task SaveFile(string relativePath, string content)
{
try
{
await _fileManager.SaveFileAsync(relativePath, content, Context.ConnectionId);
_consoleHubContext.LogInfo($"File saved successfully: {relativePath}");
// Notify other clients that file was saved (fire-and-forget)
_ = Clients.Others.SendAsync("FileSaved", relativePath, Context.UserIdentifier);
}
catch (UnauthorizedAccessException ex)
{
_consoleHubContext.LogError($"Failed to save file '{relativePath}': {ex.Message}");
throw;
}
catch (Exception ex)
{
_consoleHubContext.LogError($"Error saving file '{relativePath}': {ex.Message}");
throw;
}
}
/// <summary>
/// Creates a new file.
/// </summary>
public async Task CreateFile(string relativePath, string content)
{
try
{
await _fileManager.CreateFileAsync(relativePath, content, Context.ConnectionId);
_consoleHubContext.LogInfo($"File created successfully: {relativePath}");
// Notify other clients that file was created (fire-and-forget)
_ = Clients.Others.SendAsync("FileCreated", relativePath, Context.UserIdentifier);
}
catch (UnauthorizedAccessException ex)
{
_consoleHubContext.LogError($"Failed to create file '{relativePath}': {ex.Message}");
throw;
}
catch (IOException ex)
{
_consoleHubContext.LogWarning($"Failed to create file '{relativePath}': {ex.Message}");
throw;
}
catch (Exception ex)
{
_consoleHubContext.LogError($"Error creating file '{relativePath}': {ex.Message}");
throw;
}
}
/// <summary>
/// Creates a new folder.
/// </summary>
public Task CreateFolder(string relativePath)
{
try
{
_fileManager.CreateFolder(relativePath, Context.ConnectionId);
_consoleHubContext.LogInfo($"Folder created successfully: {relativePath}");
// Notify other clients that folder was created (fire-and-forget)
_ = Clients.Others.SendAsync("FolderCreated", relativePath, Context.UserIdentifier);
}
catch (UnauthorizedAccessException ex)
{
_consoleHubContext.LogError($"Failed to create folder '{relativePath}': {ex.Message}");
throw;
}
catch (IOException ex)
{
_consoleHubContext.LogWarning($"Failed to create folder '{relativePath}': {ex.Message}");
throw;
}
catch (Exception ex)
{
_consoleHubContext.LogError($"Error creating folder '{relativePath}': {ex.Message}");
throw;
}
return Task.CompletedTask;
}
/// <summary>
/// Deletes a file.
/// </summary>
public Task DeleteFile(string relativePath)
{
try
{
_fileManager.DeleteFile(relativePath, Context.ConnectionId);
_consoleHubContext.LogInfo($"File deleted successfully: {relativePath}");
// Notify other clients that file was deleted (fire-and-forget)
_ = Clients.Others.SendAsync("FileDeleted", relativePath, Context.UserIdentifier);
}
catch (UnauthorizedAccessException ex)
{
_consoleHubContext.LogError($"Failed to delete file '{relativePath}': {ex.Message}");
throw;
}
catch (FileNotFoundException ex)
{
_consoleHubContext.LogWarning($"Failed to delete file '{relativePath}': {ex.Message}");
throw;
}
catch (Exception ex)
{
_consoleHubContext.LogError($"Error deleting file '{relativePath}': {ex.Message}");
throw;
}
return Task.CompletedTask;
}
/// <summary>
/// Deletes a folder.
/// </summary>
public Task DeleteFolder(string relativePath)
{
try
{
_fileManager.DeleteFolder(relativePath, Context.ConnectionId);
_consoleHubContext.LogInfo($"Folder deleted successfully: {relativePath}");
// Notify other clients that folder was deleted (fire-and-forget)
_ = Clients.Others.SendAsync("FolderDeleted", relativePath, Context.UserIdentifier);
}
catch (UnauthorizedAccessException ex)
{
_consoleHubContext.LogError($"Failed to delete folder '{relativePath}': {ex.Message}");
throw;
}
catch (DirectoryNotFoundException ex)
{
_consoleHubContext.LogWarning($"Failed to delete folder '{relativePath}': {ex.Message}");
throw;
}
catch (Exception ex)
{
_consoleHubContext.LogError($"Error deleting folder '{relativePath}': {ex.Message}");
throw;
}
return Task.CompletedTask;
}
/// <summary>
/// Creates a backup of all scripts.
/// </summary>
public async Task<string> CreateBackup(string? backupName = null)
{
try
{
var backupFileName = await _fileManager.CreateBackupAsync(backupName);
_consoleHubContext.LogInfo($"Backup created successfully: {backupFileName}");
// Notify all clients that backup was created (fire-and-forget)
_ = Clients.All.SendAsync("BackupCreated", backupFileName, Context.UserIdentifier);
return backupFileName;
}
catch (Exception ex)
{
_consoleHubContext.LogError($"Error creating backup: {ex.Message}");
throw;
}
}
/// <summary>
/// Lists available backup files.
/// </summary>
public Task<ScriptBackupInfo[]> ListBackups()
{
var backups = _fileManager.ListBackups();
return Task.FromResult(backups);
}
/// <summary>
/// Restores scripts from a backup.
/// </summary>
public async Task RestoreBackup(string backupFileName, bool replaceExisting = true)
{
try
{
await _fileManager.RestoreBackupAsync(backupFileName, replaceExisting);
_consoleHubContext.LogInfo($"Backup restored successfully: {backupFileName}");
// Notify all clients that backup was restored (fire-and-forget)
_ = Clients.All.SendAsync("BackupRestored", backupFileName, Context.UserIdentifier);
}
catch (Exception ex)
{
_consoleHubContext.LogError($"Error restoring backup '{backupFileName}': {ex.Message}");
throw;
}
}
/// <summary>
/// Deletes a backup file.
/// </summary>
public Task DeleteBackup(string backupFileName)
{
try
{
_fileManager.DeleteBackup(backupFileName);
_consoleHubContext.LogInfo($"Backup deleted successfully: {backupFileName}");
// Notify all clients that backup was deleted (fire-and-forget)
_ = Clients.All.SendAsync("BackupDeleted", backupFileName, Context.UserIdentifier);
}
catch (Exception ex)
{
_consoleHubContext.LogError($"Error deleting backup '{backupFileName}': {ex.Message}");
throw;
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,166 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using RobotNet10.ScriptEngine.Data;
using RobotNet10.ScriptEngine.Shared;
using RobotNet10.Shared;
using System.Security.Claims;
using System.Text.Json;
namespace RobotNet10.ScriptEngine.Hubs;
/// <summary>
/// SignalR hub for InstanceMission management operations.
/// </summary>
[Authorize]
public class InstanceMissionHub : Hub
{
private readonly ScriptEngineDbContext _dbContext;
private readonly MissionManager _missionManager;
public InstanceMissionHub(ScriptEngineDbContext dbContext, MissionManager missionManager)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_missionManager = missionManager ?? throw new ArgumentNullException(nameof(missionManager));
}
/// <summary>
/// Searches instance missions with pagination and text search.
/// Running missions are prioritized.
/// </summary>
public async Task<SearchResult<InstanceMissionDto>> SearchInstanceMissions(SearchRequest request)
{
var query = _dbContext.InstanceMissions.AsQueryable();
// Text search on MissionName
if (!string.IsNullOrWhiteSpace(request.TxtSearch))
{
var searchText = request.TxtSearch.Trim();
query = query.Where(m => m.MissionName.Contains(searchText));
}
// Get total count before pagination
var total = await query.CountAsync();
// Order by: running missions first (Running, Pausing, Resuming), then by CreatedAt descending
var orderedQuery = query
.OrderByDescending(m => m.State == ScriptMissionState.Running ||
m.State == ScriptMissionState.Pausing ||
m.State == ScriptMissionState.Resuming)
.ThenByDescending(m => m.CreatedAt);
// Apply pagination
var items = await orderedQuery
.Skip((request.Page - 1) * request.Size)
.Take(request.Size)
.Select(m => new InstanceMissionDto
{
Id = m.Id,
MissionName = m.MissionName,
Parameters = m.Parameters ?? "{}",
CreatedAt = m.CreatedAt,
State = m.State,
TotalScore = m.TotalScore,
Score = m.Score,
StoppedAt = m.StoppedAt,
Log = m.Log
})
.ToArrayAsync();
return new SearchResult<InstanceMissionDto>(total, request.Page, request.Size, items);
}
/// <summary>
/// Gets the log for a specific instance mission.
/// </summary>
public async Task<string?> GetInstanceMissionLog(Guid missionId)
{
var mission = await _dbContext.InstanceMissions.FindAsync([missionId]);
return mission?.Log;
}
/// <summary>
/// Cancels a running or paused mission.
/// </summary>
public Task<MessageResult> CancelMission(Guid missionId, string reason)
{
var mission = _missionManager.GetMission(missionId);
if (mission == null)
{
return Task.FromResult(new MessageResult(false, "Mission not found"));
}
try
{
if (mission.State != ScriptMissionState.Running &&
mission.State != ScriptMissionState.Paused &&
mission.State != ScriptMissionState.Pausing)
{
return Task.FromResult(new MessageResult(false, $"Mission is in {mission.State} state and cannot be canceled"));
}
mission.Cancel(reason);
return Task.FromResult(new MessageResult(true, $"Mission canceled{(string.IsNullOrWhiteSpace(reason) ? "" : $": {reason}")}"));
}
catch (Exception ex)
{
return Task.FromResult(new MessageResult(false, $"Failed to cancel mission: {ex.Message}"));
}
}
/// <summary>
/// Pauses a running mission.
/// </summary>
public Task<MessageResult> PauseMission(Guid missionId)
{
var mission = _missionManager.GetMission(missionId);
if (mission == null)
{
return Task.FromResult(new MessageResult(false, "Mission not found"));
}
try
{
if (mission.State != ScriptMissionState.Running)
{
return Task.FromResult(new MessageResult(false, $"Mission is in {mission.State} state and cannot be paused"));
}
mission.Pause();
return Task.FromResult(new MessageResult(true, "Mission paused"));
}
catch (Exception ex)
{
return Task.FromResult(new MessageResult(false, $"Failed to pause mission: {ex.Message}"));
}
}
/// <summary>
/// Resumes a paused mission.
/// </summary>
public Task<MessageResult> ResumeMission(Guid missionId)
{
var mission = _missionManager.GetMission(missionId);
if (mission == null)
{
return Task.FromResult(new MessageResult(false, "Mission not found"));
}
try
{
if (mission.State != ScriptMissionState.Paused)
{
return Task.FromResult(new MessageResult(false, $"Mission is in {mission.State} state and cannot be resumed"));
}
mission.Resume();
return Task.FromResult(new MessageResult(true, "Mission resumed"));
}
catch (Exception ex)
{
return Task.FromResult(new MessageResult(false, $"Failed to resume mission: {ex.Message}"));
}
}
}

View File

@@ -0,0 +1,227 @@
using Microsoft.AspNetCore.SignalR;
using RobotNet10.ScriptEngine.Shared;
using RobotNet10.Shared;
namespace RobotNet10.ScriptEngine.Hubs;
/// <summary>
/// SignalR hub for ScriptEngine management operations.
/// </summary>
/// <remarks>
/// Initializes a new instance of ScriptManagerHub.
/// </remarks>
public class ScriptManagerHub(ScriptEngine _scriptEngine) : Hub
{
/// <summary>
/// Gets the current state of the ScriptEngine.
/// </summary>
public Task<ScriptEngineState> GetState()
{
return Task.FromResult(_scriptEngine.State);
}
/// <summary>
/// Builds scripts from all files. Only allowed when state is Idle or BuildError.
/// </summary>
public Task<MessageResult> Build()
{
return Task.FromResult(_scriptEngine.Build());
}
/// <summary>
/// Starts the ScriptEngine. Only allowed when state is Ready.
/// </summary>
public Task<MessageResult> Start()
{
return Task.FromResult(_scriptEngine.Start());
}
/// <summary>
/// Stops the ScriptEngine. Only allowed when state is Running.
/// </summary>
public Task<MessageResult> Stop()
{
return Task.FromResult(_scriptEngine.Stop());
}
/// <summary>
/// Resets the ScriptEngine. Allowed from Idle, Ready, BuildError, Running, or Fault.
/// </summary>
public Task<MessageResult> Reset()
{
return Task.FromResult(_scriptEngine.Reset());
}
#region VariableManager Methods
/// <summary>
/// Gets all script variables.
/// </summary>
public Task<ScriptVariableDto[]> GetScriptVariables()
{
return Task.FromResult(_scriptEngine.VariableManager.GetVariables().ToArray());
}
/// <summary>
/// Finds specific script variables by names.
/// </summary>
public Task<ScriptVariableDto[]> FindScriptVariables(string[] names)
{
return Task.FromResult(_scriptEngine.VariableManager.GetVariables(names).ToArray());
}
/// <summary>
/// Sets the value of a script variable by name.
/// </summary>
public Task<MessageResult> SetValue(string name, string value)
{
return Task.FromResult(_scriptEngine.VariableManager.SetValue(name, value));
}
#endregion
#region TaskManager Methods
/// <summary>
/// Gets all script tasks.
/// </summary>
public Task<ScriptTaskDto[]> GetScriptTasks()
{
return Task.FromResult(_scriptEngine.TaskManager.GetScriptTasks());
}
/// <summary>
/// Finds specific script tasks by names.
/// </summary>
public Task<ScriptTaskDto[]> FindScriptTasks(string[] names)
{
return Task.FromResult(_scriptEngine.TaskManager.FindScriptTasks(names));
}
/// <summary>
/// Enables a task (resumes if paused, starts if stopped).
/// </summary>
public Task<MessageResult> EnableTask(string name)
{
return Task.FromResult(_scriptEngine.TaskManager.EnableTask(name));
}
/// <summary>
/// Disables a task (pauses if running).
/// </summary>
public Task<MessageResult> DisableTask(string name)
{
return Task.FromResult(_scriptEngine.TaskManager.DisableTask(name));
}
#endregion
#region MissionManager Methods
/// <summary>
/// Gets all script missions.
/// </summary>
public Task<ScriptMissionDto[]> GetScriptMissions()
{
return Task.FromResult(_scriptEngine.MissionManager.GetScriptMissions());
}
/// <summary>
/// Finds specific script missions by names.
/// </summary>
public Task<ScriptMissionDto[]> FindScriptMissions(string[] names)
{
return Task.FromResult(_scriptEngine.MissionManager.FindScriptMissions(names));
}
/// <summary>
/// Creates a mission instance with parameters provided as a dictionary.
/// </summary>
/// <param name="name">The name of the mission model.</param>
/// <param name="args">Dictionary of parameter values keyed by parameter name (as strings).</param>
/// <returns>MessageResult containing the mission ID if successful.</returns>
public async Task<MessageResult<Guid>> CreateMission(string name, IDictionary<string, string> args)
{
if (string.IsNullOrWhiteSpace(name))
return new MessageResult<Guid>(false, default, "Mission name cannot be null or empty");
if (args == null)
return new MessageResult<Guid>(false, default, "Parameters cannot be null");
// Convert string dictionary to object dictionary
var missionModel = _scriptEngine.MissionManager.GetMissionModel(name);
if (missionModel == null)
return new MessageResult<Guid>(false, default, $"Mission model '{name}' not found");
var parameters = new Dictionary<string, object?>();
foreach (var paramModel in missionModel.Parameters)
{
// Skip CancellationToken parameters
if (paramModel.Type == typeof(CancellationToken))
continue;
if (args.TryGetValue(paramModel.Name, out var stringValue))
{
try
{
object? convertedValue = null;
if (!string.IsNullOrEmpty(stringValue) && stringValue != "null")
{
if (paramModel.Type == typeof(string))
{
convertedValue = stringValue;
}
else if (paramModel.Type.IsEnum)
{
convertedValue = Enum.Parse(paramModel.Type, stringValue, true);
}
else
{
convertedValue = Convert.ChangeType(stringValue, paramModel.Type);
}
}
parameters[paramModel.Name] = convertedValue;
}
catch (Exception ex)
{
var errorMessage = $"Failed to convert parameter '{paramModel.Name}' value '{stringValue}' to type '{paramModel.Type.Name}': {ex.Message}";
// Log error via ConsoleHubContext if available
// Note: We don't have direct access to ConsoleHubContext here, but error is returned to client
return new MessageResult<Guid>(false, default, errorMessage);
}
}
else
{
// Use default value if not provided
parameters[paramModel.Name] = paramModel.DefaultValue;
}
}
return await Task.FromResult(_scriptEngine.MissionManager.CreateMission(name, parameters));
}
/// <summary>
/// Cancels a running or paused mission.
/// </summary>
/// <param name="id">The ID of the mission instance to cancel.</param>
/// <param name="reason">The reason for cancellation.</param>
/// <returns>True if the mission was canceled successfully, false otherwise.</returns>
public Task<bool> CancelMission(Guid id, string reason)
{
var mission = _scriptEngine.MissionManager.GetMission(id);
if (mission == null)
return Task.FromResult(false);
try
{
mission.Cancel(reason);
return Task.FromResult(true);
}
catch
{
return Task.FromResult(false);
}
}
#endregion
}