static public int RunBotSession( byte[] kalmitElmApp, Func <byte[], byte[]> getFileFromHashSHA256, string processStoreDirectory, Action <string> logEntry, Action <LogEntry.ProcessBotEventReport> logProcessBotEventReport, string botConfiguration) { var botId = Kalmit.CommonConversion.StringBase16FromByteArray(Kalmit.CommonConversion.HashSHA256(kalmitElmApp)); var botSessionClock = System.Diagnostics.Stopwatch.StartNew(); /* * Implementat store and process based on Kalmit Web Host * from https://github.com/Viir/Kalmit/blob/640078f59bea3fa2ba1af43372933cff304b8c94/implement/PersistentProcess/PersistentProcess.WebHost/Startup.cs * */ var process = new Kalmit.PersistentProcess.PersistentProcessWithHistoryOnFileFromElm019Code( new EmptyProcessStore(), kalmitElmApp); var processStore = new ProcessStoreInFileDirectory( processStoreDirectory, () => { var time = DateTimeOffset.UtcNow; var directoryName = time.ToString("yyyy-MM-dd"); return(System.IO.Path.Combine(directoryName, directoryName + "T" + time.ToString("HH") + ".composition.jsonl")); }); (DateTimeOffset time, string statusMessage, ImmutableList <InterfaceToBot.BotRequest>)? lastBotStep = null; ImmutableList <InterfaceToBot.BotRequest> remainingBotRequests = null; bool pauseBot = false; (string text, DateTimeOffset time)lastConsoleUpdate = (null, DateTimeOffset.MinValue); void updatePauseContinue() { if (DotNetConsole.KeyAvailable) { var inputKey = DotNetConsole.ReadKey(); if (inputKey.Key == ConsoleKey.Enter) { pauseBot = false; displayStatusInConsole(); } } if (Windows.IsKeyDown(Windows.VK_CONTROL) && Windows.IsKeyDown(Windows.VK_MENU)) { pauseBot = true; displayStatusInConsole(); } } void displayStatusInConsole() { var textToDisplay = string.Join("\n", textLinesToDisplayInConsole()); var time = DateTimeOffset.UtcNow; if (lastConsoleUpdate.text == textToDisplay && time < lastConsoleUpdate.time + TimeSpan.FromSeconds(1)) { return; } DotNetConsole.Clear(); DotNetConsole.WriteLine(textToDisplay); lastConsoleUpdate = (textToDisplay, time); } IEnumerable <string> textLinesToDisplayInConsole() { // TODO: Add display bot configuration. yield return ("Bot " + UserInterface.BotIdDisplayText(botId) + (pauseBot ? " is paused. Press the enter key to continue." : " is running. Press CTRL + ALT keys to pause the bot.")); if (!lastBotStep.HasValue) { yield break; } var lastBotStepAgeInSeconds = (int)((DateTimeOffset.UtcNow - lastBotStep.Value.time).TotalSeconds); yield return("Last bot event was " + lastBotStepAgeInSeconds + " seconds ago at " + lastBotStep.Value.time.ToString("HH-mm-ss.fff") + "."); yield return("Status message from bot:\n"); yield return(lastBotStep.Value.statusMessage); yield return(""); } var createVolatileHostAttempts = 0; var volatileHosts = new ConcurrentDictionary <string, Kalmit.CSharpScriptContext>(); InterfaceToBot.Result <InterfaceToBot.TaskResult.RunInVolatileHostError, InterfaceToBot.TaskResult.RunInVolatileHostComplete> ExecuteRequestToRunInVolatileHost( InterfaceToBot.Task.RunInVolatileHost runInVolatileHost) { if (!volatileHosts.TryGetValue(runInVolatileHost.hostId, out var volatileHost)) { return(new InterfaceToBot.Result <InterfaceToBot.TaskResult.RunInVolatileHostError, InterfaceToBot.TaskResult.RunInVolatileHostComplete> { err = new InterfaceToBot.TaskResult.RunInVolatileHostError { hostNotFound = new object(), } }); } var stopwatch = System.Diagnostics.Stopwatch.StartNew(); var fromHostResult = volatileHost.RunScript(runInVolatileHost.script); stopwatch.Stop(); return(new InterfaceToBot.Result <InterfaceToBot.TaskResult.RunInVolatileHostError, InterfaceToBot.TaskResult.RunInVolatileHostComplete> { ok = new InterfaceToBot.TaskResult.RunInVolatileHostComplete { exceptionToString = fromHostResult.Exception?.ToString(), returnValueToString = fromHostResult.ReturnValue?.ToString(), durationInMilliseconds = stopwatch.ElapsedMilliseconds, } }); } void processBotEvent(InterfaceToBot.BotEvent botEvent) { var eventTime = DateTimeOffset.UtcNow; Exception processEventException = null; string serializedEvent = null; string serializedResponse = null; string compositionRecordHash = null; try { var botEventAtTime = new InterfaceToBot.BotEventAtTime { timeInMilliseconds = botSessionClock.ElapsedMilliseconds, @event = botEvent, }; serializedEvent = SerializeToJsonForBot(botEventAtTime); var processEventResult = process.ProcessEvents( new[] { serializedEvent }); compositionRecordHash = Kalmit.CommonConversion.StringBase16FromByteArray(processEventResult.Item2.serializedCompositionRecordHash); processStore.AppendSerializedCompositionRecord(processEventResult.Item2.serializedCompositionRecord); serializedResponse = processEventResult.responses.Single(); var botResponse = Newtonsoft.Json.JsonConvert.DeserializeObject <InterfaceToBot.BotResponse>(serializedResponse); if (botResponse.decodeEventSuccess == null) { throw new Exception("Bot reported decode error: " + botResponse.decodeEventError); } var botRequests = botResponse.decodeEventSuccess.botRequests.ToImmutableList(); var setStatusMessageRequests = botRequests .Where(request => request.setStatusMessage != null) .ToImmutableList(); var statusMessage = setStatusMessageRequests?.Select(request => request.setStatusMessage)?.LastOrDefault() ?? lastBotStep?.statusMessage; lastBotStep = (eventTime, statusMessage, botRequests); var stepRemainingRequests = botRequests .Except(setStatusMessageRequests); remainingBotRequests = (remainingBotRequests ?? ImmutableList <InterfaceToBot.BotRequest> .Empty) .AddRange(stepRemainingRequests); } catch (Exception exception) { processEventException = exception; } logProcessBotEventReport(new LogEntry.ProcessBotEventReport { time = eventTime, exception = processEventException, serializedResponse = serializedResponse, compositionRecordHash = compositionRecordHash, }); if (processEventException != null) { throw new Exception("Failed to process bot event.", processEventException); } displayStatusInConsole(); } // TODO: Get the bot requests from the `init` function. processBotEvent(new InterfaceToBot.BotEvent { setBotConfiguration = botConfiguration ?? "" }); while (true) { displayStatusInConsole(); updatePauseContinue(); System.Threading.Thread.Sleep(111); if (pauseBot) { continue; } var botStepTime = DateTimeOffset.UtcNow; var lastBotStepAgeMilli = botStepTime.ToUnixTimeMilliseconds() - lastBotStep?.time.ToUnixTimeMilliseconds(); var finishSessionRequest = remainingBotRequests ?.FirstOrDefault(request => request.finishSession != null); if (finishSessionRequest != null) { logEntry("Bot has finished."); return(0); } var botRequestToExecute = remainingBotRequests ?.FirstOrDefault(); if (botRequestToExecute == null) { if (!(lastBotStepAgeMilli < 10_000)) { processBotEvent(new InterfaceToBot.BotEvent { setSessionTimeLimitInMilliseconds = 0, }); } continue; } var requestTask = botRequestToExecute.startTask; if (requestTask?.task?.createVolatileHost != null) { var volatileHostId = System.Threading.Interlocked.Increment(ref createVolatileHostAttempts).ToString(); volatileHosts[volatileHostId] = new Kalmit.CSharpScriptContext(getFileFromHashSHA256); processBotEvent(new InterfaceToBot.BotEvent { taskComplete = new InterfaceToBot.ResultFromTaskWithId { taskId = requestTask?.taskId, taskResult = new InterfaceToBot.TaskResult { createVolatileHostResponse = new InterfaceToBot.Result <object, InterfaceToBot.TaskResult.CreateVolatileHostComplete> { ok = new InterfaceToBot.TaskResult.CreateVolatileHostComplete { hostId = volatileHostId, }, }, }, }, }); } if (requestTask?.task?.releaseVolatileHost != null) { volatileHosts.TryRemove(requestTask?.task?.releaseVolatileHost.hostId, out var volatileHost); } if (requestTask?.task?.runInVolatileHost != null) { var result = ExecuteRequestToRunInVolatileHost(requestTask?.task?.runInVolatileHost); processBotEvent(new InterfaceToBot.BotEvent { taskComplete = new InterfaceToBot.ResultFromTaskWithId { taskId = requestTask?.taskId, taskResult = new InterfaceToBot.TaskResult { runInVolatileHostResponse = result, }, } }); } if (requestTask?.task?.delay != null) { var delayStopwatch = System.Diagnostics.Stopwatch.StartNew(); while (true) { var remainingWaitTime = requestTask.task.delay.milliseconds - delayStopwatch.ElapsedMilliseconds; if (remainingWaitTime <= 0) { break; } System.Threading.Thread.Sleep((int)Math.Min(100, remainingWaitTime)); updatePauseContinue(); displayStatusInConsole(); } processBotEvent(new InterfaceToBot.BotEvent { taskComplete = new InterfaceToBot.ResultFromTaskWithId { taskId = requestTask?.taskId, taskResult = new InterfaceToBot.TaskResult { completeWithoutResult = new object(), }, } }); } remainingBotRequests = remainingBotRequests.Remove(botRequestToExecute); } }
static public int RunBotSession( byte[] kalmitElmApp, Func <byte[], byte[]> getFileFromHashSHA256, string processStoreDirectory, Action <string> logEntry, Action <LogEntry.ProcessBotEventReport> logProcessBotEventReport, string botConfiguration, string sessionId, string botSource) { var botId = Kalmit.CommonConversion.StringBase16FromByteArray(Kalmit.CommonConversion.HashSHA256(kalmitElmApp)); var botSessionClock = System.Diagnostics.Stopwatch.StartNew(); /* * Implementat store and process based on Kalmit Web Host * from https://github.com/Viir/Kalmit/blob/640078f59bea3fa2ba1af43372933cff304b8c94/implement/PersistentProcess/PersistentProcess.WebHost/Startup.cs * */ var process = new Kalmit.PersistentProcess.PersistentProcessWithHistoryOnFileFromElm019Code( new EmptyProcessStore(), kalmitElmApp); var processStore = new ProcessStoreInFileDirectory( processStoreDirectory, () => { var time = DateTimeOffset.UtcNow; var directoryName = time.ToString("yyyy-MM-dd"); return(System.IO.Path.Combine(directoryName, directoryName + "T" + time.ToString("HH") + ".composition.jsonl")); }); (DateTimeOffset time, string statusDescriptionForOperator, InterfaceToBot.BotResponse.DecodeEventSuccessStructure response)? lastBotStep = null; var botSessionTaskCancellationToken = new System.Threading.CancellationTokenSource(); var activeBotTasks = new ConcurrentDictionary <InterfaceToBot.StartTask, System.Threading.Tasks.Task>(); bool pauseBot = false; (string text, DateTimeOffset time)lastConsoleUpdate = (null, DateTimeOffset.MinValue); void updatePauseContinue() { if (DotNetConsole.KeyAvailable) { var inputKey = DotNetConsole.ReadKey(); if (inputKey.Key == ConsoleKey.Enter) { pauseBot = false; displayStatusInConsole(); } } if (Windows.IsKeyDown(Windows.VK_CONTROL) && Windows.IsKeyDown(Windows.VK_MENU)) { pauseBot = true; displayStatusInConsole(); } } void cleanUpBotTasksAndPropagateExceptions() { foreach (var(request, engineTask) in activeBotTasks.ToList()) { if (engineTask.Exception != null) { throw new Exception("Bot task '" + request.taskId + "' has failed with exception", engineTask.Exception); } if (engineTask.IsCompleted) { activeBotTasks.TryRemove(request, out var _); } } } var displayLock = new object(); void displayStatusInConsole() { lock (displayLock) { cleanUpBotTasksAndPropagateExceptions(); var textToDisplay = string.Join("\n", textLinesToDisplayInConsole()); var time = DateTimeOffset.UtcNow; if (lastConsoleUpdate.text == textToDisplay && time < lastConsoleUpdate.time + TimeSpan.FromSeconds(1)) { return; } DotNetConsole.Clear(); DotNetConsole.WriteLine(textToDisplay); lastConsoleUpdate = (textToDisplay, time); } } IEnumerable <string> textLinesToDisplayInConsole() { // TODO: Add display bot configuration. yield return ("Bot " + UserInterface.BotIdDisplayText(botId) + " in session '" + sessionId + "'" + (pauseBot ? " is paused. Press the enter key to continue." : " is running. Press CTRL + ALT keys to pause the bot.")); if (!lastBotStep.HasValue) { yield break; } var lastBotStepAgeInSeconds = (int)((DateTimeOffset.UtcNow - lastBotStep.Value.time).TotalSeconds); yield return ("Last bot event was " + lastBotStepAgeInSeconds + " seconds ago at " + lastBotStep.Value.time.ToString("HH-mm-ss.fff") + ". " + "There are " + activeBotTasks.Count + " tasks in progress."); yield return("Status message from bot:\n"); yield return(lastBotStep.Value.statusDescriptionForOperator); yield return(""); } long lastRequestToReactorTimeInSeconds = 0; async System.Threading.Tasks.Task requestToReactor(RequestToReactorUseBotStruct useBot) { lastRequestToReactorTimeInSeconds = (long)botSessionClock.Elapsed.TotalSeconds; var toReactorStruct = new RequestToReactorStruct { UseBot = useBot }; var serializedToReactorStruct = Newtonsoft.Json.JsonConvert.SerializeObject(toReactorStruct); var reactorClient = new System.Net.Http.HttpClient(); reactorClient.DefaultRequestHeaders.UserAgent.Add( new System.Net.Http.Headers.ProductInfoHeaderValue(new System.Net.Http.Headers.ProductHeaderValue("windows-console", BotEngine.AppVersionId))); var content = new System.Net.Http.ByteArrayContent(System.Text.Encoding.UTF8.GetBytes(serializedToReactorStruct)); var response = await reactorClient.PostAsync("https://reactor.botengine.org/api/", content); var responseString = await response.Content.ReadAsStringAsync(); } void fireAndForgetReportToReactor(RequestToReactorUseBotStruct report) { lastRequestToReactorTimeInSeconds = (long)botSessionClock.Elapsed.TotalSeconds; System.Threading.Tasks.Task.Run(() => { try { requestToReactor(report).Wait(); } catch { } }); } fireAndForgetReportToReactor(new RequestToReactorUseBotStruct { StartSession = new RequestToReactorUseBotStruct.StartSessionStruct { botId = botId, sessionId = sessionId, botSource = BotSourceIsPublic(botSource) ? botSource : null } }); var createVolatileHostAttempts = 0; var volatileHosts = new ConcurrentDictionary <string, Kalmit.CSharpScriptContext>(); InterfaceToBot.Result <InterfaceToBot.TaskResult.RunInVolatileHostError, InterfaceToBot.TaskResult.RunInVolatileHostComplete> ExecuteRequestToRunInVolatileHost( InterfaceToBot.Task.RunInVolatileHostStructure runInVolatileHost) { if (!volatileHosts.TryGetValue(runInVolatileHost.hostId, out var volatileHost)) { return(new InterfaceToBot.Result <InterfaceToBot.TaskResult.RunInVolatileHostError, InterfaceToBot.TaskResult.RunInVolatileHostComplete> { Err = new InterfaceToBot.TaskResult.RunInVolatileHostError { hostNotFound = new object(), } }); } var stopwatch = System.Diagnostics.Stopwatch.StartNew(); var fromHostResult = volatileHost.RunScript(runInVolatileHost.script); stopwatch.Stop(); return(new InterfaceToBot.Result <InterfaceToBot.TaskResult.RunInVolatileHostError, InterfaceToBot.TaskResult.RunInVolatileHostComplete> { Ok = new InterfaceToBot.TaskResult.RunInVolatileHostComplete { exceptionToString = fromHostResult.Exception?.ToString(), returnValueToString = fromHostResult.ReturnValue?.ToString(), durationInMilliseconds = stopwatch.ElapsedMilliseconds, } }); } void processBotEvent(InterfaceToBot.BotEvent botEvent) { var eventTime = DateTimeOffset.UtcNow; Exception processEventException = null; string serializedEvent = null; string serializedResponse = null; string compositionRecordHash = null; try { serializedEvent = SerializeToJsonForBot(botEvent); var processEventResult = process.ProcessEvents( new[] { serializedEvent }); compositionRecordHash = Kalmit.CommonConversion.StringBase16FromByteArray(processEventResult.Item2.serializedCompositionRecordHash); processStore.AppendSerializedCompositionRecord(processEventResult.Item2.serializedCompositionRecord); serializedResponse = processEventResult.responses.Single(); var botResponse = Newtonsoft.Json.JsonConvert.DeserializeObject <InterfaceToBot.BotResponse>(serializedResponse); if (botResponse.DecodeEventSuccess == null) { throw new Exception("Bot reported decode error: " + botResponse.DecodeEventError); } var statusDescriptionForOperator = botResponse.DecodeEventSuccess?.ContinueSession?.statusDescriptionForOperator ?? botResponse.DecodeEventSuccess?.FinishSession?.statusDescriptionForOperator; lastBotStep = (eventTime, statusDescriptionForOperator, botResponse.DecodeEventSuccess); foreach (var startTask in botResponse.DecodeEventSuccess?.ContinueSession?.startTasks ?? Array.Empty <InterfaceToBot.StartTask>()) { var engineTask = System.Threading.Tasks.Task.Run(() => startTaskAndProcessEvent(startTask), botSessionTaskCancellationToken.Token); activeBotTasks[startTask] = engineTask; } } catch (Exception exception) { processEventException = exception; } logProcessBotEventReport(new LogEntry.ProcessBotEventReport { time = eventTime, exception = processEventException, serializedResponse = serializedResponse, compositionRecordHash = compositionRecordHash, }); if (processEventException != null) { throw new Exception("Failed to process bot event.", processEventException); } displayStatusInConsole(); } // TODO: Get the bot requests from the `init` function. processBotEvent(new InterfaceToBot.BotEvent { SetBotConfiguration = botConfiguration ?? "" }); while (true) { displayStatusInConsole(); updatePauseContinue(); var millisecondsToNextNotification = (lastBotStep?.response?.ContinueSession?.notifyWhenArrivedAtTime?.timeInMilliseconds - botSessionClock.ElapsedMilliseconds) ?? 1000; System.Threading.Thread.Sleep((int)Math.Min(1000, Math.Max(10, millisecondsToNextNotification))); var lastRequestToReactorAgeInSeconds = (long)botSessionClock.Elapsed.TotalSeconds - lastRequestToReactorTimeInSeconds; if (30 <= lastRequestToReactorAgeInSeconds) { fireAndForgetReportToReactor(new RequestToReactorUseBotStruct { ContinueSession = new RequestToReactorUseBotStruct.ContinueSessionStruct { sessionId = sessionId, statusDescriptionForOperator = lastBotStep?.statusDescriptionForOperator } }); } if (pauseBot) { continue; } var botStepTime = DateTimeOffset.UtcNow; var lastBotStepAgeMilli = botStepTime.ToUnixTimeMilliseconds() - lastBotStep?.time.ToUnixTimeMilliseconds(); if (lastBotStep?.response?.FinishSession != null) { logEntry("Bot has finished."); botSessionTaskCancellationToken.Cancel(); return(0); } if (lastBotStep?.response?.ContinueSession?.notifyWhenArrivedAtTime?.timeInMilliseconds <= botSessionClock.ElapsedMilliseconds || !(lastBotStepAgeMilli < 10_000)) { processBotEvent(new InterfaceToBot.BotEvent { ArrivedAtTime = new InterfaceToBot.TimeStructure { timeInMilliseconds = botSessionClock.ElapsedMilliseconds }, }); } } void startTaskAndProcessEvent(InterfaceToBot.StartTask startTask) { var taskResult = performTask(startTask.task); processBotEvent(new InterfaceToBot.BotEvent { TaskComplete = new InterfaceToBot.ResultFromTaskWithId { taskId = startTask.taskId, taskResult = taskResult, }, }); } InterfaceToBot.TaskResult performTask(InterfaceToBot.Task task) { if (task?.CreateVolatileHost != null) { var volatileHostId = System.Threading.Interlocked.Increment(ref createVolatileHostAttempts).ToString(); volatileHosts[volatileHostId] = new Kalmit.CSharpScriptContext(getFileFromHashSHA256); return(new InterfaceToBot.TaskResult { CreateVolatileHostResponse = new InterfaceToBot.Result <object, InterfaceToBot.TaskResult.CreateVolatileHostComplete> { Ok = new InterfaceToBot.TaskResult.CreateVolatileHostComplete { hostId = volatileHostId, }, }, }); } if (task?.ReleaseVolatileHost != null) { volatileHosts.TryRemove(task?.ReleaseVolatileHost.hostId, out var volatileHost); return(new InterfaceToBot.TaskResult { CompleteWithoutResult = new object() }); } if (task?.RunInVolatileHost != null) { var result = ExecuteRequestToRunInVolatileHost(task?.RunInVolatileHost); return(new InterfaceToBot.TaskResult { RunInVolatileHostResponse = result, }); } return(null); } }