コード例 #1
0
        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,
            BotSourceModel.LiteralNodeObject botCode)
        {
            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,
                kalmitProcessLogEntry => logEntry("kalmitProcessLogEntry: " + kalmitProcessLogEntry),
                new Kalmit.ElmAppInterfaceConfig {
                RootModuleFilePath = "src/Main.elm", RootModuleName = "Main"
            });

            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 statusDescriptionText, 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?.TaskIdFromString + "' 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;
                    }

                    if (botSessionTaskCancellationToken.IsCancellationRequested)
                    {
                        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);

                var activeBotTasksSnapshot = activeBotTasks.ToList();

                var activeBotTasksDescription =
                    TruncateWithEllipsis(string.Join(", ", activeBotTasksSnapshot.Select(task => task.Key.taskId.TaskIdFromString)), 60);

                yield return
                    ("Last bot event was " + lastBotStepAgeInSeconds + " seconds ago at " + lastBotStep.Value.time.ToString("HH-mm-ss.fff") + ". " +
                     "There are " + activeBotTasksSnapshot.Count + " tasks in progress (" + activeBotTasksDescription + ").");

                yield return("Status message from bot:\n");

                yield return(lastBotStep.Value.statusDescriptionText);

                yield return("");
            }

            string TruncateWithEllipsis(string originalString, int lengthLimit)
            {
                if (lengthLimit < originalString?.Length)
                {
                    return(originalString.Substring(0, lengthLimit) + "...");
                }

                return(originalString);
            }

            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();
            }

            System.Threading.Tasks.Task fireAndForgetReportToReactor(RequestToReactorUseBotStruct report)
            {
                lastRequestToReactorTimeInSeconds = (long)botSessionClock.Elapsed.TotalSeconds;

                return(System.Threading.Tasks.Task.Run(() =>
                {
                    try
                    {
                        requestToReactor(report).Wait();
                    }
                    catch { }
                }));
            }

            var botSourceIsPublic     = BotSourceIsPublic(botSource);
            var botPropertiesFromCode = BotSourceModel.BotCode.ReadPropertiesFromBotCode(botCode);

            fireAndForgetReportToReactor(new RequestToReactorUseBotStruct
            {
                StartSession = new RequestToReactorUseBotStruct.StartSessionStruct
                {
                    botId                 = botId,
                    sessionId             = sessionId,
                    botSource             = botSourceIsPublic ? botSource : null,
                    botPropertiesFromCode = botSourceIsPublic ? botPropertiesFromCode : null,
                }
            });

            var queuedBotEvents = new ConcurrentQueue <InterfaceToBot.BotEvent>();

            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.VolatileHostIdFromString, 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;
                long?     processingTimeInMilliseconds = null;

                try
                {
                    serializedEvent = SerializeToJsonForBot(botEvent);

                    var processingTimeStopwatch = System.Diagnostics.Stopwatch.StartNew();

                    var processEventResult = process.ProcessEvents(
                        new[]
                    {
                        serializedEvent
                    });

                    processingTimeStopwatch.Stop();

                    processingTimeInMilliseconds = processingTimeStopwatch.ElapsedMilliseconds;

                    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 statusDescriptionText =
                        botResponse.DecodeEventSuccess?.ContinueSession?.statusDescriptionText ??
                        botResponse.DecodeEventSuccess?.FinishSession?.statusDescriptionText;

                    lastBotStep = (eventTime, statusDescriptionText, 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,
                    processingTimeInMilliseconds = processingTimeInMilliseconds,
                    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 (10 <= lastRequestToReactorAgeInSeconds)
                {
                    fireAndForgetReportToReactor(new RequestToReactorUseBotStruct
                    {
                        ContinueSession = new RequestToReactorUseBotStruct.ContinueSessionStruct
                        {
                            sessionId = sessionId, statusDescriptionText = lastBotStep?.statusDescriptionText
                        }
                    });
                }

                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();

                    fireAndForgetReportToReactor(new RequestToReactorUseBotStruct
                    {
                        FinishSession = new RequestToReactorUseBotStruct.ContinueSessionStruct
                        {
                            sessionId = sessionId, statusDescriptionText = lastBotStep?.statusDescriptionText
                        }
                    }).Wait(TimeSpan.FromSeconds(3));

                    return(0);
                }

                if (lastBotStep?.response?.ContinueSession?.notifyWhenArrivedAtTime?.timeInMilliseconds <= botSessionClock.ElapsedMilliseconds ||
                    !(lastBotStepAgeMilli < 10_000))
                {
                    processBotEvent(new InterfaceToBot.BotEvent
                    {
                        ArrivedAtTime = new InterfaceToBot.TimeStructure {
                            timeInMilliseconds = botSessionClock.ElapsedMilliseconds
                        },
                    });
                }

                if (queuedBotEvents.TryDequeue(out var botEvent))
                {
                    processBotEvent(botEvent);
                }
            }

            void startTaskAndProcessEvent(InterfaceToBot.StartTask startTask)
            {
                var taskResult = performTask(startTask.task);

                queuedBotEvents.Enqueue(
                    new InterfaceToBot.BotEvent
                {
                    CompletedTask = new InterfaceToBot.CompletedTaskStructure
                    {
                        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 = new InterfaceToBot.VolatileHostId {
                                    VolatileHostIdFromString = volatileHostId
                                },
                            },
                        },
                    });
                }

                if (task?.ReleaseVolatileHost != null)
                {
                    volatileHosts.TryRemove(task?.ReleaseVolatileHost.hostId.VolatileHostIdFromString, 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);
            }
        }