case 4690 - backup: run pg_dump without a shell, password in PGPASSWORD, failures detected by exit code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -59,13 +59,13 @@ namespace AyaNova.Biz
|
||||
// var DemandFileNamePrepend = OnDemand ? "manual-" : string.Empty;
|
||||
//*************
|
||||
//DO DATA BACKUP
|
||||
//build command
|
||||
//this is valid on windows
|
||||
//C:\data\code\PostgreSQLPortable_12.0\App\PgSQL\bin\pg_dump --dbname=postgresql://postgres:raven@127.0.0.1:5432/AyaNova -Fc > huge_new.backup
|
||||
//run pg_dump directly with no shell: each connection detail is its own argument and the password goes in PGPASSWORD,
|
||||
//so a user name or password containing spaces or @ : / ? # % can't break the command (it used to be a postgresql:// URI
|
||||
//with the raw password, run through cmd.exe or /bin/sh for a > file redirect), and the password is never on a command line
|
||||
//equivalent to: pg_dump --host=localhost --port=5432 --username=postgres --dbname=AyaNova --format=custom --file=db-xxx.backup
|
||||
|
||||
// await JobsBiz.LogJobAsync(Guid.Empty, $"Data backup starting");
|
||||
Npgsql.NpgsqlConnectionStringBuilder PostgresConnectionString = new Npgsql.NpgsqlConnectionStringBuilder(ServerBootConfig.AYANOVA_DB_CONNECTION);
|
||||
var DBNameParameter = $"--dbname=postgresql://{PostgresConnectionString.Username}:{PostgresConnectionString.Password}@{PostgresConnectionString.Host}:{PostgresConnectionString.Port}/{PostgresConnectionString.Database}";
|
||||
|
||||
//var DataBackupFile = $"{DemandFileNamePrepend}db-{FileUtil.GetSafeDateFileName()}.backup";//presentation issue so don't use UTC for this one
|
||||
var DataBackupFile = $"db-{FileUtil.GetSafeDateFileName()}.backup";//presentation issue so don't use UTC for this one
|
||||
@@ -77,23 +77,24 @@ namespace AyaNova.Biz
|
||||
if (!string.IsNullOrWhiteSpace(ServerBootConfig.AYANOVA_BACKUP_PG_DUMP_PATH))
|
||||
BackupUtilityCommand = Path.Combine(ServerBootConfig.AYANOVA_BACKUP_PG_DUMP_PATH, BackupUtilityCommand);
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
//put quotes around paths if spaces in them
|
||||
if (BackupUtilityCommand.Contains(' '))
|
||||
{
|
||||
BackupUtilityCommand = $"\"{BackupUtilityCommand}\"";
|
||||
}
|
||||
var Arguments = new List<string>();
|
||||
//host and user only when set, otherwise pg_dump's own defaults apply as they would for any omitted option
|
||||
if (!string.IsNullOrWhiteSpace(PostgresConnectionString.Host))
|
||||
Arguments.AddRange(new[] { "--host", PostgresConnectionString.Host });
|
||||
Arguments.AddRange(new[] { "--port", PostgresConnectionString.Port.ToString() });
|
||||
if (!string.IsNullOrWhiteSpace(PostgresConnectionString.Username))
|
||||
Arguments.AddRange(new[] { "--username", PostgresConnectionString.Username });
|
||||
Arguments.AddRange(new[] {
|
||||
"--dbname", PostgresConnectionString.Database,
|
||||
"--format=custom",
|
||||
"--file", DataBackupFile,
|
||||
"--no-password"//never prompt, a missing or wrong password must fail rather than hang
|
||||
});
|
||||
var PgEnvironment = new Dictionary<string, string>();
|
||||
if (!string.IsNullOrEmpty(PostgresConnectionString.Password))
|
||||
PgEnvironment["PGPASSWORD"] = PostgresConnectionString.Password;
|
||||
|
||||
if (DataBackupFile.Contains(' '))
|
||||
{
|
||||
DataBackupFile = $"\"{DataBackupFile}\"";
|
||||
}
|
||||
}
|
||||
|
||||
var Arguments = $"{DBNameParameter} -Fc > {DataBackupFile}";
|
||||
|
||||
var Result = RunProgram.Run(BackupUtilityCommand, Arguments, log, MAXIMUM_MS_ALLOWED_FOR_PROCESSING);
|
||||
var Result = RunProgram.RunNoShell(BackupUtilityCommand, Arguments, PgEnvironment, log, MAXIMUM_MS_ALLOWED_FOR_PROCESSING);
|
||||
if (!string.IsNullOrWhiteSpace(Result))
|
||||
{
|
||||
var msg = $"Error during data backup \"{Result}\"";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -46,6 +47,59 @@ namespace AyaNova.Util
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Run a program directly, no shell: each argument is passed as-is (no quoting or escaping needed, nothing is interpreted by a shell)
|
||||
/// and extra environment variables are set for this process only (e.g. PGPASSWORD so a password never appears on a command line)
|
||||
/// Returns empty string on success (exit code 0) or the error text otherwise, same convention as the backup job expects
|
||||
/// </summary>
|
||||
public static string RunNoShell(string fileName, IEnumerable<string> arguments, IDictionary<string, string> environment = null, ILogger log = null, int waitForExitTimeOut = int.MaxValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var process = new Process())
|
||||
{
|
||||
process.StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
foreach (var a in arguments)
|
||||
process.StartInfo.ArgumentList.Add(a);
|
||||
if (environment != null)
|
||||
foreach (var e in environment)
|
||||
process.StartInfo.Environment[e.Key] = e.Value;
|
||||
|
||||
process.Start();
|
||||
//read both streams at once, reading one to the end first can deadlock if the other fills its buffer
|
||||
var stdOut = process.StandardOutput.ReadToEndAsync();
|
||||
var stdErr = process.StandardError.ReadToEndAsync();
|
||||
if (!process.WaitForExit(waitForExitTimeOut))
|
||||
{
|
||||
process.Kill(true);
|
||||
return $"ERROR: TIMED OUT {waitForExitTimeOut}ms BEFORE COMPLETION running {fileName}";
|
||||
}
|
||||
process.WaitForExit();//ensures output streams are fully read after the timed wait above
|
||||
if (process.ExitCode != 0)
|
||||
return $"ERROR: {Path.GetFileName(fileName)} exit code {process.ExitCode}: {stdErr.Result}{stdOut.Result}".Trim();
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//arguments deliberately not logged, they can contain connection details
|
||||
if (log != null)
|
||||
{
|
||||
log.LogError(ex, $"RunProgram error running command:{fileName}");
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static string RunWindows(string cmd, string arguments, int waitForExitTimeOut = int.MaxValue)
|
||||
{
|
||||
//RunProgram.Run("cmd.exe",FullRunCommand, log);
|
||||
|
||||
Reference in New Issue
Block a user