1 /*
2 Bacula(R) - The Network Backup Solution
3
4 Copyright (C) 2000-2020 Kern Sibbald
5
6 The original author of Bacula is Kern Sibbald, with contributions
7 from many others, a complete list can be found in the file AUTHORS.
8
9 You may use this file and others of this release according to the
10 license defined in the LICENSE file, which includes the Affero General
11 Public License, v3.0 ("AGPLv3") and some additional permissions and
12 terms pursuant to its AGPLv3 Section 7.
13
14 This notice must be preserved when any source code is
15 conveyed and/or propagated.
16
17 Bacula(R) is a registered trademark of Kern Sibbald.
18 */
19 /*
20 * Bacula Director -- msgchan.c -- handles the message channel
21 * to the Storage daemon and the File daemon.
22 *
23 * Written by Kern Sibbald, August MM
24 *
25 * This routine runs as a thread and must be thread reentrant.
26 *
27 * Basic tasks done here:
28 * Open a message channel with the Storage daemon
29 * to authenticate ourself and to pass the JobId.
30 * Create a thread to interact with the Storage daemon
31 * who returns a job status and requests Catalog services, etc.
32 */
33
34 #include "bacula.h"
35 #include "dird.h"
36
37 static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
38
39 /* Commands sent to Storage daemon */
40 static char jobcmd[] = "JobId=%s job=%s job_name=%s client_name=%s "
41 "type=%d level=%d FileSet=%s NoAttr=%d SpoolAttr=%d FileSetMD5=%s "
42 "SpoolData=%d WritePartAfterJob=%d PreferMountedVols=%d SpoolSize=%s "
43 "rerunning=%d VolSessionId=%d VolSessionTime=%d sd_client=%d "
44 "Authorization=%s\n";
45 static char use_storage[] = "use storage=%s media_type=%s pool_name=%s "
46 "pool_type=%s append=%d copy=%d stripe=%d\n";
47 static char use_device[] = "use device=%s\n";
48 //static char query_device[] = _("query device=%s");
49
50 /* Response from Storage daemon */
51 static char OKjob[] = "3000 OK Job SDid=%d SDtime=%d Authorization=%100s\n";
52 static char OK_device[] = "3000 OK use device device=%s\n";
53
54 /* Storage Daemon requests */
55 static char Job_start[] = "3010 Job %127s start\n";
56 static char Job_end[] =
57 "3099 Job %127s end JobStatus=%d JobFiles=%d JobBytes=%lld JobErrors=%u ErrMsg=%256s\n";
58
59 /* Forward referenced functions */
60 extern "C" void *msg_thread(void *arg);
61
open_sd_bsock(UAContext * ua)62 BSOCK *open_sd_bsock(UAContext *ua)
63 {
64 STORE *store = ua->jcr->wstore;
65
66 if (!is_bsock_open(ua->jcr->store_bsock)) {
67 ua->send_msg(_("Connecting to Storage daemon %s at %s:%d ...\n"),
68 store->name(), store->address, store->SDport);
69 if (!connect_to_storage_daemon(ua->jcr, 10, SDConnectTimeout, 1)) {
70 ua->error_msg(_("Failed to connect to Storage daemon.\n"));
71 return NULL;
72 }
73 }
74 return ua->jcr->store_bsock;
75 }
76
close_sd_bsock(UAContext * ua)77 void close_sd_bsock(UAContext *ua)
78 {
79 if (ua->jcr->store_bsock) {
80 ua->jcr->store_bsock->signal(BNET_TERMINATE);
81 free_bsock(ua->jcr->store_bsock);
82 }
83 }
84
85 /*
86 * Establish a message channel connection with the Storage daemon
87 * and perform authentication.
88 */
connect_to_storage_daemon(JCR * jcr,int retry_interval,int max_retry_time,int verbose)89 bool connect_to_storage_daemon(JCR *jcr, int retry_interval,
90 int max_retry_time, int verbose)
91 {
92 BSOCK *sd = jcr->store_bsock;
93 STORE *store;
94 utime_t heart_beat;
95
96 if (is_bsock_open(sd)) {
97 return true; /* already connected */
98 }
99 if (!sd) {
100 sd = new_bsock();
101 }
102
103 /* If there is a write storage use it */
104 if (jcr->wstore) {
105 store = jcr->wstore;
106 } else {
107 store = jcr->rstore;
108 }
109
110 if (store->heartbeat_interval) {
111 heart_beat = store->heartbeat_interval;
112 } else {
113 heart_beat = director->heartbeat_interval;
114 }
115
116 /*
117 * Open message channel with the Storage daemon
118 */
119 Dmsg2(100, "Connect to Storage daemon %s:%d\n", store->address,
120 store->SDport);
121 sd->set_source_address(director->DIRsrc_addr);
122 if (!sd->connect(jcr, retry_interval, max_retry_time, heart_beat, _("Storage daemon"),
123 store->address, NULL, store->SDport, verbose)) {
124
125 if (!jcr->store_bsock) { /* The bsock was locally created, so we free it here */
126 free_bsock(sd);
127 }
128 sd = NULL;
129 }
130
131 if (sd == NULL) {
132 return false;
133 }
134 sd->res = (RES *)store; /* save pointer to other end */
135 jcr->store_bsock = sd;
136
137 if (!authenticate_storage_daemon(jcr, store)) {
138 sd->close();
139 return false;
140 }
141 return true;
142 }
143
144 /*
145 * Here we ask the SD to send us the info for a
146 * particular device resource.
147 */
148 #ifdef xxx
update_device_res(JCR * jcr,DEVICE * dev)149 bool update_device_res(JCR *jcr, DEVICE *dev)
150 {
151 POOL_MEM device_name;
152 BSOCK *sd;
153 if (!connect_to_storage_daemon(jcr, 5, 30, 0)) {
154 return false;
155 }
156 sd = jcr->store_bsock;
157 pm_strcpy(device_name, dev->name());
158 bash_spaces(device_name);
159 sd->fsend(query_device, device_name.c_str());
160 Dmsg1(100, ">stored: %s\n", sd->msg);
161 /* The data is returned through Device_update */
162 if (bget_dirmsg(sd) <= 0) {
163 return false;
164 }
165 return true;
166 }
167 #endif
168
169 static char OKbootstrap[] = "3000 OK bootstrap\n";
170
171 /*
172 * Start a job with the Storage daemon
173 */
start_storage_daemon_job(JCR * jcr,alist * rstore,alist * wstore,bool send_bsr)174 bool start_storage_daemon_job(JCR *jcr, alist *rstore, alist *wstore, bool send_bsr)
175 {
176 bool ok = true;
177 STORE *storage;
178 BSOCK *sd;
179 char sd_auth_key[100];
180 POOL_MEM store_name, device_name, pool_name, pool_type, media_type;
181 POOL_MEM job_name, client_name, fileset_name;
182 int copy = 0;
183 int stripe = 0;
184 char ed1[30], ed2[30];
185 int sd_client;
186
187 sd = jcr->store_bsock;
188 /*
189 * Now send JobId and permissions, and get back the authorization key.
190 */
191 pm_strcpy(job_name, jcr->job->name());
192 bash_spaces(job_name);
193 if (jcr->client) {
194 pm_strcpy(client_name, jcr->client->name());
195 } else {
196 pm_strcpy(client_name, "**Dummy**");
197 }
198 bash_spaces(client_name);
199 pm_strcpy(fileset_name, jcr->fileset->name());
200 bash_spaces(fileset_name);
201 if (jcr->fileset->MD5[0] == 0) {
202 bstrncpy(jcr->fileset->MD5, "**Dummy**", sizeof(jcr->fileset->MD5));
203 }
204 /* If rescheduling, cancel the previous incarnation of this job
205 * with the SD, which might be waiting on the FD connection.
206 * If we do not cancel it the SD will not accept a new connection
207 * for the same jobid.
208 */
209 if (jcr->reschedule_count) {
210 sd->fsend("cancel Job=%s\n", jcr->Job);
211 while (sd->recv() >= 0)
212 { }
213 }
214
215 sd_client = jcr->sd_client;
216 if (jcr->sd_auth_key) {
217 bstrncpy(sd_auth_key, jcr->sd_auth_key, sizeof(sd_auth_key));
218 } else {
219 bstrncpy(sd_auth_key, "dummy", sizeof(sd_auth_key));
220 }
221
222 sd->fsend(jobcmd, edit_int64(jcr->JobId, ed1), jcr->Job,
223 job_name.c_str(), client_name.c_str(),
224 jcr->getJobType(), jcr->getJobLevel(),
225 fileset_name.c_str(), !jcr->pool->catalog_files,
226 jcr->job->SpoolAttributes, jcr->fileset->MD5, jcr->spool_data,
227 jcr->write_part_after_job, jcr->job->PreferMountedVolumes,
228 edit_int64(jcr->spool_size, ed2), jcr->rerunning,
229 jcr->VolSessionId, jcr->VolSessionTime, sd_client,
230 sd_auth_key);
231
232 Dmsg1(100, ">stored: %s", sd->msg);
233 Dmsg2(100, "=== rstore=%p wstore=%p\n", rstore, wstore);
234 if (bget_dirmsg(sd) > 0) {
235 Dmsg1(100, "<stored: %s", sd->msg);
236 if (sscanf(sd->msg, OKjob, &jcr->VolSessionId,
237 &jcr->VolSessionTime, &sd_auth_key) != 3) {
238 Dmsg1(100, "BadJob=%s\n", sd->msg);
239 Jmsg(jcr, M_FATAL, 0, _("Storage daemon rejected Job command: %s\n"), sd->msg);
240 return false;
241 } else {
242 bfree_and_null(jcr->sd_auth_key);
243 jcr->sd_auth_key = bstrdup(sd_auth_key);
244 Dmsg1(150, "sd_auth_key=%s\n", jcr->sd_auth_key);
245 }
246 } else {
247 Jmsg(jcr, M_FATAL, 0, _("<stored: bad response to Job command: %s\n"),
248 sd->bstrerror());
249 return false;
250 }
251
252 if (send_bsr && (!send_bootstrap_file(jcr, sd) ||
253 !response(jcr, sd, OKbootstrap, "Bootstrap", DISPLAY_ERROR))) {
254 return false;
255 }
256
257 /*
258 * We have two loops here. The first comes from the
259 * Storage = associated with the Job, and we need
260 * to attach to each one.
261 * The inner loop loops over all the alternative devices
262 * associated with each Storage. It selects the first
263 * available one.
264 *
265 */
266 /* Do read side of storage daemon */
267 if (ok && rstore) {
268 /* For the moment, only migrate, copy and vbackup have rpool */
269 if (jcr->is_JobType(JT_MIGRATE) || jcr->is_JobType(JT_COPY) ||
270 (jcr->is_JobType(JT_BACKUP) && jcr->is_JobLevel(L_VIRTUAL_FULL))) {
271 pm_strcpy(pool_type, jcr->rpool->pool_type);
272 pm_strcpy(pool_name, jcr->rpool->name());
273 } else {
274 pm_strcpy(pool_type, jcr->pool->pool_type);
275 pm_strcpy(pool_name, jcr->pool->name());
276 }
277 bash_spaces(pool_type);
278 bash_spaces(pool_name);
279 foreach_alist(storage, rstore) {
280 Dmsg1(100, "Rstore=%s\n", storage->name());
281 pm_strcpy(store_name, storage->name());
282 bash_spaces(store_name);
283 if (jcr->media_type) {
284 pm_strcpy(media_type, jcr->media_type); /* user override */
285 } else {
286 pm_strcpy(media_type, storage->media_type);
287 }
288 bash_spaces(media_type);
289 sd->fsend(use_storage, store_name.c_str(), media_type.c_str(),
290 pool_name.c_str(), pool_type.c_str(), 0, copy, stripe);
291 Dmsg1(100, "rstore >stored: %s", sd->msg);
292 DEVICE *dev;
293 /* Loop over alternative storage Devices until one is OK */
294 foreach_alist(dev, storage->device) {
295 pm_strcpy(device_name, dev->name());
296 bash_spaces(device_name);
297 sd->fsend(use_device, device_name.c_str());
298 Dmsg1(100, ">stored: %s", sd->msg);
299 }
300 sd->signal(BNET_EOD); /* end of Devices */
301 }
302 sd->signal(BNET_EOD); /* end of Storages */
303 if (bget_dirmsg(sd) > 0) {
304 Dmsg1(100, "<stored: %s", sd->msg);
305 /* ****FIXME**** save actual device name */
306 ok = sscanf(sd->msg, OK_device, device_name.c_str()) == 1;
307 } else {
308 ok = false;
309 }
310 if (ok) {
311 Jmsg(jcr, M_INFO, 0, _("Using Device \"%s\" to read.\n"), device_name.c_str());
312 }
313 }
314
315 /* Do write side of storage daemon */
316 if (ok && wstore) {
317 pm_strcpy(pool_type, jcr->pool->pool_type);
318 pm_strcpy(pool_name, jcr->pool->name());
319 bash_spaces(pool_type);
320 bash_spaces(pool_name);
321 foreach_alist(storage, wstore) {
322 Dmsg1(100, "Wstore=%s\n", storage->name());
323 pm_strcpy(store_name, storage->name());
324 bash_spaces(store_name);
325 pm_strcpy(media_type, storage->media_type);
326 bash_spaces(media_type);
327 sd->fsend(use_storage, store_name.c_str(), media_type.c_str(),
328 pool_name.c_str(), pool_type.c_str(), 1, copy, stripe);
329
330 Dmsg1(100, "wstore >stored: %s", sd->msg);
331 DEVICE *dev;
332 /* Loop over alternative storage Devices until one is OK */
333 foreach_alist(dev, storage->device) {
334 pm_strcpy(device_name, dev->name());
335 bash_spaces(device_name);
336 sd->fsend(use_device, device_name.c_str());
337 Dmsg1(100, ">stored: %s", sd->msg);
338 }
339 sd->signal(BNET_EOD); /* end of Devices */
340 }
341 sd->signal(BNET_EOD); /* end of Storages */
342 if (bget_dirmsg(sd) > 0) {
343 Dmsg1(100, "<stored: %s", sd->msg);
344 /* ****FIXME**** save actual device name */
345 ok = sscanf(sd->msg, OK_device, device_name.c_str()) == 1;
346 } else {
347 ok = false;
348 }
349 if (ok) {
350 Jmsg(jcr, M_INFO, 0, _("Using Device \"%s\" to write.\n"), device_name.c_str());
351 }
352 }
353 if (!ok) {
354 POOL_MEM err_msg;
355 if (sd->msg[0]) {
356 pm_strcpy(err_msg, sd->msg); /* save message */
357 Jmsg(jcr, M_FATAL, 0, _("\n"
358 " Storage daemon didn't accept Device \"%s\" because:\n %s"),
359 device_name.c_str(), err_msg.c_str()/* sd->msg */);
360 } else {
361 Jmsg(jcr, M_FATAL, 0, _("\n"
362 " Storage daemon didn't accept Device \"%s\" command.\n"),
363 device_name.c_str());
364 }
365 }
366 return ok;
367 }
368
369 /*
370 * Start a thread to handle Storage daemon messages and
371 * Catalog requests.
372 */
start_storage_daemon_message_thread(JCR * jcr)373 bool start_storage_daemon_message_thread(JCR *jcr)
374 {
375 int status;
376 pthread_t thid;
377
378 jcr->inc_use_count(); /* mark in use by msg thread */
379 jcr->sd_msg_thread_done = false;
380 jcr->SD_msg_chan_started = false;
381 Dmsg0(150, "Start SD msg_thread.\n");
382 if ((status=pthread_create(&thid, NULL, msg_thread, (void *)jcr)) != 0) {
383 berrno be;
384 Jmsg1(jcr, M_ABORT, 0, _("Cannot create message thread: %s\n"), be.bstrerror(status));
385 }
386 /* Wait for thread to start */
387 while (jcr->SD_msg_chan_started == false) {
388 bmicrosleep(0, 50);
389 if (job_canceled(jcr) || jcr->sd_msg_thread_done) {
390 return false;
391 }
392 }
393 Dmsg1(150, "SD msg_thread started. use=%d\n", jcr->use_count());
394 return true;
395 }
396
msg_thread_cleanup(void * arg)397 extern "C" void msg_thread_cleanup(void *arg)
398 {
399 JCR *jcr = (JCR *)arg;
400 db_end_transaction(jcr, jcr->db); /* terminate any open transaction */
401 jcr->lock();
402 jcr->sd_msg_thread_done = true;
403 jcr->SD_msg_chan_started = false;
404 jcr->unlock();
405 pthread_cond_broadcast(&jcr->term_wait); /* wakeup any waiting threads */
406 Dmsg2(100, "=== End msg_thread. JobId=%d usecnt=%d\n", jcr->JobId, jcr->use_count());
407 db_thread_cleanup(jcr->db); /* remove thread specific data */
408 free_jcr(jcr); /* release jcr */
409 }
410
411 /*
412 * Handle the message channel (i.e. requests from the
413 * Storage daemon).
414 * Note, we are running in a separate thread.
415 */
msg_thread(void * arg)416 extern "C" void *msg_thread(void *arg)
417 {
418 JCR *jcr = (JCR *)arg;
419 BSOCK *sd;
420 int JobStatus;
421 int n;
422 char Job[MAX_NAME_LENGTH];
423 char ErrMsg[256];
424 uint32_t JobFiles, JobErrors;
425 uint64_t JobBytes;
426 ErrMsg[0] = 0;
427
428 pthread_detach(pthread_self());
429 set_jcr_in_tsd(jcr);
430 jcr->SD_msg_chan = pthread_self();
431 jcr->SD_msg_chan_started = true;
432 pthread_cleanup_push(msg_thread_cleanup, arg);
433 sd = jcr->store_bsock;
434
435 /* Read the Storage daemon's output.
436 */
437 Dmsg0(100, "Start msg_thread loop\n");
438 n = 0;
439 while (!job_canceled(jcr) && (n=bget_dirmsg(sd)) >= 0) {
440 Dmsg1(400, "<stored: %s", sd->msg);
441 if (sscanf(sd->msg, Job_start, Job) == 1) {
442 continue;
443 }
444 if (sscanf(sd->msg, Job_end, Job, &JobStatus, &JobFiles,
445 &JobBytes, &JobErrors, ErrMsg) == 6) {
446 jcr->SDJobStatus = JobStatus; /* termination status */
447 jcr->SDJobFiles = JobFiles;
448 jcr->SDJobBytes = JobBytes;
449 jcr->SDErrors = JobErrors;
450 unbash_spaces(ErrMsg); /* Error message if any */
451 pm_strcpy(jcr->StatusErrMsg, ErrMsg);
452 break;
453 }
454 Dmsg1(400, "end loop use=%d\n", jcr->use_count());
455 }
456 if (n == BNET_HARDEOF && jcr->getJobStatus() != JS_Canceled) {
457 /*
458 * This probably should be M_FATAL, but I am not 100% sure
459 * that this return *always* corresponds to a dropped line.
460 */
461 Qmsg(jcr, M_ERROR, 0, _("Director's connection to SD for this Job was lost.\n"));
462 }
463 if (jcr->getJobStatus() == JS_Canceled) {
464 jcr->SDJobStatus = JS_Canceled;
465 } else if (sd->is_error()) {
466 jcr->SDJobStatus = JS_ErrorTerminated;
467 }
468 pthread_cleanup_pop(1); /* remove and execute the handler */
469 return NULL;
470 }
471
wait_for_storage_daemon_termination(JCR * jcr)472 void wait_for_storage_daemon_termination(JCR *jcr)
473 {
474 int cancel_count = 0;
475 /* Now wait for Storage daemon to terminate our message thread */
476 while (!jcr->sd_msg_thread_done) {
477 struct timeval tv;
478 struct timezone tz;
479 struct timespec timeout;
480
481 gettimeofday(&tv, &tz);
482 timeout.tv_nsec = 0;
483 timeout.tv_sec = tv.tv_sec + 5; /* wait 5 seconds */
484 Dmsg0(400, "I'm waiting for message thread termination.\n");
485 P(mutex);
486 pthread_cond_timedwait(&jcr->term_wait, &mutex, &timeout);
487 V(mutex);
488 if (jcr->is_canceled()) {
489 if (jcr->SD_msg_chan_started) {
490 jcr->store_bsock->set_timed_out();
491 jcr->store_bsock->set_terminated();
492 sd_msg_thread_send_signal(jcr, TIMEOUT_SIGNAL);
493 }
494 cancel_count++;
495 }
496 /* Give SD 30 seconds to clean up after cancel */
497 if (cancel_count == 6) {
498 break;
499 }
500 }
501 jcr->setJobStatus(JS_Terminated);
502 }
503
terminate_sd_msg_chan_thread(JCR * jcr)504 void terminate_sd_msg_chan_thread(JCR *jcr)
505 {
506 if (jcr && jcr->store_bsock) {
507 jcr->store_bsock->signal(BNET_TERMINATE);
508 jcr->lock();
509 if ( !jcr->sd_msg_thread_done
510 && jcr->SD_msg_chan_started
511 && !pthread_equal(jcr->SD_msg_chan, pthread_self())) {
512 Dmsg1(800, "Send kill to SD msg chan jid=%d\n", jcr->JobId);
513 int cnt = 6; // 6*5sec
514 while (!jcr->sd_msg_thread_done && cnt>0) {
515 jcr->unlock();
516 pthread_kill(jcr->SD_msg_chan, TIMEOUT_SIGNAL);
517 struct timeval tv;
518 struct timezone tz;
519 struct timespec timeout;
520
521 gettimeofday(&tv, &tz);
522 timeout.tv_nsec = 0;
523 timeout.tv_sec = tv.tv_sec + 5; /* wait 5 seconds */
524 Dmsg0(00, "I'm waiting for message thread termination.\n");
525 P(mutex);
526 pthread_cond_timedwait(&jcr->term_wait, &mutex, &timeout);
527 V(mutex);
528 jcr->lock();
529 cnt--;
530 }
531 }
532 jcr->unlock();
533 }
534 }
535
536 /*
537 * Send bootstrap file to Storage daemon.
538 * This is used for restore, verify VolumeToCatalog, migration,
539 * and copy Jobs.
540 */
send_bootstrap_file(JCR * jcr,BSOCK * sd)541 bool send_bootstrap_file(JCR *jcr, BSOCK *sd)
542 {
543 FILE *bs;
544 char buf[1000];
545 const char *bootstrap = "bootstrap\n";
546
547 Dmsg1(400, "send_bootstrap_file: %s\n", jcr->RestoreBootstrap);
548 if (!jcr->RestoreBootstrap) {
549 return true;
550 }
551 bs = bfopen(jcr->RestoreBootstrap, "rb");
552 if (!bs) {
553 berrno be;
554 Jmsg(jcr, M_FATAL, 0, _("Could not open bootstrap file %s: ERR=%s\n"),
555 jcr->RestoreBootstrap, be.bstrerror());
556 jcr->setJobStatus(JS_ErrorTerminated);
557 return false;
558 }
559 sd->fsend(bootstrap);
560 while (fgets(buf, sizeof(buf), bs)) {
561 sd->fsend("%s", buf);
562 }
563 sd->signal(BNET_EOD);
564 fclose(bs);
565 if (jcr->unlink_bsr) {
566 unlink(jcr->RestoreBootstrap);
567 jcr->unlink_bsr = false;
568 }
569 return true;
570 }
571
572
573 #ifdef needed
574 #define MAX_TRIES 30
575 #define WAIT_TIME 2
device_thread(void * arg)576 extern "C" void *device_thread(void *arg)
577 {
578 int i;
579 JCR *jcr;
580 DEVICE *dev;
581
582
583 pthread_detach(pthread_self());
584 jcr = new_control_jcr("*DeviceInit*", JT_SYSTEM);
585 for (i=0; i < MAX_TRIES; i++) {
586 if (!connect_to_storage_daemon(jcr, 10, 30, 1)) {
587 Dmsg0(900, "Failed connecting to SD.\n");
588 continue;
589 }
590 LockRes();
591 foreach_res(dev, R_DEVICE) {
592 if (!update_device_res(jcr, dev)) {
593 Dmsg1(900, "Error updating device=%s\n", dev->name());
594 } else {
595 Dmsg1(900, "Updated Device=%s\n", dev->name());
596 }
597 }
598 UnlockRes();
599 free_bsock(jcr->store_bsock);
600 break;
601
602 }
603 free_jcr(jcr);
604 return NULL;
605 }
606
607 /*
608 * Start a thread to handle getting Device resource information
609 * from SD. This is called once at startup of the Director.
610 */
init_device_resources()611 void init_device_resources()
612 {
613 int status;
614 pthread_t thid;
615
616 Dmsg0(100, "Start Device thread.\n");
617 if ((status=pthread_create(&thid, NULL, device_thread, NULL)) != 0) {
618 berrno be;
619 Jmsg1(NULL, M_ABORT, 0, _("Cannot create message thread: %s\n"), be.bstrerror(status));
620 }
621 }
622 #endif
623