Esempio n. 1
0
    public void Test_ProjectFileStoreReaderForAppendedCompositionLogEvent()
    {
        var compositionLogEvent = new CompositionLogRecordInFile.CompositionEvent
        {
            DeployAppConfigAndMigrateElmAppState =
                new ValueInFileStructure {
                LiteralStringUtf8 = "Actually not a valid value for an app config"
            }
        };

        var projectionResult = IProcessStoreReader.ProjectFileStoreReaderForAppendedCompositionLogEvent(
            originalFileStore: new EmptyFileStoreReader(),
            compositionLogEvent: compositionLogEvent);

        var processStoreReader = new ProcessStoreReaderInFileStore(projectionResult.projectedReader);

        var compositionLogRecords =
            processStoreReader.EnumerateSerializedCompositionLogRecordsReverse().ToImmutableList();

        Assert.IsTrue(compositionLogRecords.Any());
    }
Esempio n. 2
0
    public void Configure(
        IApplicationBuilder app,
        IWebHostEnvironment env,
        IHostApplicationLifetime appLifetime,
        Func <DateTimeOffset> getDateTimeOffset,
        FileStoreForProcessStore processStoreForFileStore)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        var configuration = app.ApplicationServices.GetService <IConfiguration>();

        var adminPassword = configuration.GetValue <string>(Configuration.AdminPasswordSettingKey);

        object avoidConcurrencyLock = new();

        var processStoreFileStore = processStoreForFileStore.fileStore;

        PublicHostConfiguration?publicAppHost = null;

        void stopPublicApp()
        {
            lock (avoidConcurrencyLock)
            {
                if (publicAppHost != null)
                {
                    logger.LogInformation("Begin to stop the public app.");

                    publicAppHost?.webHost?.StopAsync(TimeSpan.FromSeconds(10)).Wait();
                    publicAppHost?.webHost?.Dispose();
                    publicAppHost?.processLiveRepresentation?.Dispose();
                    publicAppHost = null;
                }
            }
        }

        appLifetime.ApplicationStopping.Register(() =>
        {
            stopPublicApp();
        });

        var processStoreWriter =
            new ProcessStoreWriterInFileStore(
                processStoreFileStore,
                getTimeForCompositionLogBatch: getDateTimeOffset,
                processStoreFileStore);

        void startPublicApp()
        {
            lock (avoidConcurrencyLock)
            {
                stopPublicApp();

                logger.LogInformation("Begin to build the process live representation.");

                var restoreProcessResult =
                    PersistentProcess.PersistentProcessLiveRepresentation.LoadFromStoreAndRestoreProcess(
                        new ProcessStoreReaderInFileStore(processStoreFileStore),
                        logger: logEntry => logger.LogInformation(logEntry));

                var processLiveRepresentation = restoreProcessResult.process;

                logger.LogInformation("Completed building the process live representation.");

                var            cyclicReductionStoreLock            = new object();
                DateTimeOffset?cyclicReductionStoreLastTime        = null;
                var            cyclicReductionStoreDistanceSeconds = (int)TimeSpan.FromMinutes(10).TotalSeconds;

                void maintainStoreReductions()
                {
                    var currentDateTime = getDateTimeOffset();

                    System.Threading.Thread.MemoryBarrier();
                    var cyclicReductionStoreLastAge = currentDateTime - cyclicReductionStoreLastTime;

                    if (!(cyclicReductionStoreLastAge?.TotalSeconds < cyclicReductionStoreDistanceSeconds))
                    {
                        if (System.Threading.Monitor.TryEnter(cyclicReductionStoreLock))
                        {
                            try
                            {
                                var afterLockCyclicReductionStoreLastAge = currentDateTime - cyclicReductionStoreLastTime;

                                if (afterLockCyclicReductionStoreLastAge?.TotalSeconds < cyclicReductionStoreDistanceSeconds)
                                {
                                    return;
                                }

                                lock (avoidConcurrencyLock)
                                {
                                    var(reductionRecord, _) = processLiveRepresentation.StoreReductionRecordForCurrentState(processStoreWriter !);
                                }

                                cyclicReductionStoreLastTime = currentDateTime;
                                System.Threading.Thread.MemoryBarrier();
                            }
                            finally
                            {
                                System.Threading.Monitor.Exit(cyclicReductionStoreLock);
                            }
                        }
                    }
                }

                IWebHost buildWebHost(
                    PersistentProcess.ProcessAppConfig processAppConfig,
                    IReadOnlyList <string> publicWebHostUrls)
                {
                    var appConfigTree = Composition.ParseAsTreeWithStringPath(processAppConfig.appConfigComponent).Ok !;

                    var appConfigFilesNamesAndContents =
                        appConfigTree.EnumerateBlobsTransitive();

                    var webAppConfigurationFile =
                        appConfigFilesNamesAndContents
                        .FirstOrDefault(filePathAndContent => filePathAndContent.path.SequenceEqual(JsonFilePath))
                        .blobContent;

                    var webAppConfiguration =
                        webAppConfigurationFile == null
                        ?
                        null
                        :
                        System.Text.Json.JsonSerializer.Deserialize <WebAppConfigurationJsonStructure>(Encoding.UTF8.GetString(webAppConfigurationFile.ToArray()));

                    return
                        (Microsoft.AspNetCore.WebHost.CreateDefaultBuilder()
                         .ConfigureLogging((hostingContext, logging) =>
                    {
                        logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                        logging.AddConsole();
                        logging.AddDebug();
                    })
                         .ConfigureKestrel(kestrelOptions =>
                    {
                        kestrelOptions.ConfigureHttpsDefaults(httpsOptions =>
                        {
                            httpsOptions.ServerCertificateSelector = (c, s) => FluffySpoon.AspNet.LetsEncrypt.LetsEncryptRenewalService.Certificate;
                        });
                    })
                         .UseUrls(publicWebHostUrls.ToArray())
                         .UseStartup <StartupPublicApp>()
                         .WithSettingDateTimeOffsetDelegate(getDateTimeOffset)
                         .ConfigureServices(services =>
                    {
                        services.AddSingleton(
                            new WebAppAndElmAppConfig(
                                WebAppConfiguration: webAppConfiguration,
                                ProcessEventInElmApp: serializedEvent =>
                        {
                            lock (avoidConcurrencyLock)
                            {
                                var elmEventResponse =
                                    processLiveRepresentation.ProcessElmAppEvent(
                                        processStoreWriter !, serializedEvent);

                                maintainStoreReductions();

                                return elmEventResponse;
                            }
                        },
                                SourceComposition: processAppConfig.appConfigComponent,
                                InitOrMigrateCmds: restoreProcessResult.initOrMigrateCmds
                                ));
                    })
                         .Build());
                }

                if (processLiveRepresentation?.lastAppConfig != null)
                {
                    var publicWebHostUrls = configuration.GetSettingPublicWebHostUrls();

                    var webHost = buildWebHost(
                        processLiveRepresentation.lastAppConfig,
                        publicWebHostUrls: publicWebHostUrls);

                    webHost.StartAsync(appLifetime.ApplicationStopping).Wait();

                    logger.LogInformation("Started the public app at '" + string.Join(",", publicWebHostUrls) + "'.");

                    publicAppHost = new PublicHostConfiguration(
                        processLiveRepresentation: processLiveRepresentation,
                        webHost: webHost);
                }
            }
        }

        startPublicApp();

        app.Run(async(context) =>
        {
            var syncIOFeature = context.Features.Get <Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature>();
            if (syncIOFeature != null)
            {
                syncIOFeature.AllowSynchronousIO = true;
            }

            {
                context.Request.Headers.TryGetValue("Authorization", out var requestAuthorizationHeaderValue);

                context.Response.Headers.Add("X-Powered-By", "Elm Fullstack " + elm_fullstack.Program.AppVersionId);

                AuthenticationHeaderValue.TryParse(
                    requestAuthorizationHeaderValue.FirstOrDefault(), out var requestAuthorization);

                if (!(0 < adminPassword?.Length))
                {
                    context.Response.StatusCode = 403;
                    await context.Response.WriteAsync("The admin interface is not available because the admin password is not yet configured.");
                    return;
                }

                var buffer = new byte[400];

                var decodedRequestAuthorizationParameter =
                    Convert.TryFromBase64String(requestAuthorization?.Parameter ?? "", buffer, out var bytesWritten) ?
                    Encoding.UTF8.GetString(buffer, 0, bytesWritten) : null;

                var requestAuthorizationPassword =
                    decodedRequestAuthorizationParameter?.Split(':')?.ElementAtOrDefault(1);

                if (!(string.Equals(adminPassword, requestAuthorizationPassword) &&
                      string.Equals("basic", requestAuthorization?.Scheme, StringComparison.OrdinalIgnoreCase)))
                {
                    context.Response.StatusCode = 401;
                    context.Response.Headers.Add(
                        "WWW-Authenticate",
                        @"Basic realm=""" + context.Request.Host + @""", charset=""UTF-8""");
                    await context.Response.WriteAsync("Unauthorized");
                    return;
                }
            }

            async System.Threading.Tasks.Task deployElmApp(bool initElmAppState)
            {
                var memoryStream = new MemoryStream();
                context.Request.Body.CopyTo(memoryStream);

                var webAppConfigZipArchive = memoryStream.ToArray();

                {
                    try
                    {
                        var filesFromZipArchive = ZipArchive.EntriesFromZipArchive(webAppConfigZipArchive).ToImmutableList();

                        if (filesFromZipArchive.Count < 1)
                        {
                            throw new Exception("Contains no files.");
                        }
                    }
                    catch (Exception e)
                    {
                        context.Response.StatusCode = 400;
                        await context.Response.WriteAsync("Malformed web app config zip-archive:\n" + e);
                        return;
                    }
                }

                var appConfigTree =
                    Composition.SortedTreeFromSetOfBlobsWithCommonFilePath(
                        ZipArchive.EntriesFromZipArchive(webAppConfigZipArchive));

                var appConfigComponent = Composition.FromTreeWithStringPath(appConfigTree);

                var appConfigHashBase16 = CommonConversion.StringBase16FromByteArray(Composition.GetHash(appConfigComponent));

                logger.LogInformation("Got request to deploy app config " + appConfigHashBase16);

                processStoreWriter.StoreComponent(appConfigComponent);

                var appConfigValueInFile =
                    new ValueInFileStructure
                {
                    HashBase16 = appConfigHashBase16
                };

                var compositionLogEvent =
                    CompositionLogRecordInFile.CompositionEvent.EventForDeployAppConfig(
                        appConfigValueInFile: appConfigValueInFile,
                        initElmAppState: initElmAppState);

                await attemptContinueWithCompositionEventAndSendHttpResponse(compositionLogEvent);
            }

            var apiRoutes = new[]
            {
                new ApiRoute
                (
                    path: PathApiGetDeployedAppConfig,
                    methods: ImmutableDictionary <string, Func <HttpContext, PublicHostConfiguration?, System.Threading.Tasks.Task> > .Empty
                    .Add("get", async(context, publicAppHost) =>
                {
                    var appConfig = publicAppHost?.processLiveRepresentation?.lastAppConfig.appConfigComponent;

                    if (appConfig == null)
                    {
                        context.Response.StatusCode = 404;
                        await context.Response.WriteAsync("I did not find an app config in the history. Looks like no app was deployed so far.");
                        return;
                    }

                    var appConfigHashBase16 = CommonConversion.StringBase16FromByteArray(Composition.GetHash(appConfig));

                    var appConfigTree = Composition.ParseAsTreeWithStringPath(appConfig).Ok;

                    if (appConfigTree == null)
                    {
                        throw   new Exception("Failed to parse as tree with string path");
                    }

                    var appConfigZipArchive =
                        ZipArchive.ZipArchiveFromEntries(
                            Composition.TreeToFlatDictionaryWithPathComparer(appConfigTree));

                    context.Response.StatusCode            = 200;
                    context.Response.Headers.ContentLength = appConfigZipArchive.LongLength;
                    context.Response.Headers.Add("Content-Disposition", new ContentDispositionHeaderValue("attachment")
                    {
                        FileName = appConfigHashBase16 + ".zip"
                    }.ToString());
                    context.Response.Headers.Add("Content-Type", new MediaTypeHeaderValue("application/zip").ToString());

                    await context.Response.Body.WriteAsync(appConfigZipArchive);
                })
                ),
                new ApiRoute
                (
                    path: PathApiElmAppState,
                    methods: ImmutableDictionary <string, Func <HttpContext, PublicHostConfiguration?, System.Threading.Tasks.Task> > .Empty
                    .Add("get", async(context, publicAppHost) =>
                {
                    if (publicAppHost == null)
                    {
                        context.Response.StatusCode = 400;
                        await context.Response.WriteAsync("Not possible because there is no app (state).");
                        return;
                    }

                    var processLiveRepresentation = publicAppHost?.processLiveRepresentation;

                    var components = new List <Composition.Component>();

                    var storeWriter = new DelegatingProcessStoreWriter
                                      (
                        StoreComponentDelegate: components.Add,
                        StoreProvisionalReductionDelegate: _ => { },
                        AppendCompositionLogRecordDelegate: _ => throw new Exception("Unexpected use of interface.")
                                      );

                    var reductionRecord =
                        processLiveRepresentation?.StoreReductionRecordForCurrentState(storeWriter).reductionRecord;

                    if (reductionRecord == null)
                    {
                        context.Response.StatusCode = 500;
                        await context.Response.WriteAsync("Not possible because there is no Elm app deployed at the moment.");
                        return;
                    }

                    var elmAppStateReductionHashBase16 = reductionRecord.elmAppState?.HashBase16;

                    var elmAppStateReductionComponent =
                        components.First(c => CommonConversion.StringBase16FromByteArray(Composition.GetHash(c)) == elmAppStateReductionHashBase16);

                    if (elmAppStateReductionComponent.BlobContent == null)
                    {
                        throw   new Exception("elmAppStateReductionComponent is not a blob");
                    }

                    var elmAppStateReductionString =
                        Encoding.UTF8.GetString(elmAppStateReductionComponent.BlobContent);

                    context.Response.StatusCode  = 200;
                    context.Response.ContentType = "application/json";
                    await context.Response.WriteAsync(elmAppStateReductionString);
                })
                    .Add("post", async(context, publicAppHost) =>
                {
                    if (publicAppHost == null)
                    {
                        context.Response.StatusCode = 400;
                        await context.Response.WriteAsync("Not possible because there is no app (state).");
                        return;
                    }

                    var elmAppStateToSet = new StreamReader(context.Request.Body, Encoding.UTF8).ReadToEndAsync().Result;

                    var elmAppStateComponent = Composition.Component.Blob(Encoding.UTF8.GetBytes(elmAppStateToSet));

                    var appConfigValueInFile =
                        new ValueInFileStructure
                    {
                        HashBase16 = CommonConversion.StringBase16FromByteArray(Composition.GetHash(elmAppStateComponent))
                    };

                    processStoreWriter.StoreComponent(elmAppStateComponent);

                    await attemptContinueWithCompositionEventAndSendHttpResponse(
                        new CompositionLogRecordInFile.CompositionEvent
                    {
                        SetElmAppState = appConfigValueInFile
                    });
                })
                ),
                new ApiRoute
                (
                    path: PathApiDeployAndInitAppState,
                    methods: ImmutableDictionary <string, Func <HttpContext, PublicHostConfiguration?, System.Threading.Tasks.Task> > .Empty
                    .Add("post", async(context, publicAppHost) => await deployElmApp(initElmAppState: true))
                ),
                new ApiRoute
                (
                    path: PathApiDeployAndMigrateAppState,
                    methods: ImmutableDictionary <string, Func <HttpContext, PublicHostConfiguration?, System.Threading.Tasks.Task> > .Empty
                    .Add("post", async(context, publicAppHost) => await deployElmApp(initElmAppState: false))
                ),
                new ApiRoute
                (
                    path: PathApiReplaceProcessHistory,
                    methods: ImmutableDictionary <string, Func <HttpContext, PublicHostConfiguration?, System.Threading.Tasks.Task> > .Empty
                    .Add("post", async(context, publicAppHost) =>
                {
                    var memoryStream = new MemoryStream();
                    context.Request.Body.CopyTo(memoryStream);

                    var webAppConfigZipArchive = memoryStream.ToArray();

                    var replacementFiles =
                        ZipArchive.EntriesFromZipArchive(webAppConfigZipArchive)
                        .Select(filePathAndContent =>
                                (path: filePathAndContent.name.Split(new[] { '/', '\\' }).ToImmutableList()
                                 , filePathAndContent.content))
                        .ToImmutableList();

                    lock (avoidConcurrencyLock)
                    {
                        stopPublicApp();

                        foreach (var filePath in processStoreFileStore.ListFilesInDirectory(ImmutableList <string> .Empty).ToImmutableList())
                        {
                            processStoreFileStore.DeleteFile(filePath);
                        }

                        foreach (var replacementFile in replacementFiles)
                        {
                            processStoreFileStore.SetFileContent(replacementFile.path, replacementFile.content);
                        }

                        startPublicApp();
                    }

                    context.Response.StatusCode = 200;
                    await context.Response.WriteAsync("Successfully replaced the process history.");
                })
                ),
            };

            foreach (var apiRoute in apiRoutes)
            {
                if (!context.Request.Path.Equals(new PathString(apiRoute.path)))
                {
                    continue;
                }

                var matchingMethod =
                    apiRoute.methods
                    .FirstOrDefault(m => m.Key.ToUpperInvariant() == context.Request.Method.ToUpperInvariant());

                if (matchingMethod.Value == null)
                {
                    var supportedMethodsNames =
                        apiRoute.methods.Keys.Select(m => m.ToUpperInvariant()).ToList();

                    var guide =
                        HtmlFromLines(
                            "<h2>Method Not Allowed</h2>",
                            "",
                            context.Request.Path.ToString() +
                            " is a valid path, but the method " + context.Request.Method.ToUpperInvariant() +
                            " is not supported here.",
                            "Only following " +
                            (supportedMethodsNames.Count == 1 ? "method is" : "methods are") +
                            " supported here: " + string.Join(", ", supportedMethodsNames),
                            "", "",
                            ApiGuide);

                    context.Response.StatusCode = 405;
                    await context.Response.WriteAsync(HtmlDocument(guide));
                    return;
                }

                matchingMethod.Value?.Invoke(context, publicAppHost);
                return;
            }

            if (context.Request.Path.StartsWithSegments(new PathString(PathApiRevertProcessTo),
                                                        out var revertToRemainingPath))
            {
                if (!string.Equals(context.Request.Method, "post", StringComparison.InvariantCultureIgnoreCase))
                {
                    context.Response.StatusCode = 405;
                    await context.Response.WriteAsync("Method not supported.");
                    return;
                }

                var processVersionId = revertToRemainingPath.ToString().Trim('/');

                var processVersionCompositionRecord =
                    new ProcessStoreReaderInFileStore(processStoreFileStore)
                    .EnumerateSerializedCompositionLogRecordsReverse()
                    .FirstOrDefault(compositionEntry => CompositionLogRecordInFile.HashBase16FromCompositionRecord(compositionEntry) == processVersionId);

                if (processVersionCompositionRecord == null)
                {
                    context.Response.StatusCode = 404;
                    await context.Response.WriteAsync("Did not find process version '" + processVersionId + "'.");
                    return;
                }

                await attemptContinueWithCompositionEventAndSendHttpResponse(new CompositionLogRecordInFile.CompositionEvent
                {
                    RevertProcessTo = new ValueInFileStructure {
                        HashBase16 = processVersionId
                    },
                });
                return;
            }

            TruncateProcessHistoryReport truncateProcessHistory(TimeSpan productionBlockDurationLimit)
            {
                var beginTime = CommonConversion.TimeStringViewForReport(DateTimeOffset.UtcNow);

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

                var numbersOfThreadsToDeleteFiles = 4;

                var filePathsInProcessStorePartitions =
                    processStoreFileStore.ListFiles()
                    .Select((s, i) => (s, i))
                    .GroupBy(x => x.i % numbersOfThreadsToDeleteFiles)
                    .Select(g => g.Select(x => x.s).ToImmutableList())
                    .ToImmutableList();

                lock (avoidConcurrencyLock)
                {
                    var lockStopwatch = System.Diagnostics.Stopwatch.StartNew();

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

                    var storeReductionReport =
                        publicAppHost?.processLiveRepresentation?.StoreReductionRecordForCurrentState(processStoreWriter).report;

                    storeReductionStopwatch.Stop();

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

                    var filesForRestore =
                        PersistentProcess.PersistentProcessLiveRepresentation.GetFilesForRestoreProcess(
                            processStoreFileStore).files
                        .Select(filePathAndContent => filePathAndContent.Key)
                        .ToImmutableHashSet(EnumerableExtension.EqualityComparer <IImmutableList <string> >());

                    getFilesForRestoreStopwatch.Stop();

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

                    var totalDeletedFilesCount =
                        filePathsInProcessStorePartitions
                        .AsParallel()
                        .WithDegreeOfParallelism(numbersOfThreadsToDeleteFiles)
                        .Select(partitionFilePaths =>
                    {
                        int partitionDeletedFilesCount = 0;

                        foreach (var filePath in partitionFilePaths)
                        {
                            if (filesForRestore.Contains(filePath))
                            {
                                continue;
                            }

                            if (productionBlockDurationLimit < lockStopwatch.Elapsed)
                            {
                                break;
                            }

                            processStoreFileStore.DeleteFile(filePath);
                            ++partitionDeletedFilesCount;
                        }

                        return(partitionDeletedFilesCount);
                    })
                        .Sum();

                    deleteFilesStopwatch.Stop();

                    return(new TruncateProcessHistoryReport
                           (
                               beginTime: beginTime,
                               filesForRestoreCount: filesForRestore.Count,
                               discoveredFilesCount: filePathsInProcessStorePartitions.Sum(partition => partition.Count),
                               deletedFilesCount: totalDeletedFilesCount,
                               storeReductionTimeSpentMilli: (int)storeReductionStopwatch.ElapsedMilliseconds,
                               storeReductionReport: storeReductionReport,
                               getFilesForRestoreTimeSpentMilli: (int)getFilesForRestoreStopwatch.ElapsedMilliseconds,
                               deleteFilesTimeSpentMilli: (int)deleteFilesStopwatch.ElapsedMilliseconds,
                               lockedTimeSpentMilli: (int)lockStopwatch.ElapsedMilliseconds,
                               totalTimeSpentMilli: (int)totalStopwatch.ElapsedMilliseconds
                           ));
                }
            }

            if (context.Request.Path.Equals(new PathString(PathApiTruncateProcessHistory)))
            {
                var truncateResult = truncateProcessHistory(productionBlockDurationLimit: TimeSpan.FromMinutes(1));

                context.Response.StatusCode  = 200;
                context.Response.ContentType = "application/json";
                await context.Response.WriteAsync(System.Text.Json.JsonSerializer.Serialize(truncateResult));
                return;
            }

            {
                if (context.Request.Path.StartsWithSegments(
                        new PathString(PathApiProcessHistoryFileStoreGetFileContent), out var remainingPathString))
                {
                    if (!string.Equals(context.Request.Method, "get", StringComparison.InvariantCultureIgnoreCase))
                    {
                        context.Response.StatusCode = 405;
                        await context.Response.WriteAsync("Method not supported.");
                        return;
                    }

                    var filePathInStore =
                        remainingPathString.ToString().Trim('/').Split('/').ToImmutableList();

                    var fileContent = processStoreFileStore.GetFileContent(filePathInStore);

                    if (fileContent == null)
                    {
                        context.Response.StatusCode = 404;
                        await context.Response.WriteAsync("No file at '" + string.Join("/", filePathInStore) + "'.");
                        return;
                    }

                    context.Response.StatusCode  = 200;
                    context.Response.ContentType = "application/octet-stream";
                    await context.Response.Body.WriteAsync(fileContent as byte[] ?? fileContent.ToArray());
                    return;
                }
            }

            {
                if (context.Request.Path.StartsWithSegments(
                        new PathString(PathApiProcessHistoryFileStoreListFilesInDirectory), out var remainingPathString))
                {
                    if (!string.Equals(context.Request.Method, "get", StringComparison.InvariantCultureIgnoreCase))
                    {
                        context.Response.StatusCode = 405;
                        await context.Response.WriteAsync("Method not supported.");
                        return;
                    }

                    var filePathInStore =
                        remainingPathString.ToString().Trim('/').Split('/').ToImmutableList();

                    var filesPaths = processStoreFileStore.ListFilesInDirectory(filePathInStore);

                    var filesPathsList =
                        string.Join('\n', filesPaths.Select(path => string.Join('/', path)));

                    context.Response.StatusCode  = 200;
                    context.Response.ContentType = "application/octet-stream";
                    await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes(filesPathsList));
                    return;
                }
            }

            (int statusCode, AttemptContinueWithCompositionEventReport responseReport)attemptContinueWithCompositionEvent(
                CompositionLogRecordInFile.CompositionEvent compositionLogEvent)
            {
                lock (avoidConcurrencyLock)
                {
                    var storeReductionStopwatch = System.Diagnostics.Stopwatch.StartNew();

                    var storeReductionReport =
                        publicAppHost?.processLiveRepresentation?.StoreReductionRecordForCurrentState(processStoreWriter).report;

                    storeReductionStopwatch.Stop();

                    var(statusCode, report) =
                        AttemptContinueWithCompositionEventAndCommit(
                            compositionLogEvent,
                            processStoreFileStore,
                            testContinueLogger: logEntry => logger.LogInformation(logEntry));

                    report = report with
                    {
                        storeReductionTimeSpentMilli = (int)storeReductionStopwatch.ElapsedMilliseconds,
                        storeReductionReport         = storeReductionReport
                    };

                    startPublicApp();

                    return(statusCode, report);
                }
            }

            async System.Threading.Tasks.Task attemptContinueWithCompositionEventAndSendHttpResponse(
                CompositionLogRecordInFile.CompositionEvent compositionLogEvent,
                ILogger? logger = null)
            {
                logger?.LogInformation(
                    "Begin attempt to continue with composition event: " +
                    System.Text.Json.JsonSerializer.Serialize(compositionLogEvent));

                var(statusCode, attemptReport) = attemptContinueWithCompositionEvent(compositionLogEvent);

                var responseBodyString = System.Text.Json.JsonSerializer.Serialize(attemptReport);

                context.Response.StatusCode = statusCode;
                await context.Response.WriteAsync(responseBodyString);
            }

            if (context.Request.Path.Equals(PathString.Empty) || context.Request.Path.Equals(new PathString("/")))
            {
                var httpApiGuide =
                    HtmlFromLines(
                        "<h3>HTTP APIs</h3>\n" +
                        HtmlFromLines(apiRoutes.Select(HtmlToDescribeApiRoute).ToArray())
                        );

                context.Response.StatusCode = 200;
                await context.Response.WriteAsync(
                    HtmlDocument(
                        HtmlFromLines(
                            "Welcome to the Elm Fullstack admin interface version " + elm_fullstack.Program.AppVersionId + ".",
                            httpApiGuide,
                            "",
                            ApiGuide)));
                return;
            }

            context.Response.StatusCode = 404;
            await context.Response.WriteAsync("Not Found");
            return;
        });
    }
Esempio n. 3
0
        public void Configure(
            IApplicationBuilder app,
            IWebHostEnvironment env,
            IHostApplicationLifetime appLifetime,
            Func <DateTimeOffset> getDateTimeOffset)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            var configuration = app.ApplicationServices.GetService <IConfiguration>();

            var rootPassword      = configuration.GetValue <string>(Configuration.AdminRootPasswordSettingKey);
            var publicWebHostUrls = configuration.GetValue <string>(Configuration.PublicWebHostUrlsSettingKey)?.Split(new[] { ',', ';' });

            var processStoreFileStore = app.ApplicationServices.GetService <FileStoreForProcessStore>().fileStore;

            object publicAppLock = new object();

            PublicHostConfiguration publicAppHost = null;

            void stopPublicApp()
            {
                lock (publicAppLock)
                {
                    if (publicAppHost != null)
                    {
                        publicAppHost?.webHost?.StopAsync(TimeSpan.FromSeconds(10)).Wait();
                        publicAppHost?.webHost?.Dispose();
                        publicAppHost?.processVolatileRepresentation?.Dispose();
                        publicAppHost = null;
                    }
                }
            }

            appLifetime.ApplicationStopping.Register(() =>
            {
                stopPublicApp();
            });

            var processStoreWriter =
                new ProcessStoreSupportingMigrations.ProcessStoreWriterInFileStore(processStoreFileStore);

            void startPublicApp()
            {
                lock (publicAppLock)
                {
                    stopPublicApp();

                    var newPublicAppConfig = new PublicHostConfiguration {
                    };

                    logger.LogInformation("Begin to build the process volatile representation.");

                    var processVolatileRepresentation =
                        PersistentProcess.PersistentProcessVolatileRepresentation.Restore(
                            new ProcessStoreSupportingMigrations.ProcessStoreReaderInFileStore(processStoreFileStore),
                            logger: logEntry => logger.LogInformation(logEntry));

                    logger.LogInformation("Completed building the process volatile representation.");

                    var            cyclicReductionStoreLock            = new object();
                    DateTimeOffset?cyclicReductionStoreLastTime        = null;
                    var            cyclicReductionStoreDistanceSeconds = (int)TimeSpan.FromMinutes(10).TotalSeconds;

                    void maintainStoreReductions()
                    {
                        var currentDateTime = getDateTimeOffset();

                        System.Threading.Thread.MemoryBarrier();
                        var cyclicReductionStoreLastAge = currentDateTime - cyclicReductionStoreLastTime;

                        if (!(cyclicReductionStoreLastAge?.TotalSeconds < cyclicReductionStoreDistanceSeconds))
                        {
                            if (System.Threading.Monitor.TryEnter(cyclicReductionStoreLock))
                            {
                                try
                                {
                                    var afterLockCyclicReductionStoreLastAge = currentDateTime - cyclicReductionStoreLastTime;

                                    if (afterLockCyclicReductionStoreLastAge?.TotalSeconds < cyclicReductionStoreDistanceSeconds)
                                    {
                                        return;
                                    }

                                    lock (processStoreFileStore)
                                    {
                                        var reductionRecord = processVolatileRepresentation.StoreReductionRecordForCurrentState(processStoreWriter);
                                    }

                                    cyclicReductionStoreLastTime = currentDateTime;
                                    System.Threading.Thread.MemoryBarrier();
                                }
                                finally
                                {
                                    System.Threading.Monitor.Exit(cyclicReductionStoreLock);
                                }
                            }
                        }
                    }

                    var webHost =
                        processVolatileRepresentation?.lastAppConfig?.appConfigComponent == null
                        ?
                        null
                        :
                        buildWebHost();

                    IWebHost buildWebHost()
                    {
                        var appConfigTree = Composition.ParseAsTree(
                            processVolatileRepresentation.lastAppConfig.Value.appConfigComponent).Ok;

                        var appConfigFilesNamesAndContents =
                            appConfigTree.EnumerateBlobsTransitive()
                            .Select(blobPathAndContent => (
                                        fileName: (IImmutableList <string>)blobPathAndContent.path.Select(name => System.Text.Encoding.UTF8.GetString(name.ToArray())).ToImmutableList(),
                                        fileContent: blobPathAndContent.blobContent))
                            .ToImmutableList();

                        return
                            (Microsoft.AspNetCore.WebHost.CreateDefaultBuilder()
                             .ConfigureLogging((hostingContext, logging) =>
                        {
                            logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                            logging.AddConsole();
                            logging.AddDebug();
                        })
                             .ConfigureKestrel(kestrelOptions =>
                        {
                            kestrelOptions.ConfigureHttpsDefaults(httpsOptions =>
                            {
                                httpsOptions.ServerCertificateSelector = (c, s) => FluffySpoon.AspNet.LetsEncrypt.LetsEncryptRenewalService.Certificate;
                            });
                        })
                             .UseUrls(publicWebHostUrls ?? PublicWebHostUrlsDefault)
                             .UseStartup <StartupPublicApp>()
                             .WithSettingDateTimeOffsetDelegate(getDateTimeOffset)
                             .ConfigureServices(services =>
                        {
                            services.AddSingleton <WebAppAndElmAppConfig>(
                                new WebAppAndElmAppConfig
                            {
                                WebAppConfiguration = WebAppConfiguration.FromFiles(appConfigFilesNamesAndContents),
                                ProcessEventInElmApp = serializedEvent =>
                                {
                                    lock (processStoreWriter)
                                    {
                                        lock (publicAppLock)
                                        {
                                            var elmEventResponse =
                                                processVolatileRepresentation.ProcessElmAppEvent(
                                                    processStoreWriter, serializedEvent);

                                            maintainStoreReductions();

                                            return elmEventResponse;
                                        }
                                    }
                                }
                            });
                        })
                             .Build());
                    }

                    newPublicAppConfig.processVolatileRepresentation = processVolatileRepresentation;
                    newPublicAppConfig.webHost = webHost;

                    webHost?.StartAsync(appLifetime.ApplicationStopping).Wait();
                    publicAppHost = newPublicAppConfig;
                }
            }

            startPublicApp();

            app.Run(async(context) =>
            {
                var syncIOFeature = context.Features.Get <Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature>();
                if (syncIOFeature != null)
                {
                    syncIOFeature.AllowSynchronousIO = true;
                }

                {
                    var expectedAuthorization = Configuration.BasicAuthenticationForAdminRoot(rootPassword);

                    context.Request.Headers.TryGetValue("Authorization", out var requestAuthorizationHeaderValue);

                    AuthenticationHeaderValue.TryParse(
                        requestAuthorizationHeaderValue.FirstOrDefault(), out var requestAuthorization);

                    if (!(0 < rootPassword?.Length))
                    {
                        context.Response.StatusCode = 403;
                        await context.Response.WriteAsync("Forbidden");
                        return;
                    }

                    var buffer = new byte[400];

                    var decodedRequestAuthorizationParameter =
                        Convert.TryFromBase64String(requestAuthorization?.Parameter ?? "", buffer, out var bytesWritten) ?
                        Encoding.UTF8.GetString(buffer, 0, bytesWritten) : null;

                    if (!(string.Equals(expectedAuthorization, decodedRequestAuthorizationParameter) &&
                          string.Equals("basic", requestAuthorization?.Scheme, StringComparison.OrdinalIgnoreCase)))
                    {
                        context.Response.StatusCode = 401;
                        context.Response.Headers.Add(
                            "WWW-Authenticate",
                            @"Basic realm=""" + context.Request.Host + @""", charset=""UTF-8""");
                        await context.Response.WriteAsync("Unauthorized");
                        return;
                    }
                }

                var requestPathIsDeployAppConfigAndInitElmAppState =
                    context.Request.Path.Equals(new PathString(PathApiDeployAppConfigAndInitElmAppState));

                if (context.Request.Path.Equals(new PathString(PathApiGetDeployedAppConfig)))
                {
                    if (!string.Equals(context.Request.Method, "get", StringComparison.InvariantCultureIgnoreCase))
                    {
                        context.Response.StatusCode = 405;
                        await context.Response.WriteAsync("Method not supported.");
                        return;
                    }

                    var appConfig = publicAppHost?.processVolatileRepresentation?.lastAppConfig?.appConfigComponent;

                    if (appConfig == null)
                    {
                        context.Response.StatusCode = 404;
                        await context.Response.WriteAsync("I did not find an app config in the history. Looks like no app was deployed so far.");
                        return;
                    }

                    var appConfigHashBase16 = CommonConversion.StringBase16FromByteArray(Composition.GetHash(appConfig));

                    var appConfigTree = Composition.ParseAsTree(appConfig).Ok;

                    var appConfigFilesNamesAndContents =
                        appConfigTree.EnumerateBlobsTransitive()
                        .Select(blobPathAndContent => (
                                    fileName: (IImmutableList <string>)blobPathAndContent.path.Select(name => Encoding.UTF8.GetString(name.ToArray())).ToImmutableList(),
                                    fileContent: blobPathAndContent.blobContent))
                        .ToImmutableList();

                    var appConfigZipArchive =
                        ZipArchive.ZipArchiveFromEntries(
                            ElmApp.ToFlatDictionaryWithPathComparer(appConfigFilesNamesAndContents));

                    context.Response.StatusCode            = 200;
                    context.Response.Headers.ContentLength = appConfigZipArchive.LongLength;
                    context.Response.Headers.Add("Content-Disposition", new ContentDispositionHeaderValue("attachment")
                    {
                        FileName = appConfigHashBase16 + ".zip"
                    }.ToString());
                    context.Response.Headers.Add("Content-Type", new MediaTypeHeaderValue("application/zip").ToString());

                    await context.Response.Body.WriteAsync(appConfigZipArchive);
                    return;
                }

                if (requestPathIsDeployAppConfigAndInitElmAppState ||
                    context.Request.Path.Equals(new PathString(PathApiDeployAppConfigAndMigrateElmAppState)))
                {
                    if (!string.Equals(context.Request.Method, "post", StringComparison.InvariantCultureIgnoreCase))
                    {
                        context.Response.StatusCode = 405;
                        await context.Response.WriteAsync("Method not supported.");
                        return;
                    }

                    var memoryStream = new MemoryStream();
                    context.Request.Body.CopyTo(memoryStream);

                    var webAppConfigZipArchive = memoryStream.ToArray();

                    {
                        try
                        {
                            var filesFromZipArchive = ZipArchive.EntriesFromZipArchive(webAppConfigZipArchive).ToImmutableList();

                            if (filesFromZipArchive.Count < 1)
                            {
                                throw new Exception("Contains no files.");
                            }
                        }
                        catch (Exception e)
                        {
                            context.Response.StatusCode = 400;
                            await context.Response.WriteAsync("Malformed web app config zip-archive:\n" + e);
                            return;
                        }
                    }

                    var appConfigTree =
                        Composition.TreeFromSetOfBlobsWithCommonFilePath(
                            ZipArchive.EntriesFromZipArchive(webAppConfigZipArchive));

                    var appConfigComponent = Composition.FromTree(appConfigTree);

                    processStoreWriter.StoreComponent(appConfigComponent);

                    var appConfigValueInFile =
                        new ProcessStoreSupportingMigrations.ValueInFileStructure
                    {
                        HashBase16 = CommonConversion.StringBase16FromByteArray(Composition.GetHash(appConfigComponent))
                    };

                    var compositionLogEvent =
                        requestPathIsDeployAppConfigAndInitElmAppState
                            ?
                        new ProcessStoreSupportingMigrations.CompositionLogRecordInFile.CompositionEvent
                    {
                        DeployAppConfigAndInitElmAppState = appConfigValueInFile,
                    }
                            :
                    new ProcessStoreSupportingMigrations.CompositionLogRecordInFile.CompositionEvent
                    {
                        DeployAppConfigAndMigrateElmAppState = appConfigValueInFile,
                    };

                    await attemptContinueWithCompositionEventAndSendHttpResponse(compositionLogEvent);
                    return;
                }

                if (context.Request.Path.StartsWithSegments(new PathString(PathApiRevertProcessTo),
                                                            out var revertToRemainingPath))
                {
                    if (!string.Equals(context.Request.Method, "post", StringComparison.InvariantCultureIgnoreCase))
                    {
                        context.Response.StatusCode = 405;
                        await context.Response.WriteAsync("Method not supported.");
                        return;
                    }

                    var processVersionId = revertToRemainingPath.ToString().Trim('/');

                    var processVersionComponent =
                        new ProcessStoreReaderInFileStore(processStoreFileStore).LoadComponent(processVersionId);

                    if (processVersionComponent == null)
                    {
                        context.Response.StatusCode = 404;
                        await context.Response.WriteAsync("Did not find process version '" + processVersionId + "'.");
                        return;
                    }

                    await attemptContinueWithCompositionEventAndSendHttpResponse(new CompositionLogRecordInFile.CompositionEvent
                    {
                        RevertProcessTo = new ValueInFileStructure {
                            HashBase16 = processVersionId
                        },
                    });
                    return;
                }

                if (context.Request.Path.Equals(new PathString(PathApiElmAppState)))
                {
                    if (publicAppHost == null)
                    {
                        context.Response.StatusCode = 400;
                        await context.Response.WriteAsync("Not possible because there is no app (state).");
                        return;
                    }

                    if (string.Equals(context.Request.Method, "get", StringComparison.InvariantCultureIgnoreCase))
                    {
                        var processVolatileRepresentation = publicAppHost?.processVolatileRepresentation;

                        var components = new List <Composition.Component>();

                        var storeWriter = new DelegatingProcessStoreWriter
                        {
                            StoreComponentDelegate              = components.Add,
                            StoreProvisionalReductionDelegate   = _ => { },
                            SetCompositionLogHeadRecordDelegate = _ => throw new Exception("Unexpected use of interface."),
                        };

                        var reductionRecord =
                            processVolatileRepresentation?.StoreReductionRecordForCurrentState(storeWriter);

                        if (reductionRecord == null)
                        {
                            context.Response.StatusCode = 500;
                            await context.Response.WriteAsync("Not possible because there is no Elm app deployed at the moment.");
                            return;
                        }

                        var elmAppStateReductionHashBase16 = reductionRecord.elmAppState?.HashBase16;

                        var elmAppStateReductionComponent =
                            components.First(c => CommonConversion.StringBase16FromByteArray(Composition.GetHash(c)) == elmAppStateReductionHashBase16);

                        var elmAppStateReductionString =
                            Encoding.UTF8.GetString(elmAppStateReductionComponent.BlobContent.ToArray());

                        context.Response.StatusCode  = 200;
                        context.Response.ContentType = "application/json";
                        await context.Response.WriteAsync(elmAppStateReductionString);
                        return;
                    }
                    else
                    {
                        if (string.Equals(context.Request.Method, "post", StringComparison.InvariantCultureIgnoreCase))
                        {
                            var elmAppStateToSet = new StreamReader(context.Request.Body, System.Text.Encoding.UTF8).ReadToEndAsync().Result;

                            var elmAppStateComponent = Composition.Component.Blob(Encoding.UTF8.GetBytes(elmAppStateToSet));

                            var appConfigValueInFile =
                                new ProcessStoreSupportingMigrations.ValueInFileStructure
                            {
                                HashBase16 = CommonConversion.StringBase16FromByteArray(Composition.GetHash(elmAppStateComponent))
                            };

                            processStoreWriter.StoreComponent(elmAppStateComponent);

                            await attemptContinueWithCompositionEventAndSendHttpResponse(
                                new ProcessStoreSupportingMigrations.CompositionLogRecordInFile.CompositionEvent
                            {
                                SetElmAppState = appConfigValueInFile
                            });
                            return;
                        }
                        else
                        {
                            context.Response.StatusCode = 405;
                            await context.Response.WriteAsync("Method not supported.");
                            return;
                        }
                    }
                }


                if (context.Request.Path.Equals(new PathString(PathApiReplaceProcessHistory)))
                {
                    var memoryStream = new MemoryStream();
                    context.Request.Body.CopyTo(memoryStream);

                    var webAppConfigZipArchive = memoryStream.ToArray();

                    var replacementFiles =
                        ZipArchive.EntriesFromZipArchive(webAppConfigZipArchive)
                        .Select(filePathAndContent =>
                                (path: filePathAndContent.name.Split(new[] { '/', '\\' }).ToImmutableList()
                                 , content: filePathAndContent.content))
                        .ToImmutableList();

                    lock (publicAppLock)
                    {
                        lock (processStoreFileStore)
                        {
                            stopPublicApp();

                            foreach (var filePath in processStoreFileStore.ListFilesInDirectory(ImmutableList <string> .Empty).ToImmutableList())
                            {
                                processStoreFileStore.DeleteFile(filePath);
                            }

                            foreach (var replacementFile in replacementFiles)
                            {
                                processStoreFileStore.SetFileContent(replacementFile.path, replacementFile.content);
                            }

                            startPublicApp();
                        }
                    }

                    context.Response.StatusCode = 200;
                    await context.Response.WriteAsync("Successfully replaced the process history.");
                    return;
                }

                if (context.Request.Path.StartsWithSegments(
                        new PathString(PathApiProcessHistoryFileStoreGetFileContent), out var remainingPathString))
                {
                    if (!string.Equals(context.Request.Method, "get", StringComparison.InvariantCultureIgnoreCase))
                    {
                        context.Response.StatusCode = 405;
                        await context.Response.WriteAsync("Method not supported.");
                        return;
                    }

                    var filePathInStore =
                        remainingPathString.ToString().Trim('/').Split('/').ToImmutableList();

                    var fileContent = processStoreFileStore.GetFileContent(filePathInStore);

                    if (fileContent == null)
                    {
                        context.Response.StatusCode = 404;
                        await context.Response.WriteAsync("No file at '" + string.Join("/", filePathInStore) + "'.");
                        return;
                    }

                    context.Response.StatusCode  = 200;
                    context.Response.ContentType = "application/octet-stream";
                    await context.Response.Body.WriteAsync(fileContent);
                    return;
                }

                (int statusCode, string responseBodyString)attemptContinueWithCompositionEvent(
                    ProcessStoreSupportingMigrations.CompositionLogRecordInFile.CompositionEvent compositionLogEvent)
                {
                    lock (processStoreFileStore)
                    {
                        lock (publicAppLock)
                        {
                            publicAppHost?.processVolatileRepresentation?.StoreReductionRecordForCurrentState(processStoreWriter);

                            var(projectedFiles, projectedFileReader) = IProcessStoreReader.ProjectFileStoreReaderForAppendedCompositionLogEvent(
                                originalFileStore: processStoreFileStore,
                                compositionLogEvent: compositionLogEvent);

                            using (var projectedProcess =
                                       PersistentProcess.PersistentProcessVolatileRepresentation.Restore(
                                           new ProcessStoreReaderInFileStore(projectedFileReader),
                                           _ => { }))
                            {
                                if (compositionLogEvent.DeployAppConfigAndMigrateElmAppState != null ||
                                    compositionLogEvent.SetElmAppState != null)
                                {
                                    if (projectedProcess.lastSetElmAppStateResult?.Ok == null)
                                    {
                                        return(statusCode: 400, responseBodyString: "Failed to migrate Elm app state for this deployment: " + projectedProcess.lastSetElmAppStateResult?.Err);
                                    }
                                }
                            }

                            foreach (var projectedFilePathAndContent in projectedFiles)
                            {
                                processStoreFileStore.SetFileContent(
                                    projectedFilePathAndContent.filePath, projectedFilePathAndContent.fileContent);
                            }

                            startPublicApp();

                            return(statusCode: 200, responseBodyString: "Successfully deployed this configuration and started the web server.");
                        }
                    }
                }

                async System.Threading.Tasks.Task attemptContinueWithCompositionEventAndSendHttpResponse(
                    ProcessStoreSupportingMigrations.CompositionLogRecordInFile.CompositionEvent compositionLogEvent)
                {
                    var(statusCode, responseBodyString) = attemptContinueWithCompositionEvent(compositionLogEvent);

                    context.Response.StatusCode = statusCode;
                    await context.Response.WriteAsync(responseBodyString);
                    return;
                }

                if (context.Request.Path.Equals(PathString.Empty) || context.Request.Path.Equals(new PathString("/")))
                {
                    context.Response.StatusCode = 200;
                    await context.Response.WriteAsync(
                        "Welcome to Elm-fullstack version " + Program.AppVersionId + ".\n" +
                        "To learn about this admin interface, see http://elm-fullstack.org/");
                    return;
                }

                context.Response.StatusCode = 404;
                await context.Response.WriteAsync("Not Found");
                return;
            });
        }