From 466f0abf241b813f1d3350af97cc8348d4e95d71 Mon Sep 17 00:00:00 2001 From: Kern Sibbald Date: Wed, 5 Mar 2008 15:05:01 +0000 Subject: [PATCH] kes Fix bugs in MaxFullInterval and Implement MaxDiffInterval. kes Start PluginOptions string, and refactor a bit of ua_run.c git-svn-id: https://bacula.svn.sourceforge.net/svnroot/bacula/trunk@6534 91ce42f0-d328-0410-95d8-f526ca767f89 --- bacula/src/cats/protos.h | 1 + bacula/src/cats/sql_find.c | 46 ++++++ bacula/src/dird/dird_conf.c | 7 + bacula/src/dird/dird_conf.h | 48 +++--- bacula/src/dird/fd_cmds.c | 20 +-- bacula/src/dird/ua_run.c | 310 +++++++++++++++++++++--------------- bacula/src/jcr.h | 1 + bacula/src/lib/status.h | 4 + bacula/technotes-2.3 | 2 + 9 files changed, 278 insertions(+), 161 deletions(-) diff --git a/bacula/src/cats/protos.h b/bacula/src/cats/protos.h index c13883494b..2d78a3169d 100644 --- a/bacula/src/cats/protos.h +++ b/bacula/src/cats/protos.h @@ -80,6 +80,7 @@ int db_delete_pool_record(JCR *jcr, B_DB *db, POOL_DBR *pool_dbr); int db_delete_media_record(JCR *jcr, B_DB *mdb, MEDIA_DBR *mr); /* sql_find.c */ +bool db_find_last_job_start_time(JCR *jcr, B_DB *mdb, JOB_DBR *jr, POOLMEM **stime, int JobLevel); bool db_find_job_start_time(JCR *jcr, B_DB *mdb, JOB_DBR *jr, POOLMEM **stime); bool db_find_last_jobid(JCR *jcr, B_DB *mdb, const char *Name, JOB_DBR *jr); int db_find_next_volume(JCR *jcr, B_DB *mdb, int index, bool InChanger, MEDIA_DBR *mr); diff --git a/bacula/src/cats/sql_find.c b/bacula/src/cats/sql_find.c index 4a5dd174dc..d8462bed3b 100644 --- a/bacula/src/cats/sql_find.c +++ b/bacula/src/cats/sql_find.c @@ -148,6 +148,52 @@ bail_out: return false; } + +/* + * Find the last job start time for the specified JobLevel + * + * StartTime is returned in stime + * + * Returns: false on failure + * true on success, jr is unchanged, but stime is set + */ +bool +db_find_last_job_start_time(JCR *jcr, B_DB *mdb, JOB_DBR *jr, POOLMEM **stime, int JobLevel) +{ + SQL_ROW row; + char ed1[50], ed2[50]; + + db_lock(mdb); + + pm_strcpy(stime, "0000-00-00 00:00:00"); /* default */ + + Mmsg(mdb->cmd, +"SELECT StartTime FROM Job WHERE JobStatus='T' AND Type='%c' AND " +"Level='%c' AND Name='%s' AND ClientId=%s AND FileSetId=%s " +"ORDER BY StartTime DESC LIMIT 1", + jr->JobType, JobLevel, jr->Name, + edit_int64(jr->ClientId, ed1), edit_int64(jr->FileSetId, ed2)); + if (!QUERY_DB(jcr, mdb, mdb->cmd)) { + Mmsg2(&mdb->errmsg, _("Query error for start time request: ERR=%s\nCMD=%s\n"), + sql_strerror(mdb), mdb->cmd); + goto bail_out; + } + if ((row = sql_fetch_row(mdb)) == NULL) { + sql_free_result(mdb); + Mmsg(mdb->errmsg, _("No prior Full backup Job record found.\n")); + goto bail_out; + } + Dmsg1(100, "Got start time: %s\n", row[0]); + pm_strcpy(stime, row[0]); + sql_free_result(mdb); + db_unlock(mdb); + return true; + +bail_out: + db_unlock(mdb); + return false; +} + /* * Find last failed job since given start-time * it must be either Full or Diff. diff --git a/bacula/src/dird/dird_conf.c b/bacula/src/dird/dird_conf.c index ac73b19f38..4484ba430d 100644 --- a/bacula/src/dird/dird_conf.c +++ b/bacula/src/dird/dird_conf.c @@ -326,6 +326,7 @@ RES_ITEM job_items[] = { {"allowhigherduplicates", store_bool, ITEM(res_job.AllowHigherDuplicates), 0, ITEM_DEFAULT, true}, {"cancelqueuedduplicates", store_bool, ITEM(res_job.CancelQueuedDuplicates), 0, ITEM_DEFAULT, true}, {"cancelrunningduplicates", store_bool, ITEM(res_job.CancelRunningDuplicates), 0, ITEM_DEFAULT, false}, + {"pluginoptions", store_str, ITEM(res_job.PluginOptions), 0, 0, 0}, {NULL, NULL, {0}, 0, 0, 0} }; @@ -656,6 +657,9 @@ void dump_resource(int type, RES *reshdr, void sendit(void *sock, const char *fm if (res->res_job.WriteBootstrap) { sendit(sock, _(" --> WriteBootstrap=%s\n"), NPRT(res->res_job.WriteBootstrap)); } + if (res->res_job.PluginOptions) { + sendit(sock, _(" --> PluginOptions=%s\n"), NPRT(res->res_job.PluginOptions)); + } if (res->res_job.storage) { STORE *store; foreach_alist(store, res->res_job.storage) { @@ -1236,6 +1240,9 @@ void free_resource(RES *sres, int type) if (res->res_job.WriteBootstrap) { free(res->res_job.WriteBootstrap); } + if (res->res_job.PluginOptions) { + free(res->res_job.PluginOptions); + } if (res->res_job.selection_pattern) { free(res->res_job.selection_pattern); } diff --git a/bacula/src/dird/dird_conf.h b/bacula/src/dird/dird_conf.h index a0ce2e1439..879ae17f8a 100644 --- a/bacula/src/dird/dird_conf.h +++ b/bacula/src/dird/dird_conf.h @@ -364,19 +364,21 @@ public: int JobLevel; /* default backup/verify level */ int Priority; /* Job priority */ int RestoreJobId; /* What -- JobId to restore */ + int RescheduleTimes; /* Number of times to reschedule job */ + int replace; /* How (overwrite, ..) */ + int selection_type; + char *RestoreWhere; /* Where on disk to restore -- directory */ char *RegexWhere; /* RegexWhere option */ char *strip_prefix; /* remove prefix from filename */ char *add_prefix; /* add prefix to filename */ char *add_suffix; /* add suffix to filename -- .old */ - bool where_use_regexp; /* true if RestoreWhere is a BREGEXP */ char *RestoreBootstrap; /* Bootstrap file */ - alist *RunScripts; /* Run {client} program {after|before} Job */ + char *PluginOptions; /* Options to pass to plugin */ union { char *WriteBootstrap; /* Where to write bootstrap Job updates */ char *WriteVerifyList; /* List of changed files */ }; - int replace; /* How (overwrite, ..) */ utime_t MaxRunTime; /* max run time in seconds */ utime_t MaxWaitTime; /* max blocking time in seconds */ utime_t FullMaxWaitTime; /* Max Full job wait time */ @@ -388,9 +390,28 @@ public: utime_t MaxFullInterval; /* Maximum time interval between Fulls */ utime_t MaxDiffInterval; /* Maximum time interval between Diffs */ utime_t DuplicateJobProximity; /* Permitted time between duplicicates */ - uint32_t MaxConcurrentJobs; /* Maximum concurrent jobs */ int64_t spool_size; /* Size of spool file for this job */ - int RescheduleTimes; /* Number of times to reschedule job */ + uint32_t MaxConcurrentJobs; /* Maximum concurrent jobs */ + uint32_t NumConcurrentJobs; /* number of concurrent jobs running */ + + MSGS *messages; /* How and where to send messages */ + SCHED *schedule; /* When -- Automatic schedule */ + CLIENT *client; /* Who to backup */ + FILESET *fileset; /* What to backup -- Fileset */ + alist *storage; /* Where is device -- list of Storage to be used */ + POOL *pool; /* Where is media -- Media Pool */ + POOL *full_pool; /* Pool for Full backups */ + POOL *inc_pool; /* Pool for Incremental backups */ + POOL *diff_pool; /* Pool for Differental backups */ + char *selection_pattern; + union { + JOB *verify_job; /* Job name to verify */ + }; + JOB *jobdefs; /* Job defaults */ + alist *run_cmds; /* Run commands */ + alist *RunScripts; /* Run {client} program {after|before} Job */ + + bool where_use_regexp; /* true if RestoreWhere is a BREGEXP */ bool RescheduleOnError; /* Set to reschedule on error */ bool PrefixLinks; /* prefix soft links with Where path */ bool PruneJobs; /* Force pruning of Jobs */ @@ -409,23 +430,6 @@ public: bool CancelQueuedDuplicates; /* Cancel queued jobs */ bool CancelRunningDuplicates; /* Cancel Running jobs */ - MSGS *messages; /* How and where to send messages */ - SCHED *schedule; /* When -- Automatic schedule */ - CLIENT *client; /* Who to backup */ - FILESET *fileset; /* What to backup -- Fileset */ - alist *storage; /* Where is device -- list of Storage to be used */ - POOL *pool; /* Where is media -- Media Pool */ - POOL *full_pool; /* Pool for Full backups */ - POOL *inc_pool; /* Pool for Incremental backups */ - POOL *diff_pool; /* Pool for Differental backups */ - char *selection_pattern; - int selection_type; - union { - JOB *verify_job; /* Job name to verify */ - }; - JOB *jobdefs; /* Job defaults */ - alist *run_cmds; /* Run commands */ - uint32_t NumConcurrentJobs; /* number of concurrent jobs running */ /* Methods */ char *name() const; diff --git a/bacula/src/dird/fd_cmds.c b/bacula/src/dird/fd_cmds.c index 8107dcd73a..827f6a3378 100644 --- a/bacula/src/dird/fd_cmds.c +++ b/bacula/src/dird/fd_cmds.c @@ -166,7 +166,7 @@ void get_level_since_time(JCR *jcr, char *since, int since_len) bool do_diff = false; time_t now; utime_t full_time; -// utime_t diff_time; + utime_t diff_time; since[0] = 0; /* If job cloned and a since time already given, use it */ @@ -187,26 +187,26 @@ void get_level_since_time(JCR *jcr, char *since, int since_len) switch (jcr->JobLevel) { case L_DIFFERENTIAL: case L_INCREMENTAL: + POOLMEM *stime = get_pool_memory(PM_MESSAGE); /* Look up start time of last Full job */ now = time(NULL); - jcr->jr.JobId = 0; /* flag for db_find_job_start time */ + jcr->jr.JobId = 0; /* flag to return since time */ have_full = db_find_job_start_time(jcr, jcr->db, &jcr->jr, &jcr->stime); -#ifdef xxx /* If there was a successful job, make sure it is recent enough */ if (jcr->JobLevel == L_INCREMENTAL && have_full && jcr->job->MaxDiffInterval > 0) { /* Lookup last diff job */ - jcr->jr.JobId = 0; - /* ***FIXME*** must find diff start time and not destroy jcr->stime */ - if (db_find_job_start_time(jcr, jcr->db, &jcr->jr, &jcr->stime)) { - diff_time = str_to_utime(jcr->stime); + if (db_find_last_job_start_time(jcr, jcr->db, &jcr->jr, &stime, L_DIFFERENTIAL)) { + diff_time = str_to_utime(stime); do_diff = ((now - diff_time) <= jcr->job->MaxDiffInterval); } } -#endif - if (have_full && jcr->job->MaxFullInterval > 0) { - full_time = str_to_utime(jcr->stime); + if (have_full && jcr->job->MaxFullInterval > 0 && + db_find_last_job_start_time(jcr, jcr->db, &jcr->jr, &stime, L_FULL)) { + full_time = str_to_utime(stime); do_full = ((now - full_time) <= jcr->job->MaxFullInterval); } + free_pool_memory(stime); + if (!have_full || do_full) { /* No recent Full job found, so upgrade this one to Full */ Jmsg(jcr, M_INFO, 0, "%s", db_strerror(jcr->db)); diff --git a/bacula/src/dird/ua_run.c b/bacula/src/dird/ua_run.c index ec201ab598..95787bcd42 100644 --- a/bacula/src/dird/ua_run.c +++ b/bacula/src/dird/ua_run.c @@ -46,6 +46,7 @@ public: char *when, *verify_job_name, *catalog_name; char *previous_job_name; char *since; + char *plugin_options; const char *verify_list; JOB *job; JOB *verify_job; @@ -73,6 +74,8 @@ static bool display_job_parameters(UAContext *ua, JCR *jcr, JOB *job, char *client_name); static void select_where_regexp(UAContext *ua, JCR *jcr); static bool scan_command_line_arguments(UAContext *ua, run_ctx &rc); +static bool reset_restore_context(UAContext *ua, JCR *jcr, run_ctx &rc); +static int modify_job_parameters(UAContext *ua, JCR *jcr, run_ctx &rc); /* Imported variables */ extern struct s_kw ReplaceOptions[]; @@ -92,7 +95,7 @@ int run_cmd(UAContext *ua, const char *cmd) { JCR *jcr = NULL; run_ctx rc; - int i, opt; + int status; if (!open_client_db(ua)) { return 1; @@ -107,7 +110,6 @@ int run_cmd(UAContext *ua, const char *cmd) ua->quit = true; } -try_again: /* * Create JCR to run job. NOTE!!! after this point, free_jcr() * before returning. @@ -119,116 +121,12 @@ try_again: ua->jcr->unlink_bsr = false; } - jcr->verify_job = rc.verify_job; - jcr->previous_job = rc.previous_job; - jcr->pool = rc.pool; - if (jcr->pool != jcr->job->pool) { - pm_strcpy(jcr->pool_source, _("User input")); - } - set_rwstorage(jcr, rc.store); - jcr->client = rc.client; - pm_strcpy(jcr->client_name, rc.client->name()); - jcr->fileset = rc.fileset; - jcr->ExpectedFiles = rc.files; - if (rc.catalog) { - jcr->catalog = rc.catalog; - pm_strcpy(jcr->catalog_source, _("User input")); - } - if (rc.where) { - if (jcr->where) { - free(jcr->where); - } - jcr->where = bstrdup(rc.where); - rc.where = NULL; - } - - if (rc.regexwhere) { - if (jcr->RegexWhere) { - free(jcr->RegexWhere); - } - jcr->RegexWhere = bstrdup(rc.regexwhere); - rc.regexwhere = NULL; - } - - if (rc.when) { - jcr->sched_time = str_to_utime(rc.when); - if (jcr->sched_time == 0) { - ua->send_msg(_("Invalid time, using current time.\n")); - jcr->sched_time = time(NULL); - } - rc.when = NULL; - } - - if (rc.bootstrap) { - if (jcr->RestoreBootstrap) { - free(jcr->RestoreBootstrap); - } - jcr->RestoreBootstrap = bstrdup(rc.bootstrap); - rc.bootstrap = NULL; - } - - if (rc.replace) { - jcr->replace = 0; - for (i=0; ReplaceOptions[i].name; i++) { - if (strcasecmp(rc.replace, ReplaceOptions[i].name) == 0) { - jcr->replace = ReplaceOptions[i].token; - } - } - if (!jcr->replace) { - ua->send_msg(_("Invalid replace option: %s\n"), rc.replace); - goto bail_out; - } - } else if (rc.job->replace) { - jcr->replace = rc.job->replace; - } else { - jcr->replace = REPLACE_ALWAYS; - } - rc.replace = NULL; - - if (rc.Priority) { - jcr->JobPriority = rc.Priority; - rc.Priority = 0; - } - - if (rc.since) { - if (!jcr->stime) { - jcr->stime = get_pool_memory(PM_MESSAGE); - } - pm_strcpy(jcr->stime, rc.since); - rc.since = NULL; - } - - if (rc.cloned) { - jcr->cloned = rc.cloned; - rc.cloned = false; +try_again: + if (!reset_restore_context(ua, jcr, rc)) { + goto bail_out; } - /* If pool changed, update migration write storage */ - if (jcr->JobType == JT_MIGRATE || jcr->JobType == JT_COPY) { - if (!set_migration_wstorage(jcr, rc.pool)) { - goto bail_out; - } - } - rc.replace = ReplaceOptions[0].name; - for (i=0; ReplaceOptions[i].name; i++) { - if (ReplaceOptions[i].token == jcr->replace) { - rc.replace = ReplaceOptions[i].name; - } - } - if (rc.level_name) { - if (!get_level_from_name(jcr, rc.level_name)) { - ua->send_msg(_("Level %s not valid.\n"), rc.level_name); - goto bail_out; - } - rc.level_name = NULL; - } - if (rc.jid) { - /* Note, this is also MigrateJobId */ - jcr->RestoreJobId = str_to_int64(rc.jid); - rc.jid = 0; - } - /* Run without prompting? */ if (ua->batch || find_arg(ua, NT_("yes")) > 0) { goto start_job; @@ -257,6 +155,47 @@ try_again: goto try_again; } + /* Allow the user to modify the settings */ + status = modify_job_parameters(ua, jcr, rc); + switch (status) { + case 0: + goto try_again; + case 1: + break; + case -1: + goto bail_out; + } + + + if (ua->cmd[0] == 0 || strncasecmp(ua->cmd, _("yes"), strlen(ua->cmd)) == 0) { + JobId_t JobId; + Dmsg1(800, "Calling run_job job=%x\n", jcr->job); + +start_job: + Dmsg3(100, "JobId=%u using pool %s priority=%d\n", (int)jcr->JobId, + jcr->pool->name(), jcr->JobPriority); + JobId = run_job(jcr); + Dmsg4(100, "JobId=%u NewJobId=%d using pool %s priority=%d\n", (int)jcr->JobId, + JobId, jcr->pool->name(), jcr->JobPriority); + free_jcr(jcr); /* release jcr */ + if (JobId == 0) { + ua->error_msg(_("Job failed.\n")); + } else { + char ed1[50]; + ua->send_msg(_("Job queued. JobId=%s\n"), edit_int64(JobId, ed1)); + } + return JobId; + } + +bail_out: + ua->send_msg(_("Job not run.\n")); + free_jcr(jcr); + return 0; /* do not run */ +} + +int modify_job_parameters(UAContext *ua, JCR *jcr, run_ctx &rc) +{ + int i, opt; /* * At user request modify parameters of job to be run. */ @@ -449,31 +388,144 @@ try_again: } goto bail_out; } + return 1; - if (ua->cmd[0] == 0 || strncasecmp(ua->cmd, _("yes"), strlen(ua->cmd)) == 0) { - JobId_t JobId; - Dmsg1(800, "Calling run_job job=%x\n", jcr->job); +bail_out: + return -1; -start_job: - Dmsg3(100, "JobId=%u using pool %s priority=%d\n", (int)jcr->JobId, - jcr->pool->name(), jcr->JobPriority); - JobId = run_job(jcr); - Dmsg4(100, "JobId=%u NewJobId=%d using pool %s priority=%d\n", (int)jcr->JobId, - JobId, jcr->pool->name(), jcr->JobPriority); - free_jcr(jcr); /* release jcr */ - if (JobId == 0) { - ua->error_msg(_("Job failed.\n")); - } else { - char ed1[50]; - ua->send_msg(_("Job queued. JobId=%s\n"), edit_int64(JobId, ed1)); +try_again: + return 0; +} + +/* + * Reset the restore context. + * This subroutine can be called multiple times, so it + * must keep any prior settings. + */ +static bool reset_restore_context(UAContext *ua, JCR *jcr, run_ctx &rc) +{ + int i; + + jcr->verify_job = rc.verify_job; + jcr->previous_job = rc.previous_job; + jcr->pool = rc.pool; + if (jcr->pool != jcr->job->pool) { + pm_strcpy(jcr->pool_source, _("User input")); + } + set_rwstorage(jcr, rc.store); + jcr->client = rc.client; + pm_strcpy(jcr->client_name, rc.client->name()); + jcr->fileset = rc.fileset; + jcr->ExpectedFiles = rc.files; + if (rc.catalog) { + jcr->catalog = rc.catalog; + pm_strcpy(jcr->catalog_source, _("User input")); + } + if (rc.where) { + if (jcr->where) { + free(jcr->where); } - return JobId; + jcr->where = bstrdup(rc.where); + rc.where = NULL; } -bail_out: - ua->send_msg(_("Job not run.\n")); - free_jcr(jcr); - return 0; /* do not run */ + + if (rc.regexwhere) { + if (jcr->RegexWhere) { + free(jcr->RegexWhere); + } + jcr->RegexWhere = bstrdup(rc.regexwhere); + rc.regexwhere = NULL; + } + + if (rc.when) { + jcr->sched_time = str_to_utime(rc.when); + if (jcr->sched_time == 0) { + ua->send_msg(_("Invalid time, using current time.\n")); + jcr->sched_time = time(NULL); + } + rc.when = NULL; + } + + if (rc.bootstrap) { + if (jcr->RestoreBootstrap) { + free(jcr->RestoreBootstrap); + } + jcr->RestoreBootstrap = bstrdup(rc.bootstrap); + rc.bootstrap = NULL; + } + + if (rc.plugin_options) { + if (jcr->plugin_options) { + free(jcr->plugin_options); + } + jcr->plugin_options = bstrdup(rc.plugin_options); + rc.plugin_options = NULL; + } + + + if (rc.replace) { + jcr->replace = 0; + for (i=0; ReplaceOptions[i].name; i++) { + if (strcasecmp(rc.replace, ReplaceOptions[i].name) == 0) { + jcr->replace = ReplaceOptions[i].token; + } + } + if (!jcr->replace) { + ua->send_msg(_("Invalid replace option: %s\n"), rc.replace); + return false; + } + } else if (rc.job->replace) { + jcr->replace = rc.job->replace; + } else { + jcr->replace = REPLACE_ALWAYS; + } + rc.replace = NULL; + + if (rc.Priority) { + jcr->JobPriority = rc.Priority; + rc.Priority = 0; + } + + if (rc.since) { + if (!jcr->stime) { + jcr->stime = get_pool_memory(PM_MESSAGE); + } + pm_strcpy(jcr->stime, rc.since); + rc.since = NULL; + } + + if (rc.cloned) { + jcr->cloned = rc.cloned; + rc.cloned = false; + } + + + /* If pool changed, update migration write storage */ + if (jcr->JobType == JT_MIGRATE || jcr->JobType == JT_COPY) { + if (!set_migration_wstorage(jcr, rc.pool)) { + return false; + } + } + rc.replace = ReplaceOptions[0].name; + for (i=0; ReplaceOptions[i].name; i++) { + if (ReplaceOptions[i].token == jcr->replace) { + rc.replace = ReplaceOptions[i].name; + } + } + if (rc.level_name) { + if (!get_level_from_name(jcr, rc.level_name)) { + ua->send_msg(_("Level %s not valid.\n"), rc.level_name); + return false; + } + rc.level_name = NULL; + } + if (rc.jid) { + /* Note, this is also MigrateJobId */ + jcr->RestoreJobId = str_to_int64(rc.jid); + rc.jid = 0; + } + return true; } static void select_where_regexp(UAContext *ua, JCR *jcr) diff --git a/bacula/src/jcr.h b/bacula/src/jcr.h index 7571b76c75..88fd2b5890 100644 --- a/bacula/src/jcr.h +++ b/bacula/src/jcr.h @@ -217,6 +217,7 @@ public: void *plugin_ctx; /* current plugin context */ Plugin *plugin; /* plugin instance */ save_pkt *plugin_sp; /* plugin save packet */ + char *plugin_options; /* user set options for plugin */ /* Daemon specific part of JCR */ /* This should be empty in the library */ diff --git a/bacula/src/lib/status.h b/bacula/src/lib/status.h index bf5880fdb2..55106b6184 100644 --- a/bacula/src/lib/status.h +++ b/bacula/src/lib/status.h @@ -48,6 +48,10 @@ public: BSOCK *bs; /* used on Unix machines */ void *context; /* Win32 */ void (*callback)(const char *msg, int len, void *context); /* Win32 */ + + /* Methods */ + STATUS_PKT() { memset(this, 0, sizeof(STATUS_PKT)); }; + ~STATUS_PKT() { }; }; extern void output_status(STATUS_PKT *sp); diff --git a/bacula/technotes-2.3 b/bacula/technotes-2.3 index 666b4e7ed0..337aa78249 100644 --- a/bacula/technotes-2.3 +++ b/bacula/technotes-2.3 @@ -2,6 +2,8 @@ General: 05Mar08 +kes Fix bugs in MaxFullInterval and Implement MaxDiffInterval. +kes Start PluginOptions string, and refactor a bit of ua_run.c ebl Apply Allan patch that permit to reset recyclepool. 04Mar08 kes Test patch -- possible fix or improvement for bug #1053 -- 2.39.5