diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml index e4e8c02e6d67f3725f68a6e72d0f375f5d12fe8e..e01b6df0dd8f25563e3d56c29bc0304013b596a2 100644 --- a/doc/src/sgml/ref/pgupgrade.sgml +++ b/doc/src/sgml/ref/pgupgrade.sgml @@ -106,7 +106,8 @@ PostgreSQL documentation - check clusters only, don't change any data + check clusters without performing the upgrade. + If is also specified, initialize the new cluster before checking it and remove it afterward. @@ -262,6 +263,114 @@ PostgreSQL documentation + + + + + Create the new cluster automatically by running + initdb before upgrading, instead of requiring the + user to have created it manually. + The new cluster uses the old cluster's WAL segment size and + data-checksum setting, and the encoding and locale of its + template0 database. + + + The directory specified by / + must be empty, nonexistent, or contain only a + pg_upgrade_output.d directory from previous runs. + Initialization runs directly in the requested directory and preserves + pg_upgrade_output.d. + If the new data directory is inside the old data directory, pg_upgrade does not generate the script for deleting the old cluster, because the script would also delete the new cluster. + + + When combined with /, + initializes the requested new directory and + runs the compatibility checks against it. The initialized cluster and its + separate WAL files are removed whether the checks succeed or fail, + even with . Apart from logs, the target is left unchanged. + After successful initialization, a pre-existing WAL directory is emptied + and its original permissions are restored. + To upgrade, rerun the same command without . + + + / continues to pass + options to the new cluster's server process; those options are not + passed to initdb. Use + for additional initialization options. + + + Failed initialization follows + initdb's normal cleanup, including . + Without , a later failure leaves the new data and any + separate WAL directory in place, just as without . + If the cleanup state file cannot be read or server shutdown cannot be + confirmed, pg_upgrade reports failure and + retains the initialized cluster and logs. Failures while + removing initialized data or separate WAL files keep the logs and any + remaining data. Their location is reported. Errors removing empty + target directories or restoring target permissions can occur after + normal log cleanup. + See for recovery instructions. + Before retrying, remove the failed cluster and empty any separate WAL + directory. Retained pg_upgrade_output.d logs can remain. + + + + + + options + + + Pass additional arguments to initdb. Requires + . This option can be repeated; arguments are + appended in command-line order. For example: + +pg_upgrade --initdb --initdb-options='-c huge_pages=off --waldir=/wal/new' ... + + Settings supplied with initdb's + are saved in the new postgresql.conf and can also + affect initialization itself. In particular, a library specified in + shared_preload_libraries must support being loaded + during initialization, as well as by the new server during upgrade. + + + Options are passed to initdb after the generated + arguments, so they can override generated settings. initdb + validates them and pg_upgrade checks the resulting + cluster, which must be created in the requested new directory. + Initialization, automatic checks, and upgrades use that same directory. + + + Within options, spaces and tabs separate + arguments. Single or double quotes group text into one argument and + are removed; adjacent quoted and unquoted text forms one argument. + Single quotes preserve their contents literally. Elsewhere, a + backslash escapes a following quote, backslash, space, or tab, and is + preserved before other characters. Unmatched quotes, a trailing + unquoted backslash, and newline or carriage-return characters are + rejected. No shell expansion is performed; for example, + $libdir is passed literally. The caller's shell + quoting rules still apply to the outer command line. Arguments can + appear in command output, log files, and process listings, so use + file-based inputs such as for passwords. + + + Group access to the new cluster is disabled by default. + To enable it, use . + With or , transferred relation files retain their existing permissions, even when group access is requested for the new cluster. + With , moved database directories also retain their existing permissions. + + + Authentication options, including and + , are passed unchanged. Provide credentials + accepted by both clusters, for example through a protected + password file. + pg_upgrade does not weaken authentication + rules to obtain access. + + + + @@ -457,10 +566,22 @@ make prefix=/usr/local/pgsql.new install Initialize the new cluster using initdb. - Again, use compatible initdb - flags that match the old cluster. Many - prebuilt installers do this step automatically. There is no need to - start the new cluster. + Two initdb settings must match the old cluster, or + pg_upgrade will fail with a control-data + mismatch: the WAL segment size () and the + data checksum setting ( or + ). You can read the old cluster's + values with + pg_controldata. + (The encoding and locale need not match: pg_upgrade + copies them from the old cluster.) Many prebuilt installers do this step + automatically. There is no need to start the new cluster. + + + Alternatively, pass to + pg_upgrade to have it run + initdb automatically, deriving the required settings + from the old cluster. In that case this manual step can be skipped. @@ -1064,10 +1185,14 @@ psql --username=postgres --file=script.sql postgres Notes - pg_upgrade creates various working files, such - as schema dumps, stored within pg_upgrade_output.d in - the directory of the new cluster. Each run creates a new subdirectory named - with a timestamp formatted as per ISO 8601 + pg_upgrade creates various working files, such as schema dumps, stored within pg_upgrade_output.d in the directory of the new cluster. + With , the same output directory also contains + early server and initdb output. If initialization or + a later step fails before log cleanup, these logs remain under the + requested new data directory. retains logs, including after a + successful check. It does not retain the check's initialized cluster. + Each run creates a new subdirectory named with a timestamp formatted as per + ISO 8601 (%Y%m%dT%H%M%S), where all its generated files are stored. pg_upgrade_output.d and its contained files will be diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c index 86d42a98a27aa58896c9af4ab86b4ac5436dd5ef..23d60f25ba3a979b057e1576e3b4957b9e9afa16 100644 --- a/src/bin/initdb/initdb.c +++ b/src/bin/initdb/initdb.c @@ -228,6 +228,9 @@ static const char *const backend_options = "--single -F -O -j -c search_path=pg_ /* Additional switches to pass to backend (either boot or standalone) */ static char *extra_options = ""; +static char *pg_upgrade_state_file = NULL; +static char *pg_upgrade_wal_parent = NULL; +static mode_t pg_upgrade_wal_mode; static const char *const subdirs[] = { "global", @@ -280,6 +283,7 @@ static bool test_specific_config_settings(int test_conns, int test_av_slots, int test_buffs); static void setup_config(void); static void bootstrap_template1(void); +static void write_pg_upgrade_state(void); static void setup_auth(FILE *cmdfd); static void get_su_pwd(void); static void setup_depend(FILE *cmdfd); @@ -769,7 +773,9 @@ cleanup_directories_atexit(void) { pg_log_info("removing contents of data directory \"%s\"", pg_data); - if (!rmtree(pg_data, false)) + if (!(pg_upgrade_state_file ? + rmtree_except(pg_data, "pg_upgrade_output.d") : + rmtree(pg_data, false))) pg_log_error("failed to remove contents of data directory"); } @@ -1543,6 +1549,26 @@ setup_config(void) } +/* + * Save the external WAL directory's original state for + * pg_upgrade --check --initdb. After checking, pg_upgrade removes generated + * WAL files and newly created directories, preserves any pre-existing WAL + * directory, and restores its original permissions. + */ +static void +write_pg_upgrade_state(void) +{ + FILE *state = fopen(pg_upgrade_state_file, "w"); + + if (!state) + pg_fatal("could not open file \"%s\": %m", pg_upgrade_state_file); + if (fprintf(state, "%d %o\n%s\n%s\n", found_existing_xlogdir, + pg_upgrade_wal_mode, xlog_dir ? xlog_dir : "", + pg_upgrade_wal_parent ? pg_upgrade_wal_parent : "") < 0 || + fclose(state) != 0) + pg_fatal("could not write file \"%s\": %m", pg_upgrade_state_file); +} + /* * run the BKI script in bootstrap mode to create template1 */ @@ -2900,8 +2926,10 @@ void create_data_directory(void) { int ret; + mode_t pgdata_mode = pg_dir_create_mode; - switch ((ret = pg_check_dir(pg_data))) + switch ((ret = pg_check_dir_except(pg_data, + pg_upgrade_state_file ? "pg_upgrade_output.d" : NULL))) { case 0: /* PGDATA not there, must create it */ @@ -2923,7 +2951,19 @@ create_data_directory(void) pg_data); fflush(stdout); - if (chmod(pg_data, pg_dir_create_mode) != 0) +#ifndef WIN32 + if (pg_upgrade_state_file) + { + struct stat st; + + /* Preserve setgid inherited when pg_upgrade created PGDATA. */ + if (stat(pg_data, &st) != 0) + pg_fatal("could not read permissions of directory \"%s\": %m", + pg_data); + pgdata_mode |= st.st_mode & S_ISGID; + } +#endif + if (chmod(pg_data, pgdata_mode) != 0) pg_fatal("could not change permissions of directory \"%s\": %m", pg_data); else @@ -2980,6 +3020,20 @@ create_xlog_or_symlink(void) xlog_dir); fflush(stdout); + if (pg_upgrade_state_file) + { + struct stat st; + + pg_upgrade_wal_parent = pg_strdup(xlog_dir); + while (stat(pg_upgrade_wal_parent, &st) != 0 && errno == ENOENT) + { + size_t len = strlen(pg_upgrade_wal_parent); + + get_parent_directory(pg_upgrade_wal_parent); + if (strlen(pg_upgrade_wal_parent) == len) + break; + } + } if (pg_mkdir_p(xlog_dir, pg_dir_create_mode) != 0) pg_fatal("could not create directory \"%s\": %m", xlog_dir); @@ -2995,6 +3049,14 @@ create_xlog_or_symlink(void) xlog_dir); fflush(stdout); + if (pg_upgrade_state_file) + { + struct stat st; + + if (stat(xlog_dir, &st) != 0) + pg_fatal("could not read permissions of directory \"%s\": %m", xlog_dir); + pg_upgrade_wal_mode = st.st_mode & 07777; + } if (chmod(xlog_dir, pg_dir_create_mode) != 0) pg_fatal("could not change permissions of directory \"%s\": %m", xlog_dir); @@ -3207,6 +3269,8 @@ main(int argc, char *argv[]) {"sync-method", required_argument, NULL, 19}, {"no-data-checksums", no_argument, NULL, 20}, {"no-sync-data-files", no_argument, NULL, 21}, + /* internal option for pg_upgrade */ + {"for-pg-upgrade", required_argument, NULL, 22}, {NULL, 0, NULL, 0} }; @@ -3404,6 +3468,9 @@ main(int argc, char *argv[]) case 21: sync_data_files = false; break; + case 22: + pg_upgrade_state_file = pg_strdup(optarg); + break; default: /* getopt_long already emitted a complaint */ pg_log_error_hint("Try \"%s --help\" for more information.", progname); @@ -3568,6 +3635,9 @@ main(int argc, char *argv[]) } + if (pg_upgrade_state_file) + write_pg_upgrade_state(); + success = true; return 0; } diff --git a/src/bin/pg_upgrade/Makefile b/src/bin/pg_upgrade/Makefile index 771addb675a6dcc52a3ac90ca1252a5e88a83d45..9d8602a9a72841f30cc86eab0ad254721bf76f9f 100644 --- a/src/bin/pg_upgrade/Makefile +++ b/src/bin/pg_upgrade/Makefile @@ -3,7 +3,7 @@ PGFILEDESC = "pg_upgrade - an in-place binary upgrade utility" PGAPPICON = win32 -EXTRA_INSTALL=contrib/test_decoding src/test/modules/dummy_seclabel src/test/modules/test_extensions +EXTRA_INSTALL=contrib/test_decoding src/test/modules/dummy_seclabel src/test/modules/test_extensions src/test/modules/test_slru subdir = src/bin/pg_upgrade top_builddir = ../../.. diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c index 150c4fc34b9b29773df38af7415e169cefdb14b1..90834fffb5820b96cb152d86ddab3464fb1a987d 100644 --- a/src/bin/pg_upgrade/check.c +++ b/src/bin/pg_upgrade/check.c @@ -758,6 +758,8 @@ report_clusters_compatible(void) /* stops new cluster */ stop_postmaster(false); + cleanup_initdb_cluster(); + cleanup_output_dirs(); exit(0); } diff --git a/src/bin/pg_upgrade/exec.c b/src/bin/pg_upgrade/exec.c index a1bdbf373e38543eb2147e54b4d1d776c9360c14..7f0a85dd697d99bcab1729b0da72f3f6f5ac8587 100644 --- a/src/bin/pg_upgrade/exec.c +++ b/src/bin/pg_upgrade/exec.c @@ -15,8 +15,6 @@ #include "fe_utils/version.h" #include "pg_upgrade.h" -static void check_data_dir(ClusterInfo *cluster); -static void check_bin_dir(ClusterInfo *cluster, bool check_versions); static void get_bin_version(ClusterInfo *cluster); static void check_exec(const char *dir, const char *program, bool check_version); @@ -244,15 +242,10 @@ pid_lock_file_exists(const char *datadir) /* - * verify_directories() - * - * does all the hectic work of verifying directories and executables - * of old and new server. - * - * NOTE: May update the values of all parameters + * Check that the current directory is writable */ void -verify_directories(void) +check_current_dir_write_permissions(void) { #ifndef WIN32 if (access(".", R_OK | W_OK | X_OK) != 0) @@ -260,11 +253,6 @@ verify_directories(void) if (win32_check_directory_write_permissions() != 0) #endif pg_fatal("You must have read and write access in the current directory."); - - check_bin_dir(&old_cluster, false); - check_data_dir(&old_cluster); - check_bin_dir(&new_cluster, true); - check_data_dir(&new_cluster); } @@ -329,7 +317,7 @@ check_single_dir(const char *pg_data, const char *subdir) * us) we display an error message and exit() * */ -static void +void check_data_dir(ClusterInfo *cluster) { const char *pg_data = cluster->pgdata; @@ -361,7 +349,7 @@ check_data_dir(ClusterInfo *cluster) * against the version of this pg_upgrade. This is for checking the target * bindir. */ -static void +void check_bin_dir(ClusterInfo *cluster, bool check_versions) { struct stat statBuf; diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c index 5c59cfb32e8b7fc06cc07f42e4d0a394f5ae02c3..d66fc170fa0186f194d84f9d67394eaf5aac7463 100644 --- a/src/bin/pg_upgrade/info.c +++ b/src/bin/pg_upgrade/info.c @@ -21,7 +21,6 @@ static void create_rel_filename_map(const char *old_data, const char *new_data, static void report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db); static void free_db_and_rel_infos(DbInfoArr *db_arr); -static void get_template0_info(ClusterInfo *cluster); static void get_db_infos(ClusterInfo *cluster); static char *get_rel_infos_query(void); static void process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg); @@ -328,7 +327,7 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster) * Get information about template0, which will be copied from the old cluster * to the new cluster. */ -static void +void get_template0_info(ClusterInfo *cluster) { PGconn *conn = connectToServer(cluster, "template1"); diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build index ffbf6ae8d759bf8d95559c735d846ba1869347d3..6288e0798643ada244dae8c2c9c31334b2862280 100644 --- a/src/bin/pg_upgrade/meson.build +++ b/src/bin/pg_upgrade/meson.build @@ -69,6 +69,7 @@ tests += { 't/006_transfer_modes.pl', 't/007_multixact_conversion.pl', 't/008_extension_control_path.pl', + 't/009_initdb_option.pl', ], 'deps': [test_ext], 'test_kwargs': {'priority': 40}, # pg_upgrade tests are slow diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c index f01d2f92d9548aaf648f247773e684fc60d0e59b..121cb0c9e9c0042d3f7810ebae9edaf9d57c206e 100644 --- a/src/bin/pg_upgrade/option.c +++ b/src/bin/pg_upgrade/option.c @@ -9,6 +9,8 @@ #include "postgres_fe.h" +#include + #ifdef WIN32 #include #endif @@ -20,6 +22,7 @@ #include "utils/pidfile.h" static void usage(void); +static void parse_initdb_options(const char *options); static void check_required_directory(char **dirpath, const char *envVarName, bool useCwd, const char *cmdLineOption, const char *description, @@ -63,6 +66,8 @@ parseCommandLine(int argc, char *argv[]) {"no-statistics", no_argument, NULL, 5}, {"set-char-signedness", required_argument, NULL, 6}, {"swap", no_argument, NULL, 7}, + {"initdb", no_argument, NULL, 8}, + {"initdb-options", required_argument, NULL, 9}, {NULL, 0, NULL, 0} }; @@ -234,6 +239,15 @@ parseCommandLine(int argc, char *argv[]) user_opts.transfer_mode = TRANSFER_MODE_SWAP; break; + case 8: + user_opts.initdb_new_cluster = true; + break; + + case 9: + user_opts.initdb_options_given = true; + parse_initdb_options(optarg); + break; + default: fprintf(stderr, _("Try \"%s --help\" for more information.\n"), os_info.progname); @@ -244,6 +258,9 @@ parseCommandLine(int argc, char *argv[]) if (optind < argc) pg_fatal("too many command-line arguments (first is \"%s\")", argv[optind]); + if (user_opts.initdb_options_given && !user_opts.initdb_new_cluster) + pg_fatal("--initdb-options requires --initdb"); + if (!user_opts.sync_method) user_opts.sync_method = pg_strdup("fsync"); @@ -300,6 +317,73 @@ parseCommandLine(int argc, char *argv[]) } +/* + * Split --initdb-options without invoking a shell. Single quotes preserve + * every character. Outside single quotes, backslashes escape only quotes, + * backslashes, spaces, and tabs, so ordinary Windows path separators survive. + */ +static void +parse_initdb_options(const char *options) +{ + const char *p = options; + char *argument; + + if (strpbrk(options, "\r\n") != NULL) + pg_fatal("--initdb-options must not contain a newline or carriage return"); + + argument = pg_malloc(strlen(options) + 1); + while (*p) + { + char *out = argument; + char quote = '\0'; + + while (*p == ' ' || *p == '\t') + p++; + if (*p == '\0') + break; + + while (*p) + { + if (quote == '\0' && (*p == ' ' || *p == '\t')) + break; + if (*p == quote) + { + quote = '\0'; + p++; + } + else if (quote != '\'' && *p == '\\') + { + p++; + if (*p == '\0') + pg_fatal("trailing backslash in --initdb-options"); + if (*p == '\'' || *p == '"' || *p == '\\' || + *p == ' ' || *p == '\t') + *out++ = *p++; + else + *out++ = '\\'; + } + else if (quote == '\0' && (*p == '\'' || *p == '"')) + quote = *p++; + else + *out++ = *p++; + } + + if (quote != '\0') + pg_fatal("unterminated quote in --initdb-options"); + *out = '\0'; + + if (user_opts.num_initdb_options == INT_MAX - 1) + pg_fatal("too many arguments in --initdb-options"); + user_opts.initdb_options = + pg_realloc_array(user_opts.initdb_options, char *, + (size_t) user_opts.num_initdb_options + 1); + user_opts.initdb_options[user_opts.num_initdb_options++] = + pg_strdup(argument); + } + pg_free(argument); +} + + static void usage(void) { @@ -328,6 +412,9 @@ usage(void) printf(_(" --clone clone instead of copying files to new cluster\n")); printf(_(" --copy copy files to new cluster (default)\n")); printf(_(" --copy-file-range copy files to new cluster with copy_file_range\n")); + printf(_(" --initdb create the new cluster with initdb before\n" + " checking or upgrading, using old cluster settings\n")); + printf(_(" --initdb-options=OPTIONS options to pass to initdb (requires --initdb)\n")); printf(_(" --no-statistics do not import statistics from old cluster\n")); printf(_(" --set-char-signedness=OPTION set new cluster char signedness to \"signed\" or\n" " \"unsigned\"\n")); @@ -336,7 +423,9 @@ usage(void) printf(_(" -?, --help show this help, then exit\n")); printf(_("\n" "Before running pg_upgrade you must:\n" - " create a new database cluster (using the new version of initdb)\n" + " create a new database cluster (using the new version of initdb),\n" + " unless the --initdb option is given, in which case pg_upgrade\n" + " creates the new cluster for you\n" " shutdown the postmaster servicing the old cluster\n" " shutdown the postmaster servicing the new cluster\n")); printf(_("\n" @@ -422,9 +511,10 @@ check_required_directory(char **dirpath, const char *envVarName, bool useCwd, * by querying the running server. This has limited checking because we * can't check for a running server because we can't find postmaster.pid. * - * On entry, cluster->pgdata has been set from command line or env variable, - * but cluster->pgconfig isn't set. We fill both variables with corrected - * values. + * On entry, cluster->bindir has already been set, and cluster->pgdata has + * been initially set from the command line or env variable. We fill + * cluster->pgconfig, and adjust cluster->pgdata with a corrected value if the + * real data directory is in a different location. */ void adjust_data_dir(ClusterInfo *cluster) diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c index c0fadb3f31773c80351ec48b92e5fdfff1871c03..ceddb2916b483197db70c96efe063777cb79f613 100644 --- a/src/bin/pg_upgrade/pg_upgrade.c +++ b/src/bin/pg_upgrade/pg_upgrade.c @@ -45,10 +45,14 @@ #include "access/multixact.h" #include "catalog/pg_class_d.h" +#include "catalog/pg_collation_d.h" #include "common/file_perm.h" #include "common/logging.h" #include "common/restricted_token.h" +#include "common/string.h" #include "fe_utils/string_utils.h" +#include "fe_utils/version.h" +#include "mb/pg_wchar.h" #include "pg_upgrade.h" /* @@ -65,8 +69,17 @@ static void prepare_new_globals(void); static void create_new_objects(void); static void copy_xact_xlog_xid(void); static void set_frozenxids(void); -static void make_outputdirs(char *pgdata); -static void setup(char *argv0); +static void make_outputdirs(const char *output_root); +static void check_bin_dirs(const char *argv0); +static void check_clusters_stopped(void); +static void prepare_initdb(void); +static void create_initdb_parents(char *path); +static void finalize_initdb(void); +static void set_initdb_directory_permissions(const char *path); +static void set_output_permissions(void); +static void get_old_cluster_initdb_info(void); +static void build_new_cluster_initdb_cmd(PQExpBuffer cmd); +static void create_new_cluster_via_initdb(void); static void create_logical_replication_slots(void); static void create_conflict_detection_slot(void); @@ -74,6 +87,26 @@ ClusterInfo old_cluster, new_cluster; OSInfo os_info; +/* + * Track directories created by --initdb and the original permissions of + * existing target and external WAL directories. Check-mode cleanup uses + * this state to remove initialized data and newly created directories, + * preserve pre-existing directories, and restore their permissions. + */ +static char *initdb_target; +static char *initdb_state_file; +static char *initdb_waldir; +static char *initdb_wal_parent; +static bool initdb_wal_existed; +static mode_t initdb_wal_mode; +static bool initdb_succeeded; +static bool initdb_target_created; +static bool initdb_output_root_created; +static mode_t initdb_target_mode; +static bool initdb_restore_mode; +static char **initdb_created_parents; +static int initdb_num_created_parents; + char *output_files[] = { SERVER_LOG_FILE, #ifdef WIN32 @@ -106,26 +139,54 @@ main(int argc, char **argv) get_restricted_token(); + /* + * Make sure the user has a clean environment, otherwise, we may confuse + * libpq when we connect to one (or both) of the servers. + */ + check_pghost_envvar(); + + /* + * Resolve old and new binary dirs before adjust_data_dir(), because + * adjust_data_dir() might need to call the binaries. + */ + check_bin_dirs(argv[0]); + adjust_data_dir(&old_cluster); - adjust_data_dir(&new_cluster); + + if (user_opts.initdb_new_cluster) + { + prepare_initdb(); + create_new_cluster_via_initdb(); + } + else + adjust_data_dir(&new_cluster); /* - * Set mask based on PGDATA permissions, needed for the creation of the - * output directories with correct permissions. + * Use the permissions selected by initdb, including explicit group + * access. */ if (!GetDataDirectoryCreatePerm(new_cluster.pgdata)) pg_fatal("could not read permissions of directory \"%s\": %m", new_cluster.pgdata); - umask(pg_mode_mask); - /* - * This needs to happen after adjusting the data directory of the new - * cluster in adjust_data_dir(). - */ - make_outputdirs(new_cluster.pgdata); + if (user_opts.initdb_new_cluster) + { + for (int i = 0; i < initdb_num_created_parents; i++) + set_initdb_directory_permissions(initdb_created_parents[i]); + set_output_permissions(); + if (initdb_output_root_created) + set_initdb_directory_permissions(log_opts.rootdir); + } + else + make_outputdirs(new_cluster.pgdata); - setup(argv[0]); + check_current_dir_write_permissions(); + + check_data_dir(&old_cluster); + if (!user_opts.initdb_new_cluster) + check_data_dir(&new_cluster); + check_clusters_stopped(); output_check_banner(); @@ -237,18 +298,7 @@ main(int argc, char **argv) stop_postmaster(false); } - if (user_opts.do_sync) - { - prep_status("Sync data directory to disk"); - exec_prog(UTILITY_LOG_FILE, NULL, true, true, - "\"%s/initdb\" --sync-only %s \"%s\" --sync-method %s", - new_cluster.bindir, - (user_opts.transfer_mode == TRANSFER_MODE_SWAP) ? - "--no-sync-data-files" : "", - new_cluster.pgdata, - user_opts.sync_method); - check_ok(); - } + sync_new_cluster(user_opts.transfer_mode != TRANSFER_MODE_SWAP); create_script_for_old_cluster_deletion(&deletion_script_file_name); @@ -268,13 +318,35 @@ main(int argc, char **argv) return 0; } +/* + * Sync the new cluster after shutdown, unless --no-sync is specified. + * A completed --swap upgrade can skip syncing data files here because they + * have already been synced. + */ +void +sync_new_cluster(bool sync_data_files) +{ + if (!user_opts.do_sync) + return; + + prep_status("Sync data directory to disk"); + exec_prog(UTILITY_LOG_FILE, NULL, true, true, + "\"%s/initdb\" --sync-only %s \"%s\" --sync-method %s", + new_cluster.bindir, + sync_data_files ? "" : "--no-sync-data-files", + new_cluster.pgdata, + user_opts.sync_method); + check_ok(); +} + + /* * Create and assign proper permissions to the set of output directories * used to store any data generated internally, filling in log_opts in * the process. */ static void -make_outputdirs(char *pgdata) +make_outputdirs(const char *output_root) { FILE *fp; char **filename; @@ -285,8 +357,9 @@ make_outputdirs(char *pgdata) time_t tt; int len; + pg_free(log_opts.rootdir); log_opts.rootdir = (char *) pg_malloc0(MAXPGPATH); - len = snprintf(log_opts.rootdir, MAXPGPATH, "%s/%s", pgdata, BASE_OUTPUTDIR); + len = snprintf(log_opts.rootdir, MAXPGPATH, "%s/%s", output_root, BASE_OUTPUTDIR); if (len >= MAXPGPATH) pg_fatal("directory path for new cluster is too long"); @@ -297,6 +370,7 @@ make_outputdirs(char *pgdata) /* append milliseconds */ snprintf(timebuf + strlen(timebuf), sizeof(timebuf) - strlen(timebuf), ".%03d", (int) (time.tv_usec / 1000)); + pg_free(log_opts.basedir); log_opts.basedir = (char *) pg_malloc0(MAXPGPATH); len = snprintf(log_opts.basedir, MAXPGPATH, "%s/%s", log_opts.rootdir, timebuf); @@ -304,6 +378,7 @@ make_outputdirs(char *pgdata) pg_fatal("directory path for new cluster is too long"); /* BASE_OUTPUTDIR/$timestamp/dump/ */ + pg_free(log_opts.dumpdir); log_opts.dumpdir = (char *) pg_malloc0(MAXPGPATH); len = snprintf(log_opts.dumpdir, MAXPGPATH, "%s/%s/%s", log_opts.rootdir, timebuf, DUMP_OUTPUTDIR); @@ -311,6 +386,7 @@ make_outputdirs(char *pgdata) pg_fatal("directory path for new cluster is too long"); /* BASE_OUTPUTDIR/$timestamp/log/ */ + pg_free(log_opts.logdir); log_opts.logdir = (char *) pg_malloc0(MAXPGPATH); len = snprintf(log_opts.logdir, MAXPGPATH, "%s/%s/%s", log_opts.rootdir, timebuf, LOG_OUTPUTDIR); @@ -358,20 +434,13 @@ make_outputdirs(char *pgdata) } +/* + * Resolve the new binary directory from pg_upgrade's path when -B or + * PGBINNEW was not specified, then validate both clusters' binaries. + */ static void -setup(char *argv0) +check_bin_dirs(const char *argv0) { - /* - * make sure the user has a clean environment, otherwise, we may confuse - * libpq when we connect to one (or both) of the servers. - */ - check_pghost_envvar(); - - /* - * In case the user hasn't specified the directory for the new binaries - * with -B, default to using the path of the currently executed pg_upgrade - * binary. - */ if (!new_cluster.bindir) { char exec_path[MAXPGPATH]; @@ -384,8 +453,422 @@ setup(char *argv0) new_cluster.bindir = pg_strdup(exec_path); } - verify_directories(); + check_bin_dir(&old_cluster, false); + check_bin_dir(&new_cluster, true); +} + + +/* + * Before creating logs or changing permissions, check that the target + * directory is missing or empty except for pg_upgrade_output.d. + */ +static void +prepare_initdb(void) +{ + int status = pg_check_dir_except(new_cluster.pgdata, BASE_OUTPUTDIR); + struct stat st; + + if (status < 0) + pg_fatal("could not access directory \"%s\": %m", new_cluster.pgdata); + if (status > 1) + pg_fatal("new cluster data directory \"%s\" is not empty", + new_cluster.pgdata); + + initdb_target = make_absolute_path(new_cluster.pgdata); + if (!initdb_target) + exit(1); + { + char *output_root = psprintf("%s/%s", initdb_target, BASE_OUTPUTDIR); + + initdb_output_root_created = stat(output_root, &st) != 0 && errno == ENOENT; + pg_free(output_root); + } + if (status != 0) + { + if (stat(initdb_target, &st) != 0) + pg_fatal("could not read permissions of directory \"%s\": %m", + initdb_target); + initdb_target_mode = st.st_mode & 07777; + } + + /* + * atexit() runs handlers in reverse registration order. Register these + * before starting a server so its shutdown handler runs before data + * cleanup. Restore the target's original permissions last so cleanup can + * still access the directory. + */ + atexit(finalize_initdb); + atexit(cleanup_initdb_cluster); + if (status == 0) + { + char *parent = pg_strdup(initdb_target); + + get_parent_directory(parent); + create_initdb_parents(parent); + pg_free(parent); + if (mkdir(initdb_target, PG_DIR_MODE_OWNER) != 0) + pg_fatal("could not create directory \"%s\": %m", initdb_target); + initdb_target_created = true; + } + else + { + if (chmod(initdb_target, PG_DIR_MODE_OWNER) != 0) + pg_fatal("could not change permissions of directory \"%s\": %m", + initdb_target); + initdb_restore_mode = user_opts.check; + } + + /* Keep early logs private until initdb's selected permissions are known. */ + make_outputdirs(initdb_target); + + initdb_state_file = psprintf("%s/initdb_state", log_opts.logdir); + pg_free(new_cluster.pgdata); + new_cluster.pgdata = new_cluster.pgconfig = initdb_target; +} + +/* + * Track parent directories created by this run so we can apply initdb's + * permissions and remove them after --check if empty. + */ +static void +create_initdb_parents(char *path) +{ + struct stat st; + char *parent; + char *created; + + if (stat(path, &st) == 0) + { + if (!S_ISDIR(st.st_mode)) + pg_fatal("\"%s\" is not a directory", path); + return; + } + if (errno != ENOENT) + pg_fatal("could not access directory \"%s\": %m", path); + parent = pg_strdup(path); + get_parent_directory(parent); + if (parent[0] == '\0' || strcmp(parent, path) == 0) + pg_fatal("could not access directory \"%s\"", path); + create_initdb_parents(parent); + pg_free(parent); + + created = pg_strdup(path); + initdb_created_parents = pg_realloc_array(initdb_created_parents, char *, + initdb_num_created_parents + 1); + if (mkdir(path, PG_DIR_MODE_OWNER) != 0) + { + if (errno != EEXIST || stat(path, &st) != 0 || !S_ISDIR(st.st_mode)) + pg_fatal("could not create directory \"%s\": %m", path); + pg_free(created); + return; + } + initdb_created_parents[initdb_num_created_parents++] = created; +} + +/* Runs after log cleanup on success; failed checks keep their logs. */ +static void +finalize_initdb(void) +{ + bool failed = false; + + if (!user_opts.check) + return; + for (int i = initdb_num_created_parents; i >= 0; i--) + { + const char *path = i == initdb_num_created_parents ? initdb_target : + initdb_created_parents[i]; + + if (i == initdb_num_created_parents && !initdb_target_created) + continue; + if (rmdir(path) != 0 && errno != ENOENT && + errno != ENOTEMPTY && errno != EEXIST) + { + pg_log(PG_WARNING, "could not remove directory \"%s\": %m", path); + failed = true; + } + } + if (initdb_restore_mode) + { + if (chmod(initdb_target, initdb_target_mode) != 0) + { + pg_log(PG_WARNING, "could not restore permissions of directory \"%s\": %m", + initdb_target); + failed = true; + } + else + initdb_restore_mode = false; + } + if (failed) + { + fflush(NULL); + _exit(1); + } +} + +/* Apply initdb's permissions without clearing inherited setgid. */ +static void +set_initdb_directory_permissions(const char *path) +{ + mode_t mode = pg_dir_create_mode; +#ifndef WIN32 + struct stat st; + /* Keep the group inheritance of directories we created under setgid. */ + if (stat(path, &st) != 0) + pg_fatal("could not read permissions of directory \"%s\": %m", path); + mode |= st.st_mode & S_ISGID; +#endif + if (chmod(path, mode) != 0) + pg_fatal("could not change permissions of directory \"%s\": %m", path); +} + +/* + * Set permissions on this run's output directory, its log and dump + * subdirectories, and log files to match initdb's selected permissions. + */ +static void +set_output_permissions(void) +{ + const char *dirs[] = {log_opts.basedir, log_opts.dumpdir, log_opts.logdir, NULL}; + char path[MAXPGPATH]; + + for (const char **dir = dirs; *dir; dir++) + set_initdb_directory_permissions(*dir); + for (char **file = output_files; *file; file++) + { + snprintf(path, sizeof(path), "%s/%s", log_opts.logdir, *file); + if (chmod(path, pg_file_create_mode) != 0) + pg_fatal("could not change permissions of file \"%s\": %m", path); + } +} + +/* + * Before initdb, the target contained only pg_upgrade_output.d. + * Remove initialized data after --check, preserving that log directory. + */ +void +cleanup_initdb_cluster(void) +{ + struct stat st; + char path[MAXPGPATH]; + + if (!user_opts.check || !initdb_succeeded) + return; + + snprintf(path, sizeof(path), "%s/postmaster.pid", initdb_target); + if (os_info.running_cluster == &new_cluster || + lstat(path, &st) == 0 || (errno != ENOENT && errno != ENOTDIR)) + { + pg_log(PG_WARNING, "cannot confirm new server shutdown, retaining cluster at \"%s\"", initdb_target); + goto fail; + } + + if (initdb_waldir[0]) + { + if (!rmtree(initdb_waldir, !initdb_wal_existed) || + (initdb_wal_existed && chmod(initdb_waldir, initdb_wal_mode) != 0)) + goto fail; + strlcpy(path, initdb_waldir, sizeof(path)); + get_parent_directory(path); + while (!initdb_wal_existed && strcmp(path, initdb_wal_parent) != 0) + { + if (rmdir(path) != 0 && errno != ENOENT) + { + if (errno == ENOTEMPTY || errno == EEXIST) + break; + pg_log(PG_WARNING, "could not remove directory \"%s\": %m", path); + goto fail; + } + get_parent_directory(path); + } + } + + if (!rmtree_except(initdb_target, BASE_OUTPUTDIR)) + goto fail; + initdb_succeeded = false; + return; + +fail: + pg_log(PG_WARNING, "could not clean up initialization under \"%s\"", initdb_target); + /* _exit() skips the finalizer registered to restore target permissions. */ + finalize_initdb(); + fflush(NULL); + _exit(1); +} + + +/* + * Read the old cluster's control data and template0 settings for initdb. + */ +static void +get_old_cluster_initdb_info(void) +{ + old_cluster.major_version = get_pg_version(old_cluster.pgdata, + &old_cluster.major_version_str); + + if (!old_cluster.sockdir) + old_cluster.sockdir = user_opts.socketdir ? user_opts.socketdir : "."; + + /* + * Allow a running old server for --check, as check_clusters_stopped() + * does. + */ + if (pid_lock_file_exists(old_cluster.pgdata)) + { + if (start_postmaster(&old_cluster, false)) + stop_postmaster(false); + else if (!user_opts.check) + pg_fatal("There seems to be a postmaster servicing the old cluster.\n" + "Please shutdown that postmaster and try again."); + else + user_opts.live_check = true; + } + + get_sock_dir(&old_cluster); + get_control_data(&old_cluster); + + prep_status("Examining old cluster settings"); + if (!user_opts.live_check) + start_postmaster(&old_cluster, true); + get_template0_info(&old_cluster); + if (!user_opts.live_check) + stop_postmaster(false); + check_ok(); +} + + +/* + * Build the initdb command using the old cluster's settings and any + * arguments supplied with --initdb-options. + */ +static void +build_new_cluster_initdb_cmd(PQExpBuffer cmd) +{ + DbLocaleInfo *locale = old_cluster.template0; + const char *encoding_name = pg_encoding_to_char(locale->db_encoding); + char initdb_path[MAXPGPATH]; + + snprintf(initdb_path, sizeof(initdb_path), "%s/initdb", new_cluster.bindir); + + prep_status("Constructing new cluster initdb command"); + + initPQExpBuffer(cmd); + + /* + * Build the command with appendShellString() for every value that comes + * from outside our control: the username is from the command line, and + * the encoding and locale strings are read from the old cluster's + * template0. This prevents shell metacharacters in any of them from + * breaking out of their argument when the command is run through the + * shell. + */ + appendShellString(cmd, initdb_path); + appendPQExpBufferStr(cmd, " --for-pg-upgrade="); + appendShellString(cmd, initdb_state_file); + appendPQExpBufferStr(cmd, " -N -D "); + appendShellString(cmd, new_cluster.pgdata); + appendPQExpBufferStr(cmd, " -U "); + appendShellString(cmd, os_info.user); + appendPQExpBuffer(cmd, " --wal-segsize=%u", + old_cluster.controldata.walseg / (1024 * 1024)); + + /* + * Pass --data-checksums or --no-data-checksums explicitly. Starting from + * PG18, initdb enables checksums by default, so we must mirror the old + * cluster's setting to avoid a mismatch that check_control_data() would + * reject. + */ + if (old_cluster.controldata.data_checksum_version != 0) + appendPQExpBufferStr(cmd, " --data-checksums"); + else + appendPQExpBufferStr(cmd, " --no-data-checksums"); + + appendPQExpBufferStr(cmd, " --encoding="); + appendShellString(cmd, encoding_name); + appendPQExpBufferStr(cmd, " --locale-provider="); + appendShellString(cmd, collprovider_name(locale->db_collprovider)); + appendPQExpBufferStr(cmd, " --lc-collate="); + appendShellString(cmd, locale->db_collate); + appendPQExpBufferStr(cmd, " --lc-ctype="); + appendShellString(cmd, locale->db_ctype); + + if (locale->db_locale) + { + if (locale->db_collprovider == COLLPROVIDER_ICU) + { + appendPQExpBufferStr(cmd, " --icu-locale="); + appendShellString(cmd, locale->db_locale); + } + else if (locale->db_collprovider == COLLPROVIDER_BUILTIN) + { + appendPQExpBufferStr(cmd, " --builtin-locale="); + appendShellString(cmd, locale->db_locale); + } + } + + for (int i = 0; i < user_opts.num_initdb_options; i++) + { + appendPQExpBufferChar(cmd, ' '); + appendShellString(cmd, user_opts.initdb_options[i]); + } + + check_ok(); +} + + +/* Initialize the new cluster before checking compatibility with the old cluster. */ +static void +create_new_cluster_via_initdb(void) +{ + PQExpBufferData cmd; + + get_old_cluster_initdb_info(); + build_new_cluster_initdb_cmd(&cmd); + + prep_status("Creating new cluster with initdb"); + + exec_prog(UTILITY_LOG_FILE, NULL, true, true, "%s", cmd.data); + + /* + * --initdb-options may override -D. Verify that the requested target + * contains a valid cluster before enabling its automatic cleanup after + * --check. + */ + check_data_dir(&new_cluster); + if (user_opts.check) + { + FILE *state = fopen(initdb_state_file, "r"); + char *header = state ? pg_get_line(state, NULL) : NULL; + int existed; + unsigned int mode; + + initdb_waldir = state ? pg_get_line(state, NULL) : NULL; + initdb_wal_parent = state ? pg_get_line(state, NULL) : NULL; + if (!header || !initdb_waldir || !initdb_wal_parent || + sscanf(header, "%d %o", &existed, &mode) != 2 || + (existed != 0 && existed != 1)) + pg_fatal("could not read initialization state from \"%s\", retaining cluster", + initdb_state_file); + if (fclose(state) != 0) + pg_fatal("could not close file \"%s\": %m", initdb_state_file); + pg_strip_crlf(initdb_waldir); + pg_strip_crlf(initdb_wal_parent); + initdb_wal_existed = existed; + initdb_wal_mode = mode; + pg_free(header); + } + initdb_succeeded = true; + + termPQExpBuffer(&cmd); + check_ok(); +} + + + + +static void +check_clusters_stopped(void) +{ /* no postmasters should be running, except for a live check */ if (pid_lock_file_exists(old_cluster.pgdata)) { diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h index 4cb79bb780e612e54898b1859003345fc8094de2..20e22c763826dc0226f5677088769f162d25e90e 100644 --- a/src/bin/pg_upgrade/pg_upgrade.h +++ b/src/bin/pg_upgrade/pg_upgrade.h @@ -32,7 +32,7 @@ /* * Base directories that include all the files generated internally, from the - * root path of the new cluster. The paths are dynamically built as of + * selected output root. The paths are dynamically built as of * BASE_OUTPUTDIR/$timestamp/{LOG_OUTPUTDIR,DUMP_OUTPUTDIR} to ensure their * uniqueness in each run. */ @@ -314,7 +314,8 @@ typedef struct */ typedef struct { - bool check; /* check clusters only, don't change any data */ + bool check; /* check clusters without performing the + * upgrade */ bool live_check; /* check clusters only, old server is running */ bool do_sync; /* flush changes to disk */ transferMode transfer_mode; /* copy files or link them? */ @@ -325,6 +326,11 @@ typedef struct int char_signedness; /* default char signedness: -1 for initial * value, 1 for "signed" and 0 for * "unsigned" */ + bool initdb_new_cluster; /* run initdb before compatibility checks */ + bool initdb_options_given; /* --initdb-options given, even if + * empty */ + char **initdb_options; /* additional arguments to initdb */ + int num_initdb_options; } UserOpts; typedef struct @@ -391,7 +397,9 @@ void generate_old_dump(void); bool exec_prog(const char *log_filename, const char *opt_log_file, bool report_error, bool exit_on_error, const char *fmt, ...) pg_attribute_printf(5, 6); -void verify_directories(void); +void check_current_dir_write_permissions(void); +void check_bin_dir(ClusterInfo *cluster, bool check_versions); +void check_data_dir(ClusterInfo *cluster); bool pid_lock_file_exists(const char *datadir); @@ -423,6 +431,7 @@ FileNameMap *gen_db_file_maps(DbInfo *old_db, DbInfo *new_db, int *nmaps, const char *old_pgdata, const char *new_pgdata); void get_db_rel_and_slot_infos(ClusterInfo *cluster); +void get_template0_info(ClusterInfo *cluster); int count_old_cluster_logical_slots(void); void get_subscription_info(ClusterInfo *cluster); @@ -432,6 +441,11 @@ void parseCommandLine(int argc, char *argv[]); void adjust_data_dir(ClusterInfo *cluster); void get_sock_dir(ClusterInfo *cluster); +/* pg_upgrade.c */ + +void cleanup_initdb_cluster(void); +void sync_new_cluster(bool sync_data_files); + /* relfilenumber.c */ void transfer_all_new_tablespaces(DbInfoArr *old_db_arr, diff --git a/src/bin/pg_upgrade/server.c b/src/bin/pg_upgrade/server.c index 7da9dffe5854300048a532cfee022523ebbb0196..c9a37063a2476871d0051b2ec7c4497e5ce679a9 100644 --- a/src/bin/pg_upgrade/server.c +++ b/src/bin/pg_upgrade/server.c @@ -165,6 +165,7 @@ start_postmaster(ClusterInfo *cluster, bool report_and_exit_on_error) bool pg_ctl_return = false; char socket_string[MAXPGPATH + 200]; PQExpBufferData pgoptions; + PQExpBufferData quoted_options; static bool exit_hook_registered = false; @@ -191,6 +192,8 @@ start_postmaster(ClusterInfo *cluster, bool report_and_exit_on_error) #endif initPQExpBuffer(&pgoptions); + initPQExpBuffer("ed_options); + appendPQExpBuffer(&pgoptions, "-p %d -b", cluster->port); /* * Construct a parameter string which is passed to the server process. @@ -207,15 +210,17 @@ start_postmaster(ClusterInfo *cluster, bool report_and_exit_on_error) * Use -b to disable autovacuum and logical replication launcher * (effective in PG17 or later for the latter). */ + appendPQExpBuffer(&pgoptions, " %s%s", + cluster->pgopts ? cluster->pgopts : "", socket_string); + appendShellString("ed_options, pgoptions.data); snprintf(cmd, sizeof(cmd), - "\"%s/pg_ctl\" -w -l \"%s/%s\" -D \"%s\" -o \"-p %d -b%s %s%s\" start", + "\"%s/pg_ctl\" -w -l \"%s/%s\" -D \"%s\" -o %s start", cluster->bindir, log_opts.logdir, - SERVER_LOG_FILE, cluster->pgconfig, cluster->port, - pgoptions.data, - cluster->pgopts ? cluster->pgopts : "", socket_string); + SERVER_LOG_FILE, cluster->pgconfig, quoted_options.data); termPQExpBuffer(&pgoptions); + termPQExpBuffer("ed_options); /* * Don't throw an error right away, let connecting throw the error because @@ -293,6 +298,8 @@ void stop_postmaster(bool in_atexit) { ClusterInfo *cluster; + PQExpBufferData options; + bool stopped; if (os_info.running_cluster == &old_cluster) cluster = &old_cluster; @@ -301,11 +308,20 @@ stop_postmaster(bool in_atexit) else return; /* no cluster running */ - exec_prog(SERVER_STOP_LOG_FILE, NULL, !in_atexit, !in_atexit, - "\"%s/pg_ctl\" -w -D \"%s\" -o \"%s\" %s stop", - cluster->bindir, cluster->pgconfig, - cluster->pgopts ? cluster->pgopts : "", - in_atexit ? "-m fast" : "-m smart"); + initPQExpBuffer(&options); + appendShellString(&options, cluster->pgopts ? cluster->pgopts : ""); + stopped = exec_prog(SERVER_STOP_LOG_FILE, NULL, !in_atexit, !in_atexit, + "\"%s/pg_ctl\" -w -D \"%s\" -o %s %s stop", + cluster->bindir, cluster->pgconfig, + options.data, in_atexit ? "-m fast" : "-m smart"); + termPQExpBuffer(&options); + + /* + * Clear the running-cluster marker only after confirmed shutdown. + * Otherwise, --check --initdb cleanup could delete data still in use. + */ + if (!stopped) + return; os_info.running_cluster = NULL; } diff --git a/src/bin/pg_upgrade/t/002_pg_upgrade.pl b/src/bin/pg_upgrade/t/002_pg_upgrade.pl index 9590625fc320352bfc7f3bcde62557b264b60eef..be57b2d838cf0e4b28ee670991c315e72f35049c 100644 --- a/src/bin/pg_upgrade/t/002_pg_upgrade.pl +++ b/src/bin/pg_upgrade/t/002_pg_upgrade.pl @@ -561,35 +561,12 @@ $oldnode->safe_psql( # Upgrade the instance. $oldnode->stop; -# Cause a failure at the start of pg_upgrade, this should create the logging -# directory pg_upgrade_output.d but leave it around. Keep --check for an -# early exit. -command_checks_all( - [ - 'pg_upgrade', '--no-sync', - '--old-datadir' => $oldnode->data_dir, - '--new-datadir' => $newnode->data_dir, - '--old-bindir' => $oldbindir . '/does/not/exist/', - '--new-bindir' => $newbindir, - '--socketdir' => $newnode->host, - '--old-port' => $oldnode->port, - '--new-port' => $newnode->port, - $mode, '--check', - ], - 1, - [qr{check for ".*?does/not/exist" failed}], - [], - 'run of pg_upgrade --check for new instance with incorrect binary path'); -ok(-d $newnode->data_dir . "/pg_upgrade_output.d", - "pg_upgrade_output.d/ not removed after pg_upgrade failure"); -rmtree($newnode->data_dir . "/pg_upgrade_output.d"); - # Check that pg_upgrade aborts when encountering an invalid database # (However, versions that were out of support by commit c66a7d75e652 don't # know how to do this, so skip this test there.) SKIP: { - skip "database invalidation not implemented", 1 + skip "database invalidation not implemented", 4 if $oldnode->pg_version < 11; command_checks_all( @@ -608,6 +585,12 @@ SKIP: [qr/datconnlimit/], [qr/^$/], 'invalid database causes failure'); + + # Check that the failure creates the logging directory pg_upgrade_output.d + # and leaves it around + ok(-d $newnode->data_dir . "/pg_upgrade_output.d", + "pg_upgrade_output.d/ not removed after pg_upgrade failure"); + rmtree($newnode->data_dir . "/pg_upgrade_output.d"); } diff --git a/src/bin/pg_upgrade/t/009_initdb_option.pl b/src/bin/pg_upgrade/t/009_initdb_option.pl new file mode 100644 index 0000000000000000000000000000000000000000..33d6cd7b5bb4e1c44e68c3686a54da242ea5610b --- /dev/null +++ b/src/bin/pg_upgrade/t/009_initdb_option.pl @@ -0,0 +1,547 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test pg_upgrade's --initdb and --initdb-options options. + +use strict; +use warnings FATAL => 'all'; + +use Config; +use Cwd qw(abs_path); +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Use nondefault settings to verify which settings --initdb carries over to the +# new cluster. +my $oldnode = PostgreSQL::Test::Cluster->new('old_node'); +$oldnode->init( + extra => [ + '--allow-group-access', + '--no-data-checksums', + '--wal-segsize' => '2', + '--locale' => 'C', + ]); +$oldnode->start; +$oldnode->safe_psql('postgres', + "CREATE TABLE t (id int primary key, note text); " + . "INSERT INTO t SELECT g, 'row ' || g FROM generate_series(1, 100) g; " + . "CREATE DATABASE extra_db;"); +my $rows_before = $oldnode->safe_psql('postgres', 'SELECT count(*) FROM t'); +is($rows_before, '100', 'old cluster has expected rows before upgrade'); + +# Capture the old settings for the post-upgrade comparison. +my %setting_queries = ( + data_checksums => 'SHOW data_checksums', + wal_segment_size => 'SHOW wal_segment_size', + encoding => + "SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = 'template0'", + collation => + "SELECT datcollate FROM pg_database WHERE datname = 'template0'", + ctype => "SELECT datctype FROM pg_database WHERE datname = 'template0'", + provider => + "SELECT datlocprovider FROM pg_database WHERE datname = 'template0'",); +my %old_settings; +for my $setting (sort keys %setting_queries) +{ + $old_settings{$setting} = + $oldnode->safe_psql('postgres', $setting_queries{$setting}); +} +$oldnode->stop; + +# Leave the new cluster uninitialized so pg_upgrade --initdb creates it. +my $newnode = PostgreSQL::Test::Cluster->new('new_node'); + +my $oldbindir = $oldnode->config_data('--bindir'); +my $newbindir = $newnode->config_data('--bindir'); + +sub upgrade_command +{ + my ($old, $new, @extra) = @_; + return [ + 'pg_upgrade', '--no-sync', '--initdb', + '--old-datadir' => $old->data_dir, + '--new-datadir' => $new->data_dir, + '--old-bindir' => $oldbindir, + '--new-bindir' => $newbindir, + '--socketdir' => $new->host, + '--old-port' => $old->port, + '--new-port' => $new->port, + @extra, + ]; +} + +sub output_sessions +{ + my ($target) = @_; + return () unless -d "$target/pg_upgrade_output.d"; + return map { "$target/pg_upgrade_output.d/$_" } + grep { $_ ne '.' && $_ ne '..' } + slurp_dir("$target/pg_upgrade_output.d"); +} + +# Configure the connection settings normally written by init(), without +# changing initdb's pg_hba.conf. +sub start_new_cluster +{ + my ($node) = @_; + my $listen = $PostgreSQL::Test::Cluster::use_tcp ? $node->host : ''; + my $socketdir = $PostgreSQL::Test::Cluster::use_tcp ? '' : $node->host; + $node->append_conf('postgresql.conf', + 'port = ' + . $node->port + . "\nlisten_addresses = '$listen'\nunix_socket_directories = '$socketdir'" + ); + $node->start; +} + +ok(!-d $newnode->data_dir, + 'new cluster data directory does not exist before --initdb'); + +# Use a working directory separate from the target to verify that logs go +# under the new data directory. +# Pass the server-only option -F through -O to verify that it reaches +# the new server without being passed to initdb. +# Repeated --initdb-options must preserve argument order and quoted values in +# the configuration used after the upgrade. +chdir ${PostgreSQL::Test::Utils::tmp_check}; + +command_ok( + upgrade_command( + $oldnode, $newnode, + '--retain', + '--new-options' => '-F', + '--initdb-options' => '-c data_directory=' + . option_quote($newnode->data_dir), + '--initdb-options' => + '-c huge_pages=try -c custom.initdb_first=first', + '--initdb-options' => + q{-c huge_pages=off --set=custom.initdb_first=last -c "custom.initdb_text=space, apostrophe's $libdir" -c custom.initdb_path=C:\new\path -c custom.initdb_empty=""}, + '--initdb-options' => + q{-c 'custom.initdb_single=single quoted $libdir' -c "custom.initdb_escaped=quote\" slash\\\\" -c custom.initdb_join='joined 'pieces -c custom.initdb_space=escaped\ space}, + ), + 'run of pg_upgrade --initdb with -O creates and upgrades the new cluster' +); + +# Check before the test harness starts the target and overwrites this file. +like(slurp_file($newnode->data_dir . '/postmaster.opts'), + qr/(?:^|\s)"-F"(?:\s|$)/, '-O option reached the target postmaster'); + +ok(-f $newnode->data_dir . '/PG_VERSION', + 'new cluster data directory created by --initdb'); + +# Group access requires an explicit initdb option, even if the old cluster +# allows it. +SKIP: +{ + skip "unix-style permissions not supported on Windows", 1 + if ($windows_os || $Config::Config{osname} eq 'cygwin'); + + my $new_mode = (stat($newnode->data_dir))[2] & 0777; + is($new_mode, 0700, 'new cluster does not inherit PGDATA group access'); +} + +# --initdb logs use pg_upgrade_output.d with the normal cleanup and --retain +# handling. Check that no separate .initdb_log directory remains +# beside the new data directory. +ok(!-d $newnode->data_dir . '.initdb_log', + '--initdb does not leave a sibling log directory'); + +start_new_cluster($newnode); + +my %expected_settings = ( + huge_pages => 'off', + 'custom.initdb_first' => 'last', + 'custom.initdb_text' => q{space, apostrophe's $libdir}, + 'custom.initdb_path' => q{C:\new\path}, + 'custom.initdb_empty' => '', + 'custom.initdb_single' => q{single quoted $libdir}, + 'custom.initdb_escaped' => "quote\" slash\\", + 'custom.initdb_join' => 'joined pieces', + 'custom.initdb_space' => 'escaped space',); +for my $setting (sort keys %expected_settings) +{ + is( $newnode->safe_psql('postgres', "SHOW $setting"), + $expected_settings{$setting}, + "$setting survives initialization and upgrade"); +} + +my $rows_after = $newnode->safe_psql('postgres', 'SELECT count(*) FROM t'); +is($rows_after, '100', 'user data survived --initdb upgrade'); + +my $has_extra = $newnode->safe_psql('postgres', + "SELECT count(*) FROM pg_database WHERE datname = 'extra_db'"); +is($has_extra, '1', 'user database carried over by --initdb upgrade'); + +# The new cluster must match the old checksum, WAL segment, encoding, and +# locale settings. +for my $setting (sort keys %setting_queries) +{ + is($newnode->safe_psql('postgres', $setting_queries{$setting}), + $old_settings{$setting}, "$setting propagated by --initdb"); +} + +$newnode->stop; + +# Check that pg_upgrade rejects an existing cluster before invoking initdb. +# The error is reported on stdout. +command_checks_all(upgrade_command($oldnode, $newnode), + 1, [qr/is not empty/], [qr/^$/], + '--initdb refuses to overwrite an existing cluster'); + +my @sessions = output_sessions($newnode->data_dir); +is(scalar @sessions, 1, 'upgrade uses one final-target output session'); +like( + slurp_file("$sessions[0]/log/pg_upgrade_utility.log"), + qr/Success\. You can now start/, + 'session contains successful initdb output'); +ok(!-d 'pg_upgrade_output.d', 'no logs in the working directory'); + +# Validate the new binaries before initialization. +my $empty_bindir = PostgreSQL::Test::Utils::tempdir; +command_checks_all( + upgrade_command( + $oldnode, $newnode, + '--new-datadir' => $newnode->data_dir . '_nonexistent', + '--new-bindir' => $empty_bindir), + 1, + [qr/check for .*postgres.* failed/], + [qr/^$/], + '--initdb fails early when the new binaries are missing'); + +# Repeated checks initialize afresh, accepting logs left by the previous run. +my $checked_target = $newnode->data_dir . '_check'; +mkdir $checked_target or die "mkdir $checked_target: $!"; +chmod 0500, $checked_target + or die "chmod $checked_target: $!" + unless $windows_os || $Config::Config{osname} eq 'cygwin'; +for my $run (1, 2) +{ + command_checks_all( + upgrade_command( + $oldnode, $newnode, + '--new-datadir' => $checked_target, + '--new-options' => '-c config_file=' + . option_quote("$checked_target/postgresql.conf"), + '--initdb-options' => join( + ' ', + map { + '-c ' + . $_->[0] . '=' + . option_quote("$checked_target/$_->[1]") + }[ data_directory => '.' ], + [ hba_file => 'pg_hba.conf' ], + [ ident_file => 'pg_ident.conf' ], + [ hosts_file => 'pg_hosts.conf' ]), + '--check', + '--retain'), + 0, + [ qr/Creating new cluster with initdb/, qr/Clusters are compatible/ ], + [qr/^$/], + "--check --initdb run $run initializes and checks both clusters"); + is_deeply( + [ grep { $_ ne '.' && $_ ne '..' } slurp_dir($checked_target) ], + ['pg_upgrade_output.d'], + '--retain keeps logs but removes initialized check data'); + SKIP: + { + skip 'Unix permissions are not supported on this platform', 1 + if $windows_os || $Config::Config{osname} eq 'cygwin'; + is((stat($checked_target))[2] & 0777, + 0500, 'successful check restores existing target permissions'); + } +} + +# Without -B, --initdb must derive the new bindir from the original argv[0], +# even when that directory is not in PATH. +SKIP: +{ + skip "restricted PATH test is not portable to Windows", 3 if $windows_os; + + local %ENV = %ENV; + $ENV{PATH} = '/usr/bin:/bin'; + command_checks_all( + [ + abs_path("$newbindir/pg_upgrade"), '--no-sync', + '--old-datadir' => $oldnode->data_dir, + '--new-datadir' => $newnode->data_dir . '_no_new_bindir', + '--old-bindir' => $oldbindir, + '--socketdir' => $newnode->host, + '--old-port' => $oldnode->port, + '--new-port' => $newnode->port, + '--initdb', + '--check', + ], + 0, + [qr/Clusters are compatible/], + [qr/^$/], + '--initdb derives the new bindir from an absolute argv[0]'); +} + +# --check --initdb must accept a running old server and leave it running. +# Use a different port for the new server. +SKIP: +{ + skip "Timing issues with live server detection on Windows", 4 + if $windows_os; + + $oldnode->start; + command_checks_all( + upgrade_command( + $oldnode, $newnode, + '--new-datadir' => $newnode->data_dir . '_live_check', + '--check'), + 0, + [qr/Clusters are compatible/], + [qr/^$/], + '--check --initdb accepts a live source'); + is($oldnode->safe_psql('postgres', 'SELECT 1'), + '1', 'live source remains running after --check --initdb'); + $oldnode->stop; +} + +# Both --initdb and --check --initdb reject regproc columns in the old +# cluster's user tables. After this failure, --initdb keeps the initialized +# new cluster, while --check --initdb removes it. +$oldnode->start; +$oldnode->safe_psql('postgres', 'CREATE TABLE bad (c regproc)'); +$oldnode->stop; + +for my $check (0, 1) +{ + my $target = $newnode->data_dir . "_incompatible_$check"; + my $mode = $check ? '--check --initdb' : '--initdb'; + if ($check) + { + mkdir $target or die "mkdir $target: $!"; + chmod 0500, $target + or die "chmod $target: $!" + unless $windows_os || $Config::Config{osname} eq 'cygwin'; + } + command_checks_all( + upgrade_command( + $oldnode, $newnode, + '--new-datadir' => $target, + $check ? '--check' : ()), + 1, + [qr/failed check: Checking for reg\* data types in user tables/], + [qr/^$/], + "$mode rejects an incompatible old cluster"); + is( -f "$target/PG_VERSION" ? 1 : 0, + $check ? 0 : 1, + "failed $mode has the expected data lifetime"); + my @logs = output_sessions($target); + is(scalar @logs, 1, 'failure retains one final-target output session'); + like( + slurp_file("$logs[0]/log/pg_upgrade_utility.log"), + qr/Success\. You can now start/, + 'later failure retains successful initdb output'); + if ($check && !$windows_os && $Config::Config{osname} ne 'cygwin') + { + is((stat($target))[2] & 0777, + 0500, 'failed check restores existing target permissions'); + } +} + +# Require --initdb even when --initdb-options is empty. +# Reject invalid quoting and line breaks before examining either cluster. +command_checks_all( + [ 'pg_upgrade', '--initdb-options=' ], + 1, + [qr/--initdb-options requires --initdb/], + [qr/^$/], + '--initdb-options requires --initdb even for an empty argument'); + +for my $case ( + [ + q{-c 'huge_pages=off}, qr/unterminated quote/, + 'unclosed single quote' + ], + [ + q{-c "huge_pages=off}, qr/unterminated quote/, + 'unclosed double quote' + ], + [ "-c huge_pages=off\\", qr/trailing backslash/, 'trailing escape' ], + [ "-c huge_pages=off\n", qr/newline or carriage return/, 'newline' ]) +{ + command_checks_all( + [ 'pg_upgrade', '--initdb', "--initdb-options=$case->[0]" ], + 1, [ $case->[1] ], + [qr/^$/], "reject $case->[2]"); +} + +sub option_quote +{ + my ($value) = @_; + $value =~ s/'/'\\''/g; + return "'$value'"; +} + +# Detach test-owned WAL junctions before native Perl recursively removes +# the node. rmdir removes the junction itself, leaving its target intact. +my @test_wal_junctions; + +sub detach_test_wal_junction +{ + my ($node, $wal) = @_; + return unless $windows_os; + return if defined $ENV{PG_TEST_NOCLEAN}; + return unless PostgreSQL::Test::Utils::all_tests_passing(); + BAIL_OUT 'WAL junction cleanup requires a stopped node' + if -f $node->data_dir . '/postmaster.pid'; + ok(rmdir($node->data_dir . '/pg_wal'), 'detach test-owned WAL junction'); + ok(-d $wal, 'detaching WAL junction preserves its target'); +} + +# test_slru rejects LOAD unless preloaded. The new cluster must preload it +# for pg_upgrade's loadable-library check to pass. +my $preload_old = PostgreSQL::Test::Cluster->new('preload_old'); +$preload_old->init; +$preload_old->append_conf('postgresql.conf', + "shared_preload_libraries = 'test_slru'"); +$preload_old->start; +$preload_old->safe_psql('postgres', 'CREATE EXTENSION test_slru'); +my $quoted_superuser = + $preload_old->safe_psql('postgres', 'SELECT quote_ident(current_user)'); +# This password is used only by the temporary test clusters. +my $password = 'initdb-options-test-password'; +$preload_old->safe_psql('postgres', + "ALTER ROLE $quoted_superuser PASSWORD '$password'"); +$preload_old->stop; + +my $preload_missing = PostgreSQL::Test::Cluster->new('preload_missing'); +my $preload_missing_wal = + $preload_missing->data_dir . '/temporary_data_dir/wal with spaces'; +command_checks_all( + upgrade_command( + $preload_old, + $preload_missing, + '--check', + '--initdb-options' => '--waldir=' . option_quote($preload_missing_wal) + ), + 1, + [qr/references loadable libraries that are missing/], + [qr/^$/], + '--check --initdb checks required libraries in the new cluster'); +ok( !-f $preload_missing->data_dir . '/PG_VERSION', + 'failed preload check removes the initialized cluster'); +ok(!-d $preload_missing_wal, + 'failed preload check removes the WAL directory'); +ok( !-f $preload_missing->data_dir . '/postmaster.pid', + 'failed preload check stops the new server'); + +my $credential_dir = PostgreSQL::Test::Utils::tempdir; +my $pwfile = "$credential_dir/initdb.pw"; +append_to_file($pwfile, "$password\n"); +chmod(0600, $pwfile) or die "could not protect $pwfile: $!"; +my $pgpass = "$credential_dir/pgpass"; +append_to_file($pgpass, "*:*:*:*:$password\n"); +chmod(0600, $pgpass) or die "could not protect $pgpass: $!"; + +my $auth_options = + q{-c shared_preload_libraries=$libdir/test_slru --auth-local=scram-sha-256 --auth-host=scram-sha-256 --pwfile="} + . $pwfile . '"'; + +{ + local $ENV{PGPASSFILE} = "$credential_dir/no-password-file"; + delete local $ENV{PGPASSWORD}; + my $auth_missing = PostgreSQL::Test::Cluster->new('auth_missing'); + command_checks_all( + upgrade_command( + $preload_old, $auth_missing, + '--initdb-options' => $auth_options), + 1, + [qr/fe_sendauth: no password supplied/], + [qr/^$/], + 'password authentication is not weakened when credentials are missing' + ); + ok( -f $auth_missing->data_dir . '/PG_VERSION', + 'authentication failure retains the new cluster'); + ok( !-f $auth_missing->data_dir . '/postmaster.pid', + 'authentication failure stops the new server'); +} + +{ + local $ENV{PGPASSFILE} = $pgpass; + my $preload_new = PostgreSQL::Test::Cluster->new('preload_new'); + my $wal = $preload_new->data_dir . '/temporary_data_dir/wal with spaces'; + command_ok( + upgrade_command( + $preload_old, + $preload_new, + '--initdb-options' => $auth_options + . ' --waldir=' + . option_quote($wal)), + 'initdb options support a preload-only extension and password authentication' + ); + ok(-d $wal, 'initdb uses the requested WAL directory'); + + my $hba = slurp_file($preload_new->data_dir . '/pg_hba.conf'); + my @rules = grep { /\S/ && !/^\s*#/ } split(/\n/, $hba); + ok(!(grep { !/\bscram-sha-256\s*$/ } @rules), + 'all generated authentication rules keep the requested method'); + SKIP: + { + skip 'unix-style permissions not supported on Windows', 1 + if ($windows_os || $Config::Config{osname} eq 'cygwin'); + is((stat($preload_new->data_dir))[2] & 0777, + 0700, + 'new cluster uses owner-only data directory mode by default'); + } + + start_new_cluster($preload_new); + is( $preload_new->safe_psql('postgres', 'SHOW shared_preload_libraries'), + q{$libdir/test_slru}, + 'preload setting preserves literal $libdir'); + $preload_new->safe_psql('postgres', + "SELECT test_slru_page_write(0, 'upgraded')"); + is( $preload_new->safe_psql('postgres', 'SELECT test_slru_page_read(0)'), + 'upgraded', + 'restored preload-only extension is usable'); + $preload_new->stop; + push @test_wal_junctions, [ $preload_new, $wal ]; +} + +# Link and swap retain old file modes. +SKIP: +{ + skip 'Unix permissions are not supported on this platform', 1 + if $windows_os || $Config::Config{osname} eq 'cygwin'; + + subtest 'group access with link and swap' => sub { + for my $mode ('--link', '--swap') + { + my $name = substr($mode, 2); + my $old = PostgreSQL::Test::Cluster->new("old_$name"); + my $new = PostgreSQL::Test::Cluster->new("new_$name"); + $old->init(extra => ['--allow-group-access']); + $old->start; + $old->safe_psql('postgres', + 'CREATE TABLE permissions (id integer); INSERT INTO permissions VALUES (1)' + ); + my $relpath = $old->safe_psql('postgres', + "SELECT pg_relation_filepath('permissions')"); + $old->stop; + command_ok( + upgrade_command( + $old, $new, $mode, '--retain', + '--initdb-options' => '--allow-group-access'), + "$mode accepts matching group access settings"); + is((stat($new->data_dir))[2] & 0777, + 0750, "$mode preserves data directory permissions"); + is((stat($new->data_dir . "/$relpath"))[2] & 0777, + 0640, "$mode preserves relation permissions"); + my @logs = output_sessions($new->data_dir); + is((stat("$logs[0]/log/pg_upgrade_utility.log"))[2] & 0777, + 0640, "$mode gives early logs explicit group access"); + } + done_testing(); + }; +} + +# Match Cluster.pm: preserve the original cluster layout on a failed test. +foreach my $junction (@test_wal_junctions) +{ + detach_test_wal_junction(@{$junction}); +} + +done_testing(); diff --git a/src/bin/pg_upgrade/util.c b/src/bin/pg_upgrade/util.c index e7d7ab56445224df5abf4ad46782c46cf307486c..47705b6853cfd3763ad249f87d8887a83da201c5 100644 --- a/src/bin/pg_upgrade/util.c +++ b/src/bin/pg_upgrade/util.c @@ -57,12 +57,13 @@ end_progress_output(void) } /* - * Remove any logs generated internally. To be used once when exiting. + * Close the internal log and remove output directories unless --retain is set. */ void cleanup_output_dirs(void) { fclose(log_opts.internal); + log_opts.internal = NULL; /* Remove dump and log files? */ if (log_opts.retain) diff --git a/src/common/rmtree.c b/src/common/rmtree.c index 865604171238b7883d5bac53a783801ef9921e96..d854a52b417d7412f340412dacc44a320bd4b5e7 100644 --- a/src/common/rmtree.c +++ b/src/common/rmtree.c @@ -46,8 +46,8 @@ * (The details of the problem are reported already, so caller * doesn't really have to say anything more, but most do.) */ -bool -rmtree(const char *path, bool rmtopdir) +static bool +rmtree_internal(const char *path, bool rmtopdir, const char *exclude) { char pathbuf[MAXPGPATH]; DIR *dir; @@ -69,7 +69,8 @@ rmtree(const char *path, bool rmtopdir) while (errno = 0, (de = readdir(dir))) { if (strcmp(de->d_name, ".") == 0 || - strcmp(de->d_name, "..") == 0) + strcmp(de->d_name, "..") == 0 || + (exclude && strcmp(de->d_name, exclude) == 0)) continue; snprintf(pathbuf, sizeof(pathbuf), "%s/%s", path, de->d_name); switch (get_dirent_type(pathbuf, de, false, LOG_LEVEL)) @@ -129,3 +130,20 @@ rmtree(const char *path, bool rmtopdir) return result; } + +bool +rmtree(const char *path, bool rmtopdir) +{ + return rmtree_internal(path, rmtopdir, NULL); +} + +/* + * Recursively delete everything under path except the top-level entry named + * exclude. Preserve path itself and the excluded entry's contents. + * The caller must ensure that everything else is safe to delete. + */ +bool +rmtree_except(const char *path, const char *exclude) +{ + return rmtree_internal(path, false, exclude); +} diff --git a/src/include/port.h b/src/include/port.h index 172acf7d02f04deb5be3199cc3d5a39c32343c72..33876f199ab2fa361fc45d736f8050de92b0e45e 100644 --- a/src/include/port.h +++ b/src/include/port.h @@ -326,6 +326,7 @@ extern ssize_t pgreadlink(const char *path, char *buf, size_t size); #endif extern bool rmtree(const char *path, bool rmtopdir); +extern bool rmtree_except(const char *path, const char *exclude); #if defined(WIN32) && !defined(__CYGWIN__) @@ -537,6 +538,7 @@ extern bool pg_strong_random(void *buf, size_t len); /* port/pgcheckdir.c */ extern int pg_check_dir(const char *dir); +extern int pg_check_dir_except(const char *dir, const char *exclude); /* port/pgmkdirp.c */ extern int pg_mkdir_p(char *path, int omode); diff --git a/src/port/pgcheckdir.c b/src/port/pgcheckdir.c index 807c21131d84185d388d00b58fe2e5014d1ddaf5..91ce54fd9c46f1d8e0786289c4022a6339313e6d 100644 --- a/src/port/pgcheckdir.c +++ b/src/port/pgcheckdir.c @@ -16,6 +16,7 @@ #include "c.h" #include +#include /* @@ -31,6 +32,16 @@ */ int pg_check_dir(const char *dir) +{ + return pg_check_dir_except(dir, NULL); +} + +/* + * Return the same status codes as pg_check_dir(), ignoring the top-level + * entry named exclude only if it is a directory. + */ +int +pg_check_dir_except(const char *dir, const char *exclude) { int result = 1; DIR *chkdir; @@ -51,6 +62,17 @@ pg_check_dir(const char *dir) /* skip this and parent directory */ continue; } + if (exclude && strcmp(file->d_name, exclude) == 0) + { + char path[MAXPGPATH]; + struct stat st; + + snprintf(path, sizeof(path), "%s/%s", dir, exclude); + if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) + continue; + result = 4; + break; + } #ifndef WIN32 /* file starts with "." */ else if (file->d_name[0] == '.')