public void Configure(
            IApplicationBuilder app,
            IWebHostEnvironment env,
            WebAppAndElmAppConfig webAppAndElmAppConfig,
            Func <DateTimeOffset> getDateTimeOffset)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            if (webAppAndElmAppConfig == null)
            {
                throw new Exception("Missing reference to the web app config.");
            }

            var nextHttpRequestIndex = 0;

            if (webAppAndElmAppConfig.WebAppConfiguration.JsonStructure?.letsEncryptOptions != null)
            {
                app.UseFluffySpoonLetsEncryptChallengeApprovalMiddleware();
            }

            var createVolatileHostAttempts = 0;

            var volatileHosts = new ConcurrentDictionary <string, VolatileHost>();

            InterfaceToHost.Result <InterfaceToHost.TaskResult.RequestToVolatileHostError, InterfaceToHost.TaskResult.RequestToVolatileHostComplete>
            performProcessTaskRequestToVolatileHost(
                InterfaceToHost.Task.RequestToVolatileHostStructure requestToVolatileHost)
            {
                if (!volatileHosts.TryGetValue(requestToVolatileHost.hostId, out var volatileHost))
                {
                    return(new InterfaceToHost.Result <InterfaceToHost.TaskResult.RequestToVolatileHostError, InterfaceToHost.TaskResult.RequestToVolatileHostComplete>
                    {
                        Err = new InterfaceToHost.TaskResult.RequestToVolatileHostError
                        {
                            HostNotFound = new object(),
                        }
                    });
                }

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

                var fromVolatileHostResult = volatileHost.ProcessRequest(requestToVolatileHost.request);

                stopwatch.Stop();

                return(new InterfaceToHost.Result <InterfaceToHost.TaskResult.RequestToVolatileHostError, InterfaceToHost.TaskResult.RequestToVolatileHostComplete>
                {
                    Ok = new InterfaceToHost.TaskResult.RequestToVolatileHostComplete
                    {
                        exceptionToString = fromVolatileHostResult.Exception?.ToString(),
                        returnValueToString = fromVolatileHostResult.ReturnValue?.ToString(),
                        durationInMilliseconds = stopwatch.ElapsedMilliseconds,
                    }
                });
            }

            InterfaceToHost.TaskResult performProcessTask(InterfaceToHost.Task task)
            {
                if (task?.CreateVolatileHost != null)
                {
                    try
                    {
                        var volatileHost = new VolatileHost(BlobLibrary.GetBlobWithSHA256, task?.CreateVolatileHost.script);

                        var volatileHostId = System.Threading.Interlocked.Increment(ref createVolatileHostAttempts).ToString();

                        volatileHosts[volatileHostId] = volatileHost;

                        return(new InterfaceToHost.TaskResult
                        {
                            CreateVolatileHostResponse = new InterfaceToHost.Result <InterfaceToHost.TaskResult.CreateVolatileHostErrorStructure, InterfaceToHost.TaskResult.CreateVolatileHostComplete>
                            {
                                Ok = new InterfaceToHost.TaskResult.CreateVolatileHostComplete
                                {
                                    hostId = volatileHostId,
                                },
                            },
                        });
                    }
                    catch (Exception createVolatileHostException)
                    {
                        return(new InterfaceToHost.TaskResult
                        {
                            CreateVolatileHostResponse = new InterfaceToHost.Result <InterfaceToHost.TaskResult.CreateVolatileHostErrorStructure, InterfaceToHost.TaskResult.CreateVolatileHostComplete>
                            {
                                Err = new InterfaceToHost.TaskResult.CreateVolatileHostErrorStructure
                                {
                                    exceptionToString = createVolatileHostException.ToString(),
                                },
                            },
                        });
                    }
                }

                if (task?.ReleaseVolatileHost != null)
                {
                    volatileHosts.TryRemove(task?.ReleaseVolatileHost.hostId, out var volatileHost);

                    return(new InterfaceToHost.TaskResult
                    {
                        CompleteWithoutResult = new object(),
                    });
                }

                if (task?.RequestToVolatileHost != null)
                {
                    return(new InterfaceToHost.TaskResult
                    {
                        RequestToVolatileHostResponse = performProcessTaskRequestToVolatileHost(task?.RequestToVolatileHost),
                    });
                }

                throw new NotImplementedException("Unexpected task structure.");
            }

            void performProcessTaskAndFeedbackEvent(InterfaceToHost.StartTask taskWithId)
            {
                var taskResult = performProcessTask(taskWithId.task);

                var interfaceEvent = new InterfaceToHost.Event
                {
                    taskComplete = new InterfaceToHost.ResultFromTaskWithId
                    {
                        taskId     = taskWithId.taskId,
                        taskResult = taskResult,
                    }
                };

                processEventAndResultingRequests(interfaceEvent);
            }

            var processRequestCompleteHttpResponse = new ConcurrentDictionary <string, InterfaceToHost.HttpResponse>();

            void processEventAndResultingRequests(InterfaceToHost.Event interfaceEvent)
            {
                var serializedInterfaceEvent = Newtonsoft.Json.JsonConvert.SerializeObject(interfaceEvent, jsonSerializerSettings);

                var serializedResponse = webAppAndElmAppConfig.ProcessEventInElmApp(serializedInterfaceEvent);

                InterfaceToHost.ResponseOverSerialInterface structuredResponse = null;

                try
                {
                    structuredResponse =
                        Newtonsoft.Json.JsonConvert.DeserializeObject <InterfaceToHost.ResponseOverSerialInterface>(
                            serializedResponse);
                }
                catch (Exception parseException)
                {
                    throw new Exception(
                              "Failed to parse event response from app. Looks like the loaded elm app is not compatible with the interface.\nResponse from app follows:\n" + serializedResponse,
                              parseException);
                }

                if (structuredResponse?.decodeEventSuccess == null)
                {
                    throw new Exception("Hosted app failed to decode the event: " + structuredResponse.decodeEventError);
                }

                foreach (var requestFromProcess in structuredResponse.decodeEventSuccess)
                {
                    if (requestFromProcess.completeHttpResponse != null)
                    {
                        processRequestCompleteHttpResponse[requestFromProcess.completeHttpResponse.httpRequestId] =
                            requestFromProcess.completeHttpResponse.response;
                    }

                    if (requestFromProcess.startTask != null)
                    {
                        System.Threading.Tasks.Task.Run(() => performProcessTaskAndFeedbackEvent(requestFromProcess.startTask));
                    }
                }
            }

            app
            .Use(async(context, next) => await Asp.MiddlewareFromWebAppConfig(webAppAndElmAppConfig.WebAppConfiguration, context, next))
            .Run(async(context) =>
            {
                var currentDateTime  = getDateTimeOffset();
                var timeMilli        = currentDateTime.ToUnixTimeMilliseconds();
                var httpRequestIndex = System.Threading.Interlocked.Increment(ref nextHttpRequestIndex);

                var httpRequestId = timeMilli.ToString() + "-" + httpRequestIndex.ToString();

                {
                    var httpEvent = AsPersistentProcessInterfaceHttpRequestEvent(context, httpRequestId, currentDateTime);

                    var httpRequestInterfaceEvent = new InterfaceToHost.Event
                    {
                        httpRequest = httpEvent,
                    };

                    processEventAndResultingRequests(httpRequestInterfaceEvent);
                }

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

                while (true)
                {
                    if (processRequestCompleteHttpResponse.TryRemove(httpRequestId, out var httpResponse))
                    {
                        var headerContentType =
                            httpResponse.headersToAdd
                            ?.FirstOrDefault(header => header.name?.ToLowerInvariant() == "content-type")
                            ?.values?.FirstOrDefault();

                        context.Response.StatusCode = httpResponse.statusCode;

                        foreach (var headerToAdd in (httpResponse.headersToAdd).EmptyIfNull())
                        {
                            context.Response.Headers[headerToAdd.name] =
                                new Microsoft.Extensions.Primitives.StringValues(headerToAdd.values);
                        }

                        if (headerContentType != null)
                        {
                            context.Response.ContentType = headerContentType;
                        }

                        await context.Response.WriteAsync(httpResponse?.bodyAsString ?? "");
                        break;
                    }

                    if (60 <= waitForHttpResponseClock.Elapsed.TotalSeconds)
                    {
                        throw new TimeoutException(
                            "Persistent process did not return a HTTP response within " +
                            (int)waitForHttpResponseClock.Elapsed.TotalSeconds +
                            " seconds.");
                    }

                    System.Threading.Thread.Sleep(100);
                }
            });
        }
Example #2
0
        public void Configure(
            IApplicationBuilder app,
            IWebHostEnvironment env,
            WebAppAndElmAppConfig webAppAndElmAppConfig,
            Func <DateTimeOffset> getDateTimeOffset,
            IHostApplicationLifetime appLifetime)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            if (webAppAndElmAppConfig == null)
            {
                throw new Exception("Missing reference to the web app config.");
            }

            var applicationStoppingCancellationTokenSource = new System.Threading.CancellationTokenSource();

            appLifetime.ApplicationStopping.Register(() =>
            {
                applicationStoppingCancellationTokenSource.Cancel();
                _logger?.LogInformation("Public app noticed ApplicationStopping.");
            });

            var nextHttpRequestIndex = 0;

            if (webAppAndElmAppConfig.WebAppConfiguration?.letsEncryptOptions != null)
            {
                app.UseFluffySpoonLetsEncryptChallengeApprovalMiddleware();
            }

            var createVolatileHostAttempts = 0;

            var volatileHosts = new ConcurrentDictionary <string, VolatileHost>();

            var appTaskCompleteHttpResponse = new ConcurrentDictionary <string, InterfaceToHost.HttpResponse>();

            System.Threading.Timer notifyTimeHasArrivedTimer  = null;
            DateTimeOffset?        lastAppEventTimeHasArrived = null;

            InterfaceToHost.NotifyWhenArrivedAtTimeRequestStructure nextTimeToNotify = null;
            var nextTimeToNotifyLock = new object();

            InterfaceToHost.Result <InterfaceToHost.TaskResult.RequestToVolatileHostError, InterfaceToHost.TaskResult.RequestToVolatileHostComplete>
            performProcessTaskRequestToVolatileHost(
                InterfaceToHost.Task.RequestToVolatileHostStructure requestToVolatileHost)
            {
                if (!volatileHosts.TryGetValue(requestToVolatileHost.hostId, out var volatileHost))
                {
                    return(new InterfaceToHost.Result <InterfaceToHost.TaskResult.RequestToVolatileHostError, InterfaceToHost.TaskResult.RequestToVolatileHostComplete>
                    {
                        Err = new InterfaceToHost.TaskResult.RequestToVolatileHostError
                        {
                            HostNotFound = new object(),
                        }
                    });
                }

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

                var fromVolatileHostResult = volatileHost.ProcessRequest(requestToVolatileHost.request);

                stopwatch.Stop();

                return(new InterfaceToHost.Result <InterfaceToHost.TaskResult.RequestToVolatileHostError, InterfaceToHost.TaskResult.RequestToVolatileHostComplete>
                {
                    Ok = new InterfaceToHost.TaskResult.RequestToVolatileHostComplete
                    {
                        exceptionToString = fromVolatileHostResult.Exception?.ToString(),
                        returnValueToString = fromVolatileHostResult.ReturnValue?.ToString(),
                        durationInMilliseconds = stopwatch.ElapsedMilliseconds,
                    }
                });
            }

            InterfaceToHost.TaskResult performProcessTask(InterfaceToHost.Task task)
            {
                if (task?.CreateVolatileHost != null)
                {
                    try
                    {
                        var volatileHost = new VolatileHost(BlobLibrary.GetBlobWithSHA256, task?.CreateVolatileHost.script);

                        var volatileHostId = System.Threading.Interlocked.Increment(ref createVolatileHostAttempts).ToString();

                        volatileHosts[volatileHostId] = volatileHost;

                        return(new InterfaceToHost.TaskResult
                        {
                            CreateVolatileHostResponse = new InterfaceToHost.Result <InterfaceToHost.TaskResult.CreateVolatileHostErrorStructure, InterfaceToHost.TaskResult.CreateVolatileHostComplete>
                            {
                                Ok = new InterfaceToHost.TaskResult.CreateVolatileHostComplete
                                {
                                    hostId = volatileHostId,
                                },
                            },
                        });
                    }
                    catch (Exception createVolatileHostException)
                    {
                        return(new InterfaceToHost.TaskResult
                        {
                            CreateVolatileHostResponse = new InterfaceToHost.Result <InterfaceToHost.TaskResult.CreateVolatileHostErrorStructure, InterfaceToHost.TaskResult.CreateVolatileHostComplete>
                            {
                                Err = new InterfaceToHost.TaskResult.CreateVolatileHostErrorStructure
                                {
                                    exceptionToString = createVolatileHostException.ToString(),
                                },
                            },
                        });
                    }
                }

                if (task?.ReleaseVolatileHost != null)
                {
                    volatileHosts.TryRemove(task?.ReleaseVolatileHost.hostId, out var volatileHost);

                    return(new InterfaceToHost.TaskResult
                    {
                        CompleteWithoutResult = new object(),
                    });
                }

                if (task?.RequestToVolatileHost != null)
                {
                    return(new InterfaceToHost.TaskResult
                    {
                        RequestToVolatileHostResponse = performProcessTaskRequestToVolatileHost(task?.RequestToVolatileHost),
                    });
                }

                throw new NotImplementedException("Unexpected task structure.");
            }

            void performProcessTaskAndFeedbackEvent(InterfaceToHost.StartTask taskWithId)
            {
                var taskResult = performProcessTask(taskWithId.task);

                var interfaceEvent = new InterfaceToHost.AppEventStructure
                {
                    TaskCompleteEvent = new InterfaceToHost.ResultFromTaskWithId
                    {
                        taskId     = taskWithId.taskId,
                        taskResult = taskResult,
                    }
                };

                processEventAndResultingRequests(interfaceEvent);
            }

            void processEventAndResultingRequests(InterfaceToHost.AppEventStructure interfaceEvent)
            {
                var prepareProcessEvent = prepareProcessEventAndResultingRequests(interfaceEvent);

                prepareProcessEvent.processEventAndResultingRequests();
            }

            (string serializedInterfaceEvent, Action processEventAndResultingRequests) prepareProcessEventAndResultingRequests(
                InterfaceToHost.AppEventStructure interfaceEvent)
            {
                var serializedInterfaceEvent = Newtonsoft.Json.JsonConvert.SerializeObject(interfaceEvent, jsonSerializerSettings);

                var processEvent = new Action(() =>
                {
                    if (applicationStoppingCancellationTokenSource.IsCancellationRequested)
                    {
                        return;
                    }

                    string serializedResponse = null;

                    try
                    {
                        serializedResponse = webAppAndElmAppConfig.ProcessEventInElmApp(serializedInterfaceEvent);
                    }
                    catch (Exception) when(applicationStoppingCancellationTokenSource.IsCancellationRequested)
                    {
                        return;
                    }

                    InterfaceToHost.ResponseOverSerialInterface structuredResponse = null;

                    try
                    {
                        structuredResponse =
                            Newtonsoft.Json.JsonConvert.DeserializeObject <InterfaceToHost.ResponseOverSerialInterface>(
                                serializedResponse);
                    }
                    catch (Exception parseException)
                    {
                        throw new Exception(
                            "Failed to parse event response from app. Looks like the loaded elm app is not compatible with the interface.\nResponse from app follows:\n" + serializedResponse,
                            parseException);
                    }

                    if (structuredResponse?.DecodeEventSuccess == null)
                    {
                        throw new Exception("Hosted app failed to decode the event: " + structuredResponse.DecodeEventError);
                    }

                    if (structuredResponse.DecodeEventSuccess.notifyWhenArrivedAtTime != null)
                    {
                        System.Threading.Tasks.Task.Run(() =>
                        {
                            lock (nextTimeToNotifyLock)
                            {
                                nextTimeToNotify = structuredResponse.DecodeEventSuccess.notifyWhenArrivedAtTime;
                            }
                        });
                    }

                    foreach (var startTask in structuredResponse.DecodeEventSuccess.startTasks)
                    {
                        System.Threading.Tasks.Task.Run(() => performProcessTaskAndFeedbackEvent(startTask));
                    }

                    foreach (var completeHttpResponse in structuredResponse.DecodeEventSuccess.completeHttpResponses)
                    {
                        appTaskCompleteHttpResponse[completeHttpResponse.httpRequestId] =
                            completeHttpResponse.response;
                    }
                });

                return(serializedInterfaceEvent, processEvent);
            }

            void processEventTimeHasArrived()
            {
                var currentTime = getDateTimeOffset();

                lastAppEventTimeHasArrived = currentTime;

                processEventAndResultingRequests(new InterfaceToHost.AppEventStructure
                {
                    ArrivedAtTimeEvent = new InterfaceToHost.ArrivedAtTimeEventStructure
                    {
                        posixTimeMilli = currentTime.ToUnixTimeMilliseconds()
                    }
                });
            }

            notifyTimeHasArrivedTimer = new System.Threading.Timer(
                callback: _ =>
            {
                if (applicationStoppingCancellationTokenSource.IsCancellationRequested)
                {
                    notifyTimeHasArrivedTimer?.Dispose();
                    return;
                }

                lock (nextTimeToNotifyLock)
                {
                    if (applicationStoppingCancellationTokenSource.IsCancellationRequested)
                    {
                        notifyTimeHasArrivedTimer?.Dispose();
                        return;
                    }

                    var localNextTimeToNotify = nextTimeToNotify;

                    if (localNextTimeToNotify != null && localNextTimeToNotify.posixTimeMilli <= getDateTimeOffset().ToUnixTimeMilliseconds())
                    {
                        nextTimeToNotify = null;
                        processEventTimeHasArrived();
                        return;
                    }
                }

                if (lastAppEventTimeHasArrived.HasValue
                        ?
                    notifyTimeHasArrivedMaximumDistance <= (getDateTimeOffset() - lastAppEventTimeHasArrived.Value)
                        :
                    true)
                {
                    processEventTimeHasArrived();
                }
            },
                state: null,
                dueTime: TimeSpan.Zero,
                period: TimeSpan.FromMilliseconds(10));

            processEventTimeHasArrived();

            app
            .Use(async(context, next) => await Asp.MiddlewareFromWebAppConfig(webAppAndElmAppConfig.WebAppConfiguration, context, next))
            .Run(async(context) =>
            {
                var currentDateTime  = getDateTimeOffset();
                var timeMilli        = currentDateTime.ToUnixTimeMilliseconds();
                var httpRequestIndex = System.Threading.Interlocked.Increment(ref nextHttpRequestIndex);

                var httpRequestId = timeMilli.ToString() + "-" + httpRequestIndex.ToString();

                var httpRequestEvent =
                    await AsPersistentProcessInterfaceHttpRequestEvent(context, httpRequestId, currentDateTime);

                var httpRequestInterfaceEvent = new InterfaceToHost.AppEventStructure
                {
                    HttpRequestEvent = httpRequestEvent,
                };

                var preparedProcessEvent = prepareProcessEventAndResultingRequests(httpRequestInterfaceEvent);

                if (webAppAndElmAppConfig.WebAppConfiguration?.httpRequestEventSizeLimit < preparedProcessEvent.serializedInterfaceEvent?.Length)
                {
                    context.Response.StatusCode = StatusCodes.Status400BadRequest;
                    await context.Response.WriteAsync("Request is too large.");
                    return;
                }

                preparedProcessEvent.processEventAndResultingRequests();

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

                while (true)
                {
                    if (appTaskCompleteHttpResponse.TryRemove(httpRequestId, out var httpResponse))
                    {
                        var headerContentType =
                            httpResponse.headersToAdd
                            ?.FirstOrDefault(header => header.name?.ToLowerInvariant() == "content-type")
                            ?.values?.FirstOrDefault();

                        context.Response.StatusCode = httpResponse.statusCode;

                        foreach (var headerToAdd in (httpResponse.headersToAdd).EmptyIfNull())
                        {
                            context.Response.Headers[headerToAdd.name] = new Microsoft.Extensions.Primitives.StringValues(headerToAdd.values);
                        }

                        if (headerContentType != null)
                        {
                            context.Response.ContentType = headerContentType;
                        }

                        byte[] contentAsByteArray = null;

                        if (httpResponse?.bodyAsBase64 != null)
                        {
                            var buffer = new byte[httpResponse.bodyAsBase64.Length * 3 / 4];

                            if (!Convert.TryFromBase64String(httpResponse.bodyAsBase64, buffer, out var bytesWritten))
                            {
                                throw new FormatException(
                                    "Failed to convert from base64. bytesWritten=" + bytesWritten +
                                    ", input.length=" + httpResponse.bodyAsBase64.Length + ", input:\n" +
                                    httpResponse.bodyAsBase64);
                            }

                            contentAsByteArray = buffer.AsSpan(0, bytesWritten).ToArray();
                        }

                        context.Response.ContentLength = contentAsByteArray?.Length ?? 0;

                        if (contentAsByteArray != null)
                        {
                            await context.Response.Body.WriteAsync(contentAsByteArray);
                        }

                        break;
                    }

                    if (60 <= waitForHttpResponseClock.Elapsed.TotalSeconds)
                    {
                        throw new TimeoutException(
                            "The app did not return a HTTP response within " +
                            (int)waitForHttpResponseClock.Elapsed.TotalSeconds +
                            " seconds.");
                    }

                    System.Threading.Thread.Sleep(100);
                }
            });
        }