Exemple #1
0
        static PersistentProcessWithHistoryOnFileFromElm019Code BuildPersistentProcess(IServiceProvider services)
        {
            var logger      = services.GetService <ILogger <Startup> >();
            var elmAppFiles = services.GetService <WebAppConfiguration>()?.ElmAppFiles;

            if (!(0 < elmAppFiles?.Count))
            {
                logger.LogInformation("Found no ElmAppFile in configuration.");
                return(null);
            }

            var elmAppComposition =
                Composition.FromTree(
                    Composition.TreeFromSetOfBlobsWithStringPath(elmAppFiles, System.Text.Encoding.UTF8));

            logger.LogInformation("Begin to build the persistent process for Elm app " +
                                  CommonConversion.StringBase16FromByteArray(Composition.GetHash(elmAppComposition)));

            var persistentProcess =
                new PersistentProcessWithHistoryOnFileFromElm019Code(
                    services.GetService <ProcessStore.IProcessStoreReader>(),
                    elmAppFiles,
                    logger: logEntry => logger.LogInformation(logEntry));

            logger.LogInformation("Completed building the persistent process.");

            return(persistentProcess);
        }
        ProcessEvents(IReadOnlyList <string> serializedEvents)
        {
            lock (process)
            {
                var responses =
                    serializedEvents.Select(serializedEvent => process.ProcessEvent(serializedEvent))
                    .ToImmutableList();

                var compositionRecord = new CompositionRecordInFile
                {
                    ParentHashBase16 = CommonConversion.StringBase16FromByteArray(lastStateHash),
                    AppendedEvents   = serializedEvents.Select(@event => new ValueInFile {
                        LiteralString = @event
                    }).ToImmutableList(),
                };

                var serializedCompositionRecord =
                    Encoding.UTF8.GetBytes(Serialize(compositionRecord));

                var compositionHash = CompositionRecordInFile.HashFromSerialRepresentation(serializedCompositionRecord);

                lastStateHash = compositionHash;

                return(responses, (serializedCompositionRecord, compositionHash));
            }
        }
    BuildConfigurationZipArchiveFromPath(string sourcePath)
    {
        var loadCompositionResult =
            LoadComposition.LoadFromPathResolvingNetworkDependencies(sourcePath)
            .LogToActions(Console.WriteLine);

        if (loadCompositionResult?.Ok == null)
        {
            throw new Exception("Failed to load from path '" + sourcePath + "': " + loadCompositionResult?.Err);
        }

        var sourceTree = loadCompositionResult.Ok.tree;

        /*
         * TODO: Provide a better way to avoid unnecessary files ending up in the config: Get the source files from git.
         */
        var filteredSourceTree =
            loadCompositionResult.Ok.origin?.FromLocalFileSystem != null
            ?
            RemoveNoiseFromTreeComingFromLocalFileSystem(sourceTree)
            :
            sourceTree;

        var filteredSourceComposition = Composition.FromTreeWithStringPath(filteredSourceTree);

        var filteredSourceCompositionId = CommonConversion.StringBase16FromByteArray(Composition.GetHash(filteredSourceComposition));

        Console.WriteLine("Loaded source composition " + filteredSourceCompositionId + " from '" + sourcePath + "'.");

        var configZipArchive =
            BuildConfigurationZipArchive(sourceComposition: filteredSourceComposition);

        return(sourceTree, filteredSourceCompositionId, configZipArchive);
    }
Exemple #4
0
        public ReductionRecord GetReduction(byte[] reducedCompositionHash)
        {
            var reducedCompositionHashBase16 = CommonConversion.StringBase16FromByteArray(reducedCompositionHash);

            var filePath = Path.Combine(ReductionDirectoryPath, reducedCompositionHashBase16);

            if (!File.Exists(filePath))
            {
                return(null);
            }

            var reductionRecordFromFile =
                JsonConvert.DeserializeObject <ReductionRecordInFile>(
                    File.ReadAllText(filePath, Encoding.UTF8));

            if (reducedCompositionHashBase16 != reductionRecordFromFile.ReducedCompositionHashBase16)
            {
                throw new Exception("Unexpected content in file " + filePath + ", composition hash does not match.");
            }

            return(new ReductionRecord
            {
                ReducedCompositionHash = reducedCompositionHash,
                ReducedValueLiteralString = reductionRecordFromFile.ReducedValue?.LiteralString,
            });
        }
Exemple #5
0
        static public void BuildConfiguration(string[] args)
        {
            string argumentValueFromParameterName(string parameterName) =>
            args
            .Select(arg => Regex.Match(arg, parameterName + "=(.*)", RegexOptions.IgnoreCase))
            .FirstOrDefault(match => match.Success)
            ?.Groups[1].Value;

            var outputArgument = argumentValueFromParameterName("--output");

            var loweredElmOutputArgument = argumentValueFromParameterName("--lowered-elm-output");

            var frontendWebElmMakeCommandAppendix = argumentValueFromParameterName("--frontend-web-elm-make-appendix");

            var(configZipArchive, loweredElmAppFiles) = BuildConfigurationZipArchive(frontendWebElmMakeCommandAppendix);

            if (0 < loweredElmOutputArgument?.Length)
            {
                Console.WriteLine("I write the lowered Elm app to '" + loweredElmOutputArgument + "'.");

                foreach (var file in loweredElmAppFiles)
                {
                    var outputPath = Path.Combine(new[] { loweredElmOutputArgument }.Concat(file.Key).ToArray());
                    Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
                    File.WriteAllBytes(outputPath, file.Value.ToArray());
                }
            }

            var configZipArchiveFileId =
                CommonConversion.StringBase16FromByteArray(CommonConversion.HashSHA256(configZipArchive));

            var webAppConfigFileId =
                Composition.GetHash(Composition.FromTree(Composition.TreeFromSetOfBlobsWithCommonOSPath(
                                                             ZipArchive.EntriesFromZipArchive(configZipArchive), System.Text.Encoding.UTF8)));

            Console.WriteLine(
                "I built zip archive " + configZipArchiveFileId + " containing web app config " + webAppConfigFileId + ".");

            if (outputArgument == null)
            {
                Console.WriteLine("I did not see a path for output, so I don't attempt to save the configuration to a file.");
            }
            else
            {
                Directory.CreateDirectory(Path.GetDirectoryName(outputArgument));
                File.WriteAllBytes(outputArgument, configZipArchive);

                Console.WriteLine("I saved zip arcchive " + configZipArchiveFileId + " to '" + outputArgument + "'");
            }
        }
Exemple #6
0
        public ReductionRecord GetReduction(byte[] reducedCompositionHash)
        {
            var reducedCompositionHashBase16 = CommonConversion.StringBase16FromByteArray(reducedCompositionHash);

            var filePath = ImmutableList.Create(reducedCompositionHashBase16);

            var fileContent = reductionFileStoreReader.GetFileContent(filePath);

            if (fileContent == null)
            {
                return(null);
            }

            try
            {
                var payloadStartIndex =

                    /*
                     * Previous implementation used `File.WriteAllText`:
                     * https://github.com/elm-fullstack/elm-fullstack/blob/1cd3f00bdf5a05e9bda479c534b0458b2496393c/implement/PersistentProcess/PersistentProcess.Common/ProcessStore.cs#L183
                     * Looking at the files from stores in production, it seems like that caused addition of BOM.
                     */
                    fileContent.Take(3).SequenceEqual(new byte[] { 0xEF, 0xBB, 0xBF })
                    ?
                    3
                    :
                    0;

                var reductionRecordFromFile =
                    JsonConvert.DeserializeObject <ReductionRecordInFile>(Encoding.UTF8.GetString(fileContent.AsSpan(payloadStartIndex)));

                if (reducedCompositionHashBase16 != reductionRecordFromFile.ReducedCompositionHashBase16)
                {
                    throw new Exception("Unexpected content in file " + string.Join("/", filePath) + ", composition hash does not match.");
                }

                return(new ReductionRecord
                {
                    ReducedCompositionHash = reducedCompositionHash,
                    ReducedValueLiteralString = reductionRecordFromFile.ReducedValue?.LiteralString,
                });
            }
            catch (Exception e)
            {
                throw new Exception("Failed to read reduction from file '" + string.Join("/", filePath) + "'.", e);
            }
        }
Exemple #7
0
        static public void BuildConfiguration(
            string outputOption,
            string loweredElmOutputOption,
            string frontendWebElmMakeCommandAppendixOption,
            Action <string> verboseLogWriteLine)
        {
            var(compileConfigZipArchive, loweredElmAppFiles) =
                Kalmit.PersistentProcess.WebHost.BuildConfigurationFromArguments.BuildConfigurationZipArchive(
                    frontendWebElmMakeCommandAppendixOption, verboseLogWriteLine);

            if (0 < loweredElmOutputOption?.Length)
            {
                Console.WriteLine("I write the lowered Elm app to '" + loweredElmOutputOption + "'.");

                foreach (var file in loweredElmAppFiles)
                {
                    var outputPath = Path.Combine(new[] { loweredElmOutputOption }.Concat(file.Key).ToArray());
                    Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
                    File.WriteAllBytes(outputPath, file.Value.ToArray());
                }
            }

            var configZipArchive = compileConfigZipArchive();

            var configZipArchiveFileId =
                CommonConversion.StringBase16FromByteArray(CommonConversion.HashSHA256(configZipArchive));

            var webAppConfigFileId =
                Composition.GetHash(Composition.FromTree(Composition.TreeFromSetOfBlobsWithCommonFilePath(
                                                             Kalmit.ZipArchive.EntriesFromZipArchive(configZipArchive))));

            Console.WriteLine(
                "I built zip archive " + configZipArchiveFileId + " containing web app config " + webAppConfigFileId + ".");

            if (outputOption == null)
            {
                Console.WriteLine("I did not see a path for output, so I don't attempt to save the configuration to a file.");
            }
            else
            {
                Directory.CreateDirectory(Path.GetDirectoryName(outputOption));
                File.WriteAllBytes(outputOption, configZipArchive);

                Console.WriteLine("I saved zip archive " + configZipArchiveFileId + " to '" + outputOption + "'");
            }
        }
Exemple #8
0
        public void StoreReduction(ReductionRecord record)
        {
            var recordInFile = new ReductionRecordInFile
            {
                ReducedCompositionHashBase16 = CommonConversion.StringBase16FromByteArray(record.ReducedCompositionHash),
                ReducedValue = new ValueInFile {
                    LiteralString = record.ReducedValueLiteralString
                },
            };

            var fileName = recordInFile.ReducedCompositionHashBase16;

            var filePath = ImmutableList.Create(fileName);

            reductionFileStoreWriter.SetFileContent(
                filePath, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(recordInFile, recordSerializationSettings)));
        }
Exemple #9
0
        public void StoreReduction(ReductionRecord record)
        {
            var recordInFile = new ReductionRecordInFile
            {
                ReducedCompositionHashBase16 = CommonConversion.StringBase16FromByteArray(record.ReducedCompositionHash),
                ReducedValue = new ValueInFile {
                    LiteralString = record.ReducedValueLiteralString
                },
            };

            var fileName = recordInFile.ReducedCompositionHashBase16;

            var filePath = Path.Combine(ReductionDirectoryPath, fileName);

            Directory.CreateDirectory(Path.GetDirectoryName(filePath));
            File.WriteAllText(filePath, JsonConvert.SerializeObject(recordInFile, RecordSerializationSettings), Encoding.UTF8);
        }
        WebHostAdminInterfaceTestSetup(
            string testDirectory,
            string adminRootPassword,
            Composition.Component deployAppConfigAndInitElmState,
            Func <IWebHostBuilder, IWebHostBuilder> webHostBuilderMap,
            string adminWebHostUrlOverride,
            string publicWebHostUrlOverride)
        {
            this.testDirectory            = testDirectory;
            this.adminRootPassword        = adminRootPassword ?? "notempty";
            this.webHostBuilderMap        = webHostBuilderMap;
            this.adminWebHostUrlOverride  = adminWebHostUrlOverride;
            this.publicWebHostUrlOverride = publicWebHostUrlOverride;

            if (deployAppConfigAndInitElmState != null)
            {
                var compositionLogEvent =
                    new WebHost.ProcessStoreSupportingMigrations.CompositionLogRecordInFile.CompositionEvent
                {
                    DeployAppConfigAndInitElmAppState =
                        new WebHost.ProcessStoreSupportingMigrations.ValueInFileStructure
                    {
                        HashBase16 = CommonConversion.StringBase16FromByteArray(Composition.GetHash(deployAppConfigAndInitElmState))
                    }
                };

                var processStoreWriter =
                    new WebHost.ProcessStoreSupportingMigrations.ProcessStoreWriterInFileStore(
                        defaultFileStore);

                processStoreWriter.StoreComponent(deployAppConfigAndInitElmState);

                var compositionRecord = new WebHost.ProcessStoreSupportingMigrations.CompositionLogRecordInFile
                {
                    parentHashBase16 = WebHost.ProcessStoreSupportingMigrations.CompositionLogRecordInFile.compositionLogFirstRecordParentHashBase16,
                    compositionEvent = compositionLogEvent
                };

                var serializedCompositionLogRecord =
                    WebHost.ProcessStoreSupportingMigrations.ProcessStoreInFileStore.Serialize(compositionRecord);

                processStoreWriter.SetCompositionLogHeadRecord(serializedCompositionLogRecord);
            }
        }
Exemple #11
0
    WebHostAdminInterfaceTestSetup(
        string testDirectory,
        string?adminPassword,
        IFileStore?fileStore,
        Composition.Component?deployAppConfigAndInitElmState,
        Func <IWebHostBuilder, IWebHostBuilder>?webHostBuilderMap,
        string?adminWebHostUrlOverride,
        string?publicWebHostUrlOverride,
        Func <DateTimeOffset>?persistentProcessHostDateTime = null)
    {
        this.testDirectory = testDirectory;

        fileStore ??= defaultFileStore;

        this.adminPassword            = adminPassword ?? "notempty";
        this.fileStore                = fileStore;
        this.webHostBuilderMap        = webHostBuilderMap;
        this.adminWebHostUrlOverride  = adminWebHostUrlOverride;
        this.publicWebHostUrlOverride = publicWebHostUrlOverride;

        if (deployAppConfigAndInitElmState != null)
        {
            var compositionLogEvent =
                new ElmFullstack.WebHost.ProcessStoreSupportingMigrations.CompositionLogRecordInFile.CompositionEvent
            {
                DeployAppConfigAndInitElmAppState =
                    new ElmFullstack.WebHost.ProcessStoreSupportingMigrations.ValueInFileStructure
                {
                    HashBase16 = CommonConversion.StringBase16FromByteArray(Composition.GetHash(deployAppConfigAndInitElmState))
                }
            };

            var processStoreWriter =
                new ElmFullstack.WebHost.ProcessStoreSupportingMigrations.ProcessStoreWriterInFileStore(
                    fileStore,
                    getTimeForCompositionLogBatch: persistentProcessHostDateTime ?? (() => DateTimeOffset.UtcNow),
                    fileStore);

            processStoreWriter.StoreComponent(deployAppConfigAndInitElmState);

            processStoreWriter.AppendCompositionLogRecord(compositionLogEvent);
        }
    }
        public (byte[] serializedCompositionRecord, byte[] serializedCompositionRecordHash) SetState(string state)
        {
            lock (process)
            {
                process.SetSerializedState(state);

                var compositionRecord = new CompositionRecordInFile
                {
                    ParentHashBase16 = CommonConversion.StringBase16FromByteArray(lastStateHash),
                    SetState         = new ValueInFile {
                        LiteralString = state
                    },
                };

                var serializedCompositionRecord =
                    Encoding.UTF8.GetBytes(Serialize(compositionRecord));

                var compositionHash = CompositionRecordInFile.HashFromSerialRepresentation(serializedCompositionRecord);

                lastStateHash = compositionHash;

                return(serializedCompositionRecord, compositionHash);
            }
        }
Exemple #13
0
    public void Composition_from_link_in_elm_editor()
    {
        var testCases = new[]
        {
            new
            {
                input = "https://elm-editor.com/?project-state=https%3A%2F%2Fgithub.com%2Felm-fullstack%2Felm-fullstack%2Ftree%2F742650b6a6f1e3dc723d76fbb8c189ca16a0bee6%2Fimplement%2Fexample-apps%2Felm-editor%2Fdefault-app&file-path-to-open=src%2FMain.elm",
                expectedCompositionId = "ba36b62d7a0e2ffd8ed107782138be0e2b25257a67dc9273508b00daa003b6f3"
            },
            new
            {
                input = "https://elm-editor.com/?project-state-deflate-base64=XZDLasMwEEX%2FZdZO5Ecip97FLYVQWmi3RgQ9xg9qW0aSQ4vRv9cyZNHsZg5zzwyzwA2N7fR4TeM0ucYJFAsIbhEKaJ2bbEFI07l2FnupB4L9sKvnvreOy%2B%2BHzhlEkh9SeowF5bROMFMyTzOV01qIk0xOT5InlMcCkZJumHoccHQEf3iod3ya7KZE1TltiMKaz70LHCJQXV2jwVHiq9FDuV24gMFB3%2FBDK7RQVCwC2fKxwbLXIoCqAmvkmn7n3bhf3cCiaoEvnC2Wv24LHbOc%2BSjAoLpTurGzUnewNjZspYf1M5fnc3N5%2BXwDv4399x0z5hlj3vs%2F&file-path-to-open=src%2FMain.elm",
                expectedCompositionId = "c34a6a5e4ee0ea6308c9965dfbfbe68d28ecc07dca1cba8f9a2dac50700324e9"
            },
            new
            {
                input = "https://elm-editor.com/?project-state-deflate-base64=dZDJasMwFEX%2FRWsnnmJ5gC6SDhBKQ9NFQ2pM0PDsmNqWkeTQYPzvtUwDbUl2ege9ew%2BvRyeQqhTNwXM89%2BC4KOkRJQpQgo5atyqx7aLUx47OmahtqOpZ3lWV0oR9%2Fpu0BLDDhYcDh2KCcxd8zkLP5yHOKY2YG8WMuJg4FADbZd1WUEOjbfgi5j0jbaumSOClFtLmkJOu0oYjC%2FEyz0FCw%2BBJino1GfZIQi1OsBEcFErSzELsSJoCVpWgBqQpUpKN2y%2BkbOZjNsqstEdv0ClYnfW0FPhhNlgGmqgLxRNbcn4B46BMK16Ml1nfL4v1w%2FYZDdO3v3mBnw3Z2HPpfq3IuZCia%2FgNgzCOfmJ%2BGywmdssAzm60ftyI%2FS5oPnbbYu%2FFmu7eO74Ud9es%2FDgOAuOVDcPwDQ%3D%3D&file-path-to-open=src%2FMain.elm",
                expectedCompositionId = "037e66cbbb5c06cecb6760efcbb0ad4c7b8e4ed036f331e1a28c897839dd55b1"
            },
            new
            {
                input = "https://elm-editor.com/?project-state-deflate-base64=jY%2FNasMwEITfZc9OJMuO3RhyiAs9lUIbSH%2BMCJa9tgWWFSQ5UIzevUqh0F7a3nZnZ7%2BdXeCCxko9nRhl8YnGUCwgaotQwODc2RaE9NINs1g3WpGjlIYI7SxxBpFkaZZ3adZkLBFxnG5YxyiNRUO7JKdZl28pbWOW3xCpziMqnBxxGMraoSUmCEqgWQXeyqJzcuotRNDKrkODU4N3RqvyM8sCwa0v%2BKBbtFBUPIJmqKcey1GLq1BVUGq3xlEBj6oFnnC2WL6HO2G4yRPuowX2bfslhcZe0VkaHr3tdzvwv1ruD7R%2FPJTz20vTv7KtE8%2FHud3rv9b%2BQf5m%2BRGaJTTlnnPuvf8A&file-path-to-open=Bot.elm",
                expectedCompositionId = "3fe337ab4616321a2b761bd59f9e6e7111d43f5877abee1664e82fda47d3458b",
            }
        };

        foreach (var testCase in testCases)
        {
            try
            {
                var loadCompositionResult =
                    LoadComposition.LoadFromPathResolvingNetworkDependencies(testCase.input).LogToList();

                if (loadCompositionResult.result.Ok.tree == null)
                {
                    throw new Exception("Failed to load from path: " + loadCompositionResult.result.Err);
                }

                var inspectComposition =
                    loadCompositionResult.result.Ok.tree.EnumerateBlobsTransitive()
                    .Select(blobAtPath =>
                {
                    string?utf8 = null;

                    try
                    {
                        utf8 = System.Text.Encoding.UTF8.GetString(blobAtPath.blobContent.ToArray());
                    }
                    catch { }

                    return
                    (new
                    {
                        blobAtPath.path,
                        blobAtPath.blobContent,
                        utf8
                    });
                })
                    .ToImmutableList();

                var composition   = Composition.FromTreeWithStringPath(loadCompositionResult.result.Ok.tree) !;
                var compositionId = CommonConversion.StringBase16FromByteArray(Composition.GetHash(composition));

                Assert.AreEqual(testCase.expectedCompositionId, compositionId);
            }
            catch (Exception e)
            {
                throw new Exception("Failed in test case " + testCase.input, e);
            }
        }
    }
Exemple #14
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;
        });
    }
Exemple #15
0
        public void ConfigureServices(IServiceCollection services)
        {
            var serviceProvider = services.BuildServiceProvider();
            var config          = serviceProvider.GetService <IConfiguration>();

            Composition.Component webAppConfig = null;

            {
                byte[] webAppConfigFileZipArchive = null;

                var webAppConfigurationFilePath = config.GetValue <string>(Configuration.WebAppConfigurationFilePathSettingKey);

                if (0 < webAppConfigurationFilePath?.Length)
                {
                    _logger.LogInformation(
                        "Loading configuration from single file '" + webAppConfigurationFilePath + "'.");

                    webAppConfigFileZipArchive = System.IO.File.ReadAllBytes(webAppConfigurationFilePath);
                }
                else
                {
                    _logger.LogInformation(
                        "Loading configuration from current directory.");

                    var(configZipArchive, _) = BuildConfigurationFromArguments.BuildConfigurationZipArchive(null);

                    webAppConfigFileZipArchive = configZipArchive;
                }

                webAppConfig = Composition.FromTree(Composition.TreeFromSetOfBlobsWithCommonOSPath(
                                                        ZipArchive.EntriesFromZipArchive(webAppConfigFileZipArchive), System.Text.Encoding.UTF8));
            }

            _logger.LogInformation("Loaded configuration " +
                                   CommonConversion.StringBase16FromByteArray(Composition.GetHash(webAppConfig)));

            var webAppConfigObject = WebAppConfiguration.FromFiles(
                Composition.ParseAsTree(webAppConfig).ok.EnumerateBlobsTransitive()
                .Select(blobWithPath =>
                        (path: (IImmutableList <string>)blobWithPath.path.Select(pathComponent => System.Text.Encoding.UTF8.GetString(pathComponent.ToArray())).ToImmutableList(),
                         content: blobWithPath.blobContent))
                .ToList());

            services.AddSingleton <WebAppConfiguration>(webAppConfigObject);

            var getDateTimeOffset = serviceProvider.GetService <Func <DateTimeOffset> >();

            if (getDateTimeOffset == null)
            {
                getDateTimeOffset = () => DateTimeOffset.UtcNow;
                services.AddSingleton <Func <DateTimeOffset> >(getDateTimeOffset);
            }

            var processStoreDirectory = config.GetValue <string>(Configuration.ProcessStoreDirectoryPathSettingKey);
            var processStore          = new Kalmit.ProcessStore.ProcessStoreInFileDirectory(
                processStoreDirectory,
                () =>
            {
                var time          = getDateTimeOffset();
                var directoryName = time.ToString("yyyy-MM-dd");
                return(System.IO.Path.Combine(directoryName, directoryName + "T" + time.ToString("HH") + ".composition.jsonl"));
            });

            services.AddSingleton <ProcessStore.IProcessStoreReader>(processStore);
            services.AddSingleton <ProcessStore.IProcessStoreWriter>(processStore);
            services.AddSingleton <IPersistentProcess>(BuildPersistentProcess);

            var letsEncryptOptions = webAppConfigObject?.JsonStructure?.letsEncryptOptions;

            if (letsEncryptOptions == null)
            {
                _logger.LogInformation("I did not find 'letsEncryptOptions' in the configuration. I continue without Let's Encrypt.");
            }
            else
            {
                _logger.LogInformation("I found 'letsEncryptOptions' in the configuration.");
                services.AddFluffySpoonLetsEncryptRenewalService(letsEncryptOptions);
                services.AddFluffySpoonLetsEncryptFileCertificatePersistence();
                services.AddFluffySpoonLetsEncryptMemoryChallengePersistence();
            }

            Asp.ConfigureServices(services);
        }
Exemple #16
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;
            });
        }
        public PersistentProcessWithHistoryOnFileFromElm019Code(
            IProcessStoreReader storeReader,
            IImmutableDictionary <IImmutableList <string>, IImmutableList <byte> > elmAppFiles,
            Action <string> logger,
            ElmAppInterfaceConfig?overrideElmAppInterfaceConfig = null)
        {
            (process, (JavascriptFromElmMake, JavascriptPreparedToRun)) =
                ProcessFromElm019Code.ProcessFromElmCodeFiles(elmAppFiles, overrideElmAppInterfaceConfig);

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

            logger?.Invoke("Begin to restore the process state using the storeReader.");

            var emptyInitHash = CompositionRecordInFile.HashFromSerialRepresentation(new byte[0]);

            string dictKeyForHash(byte[] hash) => Convert.ToBase64String(hash);

            var compositionRecords = new Dictionary <string, (byte[] compositionRecordHash, CompositionRecord compositionRecord)>();

            var compositionChain = new Stack <(byte[] hash, CompositionRecord composition)>();

            foreach (var serializedCompositionRecord in storeReader.EnumerateSerializedCompositionsRecordsReverse())
            {
                {
                    var compositionRecordFromFile = JsonConvert.DeserializeObject <CompositionRecordInFile>(
                        System.Text.Encoding.UTF8.GetString(serializedCompositionRecord));

                    var compositionRecordHash = CompositionRecordInFile.HashFromSerialRepresentation(serializedCompositionRecord);

                    var compositionRecord =
                        new CompositionRecord
                    {
                        ParentHash =
                            CommonConversion.ByteArrayFromStringBase16(compositionRecordFromFile.ParentHashBase16),

                        SetStateLiteralString = compositionRecordFromFile.SetState?.LiteralString,

                        AppendedEventsLiteralString =
                            compositionRecordFromFile.AppendedEvents?.Select(@event => @event.LiteralString)?.ToImmutableList(),
                    };

                    var compositionChainElement = (compositionRecordHash, compositionRecord);

                    if (!compositionChain.Any())
                    {
                        compositionChain.Push(compositionChainElement);
                    }
                    else
                    {
                        compositionRecords[dictKeyForHash(compositionRecordHash)] = compositionChainElement;
                    }
                }

                while (true)
                {
                    var(compositionRecordHash, compositionRecord) = compositionChain.Peek();

                    var reduction = storeReader.GetReduction(compositionRecordHash);

                    if (reduction != null || emptyInitHash.SequenceEqual(compositionRecord.ParentHash))
                    {
                        if (reduction != null)
                        {
                            compositionChain.Pop();
                            process.SetSerializedState(reduction.ReducedValueLiteralString);
                            lastStateHash = reduction.ReducedCompositionHash;
                        }

                        foreach (var followingComposition in compositionChain)
                        {
                            if (followingComposition.composition.SetStateLiteralString != null)
                            {
                                process.SetSerializedState(followingComposition.composition.SetStateLiteralString);
                            }

                            foreach (var appendedEvent in followingComposition.composition.AppendedEventsLiteralString.EmptyIfNull())
                            {
                                process.ProcessEvent(appendedEvent);
                            }

                            lastStateHash = followingComposition.hash;
                        }

                        logger?.Invoke("Restored the process state in " + ((int)restoreStopwatch.Elapsed.TotalSeconds) + " seconds.");
                        return;
                    }

                    var parentKey = dictKeyForHash(compositionRecord.ParentHash);

                    if (!compositionRecords.TryGetValue(parentKey, out var compositionChainElementFromPool))
                    {
                        break;
                    }

                    compositionChain.Push(compositionChainElementFromPool);
                    compositionRecords.Remove(parentKey);
                }
            }

            if (compositionChain.Any())
            {
                throw new NotImplementedException(
                          "I did not find a reduction for any composition on the chain to the last composition (" +
                          CommonConversion.StringBase16FromByteArray(compositionChain.Last().hash) +
                          ").");
            }

            logger?.Invoke("Found no composition record, default to initial state.");

            lastStateHash = emptyInitHash;
        }