00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013 #include <errno.h>
00014 #include <fcntl.h>
00015 #include <grp.h>
00016 #include <string.h>
00017 #include <time.h>
00018 #include <pwd.h>
00019 #include <stdio.h>
00020 #include <unistd.h>
00021 #include <sys/types.h>
00022 #include <sys/stat.h>
00023
00024 #include "XrdFrm/XrdFrmAdmin.hh"
00025 #include "XrdFrm/XrdFrmConfig.hh"
00026 #include "XrdFrm/XrdFrmProxy.hh"
00027 #include "XrdFrm/XrdFrmTrace.hh"
00028 #include "XrdFrm/XrdFrmUtils.hh"
00029 #include "XrdOss/XrdOss.hh"
00030 #include "XrdOuc/XrdOuca2x.hh"
00031 #include "XrdOuc/XrdOucArgs.hh"
00032 #include "XrdOuc/XrdOucExport.hh"
00033 #include "XrdOuc/XrdOucTList.hh"
00034 #include "XrdOuc/XrdOucTokenizer.hh"
00035 #include "XrdSys/XrdSysTimer.hh"
00036
00037 const char *XrdFrmAdminCVSID = "$Id: XrdFrmAdmin.cc 38011 2011-02-08 18:35:57Z ganis $";
00038
00039 using namespace XrdFrm;
00040
00041
00042
00043
00044
00045 const char *XrdFrmAdmin::AuditHelp =
00046
00047 "audit [opts] {names ldir | space name[:pdir] | usage [name]}\n\n"
00048
00049 "opts: -fix -f[orce] -m[igratable] -p[urgeable] -r[ecursive]";
00050
00051 int XrdFrmAdmin::Audit()
00052 {
00053 static XrdOucArgs Spec(&Say, "frm_admin: ", "",
00054 "fix", 3, "f",
00055 "force", 1, "F",
00056 "migratable", 1, "m",
00057 "purgeable", 1, "p",
00058 "recursive", 1, "r",
00059 (const char *)0);
00060
00061 static const char *Reqs[] = {"type", 0};
00062
00063
00064
00065 if (!Parse("audit ", Spec, Reqs)) return 1;
00066 Opt.Args[1] = Spec.getarg();
00067
00068
00069
00070 if (!strcmp(Opt.Args[0], "usage")) return AuditUsage();
00071 if (!Opt.Args[1]) Emsg("audit target not specified.");
00072 else if (!strcmp(Opt.Args[0], "names")) return AuditNames();
00073 else if (!strcmp(Opt.Args[0], "space")) return AuditSpace();
00074 else Emsg("Unknown audit type - ", Opt.Args[0]);
00075
00076
00077
00078 return 4;
00079 }
00080
00081
00082
00083
00084
00085 const char *XrdFrmAdmin::FindHelp = "find [-r[ecursive]] what ldir [ldir [...]]\n\n"
00086
00087 "what: fail[files] | nolk[files] | unmig[rated]";
00088
00089 int XrdFrmAdmin::Find()
00090 {
00091 static XrdOucArgs Spec(&Say, "frm_admin: ", "",
00092 "recursive", 1, "r", (const char *)0);
00093
00094 static const char *Reqs[] = {"type", "target", 0};
00095
00096
00097
00098 if (!Parse("find ", Spec, Reqs)) return 1;
00099
00100
00101
00102 if (!strncmp(Opt.Args[0], "failfiles", 4)) return FindFail(Spec);
00103 else if (!strncmp(Opt.Args[0], "nolkfiles", 4)) return FindNolk(Spec);
00104 else if (!strncmp(Opt.Args[0], "unmigrated",4)) return FindUnmi(Spec);
00105
00106
00107
00108 Emsg("Unknown find type - ", Opt.Args[0]);
00109 return 4;
00110 }
00111
00112
00113
00114
00115
00116 const char *XrdFrmAdmin::HelpHelp =
00117 "[help] {audit | exit | f[ind] | makelf | pin | q[uery] | quit | reloc | rm} ...";
00118
00119 int XrdFrmAdmin::Help()
00120 {
00121 static struct CmdInfo {const char *Name;
00122 int minL;
00123 int maxL;
00124 const char *Help;
00125 }
00126 CmdTab[] = {{"audit", 5, 5, AuditHelp },
00127 {"find", 1, 4, FindHelp },
00128 {"makelf", 6, 6, MakeLFHelp},
00129 {"pin", 3, 3, PinHelp },
00130 {"query", 1, 5, QueryHelp },
00131 {"reloc", 5, 5, RelocHelp },
00132 {"rm", 2, 2, RemoveHelp}
00133 };
00134 static int CmdNum = sizeof(CmdTab)/sizeof(struct CmdInfo);
00135 const char *theHelp = HelpHelp;
00136 char *Cmd;
00137 int i, n;
00138
00139
00140
00141 if (!ArgS) Cmd = ArgV[0];
00142 else {XrdOucTokenizer Tokens(ArgS);
00143 if ((Cmd = Tokens.GetLine())) Cmd = Tokens.GetToken();
00144 }
00145
00146
00147
00148 if (Cmd)
00149 {n = strlen(Cmd);
00150 for (i = 0; i < CmdNum; i++)
00151 if (n <= CmdTab[i].maxL && n >= CmdTab[i].minL
00152 && !strncmp(CmdTab[i].Name, Cmd, n)) break;
00153 if (i < CmdNum) {Msg("Usage: ", CmdTab[i].Help); return 0;}
00154 }
00155 Emsg(0, "Usage: ", theHelp);
00156 return 0;
00157 }
00158
00159
00160
00161
00162
00163 const char *XrdFrmAdmin::MakeLFHelp = "makelf [opts] lspec [lspec [...]]\n\n"
00164
00165 "opts: -m[igratable] -o[wner] [usr][:[grp]] -p[urgeable] "
00166 "-r[ecursive]\n\n"
00167
00168 "lspec: lfn | ldir[*]";
00169
00170 int XrdFrmAdmin::MakeLF()
00171 {
00172 static XrdOucArgs Spec(&Say, "frm_admin: ", "",
00173 "migratable", 1, "m",
00174 "owner", 1, "o:",
00175 "purgeable", 1, "p",
00176 "recursive", 1, "r",
00177 (const char *)0);
00178
00179 static const char *Reqs[] = {"lfn", 0};
00180
00181 char *lfn, buff[80], Resp;
00182 int ok = 1;
00183
00184
00185
00186 if (!Parse("makelf ", Spec, Reqs)) return 1;
00187
00188
00189
00190 numFiles = 0;
00191 lfn = Opt.Args[0];
00192 if (!Opt.MPType) Opt.MPType = 'm';
00193 do {Opt.All = VerifyAll(lfn);
00194 if ((Resp = VerifyMP("makelf", lfn)) == 'y') ok = mkLock(lfn);
00195 } while(Resp != 'a' && ok && (lfn = Spec.getarg()));
00196
00197
00198
00199 if (Resp == 'a' || !ok) Msg("makelf aborted!");
00200 sprintf(buff, "%d lock file%s made.", numFiles, (numFiles == 1 ? "" : "s"));
00201 Msg(buff);
00202 return 0;
00203 }
00204
00205
00206
00207
00208
00209 const char *XrdFrmAdmin::PinHelp = "pin [opts] lspec [lspec [...]]\n\n"
00210
00211 "opts: -k[eep] <time> -o[wner] [usr][:[grp]] -r[ecursive]\n\n"
00212
00213 "time: [+]<n>[d|h|m|s] | mm/dd/[yy]yy | forever\n\n"
00214
00215 "lspec: lfn | ldir[*]";
00216
00217 int XrdFrmAdmin::Pin()
00218 {
00219 static XrdOucArgs Spec(&Say, "frm_admin: ", "",
00220 "keep", 1, "k:",
00221 "owner", 1, "o:",
00222 "recursive", 1, "r",
00223 (const char *)0);
00224
00225 static const char *Reqs[] = {"lfn", 0};
00226
00227 const char *Act;
00228 char *lfn, itbuff[80], *itP = itbuff, Resp;
00229 int itL = 0, ok = 1;
00230
00231
00232
00233 if (!Parse("pin ", Spec, Reqs)) return 1;
00234
00235
00236
00237 if (!Opt.Keep) Opt.KeepTime = time(0) + 24*3600;
00238 else if (Opt.ktIdle && Opt.KeepTime)
00239 itL = sprintf(itbuff, "&inact_time=%d\n",
00240 static_cast<int>(Opt.KeepTime));
00241
00242
00243
00244 numFiles = 0;
00245 lfn = Opt.Args[0];
00246 Opt.MPType = 'p';
00247 do {Opt.All = VerifyAll(lfn);
00248 if ((Resp = VerifyMP("pin", lfn)) == 'y') ok = mkPin(lfn, itP, itL);
00249 } while(Resp != 'a' && ok && (lfn = Spec.getarg()));
00250
00251
00252
00253 Act = (Opt.KeepTime || itL ? "" : "un");
00254 if (Resp == 'a' || !ok) Msg("pin aborted!");
00255 sprintf(itbuff,"%d %spin%s processed.",numFiles,Act,(numFiles==1?"":"s"));
00256 Msg(itbuff);
00257 return 0;
00258 }
00259
00260
00261
00262
00263
00264 const char *XrdFrmAdmin::QueryHelp = "\n"
00265 "query pfn lspec [lspec [...]]\n"
00266 "query rfn lspec [lspec [...]]\n"
00267 "query space [[-r[ecursive]] lspec [...]]\n"
00268 "query usage [name]\n"
00269 "query xfrq [name] [vars]\n\n"
00270
00271 "lspec: lfn | ldir[*]";
00272
00273 int XrdFrmAdmin::Query()
00274 {
00275 static XrdOucArgs Spec(&Say, "frm_admin: ", "", (const char *)0);
00276
00277 static const char *Reqs[] = {"type", 0};
00278 static struct CmdInfo {const char *Name;
00279 int (XrdFrmAdmin::*Method)(XrdOucArgs &Spec);
00280 }
00281 CmdTab[] = {{"pfn", &XrdFrmAdmin::QueryPfn},
00282 {"rfn", &XrdFrmAdmin::QueryRfn},
00283 {"space", &XrdFrmAdmin::QuerySpace},
00284 {"usage", &XrdFrmAdmin::QueryUsage},
00285 {"xfrq", &XrdFrmAdmin::QueryXfrQ}
00286 };
00287 static int CmdNum = sizeof(CmdTab)/sizeof(struct CmdInfo);
00288
00289 int i;
00290
00291
00292
00293 if (!Parse("query ", Spec, Reqs)) return 1;
00294
00295
00296
00297 for (i = 0; i < CmdNum; i++)
00298 if (!strcmp(CmdTab[i].Name, Opt.Args[0])) break;
00299
00300
00301
00302 if (i >= CmdNum)
00303 {Emsg("Invalid query type - ", Opt.Args[0]);
00304 return 1;
00305 }
00306
00307
00308
00309 return (*this.*CmdTab[i].Method)(Spec);
00310 }
00311
00312
00313
00314
00315
00316 const char *XrdFrmAdmin::RelocHelp = "reloc lfn {cgroup[:path]}";
00317
00318 int XrdFrmAdmin::Reloc()
00319 {
00320 static XrdOucArgs Spec(&Say, "frm_admin: ", "", (const char *)0);
00321
00322 static const char *Reqs[] = {"lfn", "target", 0};
00323
00324 int rc;
00325
00326
00327
00328 if (!Parse("reloc ", Spec, Reqs)) return 1;
00329
00330
00331
00332 if ((rc = Config.ossFS->Reloc("admin", Opt.Args[0], Opt.Args[1])))
00333 Emsg(-rc, "reloc ", Opt.Args[0]);
00334 else Msg(Opt.Args[0], " relocated to space ", Opt.Args[1]);
00335 return rc != 0;
00336 }
00337
00338
00339
00340
00341
00342 const char *XrdFrmAdmin::RemoveHelp = "rm [opts] lspec [lspec [...]]\n\n"
00343
00344 "opts: -e[cho] -f[orce] -n[otify] -r[ecursive]\n\n"
00345
00346 "lspec: lfn | ldir[*]";
00347
00348 int XrdFrmAdmin::Remove()
00349 {
00350 static XrdOucArgs Spec(&Say, "frm_admin: ", "",
00351 "echo", 1, "E",
00352 "force", 1, "F",
00353 "recursive", 1, "r",
00354 (const char *)0);
00355
00356 static const char *Reqs[] = {"lfn", 0};
00357
00358 const char *Txt = "";
00359 char buff[80];
00360 int rc = 0, aOK = 1;
00361
00362
00363
00364 if (!Parse("rm ", Spec, Reqs)) return 1;
00365
00366
00367
00368 numDirs = numFiles = numProb = 0;
00369
00370
00371
00372 do {Opt.All = VerifyAll(Opt.Args[0]);
00373 if ((rc = Unlink(Opt.Args[0])) < 0) aOK = 0;
00374 } while(rc && (Opt.Args[0] = Spec.getarg()));
00375
00376 if (!rc) {Txt = "rm aborted; only "; finalRC = 4;}
00377 else if (numProb || !aOK) {Txt = "rm incomplete; only "; finalRC = 4;}
00378
00379
00380
00381 sprintf(buff, "%s%d %s and %d %s deleted.", Txt,
00382 numFiles, (numFiles != 1 ? "files" : "file"),
00383 numDirs, (numDirs != 1 ? "directories" : "directory"));
00384 Msg(buff);
00385 return 0;
00386 }
00387
00388
00389
00390
00391
00392 void XrdFrmAdmin::setArgs(int argc, char **argv)
00393 {
00394 ArgC = argc; ArgV = argv; ArgS = 0;
00395 }
00396
00397
00398 void XrdFrmAdmin::setArgs(char *args)
00399 {
00400 ArgC = 0; ArgV = 0; ArgS = args;
00401 }
00402
00403
00404
00405
00406
00407 int XrdFrmAdmin::xeqArgs(char *Cmd)
00408 {
00409 static struct CmdInfo {const char *Name;
00410 int minLen;
00411 int maxLen;
00412 int (XrdFrmAdmin::*Method)();
00413 }
00414 CmdTab[] = {{"audit", 5, 5, &XrdFrmAdmin::Audit},
00415 {"exit", 4, 4, &XrdFrmAdmin::Quit},
00416 {"find", 1, 4, &XrdFrmAdmin::Find},
00417 {"help", 1, 4, &XrdFrmAdmin::Help},
00418 {"makelf", 6, 6, &XrdFrmAdmin::MakeLF},
00419 {"pin", 3, 3, &XrdFrmAdmin::Pin},
00420 {"query", 1, 5, &XrdFrmAdmin::Query},
00421 {"quit", 4, 4, &XrdFrmAdmin::Quit},
00422 {"reloc", 5, 5, &XrdFrmAdmin::Reloc},
00423 {"rm", 2, 2, &XrdFrmAdmin::Remove}
00424 };
00425 static int CmdNum = sizeof(CmdTab)/sizeof(struct CmdInfo);
00426
00427 int i, n = strlen(Cmd);
00428
00429
00430
00431 for (i = 0; i < CmdNum; i++)
00432 if (n >= CmdTab[i].minLen && n <= CmdTab[i].maxLen
00433 && !strncmp(CmdTab[i].Name, Cmd, n)) break;
00434
00435
00436
00437 if (i >= CmdNum)
00438 {Emsg("Invalid command - ", Cmd);
00439 return 1;
00440 }
00441
00442
00443
00444 return (*this.*CmdTab[i].Method)();
00445 }
00446
00447
00448
00449
00450
00451
00452
00453
00454 void XrdFrmAdmin::ConfigProxy()
00455 {
00456 static struct {const char *qFN; int qID;} qVec[] =
00457 {{"getfQ.0", XrdFrmProxy::opGet},
00458 {"migrQ.0", XrdFrmProxy::opMig},
00459 {"pstgQ.0", XrdFrmProxy::opStg},
00460 {"putfQ.0", XrdFrmProxy::opPut},
00461 {0, 0}};
00462 struct stat Stat;
00463 char qBuff[1032], *qBase;
00464 int i, qTypes = 0;
00465
00466
00467
00468 if (frmProxy || frmProxz) return;
00469
00470
00471
00472 strcpy(qBuff, Config.QPath);
00473 qBase = XrdFrmUtils::makeQDir(qBuff, -1);
00474 strcpy(qBuff, qBase); free(qBase); qBase = qBuff+strlen(qBuff);
00475
00476
00477
00478
00479 for (i = 0; qVec[i].qFN; i++)
00480 {strcpy(qBase, qVec[i].qFN);
00481 if (!stat(qBuff, &Stat)) qTypes |= qVec[i].qID;
00482 }
00483
00484
00485
00486 if (qTypes)
00487 {frmProxy = new XrdFrmProxy(Say.logger(),Config.myInst,Trace.What != 0);
00488 frmProxz = frmProxy->Init(qTypes, 0, -1, Config.QPath);
00489 } else {
00490 *qBase = 0; frmProxz = 1;
00491 Emsg("No transfer queues found in ", qBuff);
00492 }
00493 }
00494
00495
00496
00497
00498
00499 void XrdFrmAdmin::Emsg(const char *tx1, const char *tx2, const char *tx3,
00500 const char *tx4, const char *tx5)
00501 {
00502 Say.Say("frm_admin: ", tx1, tx2, tx3, tx4, tx5);
00503 finalRC = 4;
00504 }
00505
00506 void XrdFrmAdmin::Emsg(int ec, const char *tx2, const char *tx3,
00507 const char *tx4, const char *tx5)
00508 {
00509 char buff[128];
00510
00511 if (!ec) Say.Say(tx2, tx3, tx4, tx5);
00512 else {strcpy(buff+2, strerror(ec));
00513 if (strncmp(buff+2, "Unknown", 7)) buff[2] = tolower(buff[2]);
00514 else sprintf(buff+2, "error %d", ec);
00515 buff[0] = ';'; buff[1] = ' ';
00516 Say.Say("frm_admin: Unable to ", tx2, tx3, tx4, tx5, buff);
00517 }
00518 finalRC = 4;
00519 }
00520
00521
00522
00523
00524
00525 void XrdFrmAdmin::Msg(const char *tx1, const char *tx2, const char *tx3,
00526 const char *tx4, const char *tx5)
00527 {
00528 Say.Say(tx1, tx2, tx3, tx4, tx5);
00529 }
00530
00531
00532
00533
00534
00535 int XrdFrmAdmin::Parse(const char *What, XrdOucArgs &Spec, const char **Reqs)
00536 {
00537 static const int MaxArgs = sizeof(Opt.Args)/sizeof(char *);
00538 char theOpt;
00539 int i;
00540
00541
00542
00543 memset(&Opt, 0, sizeof(Opt));
00544 Opt.Uid = static_cast<uid_t>(-1); Opt.Gid = static_cast<gid_t>(-1);
00545
00546
00547
00548 if (ArgS) Spec.Set(ArgS);
00549 else Spec.Set(ArgC, ArgV);
00550
00551
00552
00553 while((theOpt = Spec.getopt()) != -1)
00554 {switch(theOpt)
00555 {case 'e': Opt.Erase = 1; break;
00556 case 'E': Opt.Echo = 1; break;
00557 case 'f': Opt.Fix = 1; break;
00558 case 'F': Opt.Force = 1; break;
00559 case 'k': Opt.Keep = 1;
00560 if (!ParseKeep(What, Spec.argval)) return 0;
00561 break;
00562 case 'l': Opt.Local = 1; break;
00563 case 'm': Opt.MPType ='m';break;
00564 case 'o': if (!ParseOwner(What, Spec.argval)) return 0;
00565 break;
00566 case 'p': Opt.MPType ='p';break;
00567 case 'r': Opt.Recurse = 1; break;
00568 case '?': return 0;
00569 default: Emsg("Internal error mapping options!");
00570 return 0;
00571 }
00572 }
00573
00574
00575
00576 for (i = 0; i < MaxArgs && Reqs[i]; i++)
00577 if (!(Opt.Args[i] = Spec.getarg()))
00578 {Emsg(What, Reqs[i], " not specified."); return 0;}
00579
00580
00581
00582 return 1;
00583 }
00584
00585
00586
00587
00588
00589 int XrdFrmAdmin::ParseKeep(const char *What, const char *kTime)
00590 {
00591 struct tm myTM;
00592 char *eP;
00593 int theSec;
00594 long long theVal;
00595
00596
00597
00598 Opt.ktAlways = 0;
00599 Opt.KeepTime = 0;
00600 Opt.ktIdle = 0;
00601
00602
00603
00604 if (!strcmp(kTime, "forever")) {Opt.ktAlways = 1; return 1;}
00605
00606
00607
00608 if (!index(kTime, '/'))
00609 {if (*kTime == '+') {Opt.ktIdle = 1; kTime++;}
00610 if (XrdOuca2x::a2tm(Say,"keep time", kTime, &theSec)) return 0;
00611 if (Opt.ktIdle || !theSec) Opt.KeepTime = theSec;
00612 else {theVal = static_cast<long long>(theSec);
00613 theVal = XrdSysTimer::Midnight() + 86400LL + theSec;
00614 Opt.KeepTime = static_cast<time_t>(theVal);
00615 }
00616 return 1;
00617 }
00618
00619
00620
00621 eP = strptime(kTime, "%D", &myTM);
00622 if (*eP) {Emsg("Invalid ", What, "keep date - ", kTime); return 0;}
00623 Opt.KeepTime = mktime(&myTM);
00624 return 1;
00625 }
00626
00627
00628
00629
00630
00631 int XrdFrmAdmin::ParseOwner(const char *What, char *Uname)
00632 {
00633 struct group *grP;
00634 struct passwd *pwP;
00635 char *Gname = 0;
00636 int Gnum, Unum;
00637
00638
00639
00640 Opt.Uid = Config.myUid;
00641 Opt.Gid = Config.myGid;
00642
00643
00644
00645 if (*Uname == ':') {Gname = Uname+1; Uname = 0;}
00646 else if ((Gname = index(Uname, ':'))) *Gname++ = '\0';
00647 if (Gname && *Gname == '\0') Gname = 0;
00648
00649
00650
00651 if (Uname)
00652 {if (*Uname >= 0 && *Uname <= 9)
00653 {if (XrdOuca2x::a2i(Say,"uid",Uname, &Unum)) return 0;
00654 Opt.Uid = Unum;
00655 }
00656 else {if (!(pwP = getpwnam(Uname)))
00657 {Emsg("Invalid user name - ", Uname); return 0;}
00658 Opt.Uid = pwP->pw_uid; Opt.Gid = pwP->pw_gid;
00659 }
00660 }
00661
00662
00663
00664 if (Gname)
00665 {if (*Gname >= 0 && *Gname <= 9)
00666 {if (XrdOuca2x::a2i(Say, "gid", Gname, &Gnum)) return 0;
00667 Opt.Gid = Gnum;
00668 }
00669 else {if (!(grP = getgrnam(Gname)))
00670 {Emsg("Invalid group name - ", Gname); return 0;}
00671 Opt.Gid = grP->gr_gid;
00672 }
00673 }
00674
00675
00676
00677 return 1;
00678 }
00679
00680
00681
00682
00683
00684 XrdOucTList *XrdFrmAdmin::ParseSpace(char *Space, char **Path)
00685 {
00686 XrdOucTList *pP;
00687
00688
00689
00690 if ((*Path = index(Space, ':'))) {**Path = '\0'; (*Path)++;}
00691
00692
00693
00694 if (!(pP = Config.Space(Space, *Path))) Emsg(Space, " space not found.");
00695 else if (!(pP->text))
00696 {Emsg(Space, " space does not contain ", *Path); pP = 0;}
00697 return pP;
00698 }
00699
00700
00701
00702
00703
00704 int XrdFrmAdmin::VerifyAll(char *path)
00705 {
00706 char *Slash = rindex(path, '/');
00707
00708 if (!Slash || strcmp(Slash, "/*")) return 0;
00709 *Slash = '\0';
00710 return 1;
00711 }
00712
00713
00714
00715
00716
00717 char XrdFrmAdmin::VerifyMP(const char *func, const char *path)
00718 {
00719 unsigned long long Popts = 0;
00720 const char *msg = 0;
00721 int rc;
00722
00723
00724
00725 if ((rc = Config.ossFS->StatXP(path, Popts)))
00726 {Emsg(rc, func, " ", path); return 0;}
00727
00728
00729
00730 if (Opt.MPType == 'm')
00731 {if (!(Popts & XRDEXP_MIG)) msg = " is not migratable";}
00732 else if (Opt.MPType == 'p')
00733 {if (!(Popts & XRDEXP_STAGE)) msg = " is not stageable"; }
00734 else if (Popts & XRDEXP_MIG) Opt.MPType = 'm';
00735 else if (Popts & XRDEXP_STAGE) Opt.MPType = 'p';
00736
00737 if (msg) return XrdFrmUtils::Ask('n', path, msg, "; continue?");
00738 return 'y';
00739 }