Add PostgresSubprocessException and refactor database initialization logic
- Introduced PostgresSubprocessException to handle errors during PostgreSQL subprocess execution. - Refactored PostgresDatabaseProvider to improve schema initialization script discovery. - Enhanced error handling for psql command execution, including logging of output and errors. - Implemented methods to find the schema initialization script and the psql executable path.
This commit is contained in:
+39
@@ -0,0 +1,39 @@
|
||||
// <copyright file="PostgresSubprocessException.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace Jellyfin.Database.Providers.Postgres.Exceptions;
|
||||
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an error that occurs during the execution of a PostgreSQL subprocess (e.g., psql, pg_dump).
|
||||
/// </summary>
|
||||
public class PostgresSubprocessException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PostgresSubprocessException"/> class.
|
||||
/// </summary>
|
||||
public PostgresSubprocessException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PostgresSubprocessException"/> class with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public PostgresSubprocessException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PostgresSubprocessException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
|
||||
public PostgresSubprocessException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
+109
-71
@@ -8,7 +8,6 @@ namespace Jellyfin.Database.Providers.Postgres;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -552,91 +551,71 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
|
||||
// Database tables don't exist - initialize from SQL script
|
||||
logger.LogInformation("Database tables not found (users.Users table missing). Initializing from SQL script...");
|
||||
|
||||
var schemaScriptPath = Path.Combine(AppContext.BaseDirectory, "sql", "schema_init", "create_database_schema.sql");
|
||||
|
||||
if (!File.Exists(schemaScriptPath))
|
||||
var createDbScriptPath = FindSchemaScriptPath();
|
||||
if (string.IsNullOrEmpty(createDbScriptPath))
|
||||
{
|
||||
logger.LogError("Schema script not found at: {Path}", schemaScriptPath);
|
||||
logger.LogError("Cannot initialize database automatically. Please run the SQL script manually.");
|
||||
throw new FileNotFoundException($"Schema initialization script not found: {schemaScriptPath}");
|
||||
throw new FileNotFoundException("Could not find the schema initialization script: create_database_schema.sql");
|
||||
}
|
||||
|
||||
logger.LogInformation("Found schema script at: {Path}", schemaScriptPath);
|
||||
logger.LogInformation("Executing create_database_schema.sql (this may take several minutes)...");
|
||||
var psqlPath = FindPsqlPath();
|
||||
|
||||
try
|
||||
// The SQL file contains psql meta-commands (\restrict, etc.) that can't be executed via ExecuteNonQuery
|
||||
// We need to use psql subprocess instead
|
||||
var connectionString = connection.ConnectionString;
|
||||
var builder = new NpgsqlConnectionStringBuilder(connectionString);
|
||||
|
||||
// Build psql command
|
||||
var psqlArgs = $"-h {builder.Host} -p {builder.Port} -U {builder.Username} -d {builder.Database} -f \"{createDbScriptPath}\"";
|
||||
|
||||
// Log command (password not in args, so safe to log)
|
||||
logger.LogInformation("Executing via psql: psql {Args}", psqlArgs);
|
||||
|
||||
var psqlProcess = new System.Diagnostics.Process
|
||||
{
|
||||
// The SQL file contains psql meta-commands (\restrict, etc.) that can't be executed via ExecuteNonQuery
|
||||
// We need to use psql subprocess instead
|
||||
var connectionString = connection.ConnectionString;
|
||||
var builder = new NpgsqlConnectionStringBuilder(connectionString);
|
||||
|
||||
// Build psql command
|
||||
var psqlArgs = $"-h {builder.Host} -p {builder.Port} -U {builder.Username} -d {builder.Database} -f \"{schemaScriptPath}\"";
|
||||
|
||||
// Log command (password not in args, so safe to log)
|
||||
logger.LogInformation("Executing via psql: psql {Args}", psqlArgs);
|
||||
|
||||
var psqlProcess = new System.Diagnostics.Process
|
||||
StartInfo = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
StartInfo = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = "psql",
|
||||
Arguments = psqlArgs,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
// Set password via environment variable
|
||||
if (!string.IsNullOrEmpty(builder.Password))
|
||||
{
|
||||
psqlProcess.StartInfo.EnvironmentVariables["PGPASSWORD"] = builder.Password;
|
||||
FileName = "psql",
|
||||
Arguments = psqlArgs,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
psqlProcess.Start();
|
||||
// Set password via environment variable
|
||||
if (!string.IsNullOrEmpty(builder.Password))
|
||||
{
|
||||
psqlProcess.StartInfo.EnvironmentVariables["PGPASSWORD"] = builder.Password;
|
||||
}
|
||||
|
||||
// Read output
|
||||
var output = await psqlProcess.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
var error = await psqlProcess.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
psqlProcess.Start();
|
||||
|
||||
await psqlProcess.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
// Read output
|
||||
var output = await psqlProcess.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
var error = await psqlProcess.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (psqlProcess.ExitCode == 0)
|
||||
await psqlProcess.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (psqlProcess.ExitCode == 0)
|
||||
{
|
||||
logger.LogInformation("✅ Successfully initialized database from SQL script via psql");
|
||||
logger.LogInformation("Database is ready. You can now start Jellyfin.");
|
||||
if (!string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
logger.LogInformation("✅ Successfully initialized database from SQL script via psql");
|
||||
logger.LogInformation("Database is ready. You can now start Jellyfin.");
|
||||
if (!string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
logger.LogDebug("psql output: {Output}", output);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("psql exited with code {ExitCode}", psqlProcess.ExitCode);
|
||||
if (!string.IsNullOrWhiteSpace(error))
|
||||
{
|
||||
logger.LogError("psql error: {Error}", error);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"psql failed with exit code {psqlProcess.ExitCode}. Error: {error}");
|
||||
logger.LogDebug("psql output: {Output}", output);
|
||||
}
|
||||
}
|
||||
catch (System.ComponentModel.Win32Exception psqlNotFound)
|
||||
else
|
||||
{
|
||||
// psql not found in PATH - fallback to manual instructions
|
||||
logger.LogWarning(psqlNotFound, "psql command not found in PATH. Cannot automatically initialize database.");
|
||||
logger.LogError("Please run the SQL script manually: psql -U postgres -d jellyfin_testdata -f {Path}", schemaScriptPath);
|
||||
throw new FileNotFoundException("psql command not found. Please install PostgreSQL client tools and ensure psql is in your PATH, or run the SQL script manually.", psqlNotFound);
|
||||
}
|
||||
catch (Exception scriptEx)
|
||||
{
|
||||
logger.LogError(scriptEx, "Failed to execute schema creation script");
|
||||
logger.LogError("Please run the SQL script manually: psql -U postgres -d jellyfin_testdata -f {Path}", schemaScriptPath);
|
||||
throw;
|
||||
logger.LogError("psql exited with code {ExitCode}", psqlProcess.ExitCode);
|
||||
if (!string.IsNullOrWhiteSpace(error))
|
||||
{
|
||||
logger.LogError("psql error: {Error}", error);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"psql failed with exit code {psqlProcess.ExitCode}. Error: {error}");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -1044,4 +1023,63 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
|
||||
|
||||
await dbContext.Database.ExecuteSqlRawAsync(deleteAllQuery).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the full path to the schema initialization script.
|
||||
/// </summary>
|
||||
/// <returns>The full path to the script, or null if not found.</returns>
|
||||
private string? FindSchemaScriptPath()
|
||||
{
|
||||
var scriptName = Path.Combine("schema_init", "create_database_schema.sql");
|
||||
|
||||
// 1. Check relative to the executing assembly
|
||||
var assemblyLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
if (!string.IsNullOrEmpty(assemblyLocation))
|
||||
{
|
||||
var assemblySqlPath = Path.Combine(assemblyLocation, "sql", scriptName);
|
||||
if (File.Exists(assemblySqlPath))
|
||||
{
|
||||
logger.LogInformation("Found schema script at assembly path: {Path}", assemblySqlPath);
|
||||
return assemblySqlPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check relative to the current working directory
|
||||
var currentDirSqlPath = Path.Combine(Directory.GetCurrentDirectory(), "sql", scriptName);
|
||||
if (File.Exists(currentDirSqlPath))
|
||||
{
|
||||
logger.LogInformation("Found schema script at current directory path: {Path}", currentDirSqlPath);
|
||||
return currentDirSqlPath;
|
||||
}
|
||||
|
||||
// 3. Fallback for development environment (navigate up from assembly location)
|
||||
if (!string.IsNullOrEmpty(assemblyLocation))
|
||||
{
|
||||
var dir = new DirectoryInfo(assemblyLocation);
|
||||
var root = dir.Parent?.Parent?.Parent?.Parent?.Parent; // Adjust depth as needed
|
||||
if (root != null)
|
||||
{
|
||||
var devSqlPath = Path.Combine(root.FullName, "sql", scriptName);
|
||||
if (File.Exists(devSqlPath))
|
||||
{
|
||||
logger.LogInformation("Found schema script at development path: {Path}", devSqlPath);
|
||||
return devSqlPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogError("Schema initialization script not found in any known location.");
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the path to the psql executable.
|
||||
/// </summary>
|
||||
/// <returns>The path to psql, or just "psql" if it's in the system PATH.</returns>
|
||||
private static string FindPsqlPath()
|
||||
{
|
||||
// For now, assume psql is in the PATH.
|
||||
// Future improvements could search known installation directories.
|
||||
return "psql";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user