Beispiel #1
0
        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);
            }
        }
Beispiel #2
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)
        {
            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);
            }
        }