├── src ├── Build │ ├── package.json │ ├── update-project-version.js │ └── update-project-deps-version.js ├── MysticMind.PostgresEmbed │ ├── Architecture.cs │ ├── Platform.cs │ ├── UnsupportedPlatformException.cs │ ├── PostgresEmbedException.cs │ ├── PgExtensionConfig.cs │ ├── MysticMind.PostgresEmbed.csproj │ ├── PgExtension.cs │ ├── DefaultPostgresBinaryDownloader.cs │ ├── Utils.cs │ └── PgServer.cs ├── MysticMind.PostgresEmbed.Tests │ ├── MysticMind.PostgresEmbed.Tests.csproj │ ├── PgServer_Tests.cs │ └── Pgserver_Async_Tests.cs └── MysticMind.PostgresEmbed.sln ├── .github ├── FUNDING.yml └── workflows │ ├── nuget-publish.yaml │ └── ci.yaml ├── LICENSE ├── .gitignore └── README.md /src/Build/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "jsonfile": "^2.3.1" 4 | } 5 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: https://github.com/sponsors/mysticmind 3 | -------------------------------------------------------------------------------- /src/MysticMind.PostgresEmbed/Architecture.cs: -------------------------------------------------------------------------------- 1 | namespace MysticMind.PostgresEmbed; 2 | 3 | public enum Architecture 4 | { 5 | Amd64, 6 | Arm64V8, 7 | // Ppc64Le 8 | } -------------------------------------------------------------------------------- /src/MysticMind.PostgresEmbed/Platform.cs: -------------------------------------------------------------------------------- 1 | namespace MysticMind.PostgresEmbed; 2 | 3 | public enum Platform 4 | { 5 | Windows, 6 | Linux, 7 | Darwin, 8 | // AlpineLinux, 9 | // AlpineLiteLinux 10 | } -------------------------------------------------------------------------------- /src/MysticMind.PostgresEmbed/UnsupportedPlatformException.cs: -------------------------------------------------------------------------------- 1 | namespace MysticMind.PostgresEmbed; 2 | 3 | public class UnsupportedPlatformException : PostgresEmbedException 4 | { 5 | public UnsupportedPlatformException(): base("Unsupported OS platform") 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /src/MysticMind.PostgresEmbed/PostgresEmbedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace MysticMind.PostgresEmbed; 5 | 6 | public class PostgresEmbedException : Exception 7 | { 8 | public PostgresEmbedException() 9 | { 10 | } 11 | 12 | public PostgresEmbedException(string message): base(message) 13 | { 14 | } 15 | 16 | public PostgresEmbedException(string message, Exception innerException): base(message, innerException) 17 | { 18 | } 19 | } -------------------------------------------------------------------------------- /src/MysticMind.PostgresEmbed/PgExtensionConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace MysticMind.PostgresEmbed; 5 | 6 | public class PgExtensionConfig 7 | { 8 | public PgExtensionConfig(string downloadUrl) 9 | { 10 | if (string.IsNullOrEmpty(downloadUrl)) 11 | { 12 | throw new ArgumentException("downloadUrl is required"); 13 | } 14 | 15 | DownloadUrl = downloadUrl; 16 | } 17 | 18 | public string DownloadUrl { get; private set; } 19 | 20 | public List CreateExtensionSqlList { get; } = new List(); 21 | } -------------------------------------------------------------------------------- /src/Build/update-project-version.js: -------------------------------------------------------------------------------- 1 | var jsonfile = require('jsonfile'); 2 | 3 | // Read in the file to be patched 4 | var file = process.argv[2]; 5 | if (!file) 6 | console.log("No filename provided"); 7 | console.log("File: " + file); 8 | 9 | // Read in the build version (this might be provided by the CI server) 10 | var version = process.argv[3]; 11 | if (!version) 12 | console.log("No version provided"); 13 | 14 | jsonfile.readFile(file, function (err, project) { 15 | project.version = version; 16 | jsonfile.writeFile(file, project, { spaces: 2 }, function (err) { 17 | if (err) 18 | console.error(err); 19 | else 20 | console.log("Project version succesfully set."); 21 | }); 22 | }) -------------------------------------------------------------------------------- /src/Build/update-project-deps-version.js: -------------------------------------------------------------------------------- 1 | var jsonfile = require('jsonfile'); 2 | 3 | // Read in the file to be patched 4 | var file = process.argv[2]; 5 | if (!file) 6 | console.log("No filename provided"); 7 | console.log("File: " + file); 8 | 9 | var dep = process.argv[3]; 10 | if (!dep) 11 | console.log("No dependency name provided"); 12 | console.log("Dep: " + dep); 13 | 14 | // Read in the build version (this might be provided by the CI server) 15 | var version = process.argv[4]; 16 | if (!version) 17 | console.log("No version provided"); 18 | 19 | jsonfile.readFile(file, function (err, project) { 20 | project.dependencies[dep] = version; 21 | jsonfile.writeFile(file, project, { spaces: 2 }, function (err) { 22 | if (err) 23 | console.error(err); 24 | else 25 | console.log("Project dep version " + dep + "=" + version + " succesfully set."); 26 | }); 27 | }) -------------------------------------------------------------------------------- /.github/workflows/nuget-publish.yaml: -------------------------------------------------------------------------------- 1 | name: NuGet Manual Publish 2 | 3 | on: [workflow_dispatch] 4 | 5 | env: 6 | config: Release 7 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 8 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 9 | 10 | jobs: 11 | publish_job: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Install .NET 8.0.x 19 | uses: actions/setup-dotnet@v4 20 | with: 21 | dotnet-version: 9.0.x 22 | 23 | - name: Run Pack 24 | run: dotnet pack src/MysticMind.PostgresEmbed/MysticMind.PostgresEmbed.csproj -c Release 25 | shell: bash 26 | 27 | - name: Publish to NuGet 28 | run: | 29 | find . -name '*.nupkg' -exec dotnet nuget push "{}" -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} --skip-duplicate \; 30 | # find . -name '*.snupkg' -exec dotnet nuget push "{}" -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} \; 31 | shell: bash 32 | 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | env: 11 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 12 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 13 | 14 | jobs: 15 | job: 16 | strategy: 17 | matrix: 18 | include: 19 | - os: ubuntu-latest 20 | artifact-name: Linux 21 | - os: macos-13 22 | artifact-name: Darwin 23 | - os: windows-2022 24 | artifact-name: Win64 25 | runs-on: ${{ matrix.os }} 26 | continue-on-error: true 27 | steps: 28 | - name: checkout repo 29 | uses: actions/checkout@v4 30 | - name: Install .NET 9.0.x 31 | uses: actions/setup-dotnet@v4 32 | with: 33 | dotnet-version: 9.0.x 34 | - name: Display dotnet info 35 | run: dotnet --list-sdks 36 | - name: Run tests 37 | run: dotnet test src/MysticMind.PostgresEmbed.Tests/MysticMind.PostgresEmbed.Tests.csproj --framework net9.0 38 | 39 | -------------------------------------------------------------------------------- /src/MysticMind.PostgresEmbed.Tests/MysticMind.PostgresEmbed.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net7.0;net8.0;net9.0 5 | MysticMind.PostgresEmbed.Tests 6 | MysticMind.PostgresEmbed.Tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Babu Annamalai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/MysticMind.PostgresEmbed/MysticMind.PostgresEmbed.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Postgres embedded server equivalent for .Net applications 5 | Copyright (C) Babu Annamalai 6 | MysticMind.PostgresEmbed 7 | 4.1.0 8 | Babu Annamalai 9 | net6.0;net7.0;net8.0;net9.0 10 | MysticMind.PostgresEmbed 11 | MysticMind.PostgresEmbed 12 | postgres;postgresql;pg;embed;embedded;server;.net 4.6;net 4.6;net46;.net core;net core;netcore;netcore 2.0;netcore rtm 13 | https://github.com/mysticmind/mysticmind-postgresembed 14 | MIT 15 | https://github.com/mysticmind/mysticmind-postgresembed 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/MysticMind.PostgresEmbed.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27703.2026 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MysticMind.PostgresEmbed.Tests", "MysticMind.PostgresEmbed.Tests\MysticMind.PostgresEmbed.Tests.csproj", "{3D30ABC5-3956-4537-98E0-7DF5B147449C}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MysticMind.PostgresEmbed", "MysticMind.PostgresEmbed\MysticMind.PostgresEmbed.csproj", "{261AE1C4-6B77-4999-844C-8F3B9F096F43}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1D39C306-6292-4354-91B9-2D124A9EE563}" 11 | ProjectSection(SolutionItems) = preProject 12 | ..\README.md = ..\README.md 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {3D30ABC5-3956-4537-98E0-7DF5B147449C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {3D30ABC5-3956-4537-98E0-7DF5B147449C}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {3D30ABC5-3956-4537-98E0-7DF5B147449C}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {3D30ABC5-3956-4537-98E0-7DF5B147449C}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {261AE1C4-6B77-4999-844C-8F3B9F096F43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {261AE1C4-6B77-4999-844C-8F3B9F096F43}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {261AE1C4-6B77-4999-844C-8F3B9F096F43}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {261AE1C4-6B77-4999-844C-8F3B9F096F43}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {7B918296-F47A-4F90-8976-04B2E460E154} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /src/MysticMind.PostgresEmbed/PgExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace MysticMind.PostgresEmbed; 9 | 10 | internal class PgExtension 11 | { 12 | private readonly string _binariesDir; 13 | private readonly string _pgDir; 14 | private readonly PgExtensionConfig _config; 15 | private readonly string _filename; 16 | 17 | public PgExtension( 18 | string binariesDir, 19 | string pgDir, 20 | PgExtensionConfig config) 21 | { 22 | _binariesDir = binariesDir; 23 | _pgDir = pgDir; 24 | _config = config; 25 | 26 | _filename = Path.GetFileName(_config.DownloadUrl); 27 | } 28 | 29 | public string Download() 30 | { 31 | var zipFile = Path.Combine(_binariesDir, _filename); 32 | 33 | // check if zip file exists in the destination folder 34 | // return the file path and don't require to download again 35 | if (File.Exists(zipFile)) 36 | { 37 | return zipFile; 38 | } 39 | 40 | var progress = new Progress(); 41 | progress.ProgressChanged += (_, value) => Console.WriteLine("\r %{0:N0}", value); 42 | 43 | try 44 | { 45 | // download the file 46 | Utils.Download(_config.DownloadUrl, zipFile, progress); 47 | } 48 | catch (Exception ex) 49 | { 50 | throw new Exception($"Failed to download {_config.DownloadUrl}", ex); 51 | } 52 | 53 | return zipFile; 54 | } 55 | 56 | public async Task DownloadAsync() 57 | { 58 | var zipFile = Path.Combine(_binariesDir, _filename); 59 | 60 | // check if zip file exists in the destination folder 61 | // return the file path and don't require to download again 62 | if (File.Exists(zipFile)) 63 | { 64 | return zipFile; 65 | } 66 | 67 | var progress = new Progress(); 68 | progress.ProgressChanged += (_, value) => Console.WriteLine("\r %{0:N0}", value); 69 | 70 | try 71 | { 72 | // download the file 73 | var cs = new CancellationTokenSource(); 74 | await Utils.DownloadAsync(_config.DownloadUrl, zipFile, progress, cs.Token); 75 | } 76 | catch (Exception ex) 77 | { 78 | throw new Exception($"Failed to download {_config.DownloadUrl}", ex); 79 | } 80 | 81 | return zipFile; 82 | } 83 | 84 | public void Extract() 85 | { 86 | var zipFile = Path.Combine(_binariesDir, _filename); 87 | 88 | // some extensions such as plv8 hs a container folder 89 | // when we extract the binary archive, it get extracted with the container folder 90 | // we want the contents without the container folder for the extensions to install properly 91 | var containerFolderInBinary = GetContainerFolderInBinary(zipFile); 92 | 93 | var ignoreRootFolder = !string.IsNullOrEmpty(containerFolderInBinary); 94 | 95 | Utils.ExtractZipFolder(zipFile, _pgDir, containerFolderInBinary, ignoreRootFolder); 96 | } 97 | 98 | private static string GetContainerFolderInBinary(string zipFile) 99 | { 100 | //some of the extension binaries may have a root folder which need to be ignored while extracting content 101 | var containerFolder = ""; 102 | 103 | using var archive = ZipFile.OpenRead(zipFile); 104 | var result = 105 | from entry in archive.Entries 106 | where 107 | entry.FullName.EndsWith("/bin/") || 108 | entry.FullName.EndsWith("/lib/") || 109 | entry.FullName.EndsWith("/share/") 110 | select entry; 111 | 112 | var item = result.FirstOrDefault(); 113 | 114 | if (item == null) return containerFolder; 115 | var parts = item.FullName.Split('/'); 116 | if (parts.Length > 1) 117 | { 118 | containerFolder = parts[0]; 119 | } 120 | 121 | return containerFolder; 122 | } 123 | } -------------------------------------------------------------------------------- /src/MysticMind.PostgresEmbed/DefaultPostgresBinaryDownloader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace MysticMind.PostgresEmbed; 9 | 10 | internal class DefaultPostgresBinaryDownloader 11 | { 12 | private readonly string _pgVersion; 13 | private readonly Platform _platform; 14 | private readonly Architecture? _architecture; 15 | private readonly string _destDir; 16 | private readonly string _mavenRepo; 17 | 18 | /// 19 | /// The default Postgres binary downloader uses https://github.com/zonkyio/embedded-postgres-binaries 20 | /// for downloading binary for different platform and architecture 21 | /// 22 | /// 23 | /// 24 | /// 25 | /// 26 | /// 27 | public DefaultPostgresBinaryDownloader(string pgVersion, string destDir, Platform platform, Architecture? architecture, string mavenRepo) 28 | { 29 | _destDir = destDir; 30 | _pgVersion = pgVersion; 31 | _platform = platform; 32 | _architecture = architecture; 33 | _mavenRepo = mavenRepo; 34 | } 35 | 36 | public string Download() 37 | { 38 | var platform = _platform.ToString().ToLowerInvariant(); 39 | var architecture = _architecture?.ToString().ToLowerInvariant(); 40 | 41 | if (platform == "alpine") 42 | { 43 | platform = "linux"; 44 | architecture = "alpine"; 45 | } else if (platform == "alpinelitelinux") 46 | { 47 | platform = "linux"; 48 | architecture = "alpine-lite"; 49 | } 50 | 51 | var downloadUrl = $"{_mavenRepo}/io/zonky/test/postgres/embedded-postgres-binaries-{platform}-{architecture}/{_pgVersion}/embedded-postgres-binaries-{platform}-{architecture}-{_pgVersion}.jar"; 52 | var fileName = Path.GetFileName(downloadUrl); 53 | var zipFile = Path.Join(_destDir, Path.GetFileNameWithoutExtension(fileName) + ".txz"); 54 | 55 | // check if txz file exists in the destination folder 56 | // return the file path and don't require to download again 57 | if (File.Exists(zipFile)) 58 | { 59 | return zipFile; 60 | } 61 | 62 | var progress = new Progress(); 63 | progress.ProgressChanged += (_, value) => Console.WriteLine("\r %{0:N0}", value); 64 | var downloadPath = Path.Combine(_destDir, $"embedded-postgres-binaries-{platform}-{architecture}-{_pgVersion}.jar"); 65 | Utils.Download(downloadUrl, downloadPath, progress); 66 | return ExtractContent(downloadPath, zipFile); 67 | } 68 | 69 | public async Task DownloadAsync() 70 | { 71 | var platform = _platform.ToString().ToLowerInvariant(); 72 | var architecture = _architecture?.ToString().ToLowerInvariant(); 73 | 74 | if (platform == "alpine") 75 | { 76 | platform = "linux"; 77 | architecture = "alpine"; 78 | } else if (platform == "alpinelitelinux") 79 | { 80 | platform = "linux"; 81 | architecture = "alpine-lite"; 82 | } 83 | 84 | var downloadUrl = $"{_mavenRepo}/io/zonky/test/postgres/embedded-postgres-binaries-{platform}-{architecture}/{_pgVersion}/embedded-postgres-binaries-{platform}-{architecture}-{_pgVersion}.jar"; 85 | var fileName = Path.GetFileName(downloadUrl); 86 | var zipFile = Path.Join(_destDir, Path.GetFileNameWithoutExtension(fileName) + ".txz"); 87 | 88 | // check if txz file exists in the destination folder 89 | // return the file path and don't require to download again 90 | if (File.Exists(zipFile)) 91 | { 92 | return zipFile; 93 | } 94 | 95 | var cts = new CancellationTokenSource(); 96 | var progress = new Progress(); 97 | progress.ProgressChanged += (_, value) => Console.WriteLine("\r %{0:N0}", value); 98 | var downloadPath = Path.Combine(_destDir, $"embedded-postgres-binaries-{platform}-{architecture}-{_pgVersion}.jar"); 99 | await Utils.DownloadAsync(downloadUrl, downloadPath, progress, cts.Token); 100 | return ExtractContent(downloadPath, zipFile); 101 | } 102 | 103 | private static string ExtractContent(string zipFile, string outputFile) 104 | { 105 | // extract the PG binary zip file 106 | using var archive = ZipFile.OpenRead(zipFile); 107 | var result = from entry in archive.Entries 108 | where Path.GetExtension(entry.FullName) == ".txz" 109 | where !string.IsNullOrEmpty(entry.Name) 110 | select entry; 111 | 112 | var pgBinaryTxzFile = result.FirstOrDefault(); 113 | 114 | if (pgBinaryTxzFile == null) return string.Empty; 115 | pgBinaryTxzFile.ExtractToFile(outputFile, true); 116 | 117 | return outputFile; 118 | } 119 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | *.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.pfx 193 | *.publishsettings 194 | node_modules/ 195 | orleans.codegen.cs 196 | 197 | # Since there are multiple workflows, uncomment next line to ignore bower_components 198 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 199 | #bower_components/ 200 | 201 | # RIA/Silverlight projects 202 | Generated_Code/ 203 | 204 | # Backup & report files from converting an old project file 205 | # to a newer Visual Studio version. Backup files are not needed, 206 | # because we have git ;-) 207 | _UpgradeReport_Files/ 208 | Backup*/ 209 | UpgradeLog*.XML 210 | UpgradeLog*.htm 211 | 212 | # SQL Server files 213 | *.mdf 214 | *.ldf 215 | 216 | # Business Intelligence projects 217 | *.rdl.data 218 | *.bim.layout 219 | *.bim_*.settings 220 | 221 | # Microsoft Fakes 222 | FakesAssemblies/ 223 | 224 | # GhostDoc plugin setting file 225 | *.GhostDoc.xml 226 | 227 | # Node.js Tools for Visual Studio 228 | .ntvs_analysis.dat 229 | 230 | # Visual Studio 6 build log 231 | *.plg 232 | 233 | # Visual Studio 6 workspace options file 234 | *.opt 235 | 236 | # Visual Studio LightSwitch build output 237 | **/*.HTMLClient/GeneratedArtifacts 238 | **/*.DesktopClient/GeneratedArtifacts 239 | **/*.DesktopClient/ModelManifest.xml 240 | **/*.Server/GeneratedArtifacts 241 | **/*.Server/ModelManifest.xml 242 | _Pvt_Extensions 243 | 244 | # Paket dependency manager 245 | .paket/paket.exe 246 | paket-files/ 247 | 248 | # FAKE - F# Make 249 | .fake/ 250 | 251 | # JetBrains Rider 252 | .idea/ 253 | *.sln.iml 254 | 255 | # ignore folder used for running tests 256 | pg_embed 257 | 258 | **/.DS_Store 259 | src/MysticMind.PostgresEmbed.sln.DotSettings 260 | -------------------------------------------------------------------------------- /src/MysticMind.PostgresEmbed/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.IO; 6 | using System.Net.Http; 7 | using System.Threading; 8 | using System.Net.NetworkInformation; 9 | using System.IO.Compression; 10 | using System.Text; 11 | using System.Diagnostics; 12 | using SharpCompress.Common; 13 | using SharpCompress.Readers; 14 | using System.Runtime.InteropServices; 15 | 16 | namespace MysticMind.PostgresEmbed; 17 | 18 | internal class ProcessResult 19 | { 20 | public int ExitCode { get; init; } 21 | 22 | public string Output { get; init; } 23 | 24 | public string Error { get; init; } 25 | } 26 | 27 | internal static class Utils 28 | { 29 | public static async Task DownloadAsync(string url, string downloadFullPath, IProgress progress, CancellationToken token) 30 | { 31 | var client = new HttpClient(); 32 | 33 | using HttpResponseMessage response = client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token).Result; 34 | response.EnsureSuccessStatusCode(); 35 | 36 | await using Stream contentStream = await response.Content.ReadAsStreamAsync(token), fileStream = new FileStream(downloadFullPath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true); 37 | var totalRead = 0L; 38 | var totalReads = 0L; 39 | var buffer = new byte[8192]; 40 | var isMoreToRead = true; 41 | 42 | do 43 | { 44 | var read = await contentStream.ReadAsync(buffer, token); 45 | if (read == 0) 46 | { 47 | isMoreToRead = false; 48 | } 49 | else 50 | { 51 | await fileStream.WriteAsync(buffer.AsMemory(0, read), token); 52 | 53 | totalRead += read; 54 | totalReads += 1; 55 | 56 | if (totalReads % 2000 == 0) 57 | { 58 | Console.WriteLine($"total bytes downloaded so far: {totalRead:n0}"); 59 | } 60 | } 61 | } 62 | while (isMoreToRead); 63 | } 64 | 65 | public static void Download(string url, string downloadFullPath, IProgress progress) 66 | { 67 | var client = new HttpClient(); 68 | 69 | using var response = client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None).Result; 70 | response.EnsureSuccessStatusCode(); 71 | 72 | using Stream contentStream = response.Content.ReadAsStream(), fileStream = new FileStream(downloadFullPath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true); 73 | var totalRead = 0L; 74 | var totalReads = 0L; 75 | var buffer = new byte[8192]; 76 | var isMoreToRead = true; 77 | 78 | do 79 | { 80 | var read = contentStream.Read(buffer, 0, buffer.Length); 81 | if (read == 0) 82 | { 83 | isMoreToRead = false; 84 | } 85 | else 86 | { 87 | fileStream.Write(buffer.AsSpan(0, read)); 88 | 89 | totalRead += read; 90 | totalReads += 1; 91 | 92 | if (totalReads % 2000 == 0) 93 | { 94 | Console.WriteLine($"total bytes downloaded so far: {totalRead:n0}"); 95 | } 96 | } 97 | } 98 | while (isMoreToRead); 99 | } 100 | 101 | // public static void ExtractZip(string zipFile, string destDir, string extractPath="", bool ignoreRootDir=false) 102 | // { 103 | // ZipFile.ExtractToDirectory(zipFile, destDir, overwriteFiles: true); 104 | // } 105 | 106 | public static void ExtractZip(string zipFile, string destDir, string extractPath="", bool ignoreRootDir=false) 107 | { 108 | using Stream stream = File.OpenRead(zipFile); 109 | using var reader = ReaderFactory.Open(stream); 110 | var isWindows = Utils.IsWindows(); 111 | var symbolicLinks = new Dictionary(); 112 | 113 | var opts = new ExtractionOptions() 114 | { 115 | ExtractFullPath = true, 116 | Overwrite = true, 117 | WriteSymbolicLink = (symbolicLinkPath, symbolicLinkSourceFile) => 118 | { 119 | if (isWindows) return; 120 | var fileDir = Path.GetDirectoryName(symbolicLinkPath); 121 | symbolicLinks[symbolicLinkPath] = Path.Combine(fileDir, symbolicLinkSourceFile); 122 | } 123 | }; 124 | 125 | while (reader.MoveToNextEntry()) 126 | { 127 | if (reader.Entry.IsDirectory) continue; 128 | // Specify the extraction path for the entry 129 | var extractionPath = Path.Combine(destDir, reader.Entry.Key); 130 | 131 | // Ensure that the target directory exists 132 | var targetDirectory = Path.GetDirectoryName(extractionPath); 133 | if (!Directory.Exists(targetDirectory)) 134 | { 135 | Directory.CreateDirectory(targetDirectory!); 136 | } 137 | 138 | reader.WriteEntryToFile(extractionPath, opts); 139 | } 140 | 141 | foreach (var item in symbolicLinks.Where(item => File.Exists(item.Value))) 142 | { 143 | File.Copy(item.Value, item.Key); 144 | } 145 | } 146 | 147 | public static void ExtractZipFolder(string zipFile, string destDir, string extractPath = "", bool ignoreRootDir = false) 148 | { 149 | using var archive = ZipFile.OpenRead(zipFile); 150 | var result = from entry in archive.Entries 151 | where entry.FullName.StartsWith(extractPath) 152 | select entry; 153 | 154 | foreach (var entry in result) 155 | { 156 | var fullName = entry.FullName; 157 | 158 | if (ignoreRootDir) 159 | { 160 | var pathParts = entry.FullName.Split('/'); 161 | pathParts = pathParts.Skip(1).ToArray(); 162 | 163 | fullName = Path.Combine(pathParts); 164 | } 165 | 166 | var fullPath = Path.Combine(destDir, fullName); 167 | if (string.IsNullOrEmpty(entry.Name)) 168 | { 169 | Directory.CreateDirectory(fullPath); 170 | } 171 | else 172 | { 173 | entry.ExtractToFile(fullPath, overwrite: true); 174 | } 175 | } 176 | } 177 | 178 | public static int GetAvailablePort(int startingPort=5500) 179 | { 180 | List portArray = new List(); 181 | 182 | IPGlobalProperties properties = IPGlobalProperties.GetIPGlobalProperties(); 183 | 184 | //getting active connections 185 | TcpConnectionInformation[] connections = properties.GetActiveTcpConnections(); 186 | portArray.AddRange(from n in connections 187 | where n.LocalEndPoint.Port >= startingPort 188 | select n.LocalEndPoint.Port); 189 | 190 | //getting active tcp listeners 191 | var endPoints = properties.GetActiveTcpListeners(); 192 | portArray.AddRange(from n in endPoints 193 | where n.Port >= startingPort 194 | select n.Port); 195 | 196 | //getting active udp listeners 197 | endPoints = properties.GetActiveUdpListeners(); 198 | portArray.AddRange(from n in endPoints 199 | where n.Port >= startingPort 200 | select n.Port); 201 | 202 | portArray.Sort(); 203 | 204 | for (int i = startingPort; i < UInt16.MaxValue; i++) 205 | if (!portArray.Contains(i)) 206 | return i; 207 | 208 | return 0; 209 | } 210 | 211 | public static ProcessResult RunProcess(string filename, List args) 212 | { 213 | var outputBuilder = new StringBuilder(); 214 | var errorBuilder = new StringBuilder(); 215 | 216 | using var p = new Process(); 217 | p.StartInfo.RedirectStandardError = true; 218 | p.StartInfo.RedirectStandardOutput = true; 219 | p.StartInfo.UseShellExecute = false; 220 | p.EnableRaisingEvents = true; 221 | p.OutputDataReceived += (_, e) => 222 | { 223 | if (!string.IsNullOrEmpty(e.Data)) 224 | outputBuilder.AppendLine(e.Data); 225 | }; 226 | 227 | p.ErrorDataReceived += (_, e) => 228 | { 229 | if (!string.IsNullOrEmpty(e.Data)) 230 | errorBuilder.AppendLine(e.Data); 231 | }; 232 | 233 | p.StartInfo.FileName = filename; 234 | p.StartInfo.Arguments = string.Join(" ", args); 235 | p.StartInfo.CreateNoWindow = true; 236 | 237 | p.Start(); 238 | 239 | p.BeginOutputReadLine(); 240 | p.BeginErrorReadLine(); 241 | 242 | p.WaitForExit(); 243 | 244 | p.CancelOutputRead(); 245 | p.CancelErrorRead(); 246 | 247 | var output = outputBuilder.ToString(); 248 | var error = errorBuilder.ToString(); 249 | 250 | return new ProcessResult { ExitCode = p.ExitCode, Output = output, Error = error }; 251 | } 252 | 253 | public static bool IsWindows() 254 | { 255 | return RuntimeInformation.IsOSPlatform( 256 | OSPlatform.Windows 257 | ); 258 | } 259 | 260 | public static Platform? GetPlatform() 261 | { 262 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 263 | { 264 | return Platform.Windows; 265 | } 266 | 267 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 268 | { 269 | return Platform.Linux; 270 | } 271 | 272 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 273 | { 274 | return Platform.Darwin; 275 | } 276 | 277 | return null; 278 | } 279 | 280 | public static Architecture GetArchitecture(Platform platform) 281 | { 282 | if (platform is not Platform.Darwin) return Architecture.Amd64; 283 | 284 | var processResult = Utils.RunProcess("sysctl", new List 285 | { 286 | "machdep.cpu.brand_string" 287 | }); 288 | 289 | return processResult.Output.Contains("Apple M") ? Architecture.Arm64V8 : Architecture.Amd64; 290 | } 291 | } -------------------------------------------------------------------------------- /src/MysticMind.PostgresEmbed.Tests/PgServer_Tests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Xunit; 4 | using System.IO; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace MysticMind.PostgresEmbed.Tests; 8 | 9 | [CollectionDefinition("Non-Parallel Collection", DisableParallelization = true)] 10 | public class NonParallelCollectionDefinitionClass 11 | { 12 | } 13 | 14 | [Collection("Non-Parallel Collection")] 15 | public class PgServerTests 16 | { 17 | private const string PgUser = "postgres"; 18 | private const string ConnStr = "Server=localhost;Port={0};User Id={1};Password=test;Database=postgres;Pooling=false"; 19 | 20 | // this required for the appveyor CI build to set full access for appveyor user on instance folder on Windows 21 | private const bool AddLocalUserAccessPermission = false; 22 | 23 | [Fact] 24 | public void create_server_and_table_test() 25 | { 26 | using var server = new PgServer( 27 | 28 | "15.3.0", 29 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 30 | clearInstanceDirOnStop: true); 31 | server.Start(); 32 | 33 | // Note: set pooling to false to prevent connecting issues 34 | // https://github.com/npgsql/npgsql/issues/939 35 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 36 | var conn = new Npgsql.NpgsqlConnection(connStr); 37 | var cmd = 38 | new Npgsql.NpgsqlCommand( 39 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 40 | conn); 41 | 42 | conn.Open(); 43 | cmd.ExecuteNonQuery(); 44 | conn.Close(); 45 | } 46 | 47 | [Fact] 48 | public void create_server_and_pass_server_params() 49 | { 50 | var serverParams = new Dictionary 51 | { 52 | // set generic query optimizer to off 53 | { "geqo", "off" }, 54 | // set timezone as UTC 55 | { "timezone", "UTC" }, 56 | // switch off synchronous commit 57 | { "synchronous_commit", "off" }, 58 | // set max connections 59 | { "max_connections", "300" } 60 | }; 61 | 62 | using var server = new PgServer( 63 | "15.3.0", 64 | pgServerParams: serverParams, 65 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 66 | clearInstanceDirOnStop: true); 67 | server.Start(); 68 | 69 | // Note: set pooling to false to prevent connecting issues 70 | // https://github.com/npgsql/npgsql/issues/939 71 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 72 | var conn = new Npgsql.NpgsqlConnection(connStr); 73 | var cmd = 74 | new Npgsql.NpgsqlCommand( 75 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 76 | conn); 77 | 78 | conn.Open(); 79 | cmd.ExecuteNonQuery(); 80 | conn.Close(); 81 | } 82 | 83 | [Fact] 84 | public void create_server_without_using_block() 85 | { 86 | var server = new PgServer( 87 | "15.3.0", 88 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 89 | clearInstanceDirOnStop: true); 90 | 91 | try 92 | { 93 | server.Start(); 94 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 95 | var conn = new Npgsql.NpgsqlConnection(connStr); 96 | var cmd = 97 | new Npgsql.NpgsqlCommand( 98 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 99 | conn); 100 | 101 | conn.Open(); 102 | cmd.ExecuteNonQuery(); 103 | conn.Close(); 104 | } 105 | finally 106 | { 107 | server.Stop(); 108 | } 109 | } 110 | 111 | [SkippableFact] 112 | public void create_server_with_postgis_extension_test() 113 | { 114 | Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Test supported only on Windows"); 115 | var extensions = new List 116 | { 117 | new PgExtensionConfig( 118 | "https://download.osgeo.org/postgis/windows/pg15/archive/postgis-bundle-pg15-3.3.3x64.zip" 119 | ) 120 | }; 121 | 122 | using var server = new PgServer( 123 | "15.3.0", 124 | pgExtensions: extensions, 125 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 126 | clearInstanceDirOnStop: true); 127 | server.Start(); 128 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 129 | var conn = new Npgsql.NpgsqlConnection(connStr); 130 | var cmd = 131 | new Npgsql.NpgsqlCommand( 132 | "CREATE EXTENSION postgis;CREATE EXTENSION fuzzystrmatch", 133 | conn); 134 | 135 | conn.Open(); 136 | cmd.ExecuteNonQuery(); 137 | conn.Close(); 138 | } 139 | 140 | [Fact] 141 | public void create_server_with_user_defined_instance_id_and_table_test() 142 | { 143 | using var server = new PgServer( 144 | "15.3.0", 145 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 146 | instanceId: Guid.NewGuid(), 147 | clearInstanceDirOnStop: true); 148 | server.Start(); 149 | 150 | // assert if instance id directory exists 151 | Assert.True(Directory.Exists(server.InstanceDir)); 152 | 153 | // Note: set pooling to false to prevent connecting issues 154 | // https://github.com/npgsql/npgsql/issues/939 155 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 156 | var conn = new Npgsql.NpgsqlConnection(connStr); 157 | var cmd = 158 | new Npgsql.NpgsqlCommand( 159 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 160 | conn); 161 | 162 | conn.Open(); 163 | cmd.ExecuteNonQuery(); 164 | conn.Close(); 165 | } 166 | 167 | [Fact] 168 | public void create_server_with_existing_instance_id_and_table_test() 169 | { 170 | var instanceId = Guid.NewGuid(); 171 | 172 | using (var server = new PgServer( 173 | "15.3.0", 174 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 175 | instanceId: instanceId)) 176 | { 177 | server.Start(); 178 | 179 | // assert if instance id directory exists 180 | Assert.True(Directory.Exists(server.InstanceDir)); 181 | 182 | // Note: set pooling to false to prevent connecting issues 183 | // https://github.com/npgsql/npgsql/issues/939 184 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 185 | var conn = new Npgsql.NpgsqlConnection(connStr); 186 | var cmd = 187 | new Npgsql.NpgsqlCommand( 188 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 189 | conn); 190 | 191 | conn.Open(); 192 | cmd.ExecuteNonQuery(); 193 | conn.Close(); 194 | } 195 | 196 | using ( 197 | var server = new PgServer( 198 | "15.3.0", 199 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 200 | instanceId: instanceId, 201 | clearInstanceDirOnStop: true 202 | ) 203 | ) 204 | { 205 | server.Start(); 206 | 207 | // assert if instance id directory exists 208 | Assert.True(Directory.Exists(server.InstanceDir)); 209 | } 210 | } 211 | 212 | [Fact] 213 | public void create_server_without_version_suffix() 214 | { 215 | using var server = new PgServer( 216 | "15.3.0", 217 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 218 | clearInstanceDirOnStop: true); 219 | server.Start(); 220 | 221 | // Note: set pooling to false to prevent connecting issues 222 | // https://github.com/npgsql/npgsql/issues/939 223 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 224 | var conn = new Npgsql.NpgsqlConnection(connStr); 225 | var cmd = 226 | new Npgsql.NpgsqlCommand( 227 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 228 | conn); 229 | 230 | conn.Open(); 231 | cmd.ExecuteNonQuery(); 232 | conn.Close(); 233 | } 234 | 235 | [SkippableFact] 236 | public void create_server_with_spaces_in_db_dir() 237 | { 238 | Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Test supported only on Windows"); 239 | 240 | var pathWithSpaces = Path.Combine(Directory.GetCurrentDirectory(), "folder with spaces"); 241 | if (!Directory.Exists(pathWithSpaces)) Directory.CreateDirectory(pathWithSpaces); 242 | 243 | using var server = new PgServer( 244 | "15.3.0", 245 | dbDir: pathWithSpaces, 246 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 247 | clearInstanceDirOnStop: true); 248 | server.Start(); 249 | 250 | // Note: set pooling to false to prevent connecting issues 251 | // https://github.com/npgsql/npgsql/issues/939 252 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 253 | var conn = new Npgsql.NpgsqlConnection(connStr); 254 | var cmd = 255 | new Npgsql.NpgsqlCommand( 256 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 257 | conn); 258 | 259 | conn.Open(); 260 | cmd.ExecuteNonQuery(); 261 | conn.Close(); 262 | } 263 | 264 | [SkippableFact] 265 | public void create_server_with_spaces_in_db_dir_with_local_user_perm() 266 | { 267 | Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Test supported only on Windows"); 268 | 269 | var pathWithSpaces = Path.Combine(Directory.GetCurrentDirectory(), "folder with spaces"); 270 | if (!Directory.Exists(pathWithSpaces)) Directory.CreateDirectory(pathWithSpaces); 271 | 272 | using var server = new PgServer( 273 | "15.3.0", 274 | dbDir: pathWithSpaces, 275 | addLocalUserAccessPermission: true, 276 | clearInstanceDirOnStop: true); 277 | server.Start(); 278 | 279 | // Note: set pooling to false to prevent connecting issues 280 | // https://github.com/npgsql/npgsql/issues/939 281 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 282 | var conn = new Npgsql.NpgsqlConnection(connStr); 283 | var cmd = 284 | new Npgsql.NpgsqlCommand( 285 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 286 | conn); 287 | 288 | conn.Open(); 289 | cmd.ExecuteNonQuery(); 290 | conn.Close(); 291 | } 292 | 293 | [SkippableFact] 294 | public void create_server_with_spaces_in_db_dir_parent_dir() 295 | { 296 | Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Test supported only on Windows"); 297 | 298 | var pathWithSpaces = Path.Combine(Directory.GetCurrentDirectory(), "folder with spaces", "folder"); 299 | if (!Directory.Exists(pathWithSpaces)) Directory.CreateDirectory(pathWithSpaces); 300 | 301 | using var server = new PgServer( 302 | "15.3.0", 303 | dbDir: pathWithSpaces, 304 | addLocalUserAccessPermission: true, 305 | clearInstanceDirOnStop: true); 306 | server.Start(); 307 | 308 | // Note: set pooling to false to prevent connecting issues 309 | // https://github.com/npgsql/npgsql/issues/939 310 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 311 | var conn = new Npgsql.NpgsqlConnection(connStr); 312 | var cmd = 313 | new Npgsql.NpgsqlCommand( 314 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 315 | conn); 316 | 317 | conn.Open(); 318 | cmd.ExecuteNonQuery(); 319 | conn.Close(); 320 | } 321 | } -------------------------------------------------------------------------------- /src/MysticMind.PostgresEmbed.Tests/Pgserver_Async_Tests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Xunit; 4 | using System.IO; 5 | using System.Runtime.InteropServices; 6 | using System.Threading.Tasks; 7 | 8 | namespace MysticMind.PostgresEmbed.Tests; 9 | 10 | [Collection("Non-Parallel Collection")] 11 | public class PgServerAsyncTests 12 | { 13 | private const string PgUser = "postgres"; 14 | private const string ConnStr = "Server=localhost;Port={0};User Id={1};Password=test;Database=postgres;Pooling=false"; 15 | 16 | // this required for the appveyor CI build to set full access for appveyor user on instance folder on Windows 17 | private const bool AddLocalUserAccessPermission = false; 18 | 19 | [Fact] 20 | public async void create_server_and_table_test() 21 | { 22 | await using var server = new PgServer( 23 | 24 | "15.3.0", 25 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 26 | clearInstanceDirOnStop:true); 27 | await server.StartAsync(); 28 | 29 | // Note: set pooling to false to prevent connecting issues 30 | // https://github.com/npgsql/npgsql/issues/939 31 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 32 | var conn = new Npgsql.NpgsqlConnection(connStr); 33 | var cmd = 34 | new Npgsql.NpgsqlCommand( 35 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 36 | conn); 37 | 38 | await conn.OpenAsync(); 39 | await cmd.ExecuteNonQueryAsync(); 40 | await conn.CloseAsync(); 41 | } 42 | 43 | [Fact] 44 | public async void create_server_and_pass_server_params() 45 | { 46 | var serverParams = new Dictionary 47 | { 48 | // set generic query optimizer to off 49 | { "geqo", "off" }, 50 | // set timezone as UTC 51 | { "timezone", "UTC" }, 52 | // switch off synchronous commit 53 | { "synchronous_commit", "off" }, 54 | // set max connections 55 | { "max_connections", "300" } 56 | }; 57 | 58 | await using var server = new PgServer( 59 | "15.3.0", 60 | pgServerParams: serverParams, 61 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 62 | clearInstanceDirOnStop: true); 63 | await server.StartAsync(); 64 | 65 | // Note: set pooling to false to prevent connecting issues 66 | // https://github.com/npgsql/npgsql/issues/939 67 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 68 | var conn = new Npgsql.NpgsqlConnection(connStr); 69 | var cmd = 70 | new Npgsql.NpgsqlCommand( 71 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 72 | conn); 73 | 74 | await conn.OpenAsync(); 75 | await cmd.ExecuteNonQueryAsync(); 76 | await conn.CloseAsync(); 77 | } 78 | 79 | [Fact] 80 | public async void create_server_without_using_block() 81 | { 82 | var server = new PgServer( 83 | "15.3.0", 84 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 85 | clearInstanceDirOnStop: true); 86 | 87 | try 88 | { 89 | await server.StartAsync(); 90 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 91 | var conn = new Npgsql.NpgsqlConnection(connStr); 92 | var cmd = 93 | new Npgsql.NpgsqlCommand( 94 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 95 | conn); 96 | 97 | await conn.OpenAsync(); 98 | await cmd.ExecuteNonQueryAsync(); 99 | await conn.CloseAsync(); 100 | } 101 | finally 102 | { 103 | await server.StopAsync(); 104 | } 105 | } 106 | 107 | [SkippableFact] 108 | public async void create_server_with_postgis_extension_test() 109 | { 110 | Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Test supported only on Windows"); 111 | var extensions = new List 112 | { 113 | new( 114 | "https://download.osgeo.org/postgis/windows/pg15/archive/postgis-bundle-pg15-3.3.3x64.zip" 115 | ) 116 | }; 117 | 118 | await using var server = new PgServer( 119 | "15.3.0", 120 | pgExtensions: extensions, 121 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 122 | clearInstanceDirOnStop: true); 123 | await server.StartAsync(); 124 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 125 | var conn = new Npgsql.NpgsqlConnection(connStr); 126 | var cmd = 127 | new Npgsql.NpgsqlCommand( 128 | "CREATE EXTENSION postgis;CREATE EXTENSION fuzzystrmatch", 129 | conn); 130 | 131 | await conn.OpenAsync(); 132 | await cmd.ExecuteNonQueryAsync(); 133 | await conn.CloseAsync(); 134 | } 135 | 136 | [Fact] 137 | public async void create_server_with_user_defined_instance_id_and_table_test() 138 | { 139 | await using var server = new PgServer( 140 | "15.3.0", 141 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 142 | instanceId: Guid.NewGuid(), 143 | clearInstanceDirOnStop: true); 144 | await server.StartAsync(); 145 | 146 | // assert if instance id directory exists 147 | Assert.True(Directory.Exists(server.InstanceDir)); 148 | 149 | // Note: set pooling to false to prevent connecting issues 150 | // https://github.com/npgsql/npgsql/issues/939 151 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 152 | var conn = new Npgsql.NpgsqlConnection(connStr); 153 | var cmd = 154 | new Npgsql.NpgsqlCommand( 155 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 156 | conn); 157 | 158 | await conn.OpenAsync(); 159 | await cmd.ExecuteNonQueryAsync(); 160 | await conn.CloseAsync(); 161 | } 162 | 163 | [Fact] 164 | public async void create_server_with_existing_instance_id_and_table_test() 165 | { 166 | var instanceId = Guid.NewGuid(); 167 | 168 | await using (var server = new PgServer( 169 | "15.3.0", 170 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 171 | instanceId: instanceId)) 172 | { 173 | await server.StartAsync(); 174 | 175 | // assert if instance id directory exists 176 | Assert.True(Directory.Exists(server.InstanceDir)); 177 | 178 | // Note: set pooling to false to prevent connecting issues 179 | // https://github.com/npgsql/npgsql/issues/939 180 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 181 | var conn = new Npgsql.NpgsqlConnection(connStr); 182 | var cmd = 183 | new Npgsql.NpgsqlCommand( 184 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 185 | conn); 186 | 187 | await conn.OpenAsync(); 188 | await cmd.ExecuteNonQueryAsync(); 189 | await conn.CloseAsync(); 190 | } 191 | 192 | await using ( 193 | var server = new PgServer( 194 | "15.3.0", 195 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 196 | instanceId: instanceId, 197 | clearInstanceDirOnStop:true 198 | ) 199 | ) 200 | { 201 | await server.StartAsync(); 202 | 203 | // assert if instance id directory exists 204 | Assert.True(Directory.Exists(server.InstanceDir)); 205 | } 206 | } 207 | 208 | [Fact] 209 | public async void create_server_without_version_suffix() 210 | { 211 | await using var server = new PgServer( 212 | "15.3.0", 213 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 214 | clearInstanceDirOnStop: true); 215 | await server.StartAsync(); 216 | 217 | // Note: set pooling to false to prevent connecting issues 218 | // https://github.com/npgsql/npgsql/issues/939 219 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 220 | var conn = new Npgsql.NpgsqlConnection(connStr); 221 | var cmd = 222 | new Npgsql.NpgsqlCommand( 223 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 224 | conn); 225 | 226 | await conn.OpenAsync(); 227 | await cmd.ExecuteNonQueryAsync(); 228 | await conn.CloseAsync(); 229 | } 230 | 231 | [SkippableFact] 232 | public async Task Bug_19_authors_md_file_already_exists() 233 | { 234 | Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Test supported only on Windows"); 235 | var extensions = new List 236 | { 237 | new( 238 | "https://download.osgeo.org/postgis/windows/pg15/archive/postgis-bundle-pg15-3.3.3x64.zip" 239 | ) 240 | }; 241 | 242 | await using var server = new PgServer( 243 | "15.3.0", 244 | PgUser, 245 | pgExtensions: extensions, 246 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 247 | clearInstanceDirOnStop: true); 248 | await server.StartAsync(); 249 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 250 | var conn = new Npgsql.NpgsqlConnection(connStr); 251 | var cmd = 252 | new Npgsql.NpgsqlCommand( 253 | "CREATE EXTENSION postgis", 254 | conn); 255 | 256 | await conn.OpenAsync(); 257 | await cmd.ExecuteNonQueryAsync(); 258 | await conn.CloseAsync(); 259 | } 260 | 261 | [SkippableFact] 262 | public async Task create_server_with_spaces_in_db_dir() 263 | { 264 | Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Test supported only on Windows"); 265 | 266 | var pathWithSpaces = Path.Combine(Directory.GetCurrentDirectory(), "folder with spaces"); 267 | if (!Directory.Exists(pathWithSpaces)) Directory.CreateDirectory(pathWithSpaces); 268 | 269 | await using var server = new PgServer( 270 | "15.3.0", 271 | dbDir: pathWithSpaces, 272 | addLocalUserAccessPermission: AddLocalUserAccessPermission, 273 | clearInstanceDirOnStop: true); 274 | await server.StartAsync(); 275 | 276 | // Note: set pooling to false to prevent connecting issues 277 | // https://github.com/npgsql/npgsql/issues/939 278 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 279 | var conn = new Npgsql.NpgsqlConnection(connStr); 280 | var cmd = 281 | new Npgsql.NpgsqlCommand( 282 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 283 | conn); 284 | 285 | await conn.OpenAsync(); 286 | await cmd.ExecuteNonQueryAsync(); 287 | await conn.CloseAsync(); 288 | } 289 | 290 | [SkippableFact] 291 | public async Task create_server_with_spaces_in_db_dir_with_local_user_perm() 292 | { 293 | Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Test supported only on Windows"); 294 | 295 | var pathWithSpaces = Path.Combine(Directory.GetCurrentDirectory(), "folder with spaces"); 296 | if (!Directory.Exists(pathWithSpaces)) Directory.CreateDirectory(pathWithSpaces); 297 | 298 | await using var server = new PgServer( 299 | "15.3.0", 300 | dbDir: pathWithSpaces, 301 | addLocalUserAccessPermission: true, 302 | clearInstanceDirOnStop: true); 303 | await server.StartAsync(); 304 | 305 | // Note: set pooling to false to prevent connecting issues 306 | // https://github.com/npgsql/npgsql/issues/939 307 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 308 | var conn = new Npgsql.NpgsqlConnection(connStr); 309 | var cmd = 310 | new Npgsql.NpgsqlCommand( 311 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 312 | conn); 313 | 314 | await conn.OpenAsync(); 315 | await cmd.ExecuteNonQueryAsync(); 316 | await conn.CloseAsync(); 317 | } 318 | 319 | [SkippableFact] 320 | public async Task create_server_with_spaces_in_db_dir_parent_dir() 321 | { 322 | Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Test supported only on Windows"); 323 | 324 | var pathWithSpaces = Path.Combine(Directory.GetCurrentDirectory(), "folder with spaces", "folder"); 325 | if (!Directory.Exists(pathWithSpaces)) Directory.CreateDirectory(pathWithSpaces); 326 | 327 | await using var server = new PgServer( 328 | "15.3.0", 329 | dbDir: pathWithSpaces, 330 | addLocalUserAccessPermission: true, 331 | clearInstanceDirOnStop: true); 332 | await server.StartAsync(); 333 | 334 | // Note: set pooling to false to prevent connecting issues 335 | // https://github.com/npgsql/npgsql/issues/939 336 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 337 | var conn = new Npgsql.NpgsqlConnection(connStr); 338 | var cmd = 339 | new Npgsql.NpgsqlCommand( 340 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 341 | conn); 342 | 343 | await conn.OpenAsync(); 344 | await cmd.ExecuteNonQueryAsync(); 345 | await conn.CloseAsync(); 346 | } 347 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MysticMind.PostgresEmbed _Postgres embedded database equivalent for .Net applications_ [![Build status](https://github.com/mysticmind/mysticmind-postgresembed/actions/workflows/ci.yaml/badge.svg)](https://github.com/mysticmind/mysticmind-postgresembed/actions/workflows/ci.yaml) [![NuGet Version](https://badgen.net/nuget/v/mysticmind.postgresembed)](https://www.nuget.org/packages/MysticMind.PostgresEmbed/) 2 | 3 | This is a library for running a Postgres server embedded equivalent including extensions targeting Windows, Linux and OSX (including Silicon - M1/M2) available since v3.x or above. This project also handles Postgres extensions very well with a neat way to configure and use it. 4 | 5 | Note that until v2.x, this library was only supporting Windows. 6 | 7 | By default, this project uses the minimum binaries published by [zonkyio/embedded-postgres-binaries](https://github.com/zonkyio/embedded-postgres-binaries). Note that this is a minimal set of binaries which can be quickly downloaded (around 10MB) for use rather than the official downloads which are pegged at around 100MB. A list of all available versions of postgres binaries is here: https://mvnrepository.com/artifact/io.zonky.test.postgres/embedded-postgres-binaries-bom. If you click on a specific version, you can lookup the OS platforms for which packages are published. 8 | 9 | Library automatically detects the OS environment and architecture to setup the library for use accordingly. 10 | 11 | If you have benefitted from this library and has saved you a bunch of time, please feel free to sponsor my work!
12 | GitHub Sponsor 13 | 14 | ## Usage 15 | Install the package from Nuget using `Install-Package MysticMind.PostgresEmbed` or clone the repository and build it. 16 | 17 | ### Example of using Postgres binary 18 | ```csharp 19 | // using Postgres 15.3.0 with a using block 20 | using (var server = new MysticMind.PostgresEmbed.PgServer("15.3.0")) 21 | { 22 | // start the server 23 | server.Start(); 24 | 25 | // using Npgsql to connect the server 26 | string connStr = $"Server=localhost;Port={server.PgPort};User Id=postgres;Password=test;Database=postgres"; 27 | 28 | var conn = new Npgsql.NpgsqlConnection(connStr); 29 | 30 | var cmd = 31 | new Npgsql.NpgsqlCommand( 32 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 33 | conn); 34 | 35 | conn.Open(); 36 | cmd.ExecuteNonQuery(); 37 | conn.Close(); 38 | } 39 | ``` 40 | 41 | ### Example of using Postgres binary with StartAsync 42 | ```csharp 43 | // using Postgres 15.3.0 with a using block 44 | using (var server = new MysticMind.PostgresEmbed.PgServer("15.3.0")) 45 | { 46 | // start the server 47 | await server.StartAsync(); 48 | 49 | // using Npgsql to connect the server 50 | string connStr = $"Server=localhost;Port={server.PgPort};User Id=postgres;Password=test;Database=postgres"; 51 | 52 | var conn = new Npgsql.NpgsqlConnection(connStr); 53 | 54 | var cmd = 55 | new Npgsql.NpgsqlCommand( 56 | "CREATE TABLE table1(ID CHAR(256) CONSTRAINT id PRIMARY KEY, Title CHAR)", 57 | conn); 58 | 59 | await conn.OpenAsync(); 60 | await cmd.ExecuteNonQueryAsync(); 61 | await conn.CloseAsync(); 62 | } 63 | ``` 64 | 65 | 66 | ### Example of using Postgres and extensions 67 | ```csharp 68 | // Example of using Postgres 15.3.0 with extension PostGIS 3.3.3 69 | // you can add multiple create extension sql statements to be run 70 | var extensions = new List(); 71 | 72 | extensions.Add(new PgExtensionConfig( 73 | "https://download.osgeo.org/postgis/windows/pg15/postgis-bundle-pg15-3.3.3x64.zip")); 74 | 75 | using (var server = new MysticMind.PostgresEmbed.PgServer("15.3.0", pgExtensions: extensions)) 76 | { 77 | server.Start(); 78 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 79 | var conn = new Npgsql.NpgsqlConnection(connStr); 80 | var cmd = new Npgsql.NpgsqlCommand("CREATE EXTENSION postgis;CREATE EXTENSION fuzzystrmatch", conn); 81 | conn.Open(); 82 | cmd.ExecuteNonQuery(); 83 | conn.Close(); 84 | } 85 | ``` 86 | 87 | ### Example of passing additional server parameters 88 | ```csharp 89 | var serverParams = new Dictionary(); 90 | 91 | // set generic query optimizer to off 92 | serverParams.Add("geqo", "off"); 93 | 94 | // set timezone as UTC 95 | serverParams.Add("timezone", "UTC"); 96 | 97 | // switch off synchronous commit 98 | serverParams.Add("synchronous_commit", "off"); 99 | 100 | // set max connections 101 | serverParams.Add("max_connections", "300"); 102 | 103 | using (var server = new MysticMind.PostgresEmbed.PgServer("15.3.0", pgServerParams: serverParams)) 104 | { 105 | server.Start(); 106 | 107 | // do operations here 108 | } 109 | ``` 110 | 111 | ### Example of usage in unit tests (xUnit) 112 | 113 | Since download and extraction of binaries take time, it would be good strategy to setup and teardown the server for each unit tests class instance. 114 | 115 | With xUnit, you will need to create a fixture and wire it as a class fixture. See code below: 116 | 117 | ```csharp 118 | // this example demonstrates writing an xUnit class fixture 119 | // implements IDisposable to help with the teardown logic. 120 | public class DatabaseServerFixture : IDisposable 121 | { 122 | private static PgServer _pgServer; 123 | 124 | public DatabaseServerFixture() 125 | { 126 | var pgExtensions = new List(); 127 | pgExtensions.Add( 128 | new PgExtensionConfig( 129 | "https://download.osgeo.org/postgis/windows/pg15/postgis-bundle-pg15-3.3.3x64.zip")); 130 | 131 | _pgServer = new PgServer("15.3.0", port: 5432, pgExtensions: pgExtensions); 132 | _pgServer.Start(); 133 | var connStr = string.Format(ConnStr, server.PgPort, PgUser); 134 | var conn = new Npgsql.NpgsqlConnection(connStr); 135 | var cmd = new Npgsql.NpgsqlCommand("CREATE EXTENSION postgis", conn); 136 | conn.Open(); 137 | cmd.ExecuteNonQuery(); 138 | conn.Close(); 139 | } 140 | 141 | public void Dispose() 142 | { 143 | if (_pgServer != null) 144 | { 145 | _pgServer.Stop(); 146 | } 147 | } 148 | } 149 | 150 | // wire DatabaseServerFixture fixture as a class fixture 151 | // so that it is created once for the whole class 152 | // and shared across all unit tests within the class 153 | public class my_db_tests : IClassFixture 154 | { 155 | [Fact] 156 | public void your_test() 157 | { 158 | // add your test code 159 | } 160 | } 161 | ``` 162 | 163 | ## Few gotchas 164 | - You can pass a port parameter while creating instance. If you don't pass one, system will use a free port to start the server. Use `server.PgPort` to fetch the port used by the embedded server 165 | - `postgres` is the default database created 166 | - `postgres` is the default user (super user) to be used for connection 167 | - Trust authentication is the default authentication. You can pass any password in connection string which will be ignored by the server. Since our primary motivation is to use the server for unit tests on localhost, this is pretty fine to keep it simple. 168 | - If you pass `DbDir` path while creating the server then it will be used as the working directory else it will use the current directory. You will find a folder named `pg_embed` within which the `binaries` and instance folders are created. 169 | - If you would want to clear the whole root working directory prior to start of server(clear the all the folders from prior runs), you can pass `clearWorkingDirOnStart=true` in the constuctor while creating the server. By default this value is `false`. 170 | - If you would want to clear the instance directory on stopping the server, you could pass `clearInstanceDirOnStop=true` in the constuctor while creating the server. By default this value is `false`. 171 | - If you would want to run a named instance, you can pass a guid value for `instanceId` in the constructor. This will be helpful in scenarios where you would want to rerun the same named instance already setup. In this case, if the named directory exists, system will skip the setup process and start the server. Note that `clearInstanceDirOnStop` and `clearWorkingDirOnStart` should be `false` (this is the default as well). 172 | - If you don't pass a `instanceId`, system will create a new instance by running the whole setup process for every server start. 173 | 174 | ## How it works 175 | The following steps are done when you run an embedded server: 176 | - Binaries of configured Postgres version and the extensions are downloaded. 177 | - For Postgres binary, nupkg of the published nuget package version is downloaded and the binary zip file is extracted from it 178 | - For Postgres extensions, file is downloaded from the configured url. You have to choose the right version of extension compatible with the Postgres version. 179 | - Since downloads from http endpoints can be flaky, retry logic is implemented with 3 retries for every file being downloaded. 180 | - Several steps are executed in order once you start the server 181 | - All binaries zip files once downloaded are stored under `[specified db dir]\pg_embed\binaries` and reused on further runs. 182 | - Since each run of embedded server can possibly use a combination of Postgres version and extensions. Hence implemented a concept of an instance containing the extracted Postgres binary, extensions and db data. 183 | - Each instance has a instance folder (guid) which contains the `pgsql` and `data` folders. Instance folder is created and removed for each embedded server setup and tear down. 184 | - Binary files of Postgres and the extensions are extracted into the instance `pgsql` folder 185 | - InitDb is run on the `data` folder calling `initdb` in a `Process` 186 | - Server is started by instantiating a new process on a free port (in the range of 5500 or above) using `pg_ctl`. 187 | - System will wait and check (fires a sql query using `psql` at a set interval) if the server has been started till a defined wait timeout. 188 | - Create extensions sql commands configured are run to install the extensions. All sql statements are combined together and run as a single query via a new process and psql.exe 189 | - After using the server, system will tear down by running a fast stop Process, kill the server process and clear the instance folder. 190 | - Server implements `IDisposable` to call Stop automatically within the context of a `using(..){...}` block. If using an unit test setup and teardown at the class level, you will call `Start()` and `Stop()` appropriately. 191 | 192 | ## Breaking changes in v3.x 193 | - `PgServer` class constructor signatures have changed. 194 | - Lib no more uses [PostgreSql.Binaries.Lite](https://github.com/mihasic/PostgreSql.Binaries.Lite) 195 | - With regards to postgres extensions, end-users will need to run `create extension ;` to install the extension. Library will only download and extract the extension based on the url provided. 196 | 197 | ## Known Issues 198 | - Some test tend to fail when running all at once in Rider with the exception message: "the database system is starting up". Just rerun that specific test and it will pass. 199 | 200 | ### Npgsql exception 201 | If you are using [Npgsql](https://github.com/npgsql), when you execute the server, you may sporadically notice the following exception 202 | 203 | > Npgsql.NpgsqlException : Unable to write data to the transport connection: An existing connection was forcibly closed by the remote host. 204 | 205 | Refer https://github.com/npgsql/npgsql/issues/939 to know details. Resolution is to use `Pooling=false` in connection string. 206 | 207 | ### InitDb failure while starting embedded server 208 | 209 | > fixing permissions on existing directory ./pg_embed/aa60c634-fa20-4fa8-b4fc-a43a3b08aa99/data ... initdb: could not change permissions of directory "./pg_embed/aa60c634-fa20-4fa8-b4fc-a43a3b08aa99/data": Permission denied 210 | 211 | All processes run from within the embedded server runs under local account. Postgres expects that the parent folder of the data directory has full access permission for the local account. 212 | 213 | The fix is to pass a flag `addLocalUserAccessPermission` as `true` and the system will attempt to add full access before the InitDb step as below for the case of Windows: 214 | 215 | ``` 216 | icacls.exe c:\pg_embed\aa60c634-fa20-4fa8-b4fc-a43a3b08aa99 /t /grant:r :(OI)(OC)F 217 | ``` 218 | 219 | For the case of *nix, all the binaries in bin folder are set to `755` by the library to execute. 220 | 221 | ### InitDb failure with a large negative number 222 | 223 | If you are seeing failures with `initdb` with a large negative number then it could be a dependency library issue for Postgres itself, you would need to install [Visual C++ Redistributable Packages for Visual Studio 2013](https://www.microsoft.com/en-us/download/details.aspx?id=40784) to make `MSVCR120.dll` available for Postgres to use. 224 | 225 | Note: 226 | 1. The local account should have rights to change folder permissions otherwise the operation will result in an exception. 227 | 2. You may not face this issue in development environments. 228 | 3. This step was required to be enabled for Appveyor CI builds to succeed. 229 | 230 | ## Acknowledgements 231 | - This project uses the minimal Postgres binaries published via [zonkyio/embedded-postgres-binaries](https://github.com/zonkyio/embedded-postgres-binaries). 232 | 233 | - Looked at projects [Yandex Embedded PostgresSQL](https://github.com/yandex-qatools/postgresql-embedded) and [OpenTable Embedded PostgreSQL Component](https://github.com/opentable/otj-pg-embedded) while brainstorming the implementation. 234 | 235 | Note that the above projects had only dealt with Postgres binary and none had options to deal with the Postgres extensions. 236 | 237 | ## License 238 | MysticMind.PostgresEmbed is licensed under [MIT License](http://www.opensource.org/licenses/mit-license.php). Refer to [License file](https://github.com/mysticmind/mysticmind-postgresembed/blob/master/LICENSE) for more information. 239 | 240 | Copyright © 2023 Babu Annamalai 241 | 242 | 243 | -------------------------------------------------------------------------------- /src/MysticMind.PostgresEmbed/PgServer.cs: -------------------------------------------------------------------------------- 1 | using Polly; 2 | using Polly.Retry; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Net.Sockets; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace MysticMind.PostgresEmbed; 13 | 14 | public class PgServer : IDisposable, IAsyncDisposable 15 | { 16 | private const string PgSuperuser = "postgres"; 17 | private const string PgHost = "localhost"; 18 | private const string PgDbname = "postgres"; 19 | private const string PgStopWaitS = "5"; 20 | private const string PgStopMode = "fast"; 21 | 22 | private string _pgBinaryFullPath; 23 | 24 | private readonly string _pgCtlBin; 25 | private readonly string _initDbBin; 26 | private readonly string _postgresBin; 27 | 28 | private readonly bool _clearInstanceDirOnStop; 29 | 30 | private readonly bool _clearWorkingDirOnStart; 31 | 32 | private Process _pgServerProcess; 33 | 34 | private readonly List _pgServerParams = new(); 35 | 36 | private readonly List _pgExtensions = new(); 37 | 38 | private readonly bool _addLocalUserAccessPermission; 39 | 40 | private readonly ResiliencePipeline _downloadRetryPolicy; 41 | private readonly ResiliencePipeline _deleteFoldersRetryPolicy; 42 | 43 | private readonly Platform _platform; 44 | private readonly Architecture _architecture; 45 | private readonly string _mavenRepo; 46 | 47 | private readonly int _startupWaitMs; 48 | 49 | public PgServer( 50 | string pgVersion, 51 | string pgUser = PgSuperuser, 52 | string dbDir = "", 53 | Guid? instanceId = null, 54 | int port = 0, 55 | Dictionary pgServerParams = null, 56 | List pgExtensions = null, 57 | bool addLocalUserAccessPermission = false, 58 | bool clearInstanceDirOnStop = false, 59 | bool clearWorkingDirOnStart = false, 60 | int deleteFolderRetryCount = 5, 61 | int deleteFolderInitialTimeout = 16, 62 | int deleteFolderTimeoutFactor = 2, 63 | string locale = "", 64 | Platform? platform = null, 65 | int startupWaitTime = 30000, 66 | string mavenRepo = "https://repo1.maven.org/maven2") 67 | { 68 | 69 | _pgCtlBin = "pg_ctl"; 70 | _initDbBin = "initdb"; 71 | _postgresBin = "postgres"; 72 | PgVersion = pgVersion; 73 | 74 | if (platform.HasValue) 75 | { 76 | _platform = platform.Value; 77 | } 78 | else 79 | { 80 | platform = Utils.GetPlatform(); 81 | } 82 | 83 | if (platform == null) 84 | { 85 | throw new UnsupportedPlatformException(); 86 | } 87 | 88 | _platform = platform.Value; 89 | 90 | _architecture = Utils.GetArchitecture(_platform); 91 | 92 | _mavenRepo = mavenRepo; 93 | 94 | _startupWaitMs = startupWaitTime; 95 | 96 | PgUser = String.IsNullOrEmpty(pgUser) ? PgSuperuser : pgUser; 97 | 98 | DbDir = Path.Combine(string.IsNullOrEmpty(dbDir) ? "." : dbDir, "pg_embed"); 99 | 100 | PgPort = port == 0 ? Utils.GetAvailablePort() : port; 101 | 102 | if (pgServerParams != null) 103 | { 104 | foreach (var item in pgServerParams) 105 | { 106 | _pgServerParams.Add($"-c {item.Key}={item.Value}"); 107 | } 108 | } 109 | 110 | if (pgExtensions != null) 111 | { 112 | _pgExtensions.AddRange(pgExtensions); 113 | } 114 | 115 | instanceId ??= Guid.NewGuid(); 116 | 117 | _clearInstanceDirOnStop = clearInstanceDirOnStop; 118 | _clearWorkingDirOnStart = clearWorkingDirOnStart; 119 | 120 | _addLocalUserAccessPermission = addLocalUserAccessPermission; 121 | 122 | BinariesDir = Path.Combine(DbDir, "binaries"); 123 | InstanceDir = Path.Combine(DbDir, instanceId.ToString()); 124 | PgBinDir = Path.Combine(InstanceDir, "bin"); 125 | DataDir = Path.Combine(InstanceDir, "data"); 126 | 127 | // setup the policy for retry pertaining to downloading binary 128 | _downloadRetryPolicy = 129 | new ResiliencePipelineBuilder() 130 | .AddRetry(new RetryStrategyOptions 131 | { 132 | ShouldHandle = new PredicateBuilder().Handle(), 133 | Delay = TimeSpan.FromSeconds(1), 134 | BackoffType = DelayBackoffType.Exponential, 135 | MaxRetryAttempts = 3 136 | }).Build(); 137 | //Set up the policy for retry pertaining to folder deletion. 138 | _deleteFoldersRetryPolicy = 139 | new ResiliencePipelineBuilder() 140 | .AddRetry(new RetryStrategyOptions 141 | { 142 | ShouldHandle = new PredicateBuilder().Handle(), 143 | DelayGenerator = (retryAttempt) => ValueTask.FromResult(TimeSpan.FromMilliseconds(deleteFolderInitialTimeout * (int)Math.Pow(deleteFolderTimeoutFactor, retryAttempt.AttemptNumber - 1))), 144 | MaxRetryAttempts = deleteFolderRetryCount 145 | }).Build(); 146 | 147 | if (!string.IsNullOrEmpty(locale)) 148 | { 149 | Locale = locale; 150 | } 151 | 152 | if (_platform != Platform.Windows && string.IsNullOrEmpty(Locale)) 153 | { 154 | Locale = "en_US.UTF-8"; 155 | } 156 | } 157 | 158 | public string PgVersion { get; } 159 | 160 | public string PgUser { get; } 161 | 162 | public string DbDir { get; } 163 | 164 | public string BinariesDir { get; } 165 | 166 | public string InstanceDir { get; } 167 | 168 | public string PgBinDir { get; } 169 | 170 | public string DataDir { get; } 171 | 172 | public int PgPort { get; } 173 | 174 | public string Locale { get; } 175 | 176 | public string PgDbName => PgDbname; 177 | 178 | private void DownloadPgBinary() 179 | { 180 | var downloader = new DefaultPostgresBinaryDownloader(PgVersion, BinariesDir, _platform, _architecture, _mavenRepo); 181 | 182 | try 183 | { 184 | _pgBinaryFullPath = _downloadRetryPolicy.Execute(downloader.Download); 185 | } 186 | catch (Exception ex) 187 | { 188 | throw new Exception($"Failed to download PgBinary", ex); 189 | } 190 | } 191 | 192 | private async Task DownloadPgBinaryAsync() 193 | { 194 | var downloader = new DefaultPostgresBinaryDownloader(PgVersion, BinariesDir, _platform, _architecture, _mavenRepo); 195 | 196 | try 197 | { 198 | _pgBinaryFullPath = await _downloadRetryPolicy.Execute(downloader.DownloadAsync); 199 | } 200 | catch (Exception ex) 201 | { 202 | throw new Exception($"Failed to download PgBinary", ex); 203 | } 204 | } 205 | 206 | private void DownloadPgExtensions() 207 | { 208 | foreach (var pgExtensionInstance in _pgExtensions.Select(extensionConfig => new PgExtension(BinariesDir, InstanceDir, extensionConfig))) 209 | { 210 | _downloadRetryPolicy.Execute(pgExtensionInstance.Download); 211 | } 212 | } 213 | 214 | private async Task DownloadPgExtensionsAsync() 215 | { 216 | foreach (var pgExtensionInstance in _pgExtensions.Select(extensionConfig => new PgExtension(BinariesDir, InstanceDir, extensionConfig))) 217 | { 218 | await _downloadRetryPolicy.Execute(pgExtensionInstance.DownloadAsync); 219 | } 220 | } 221 | 222 | private void CreateDirs() 223 | { 224 | Directory.CreateDirectory(DbDir); 225 | Directory.CreateDirectory(BinariesDir); 226 | } 227 | 228 | private void RemoveWorkingDir() => DeleteDirectory(DbDir); 229 | 230 | private void RemoveInstanceDir() => DeleteDirectory(InstanceDir); 231 | 232 | private void DeleteDirectory(string directoryPath) 233 | { 234 | // From http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true/329502#329502 235 | 236 | if (!Directory.Exists(directoryPath)) 237 | { 238 | Trace.WriteLine($"Directory '{directoryPath}' is missing and can't be removed."); 239 | return; 240 | } 241 | 242 | NormalizeAttributes(directoryPath); 243 | _deleteFoldersRetryPolicy.Execute(() => Directory.Delete(directoryPath, true)); 244 | } 245 | 246 | private static void NormalizeAttributes(string directoryPath) 247 | { 248 | var filePaths = Directory.GetFiles(directoryPath); 249 | var subdirectoryPaths = Directory.GetDirectories(directoryPath); 250 | 251 | foreach (var filePath in filePaths) 252 | { 253 | File.SetAttributes(filePath, FileAttributes.Normal); 254 | } 255 | 256 | foreach (var subdirectoryPath in subdirectoryPaths) 257 | { 258 | NormalizeAttributes(subdirectoryPath); 259 | } 260 | 261 | File.SetAttributes(directoryPath, FileAttributes.Normal); 262 | } 263 | 264 | private void ExtractPgBinary() 265 | { 266 | Utils.ExtractZip(_pgBinaryFullPath, InstanceDir); 267 | } 268 | 269 | private void ExtractPgExtensions() 270 | { 271 | foreach (var extensionConfig in _pgExtensions) 272 | { 273 | var pgExtensionInstance = new PgExtension(BinariesDir, InstanceDir, extensionConfig); 274 | _downloadRetryPolicy.Execute(() => pgExtensionInstance.Extract()); 275 | } 276 | } 277 | 278 | // In some cases like CI environments, local user account will have write access 279 | // on the Instance directory (Postgres expects write access on the parent of data directory) 280 | // Otherwise when running initdb, it results in 'initdb: could not change permissions of directory' 281 | // Also note that the local account should have admin rights to change folder permissions 282 | private void AddLocalUserAccessPermission() 283 | { 284 | if (_platform != Platform.Windows) 285 | { 286 | return; 287 | } 288 | 289 | var filename = "icacls.exe"; 290 | var args = new List(); 291 | 292 | // get the local user under which the program runs 293 | var currentLocalUser = Environment.GetEnvironmentVariable("Username"); 294 | 295 | args.Add($"\"{InstanceDir}\""); 296 | args.Add("/t"); 297 | args.Add("/grant:r"); 298 | args.Add($"{currentLocalUser}:(OI)(CI)F"); 299 | 300 | try 301 | { 302 | var result = Utils.RunProcess(filename, args); 303 | 304 | if (result.ExitCode != 0) 305 | { 306 | throw new Exception($"Adding full access permission to local user account on instance folder returned an error code {result.ExitCode} {result.Output} {result.Error}"); 307 | } 308 | } 309 | catch (Exception ex) 310 | { 311 | throw new Exception("Error occurred while adding full access permission to local account on instance folder", ex); 312 | } 313 | } 314 | 315 | private void SetBinariesAsExecutable() 316 | { 317 | if (_platform == Platform.Windows) 318 | { 319 | return; 320 | } 321 | 322 | Utils.RunProcess("chmod", new List 323 | { 324 | $"+x {Path.Combine(PgBinDir, _initDbBin)}" 325 | }); 326 | Utils.RunProcess("chmod", new List 327 | { 328 | $"+x {Path.Combine(PgBinDir, _pgCtlBin)}" 329 | }); 330 | Utils.RunProcess("chmod", new List 331 | { 332 | $"+x {Path.Combine(PgBinDir, _postgresBin)}" 333 | }); 334 | } 335 | 336 | private void InitDb() 337 | { 338 | var filename = Path.Combine(PgBinDir, _initDbBin); 339 | var args = new List 340 | { 341 | // add data dir 342 | $"-D \"{DataDir}\"", 343 | // add super user 344 | $"-U {PgUser}", 345 | // add encoding 346 | "-E UTF-8" 347 | }; 348 | 349 | // add locale if provided 350 | if (Locale != null) 351 | { 352 | args.Add($"--locale {Locale}"); 353 | } 354 | 355 | try 356 | { 357 | var result = Utils.RunProcess(filename, args); 358 | 359 | if (result.ExitCode != 0) 360 | { 361 | throw new Exception($"InitDb execution returned an error code {result.ExitCode} {result.Output} {result.Error}"); 362 | } 363 | } 364 | catch (Exception ex) 365 | { 366 | throw new Exception("Error occurred while executing InitDb", ex); 367 | } 368 | } 369 | 370 | private bool VerifyReady() 371 | { 372 | // var filename = Path.Combine(PgBinDir, PsqlExe); 373 | // 374 | // var args = new List 375 | // { 376 | // // add host 377 | // $"-h {PgHost}", 378 | // //add port 379 | // $"-p {PgPort}", 380 | // //add user 381 | // $"-U {PgUser}", 382 | // // add database name 383 | // $"-d {PgDbName}", 384 | // // add command 385 | // $"-c \"SELECT 1 as test\"" 386 | // }; 387 | // 388 | // var result = Utils.RunProcess(filename, args); 389 | // 390 | // return result.ExitCode == 0; 391 | using var tcpClient = new TcpClient(); 392 | try 393 | { 394 | tcpClient.Connect(PgHost, PgPort); 395 | return true; 396 | } 397 | catch (Exception) 398 | { 399 | // intentionally left unhandled 400 | } 401 | 402 | return false; 403 | } 404 | 405 | private void StartServer() 406 | { 407 | var filename = Path.Combine(PgBinDir, _pgCtlBin); 408 | 409 | var args = new List 410 | { 411 | // add the data dir arg 412 | $"-D \"{DataDir}\"", 413 | // add user 414 | $"-U {PgUser}" 415 | }; 416 | 417 | // create the init options arg 418 | var initOptions = new List 419 | { 420 | // run without fsync 421 | "-F", 422 | //set the port 423 | $"-p {PgPort}" 424 | }; 425 | 426 | // add the additional parameters passed 427 | initOptions.AddRange(_pgServerParams); 428 | 429 | // add options arg 430 | args.Add($"-o \"{string.Join(" ", initOptions)}\""); 431 | 432 | // add start arg 433 | args.Add("start"); 434 | 435 | try 436 | { 437 | _pgServerProcess = new Process(); 438 | 439 | _pgServerProcess.StartInfo.RedirectStandardError = true; 440 | _pgServerProcess.StartInfo.RedirectStandardOutput = true; 441 | _pgServerProcess.StartInfo.UseShellExecute = false; 442 | _pgServerProcess.EnableRaisingEvents = true; 443 | 444 | _pgServerProcess.StartInfo.FileName = filename; 445 | _pgServerProcess.StartInfo.Arguments = string.Join(" ", args); 446 | _pgServerProcess.StartInfo.CreateNoWindow = true; 447 | 448 | _pgServerProcess.Start(); 449 | 450 | // allow some time for postgres to start 451 | var watch = new Stopwatch(); 452 | watch.Start(); 453 | 454 | WaitForServerStartup(watch); 455 | } 456 | catch (Exception ex) 457 | { 458 | throw new Exception("Exception occurred while starting Pg server", ex); 459 | } 460 | 461 | } 462 | 463 | private void WaitForServerStartup(Stopwatch watch) 464 | { 465 | while (watch.ElapsedMilliseconds < _startupWaitMs) 466 | { 467 | // verify if server ready 468 | if (VerifyReady()) 469 | { 470 | return; 471 | } 472 | 473 | Thread.Sleep(100); 474 | } 475 | 476 | watch.Stop(); 477 | 478 | throw new IOException($"Gave up waiting for server to start after {_startupWaitMs}ms"); 479 | } 480 | 481 | private void StopServer() 482 | { 483 | var filename = Path.Combine(PgBinDir, _pgCtlBin); 484 | 485 | var args = new List 486 | { 487 | // add data dir 488 | $"-D \"{DataDir}\"", 489 | // add user 490 | $"-U {PgUser}", 491 | // add stop mode 492 | $"-m {PgStopMode}", 493 | // stop wait secs 494 | $"-t {PgStopWaitS}", 495 | // add stop action 496 | "stop" 497 | }; 498 | 499 | try 500 | { 501 | Utils.RunProcess(filename, args); 502 | } 503 | catch 504 | { 505 | // ignored 506 | } 507 | } 508 | 509 | private void KillServerProcess() 510 | { 511 | try 512 | { 513 | _pgServerProcess.Kill(); 514 | } 515 | catch 516 | { 517 | // ignored 518 | } 519 | } 520 | 521 | public void Start() 522 | { 523 | // clear working directory based on flag passed 524 | if (_clearWorkingDirOnStart) 525 | { 526 | RemoveWorkingDir(); 527 | } 528 | 529 | if (!Directory.Exists(InstanceDir)) 530 | { 531 | CreateDirs(); 532 | 533 | // if the file already exists, download will be skipped 534 | DownloadPgBinary(); 535 | 536 | // if the file already exists, download will be skipped 537 | DownloadPgExtensions(); 538 | 539 | ExtractPgBinary(); 540 | ExtractPgExtensions(); 541 | 542 | if (_addLocalUserAccessPermission) 543 | { 544 | AddLocalUserAccessPermission(); 545 | } 546 | 547 | SetBinariesAsExecutable(); 548 | InitDb(); 549 | StartServer(); 550 | } 551 | else 552 | { 553 | StartServer(); 554 | } 555 | } 556 | 557 | public async Task StartAsync(CancellationToken token) 558 | { 559 | // clear working directory based on flag passed 560 | if (_clearWorkingDirOnStart) 561 | { 562 | RemoveWorkingDir(); 563 | } 564 | 565 | if (!Directory.Exists(InstanceDir)) 566 | { 567 | CreateDirs(); 568 | 569 | // if the file already exists, download will be skipped 570 | await DownloadPgBinaryAsync(); 571 | 572 | // if the file already exists, download will be skipped 573 | await DownloadPgExtensionsAsync(); 574 | 575 | ExtractPgBinary(); 576 | ExtractPgExtensions(); 577 | 578 | if (_addLocalUserAccessPermission) 579 | { 580 | AddLocalUserAccessPermission(); 581 | } 582 | 583 | SetBinariesAsExecutable(); 584 | InitDb(); 585 | StartServer(); 586 | } 587 | else 588 | { 589 | StartServer(); 590 | } 591 | } 592 | 593 | public async Task StartAsync() => await StartAsync(CancellationToken.None); 594 | 595 | public void Stop() 596 | { 597 | StopServer(); 598 | KillServerProcess(); 599 | 600 | // clear instance directory based on flag passed 601 | if (_clearInstanceDirOnStop) 602 | { 603 | RemoveInstanceDir(); 604 | } 605 | } 606 | 607 | public Task StopAsync(CancellationToken token) 608 | { 609 | Stop(); 610 | return Task.CompletedTask; 611 | } 612 | 613 | public async Task StopAsync() => await StopAsync(CancellationToken.None); 614 | 615 | public void Dispose() 616 | { 617 | Stop(); 618 | GC.SuppressFinalize(this); 619 | } 620 | 621 | public async ValueTask DisposeAsync() 622 | { 623 | await StopAsync(); 624 | GC.SuppressFinalize(this); 625 | } 626 | } --------------------------------------------------------------------------------