/* Prototype de ninac pour prouver son utilite */ #include #include #include #include #include #include "ninac.h" #include #include #include #include #include #include #include #include #include #include using namespace std; static FILE *fjournal = NULL; static string journal_path; void ninac_journal_setpath (const char *path) { journal_path = path == NULL ? "" : path; } void ninac_journal_print (const char *ctl, ...) { if (fjournal == NULL && journal_path.size() != 0){ fjournal = fopen (journal_path.c_str(),"a"); if (fjournal == NULL){ tlmp_error ("FATAL: Can't open journal file %s (%s). Exiting." ,journal_path.c_str(),strerror(errno)); // Si on ne peut pas ecrire dans le journal, on va perdre notre integrite, // il est donc preferable de se suicider... exit(-1) ; } } if (fjournal != NULL){ va_list list; va_start (list,ctl); vfprintf (fjournal,ctl,list); va_end (list); fflush (fjournal); } } void ninac_journal_close () { if (fjournal != NULL){ fclose (fjournal); fjournal = NULL; } } PUBLIC NINAC_STATE::NINAC_STATE(const char *_name) { name = _name; } PUBLIC NINAC_STATE::NINAC_STATE(const NINAC_STATE &t) { name = t.name; } PUBLIC const char *NINAC_STATE::getname() const { return name.c_str(); } PUBLIC NINAC_COND::NINAC_COND(const char *_name) : state(_name) { nottrue = false; } PUBLIC NINAC_COND::NINAC_COND(const char *_name, bool _nottrue) : state(_name) { nottrue = _nottrue; } PUBLIC NINAC_COND::NINAC_COND(const NINAC_COND &c) : state(c.state) { nottrue = c.nottrue; } PUBLIC const char *NINAC_COND::getname() const { return state.getname(); } PUBLIC bool NINAC_COND::negative() const { return nottrue; } PUBLIC NINAC_FCOND::NINAC_FCOND( const char *_name, bool (*_f)(const char *task, const char *target , const strings &args), const strings &_args, bool _nottrue) { name = _name; f = _f; args = _args; nottrue = _nottrue; } PUBLIC bool NINAC_FCOND::test(const char *task, const char *target) { bool ret = f(task,target,args); if (nottrue) ret = !ret; return ret; } PUBLIC const char *NINAC_FCOND::getname() const { return name.c_str(); } PUBLIC string NINAC_FCOND::getargs() const { string ret; for (unsigned i=0; i &allcond) { for (vector::iterator it = conds.begin(); it != conds.end(); it++){ allcond.insert(it->getname()); } } PUBLIC void NINAC_TASK::listfcond (set &allcond) { for (vector::iterator it = fconds.begin(); it != fconds.end(); it++){ allcond.insert("\"" + string(":") + it->getname() + "(" + it->getargs() + ")" + "\"") ; } } static void task_addstatus (string &status, const char *s) { if (status.size() > 0) status += string(", "); status += s; } /* Test une condition et ajoute dans le status si condition vrai */ static bool task_testcond ( const string &state, string &status) { bool ret = ninac_testcond(state.c_str()); if (ret){ task_addstatus (status,state.c_str()); } return ret; } static bool ninac_is_task_down(const char *task, const char *target, string &status){ /* On peut bloquer l'ensemble des operations avec plusieurs etats/condition. server_down.master server_down.target task_down.task task_down.task.target task_down.task.master */ const char *master = ninac_getval("master", target) ; string tmp1 = string("task_down.") + task + "." + target ; string tmp2 = string("server_down.") + target ; string tmp3 = string("task_down.") + task ; if (task_testcond(tmp1,status) || task_testcond(tmp2,status) || task_testcond(tmp3,status)){ return true ; } if (master != NULL){ string tmp1 = string("task_down.") + task + "." + master ; string tmp2 = string("server_down.") + master ; if (task_testcond(tmp1,status) || task_testcond(tmp2,status)){ return true ; } } return false ; } bool ninac_is_task_down(const char *task, const char *target){ string dummy ; return ninac_is_task_down(task, target, dummy) ; } PUBLIC bool NINAC_TASK::maydo() { bool ret = true; string status; { const char *master = ninac_getval ("master",target.c_str()); if (master == NULL){ status = "master not defined"; ret = false; }else{ if (ninac_is_task_down(getname(), target.c_str(), status)){ status = "### DISABLED BY OPERATOR (" + status + "=1) ###" ; // task_addstatus (status,"disabled by operator"); ret = false; } } } for (vector::iterator it = conds.begin(); it != conds.end(); it++){ bool ok = ninac_testcond (it->getname()); SSTRING tmp; { int okval = 1,notokval=0; const char *prefix = ""; if (it->negative()){ okval = 0; notokval=1; prefix = "!"; } tmp.setfromf ("%s%s=%d" ,prefix ,it->getname() ,ok ? okval : notokval); } task_addstatus (status,tmp.c_str()); if (ok){ if (it->negative()){ // Au moins une condition est fausse, la tache // est re-armee set_exec (true); ret = false; } }else if (!it->negative()){ // Au moins une condition est fausse, la tache // est re-armee set_exec (true); ret = false; } } for (vector::iterator it = fconds.begin(); it != fconds.end(); it++){ int ok = it->test(getname(),target.c_str()); SSTRING tmp; tmp.setfromf (":%s(%s)=%d",it->getname() ,it->getargs().c_str() ,ok ? 1 : 0); task_addstatus (status,tmp.c_str()); if (!ok){ ret = false; } } // Add may_exec to status { SSTRING tmp; tmp.setfromf ("may_exec=%d", (may_exec() ? 1 : 0)) ; task_addstatus (status,tmp.c_str()); } string tmp = string ("ninac.status.") + getname(); ninac_setvar (tmp.c_str(),gettarget(),status.c_str()); return ret; } static string ninacdir,taskdir,outputdir,logfile,unixpath; /* Enregistre le repertoire ou sont les commandes ninac et le repertoire ou sont les commandes de taches */ void ninac_setcmddirs( const char *cmddir, const char *tdir, const char *odir, const char *lfile, const char *sockpath) { ninacdir = cmddir; taskdir = tdir; outputdir = odir; logfile = lfile; unixpath = sockpath; } PUBLIC void NINAC_TASK::runexecs(bool pre_exec) { for (vector::iterator it = fexecs.begin(); it != fexecs.end(); it++){ it->run (getname(),gettarget(),pre_exec); } } PUBLIC bool NINAC_TASK::operator < (const NINAC_TASK &t) const { bool ret = true; if (priority == t.priority){ // We sort here so the output is easier to read int cmp = strcmp(getname(),t.getname()); if (cmp == 0){ if (strcmp(gettarget(),t.gettarget())>=0){ ret = false; } }else if (cmp > 0){ ret = false; } }else if (priority > t.priority){ ret = false; } return ret; } PUBLIC void NINAC_TASK::exec() { NINAC_TASK::exec(false) ; } PUBLIC void NINAC_TASK::exec(bool force) { if (!may_exec()) return; ninac_debug ("Execute %s(%s) %s\n",getname(),target.c_str(),getcommand()); const char *master = ninac_getval ("master",target.c_str()); if (master == NULL){ tlmp_error ("Tache %s, target %s, master non defini" ,getname(),gettarget()); }else{ signal (SIGCHLD,SIG_IGN); pid_t pid = fork(); if (pid == 0){ for (int i=3; i<250; i++) close (i); const char *script = ninac_getval("script",getname()) ; SSTRING cmd("/usr/lib/ninac/ninacexec") ; const char *args[]={ cmd.c_str(), "--ninacdir", ninacdir.c_str(), "--taskdir", taskdir.c_str(), "--outputdir", outputdir.c_str(), "--logfile", logfile.c_str(), "--unixpath", unixpath.c_str(), "--master", master, "--target", target.c_str(), "--task", getname(), "--script", (script != NULL ? script : getname()), NULL, NULL }; if (force){ int nb = sizeof(args) / sizeof(char *) ; args[nb - 2] = "-f" ; } execv (cmd.c_str(),(char**)args); tlmp_error ("Can't exec %s (%s)",cmd.c_str(),strerror(errno)); _exit (-1); }else if (pid == (pid_t)-1){ tlmp_error ("Can't fork (%s)",strerror(errno)); }else{ set_exec (false); set_running (true); string tmp = string ("ninac.nb_tasks.") + getname(); int nb = ninac_getvalnum(tmp.c_str(),master,0); ninac_setvarnum (tmp.c_str(),master,nb+1); tmp = string ("ninac.nb_tasks"); nb = ninac_getvalnum(tmp.c_str(),master,0); ninac_setvarnum (tmp.c_str(),master,nb+1); // Rouler les post en dernier runexecs (true); } } } typedef map NINAC_VARS; static NINAC_VARS vars; void ninac_setcond (const char *cond) { ninac_setvarnum ("state",cond,1); } void ninac_setcondt (const char *cond, time_t t) { ninac_setvarnumt ("state",cond,1,t); } void ninac_unsetcond (const char *cond) { ninac_unsetvar ("state",cond); } bool ninac_testcond (const char *cond) { return ninac_getvalnum ("state",cond,0) != 0; } time_t ninac_getcondmtime(const char *cond) { return ninac_getvmtime ("state", cond); } /* Presente les conditions actives */ void ninac_dumpcond(SSTREAM &ss) { for (map::iterator it=vars.begin(); it != vars.end(); it++){ const char *n = it->first.c_str(); if (strncmp(n,"state.",6)==0){ ss.printf ("%s %lu\n",n+6,it->second.getmtime()); } } } static vector tasks; int ninac_setpriority (const char *name, const char *target, int priority) { int ret = -1; for (vector::iterator it = tasks.begin(); it != tasks.end() ; it++){ if (strcmp(it->getname(),name)==0 && strcmp(it->gettarget(),target)==0){ it->set_priority (priority); sort(tasks.begin(),tasks.end()); ret = 0; break; } } return ret; } static void tasks_printset (SSTREAM &ss, set &s, const char *del) { for (set::iterator it=s.begin(); it !=s.end(); it++){ ss.printf ("%s%s", it->c_str(), del); } } /* Presente toutes les conditions attendus */ void ninac_listcond(SSTREAM &ss) { set allcond; for (vector::iterator it=tasks.begin(); it != tasks.end(); it++){ it->listcond (allcond); // Genere la liste de pseudo condition permettant d'arreter // selectivement les operations const char *name = it->getname(); const char *target = it->gettarget(); const char *master = ninac_getval("master",target); if (master == NULL) master = "(NULL)"; string tmp = string("server_down.") + master; allcond.insert (tmp); tmp = string("server_down.") + target; allcond.insert (tmp); tmp = string("task_down.") + name; allcond.insert (tmp); string tmp2 = tmp + '.' + master; allcond.insert (tmp2); tmp2 = tmp + '.' + target; allcond.insert (tmp2); } tasks_printset (ss,allcond,"\n"); } void ninac_addtask (const NINAC_TASKBASE &b, const char *target) { tasks.push_back (NINAC_TASK(b,target)); sort(tasks.begin(),tasks.end()); } void ninac_solver() { #if 0 printf ("Conditions: "); for (set::iterator it=c_true.begin(); it != c_true.end(); it++){ printf ("[%s] ",it->c_str()); } printf ("\n"); #endif for (vector::iterator it=tasks.begin(); it != tasks.end(); it++){ debug_printf (debug_ninac,"Solver: task %s(%s) -> maydo: %d, may_exec: %d\n",it->getname(),it->gettarget(),it->maydo(),it->may_exec()); if (it->maydo()) it->exec(); } } void ninac_listtask(SSTREAM &ss) { for (vector::iterator it=tasks.begin(); it != tasks.end(); it++){ ss.printf ("%s %s ",it->getname(),it->gettarget()); set allcond; it->listcond (allcond); tasks_printset (ss,allcond," "); set allfcond; it->listfcond (allfcond); tasks_printset (ss,allfcond," "); ss.printf ("\n"); } } void ninac_listtarget(SSTREAM &ss) { set alltarget; for (vector::iterator it=tasks.begin(); it != tasks.end(); it++){ alltarget.insert(it->gettarget()); } tasks_printset (ss,alltarget,"\n"); } void ninac_runpost(const char *task, const char *target, SSTREAM &ss) { bool found = false; for (vector::reverse_iterator it=tasks.rbegin(); it != tasks.rend(); it++){ if (strcmp(it->getname(),task)==0 && strcmp(it->gettarget(),target)==0){ found = true; it->runexecs (false); } } if (!found){ ss.printf ("Task %s.%s not found\n",task,target); } } PUBLIC NINAC_VAL::NINAC_VAL(const char *_s) { s = _s; date = time(NULL); } PUBLIC NINAC_VAL::NINAC_VAL() { date = time(NULL); } PUBLIC NINAC_VAL::NINAC_VAL(const NINAC_VAL &v) { s = v.s; date = v.date; } PUBLIC NINAC_VAL::NINAC_VAL(const char *_s, time_t _date) { s = _s; date = _date; } PUBLIC const char *NINAC_VAL::c_str() const { return s.c_str(); } PUBLIC time_t NINAC_VAL::getmtime() const { return date; } PUBLIC NINAC_VAR::NINAC_VAR( const std::string &_name, const NINAC_VAL &_val) : NINAC_VAL(_val) { name = _name; } /* Change la variable seulement si la valeur differe. Ceci est une optimisation parce certaines variables de status sont constamment reprogramme. Cela evite de remplir le transaction log. De plus, cela permet de savoir depuis combien de temps un statut n'a pas change. */ void ninac_setvar (const char *var, const char *val) { NINAC_VAL vval(val); NINAC_VAL &cur = vars[var]; if (strcmp(cur.c_str(),val)!=0){ cur = vval; if (strncmp(var,"ninac.status",12)!=0){ ninac_journal_print ("setvar %s \"%s\" %lu\n",var,val,vval.getmtime()); } } } /* setvar avec enregistrement du timestamp */ void ninac_setvart (const char *var, const char *val, time_t t) { NINAC_VAL vval(val,t); NINAC_VAL &cur = vars[var]; if (strcmp(cur.c_str(),val)!=0 || cur.getmtime() != t){ cur = vval; if (strncmp(var,"ninac.status",12)!=0){ ninac_journal_print ("setvar %s \"%s\" %lu\n",var,val,t); } } } void ninac_setvart (const char *var, const char *subvar, const char *val, time_t t) { string tmp = string(var) + '.' + subvar; ninac_setvart (tmp.c_str(),val,t); } void ninac_unsetvar (const char *var) { if (vars.count(var)>0){ vars.erase (var); ninac_journal_print ("unsetvar %s\n",var); } } void ninac_setvarnum (const char *var, int val) { char valstr[20]; snprintf (valstr,sizeof(valstr)-1,"%d",val); ninac_setvar (var,valstr); } void ninac_setvarnumt (const char *var, int val, time_t t) { char valstr[20]; snprintf (valstr,sizeof(valstr)-1,"%d",val); ninac_setvart (var,valstr,t); } void ninac_setvar (const char *var, const char *subvar, const char *val) { string tmp = string(var) + '.' + subvar; ninac_setvar (tmp.c_str(),val); } void ninac_unsetvar (const char *var, const char *subvar) { string tmp = string(var) + '.' + subvar; ninac_unsetvar (tmp.c_str()); } void ninac_setvarnum (const char *var, const char *subvar, int val) { string tmp = string(var) + '.' + subvar; ninac_setvarnum (tmp.c_str(),val); } void ninac_setvarnumt (const char *var, const char *subvar, int val, time_t t) { string tmp = string(var) + '.' + subvar; ninac_setvarnumt (tmp.c_str(),val,t); } const char *ninac_getval(const char *var) { const char *ret = NULL; if (vars.count(var)>0){ ret = vars[var].c_str(); } return ret; } /* Trouve une variable en supportant les regex. Retourne NULL si ne trouve pas. */ const char *ninac_getval_ex(const char *var) { const char *ret = NULL; regex_t reg; if (regcomp (®,var,REG_ICASE|REG_EXTENDED) != 0){ tlmp_error ("ninac_getval_ex: Invalid regex %s",var); }else{ for (NINAC_VARS::iterator it=vars.begin(); it != vars.end(); it++){ if (regexec (®,it->first.c_str(),0,NULL,0)==0){ ret = it->second.c_str(); break; } } regfree (®); } return ret; } const char *ninac_getval(const char *var, const char *subvar) { string tmp = string(var) + '.' + subvar; return ninac_getval(tmp.c_str()) ; } int ninac_getvals (const char *prefix, vector &v) { int len = strlen(prefix); int ret = 0; for (map::iterator it=vars.lower_bound(prefix); it != vars.end(); it++){ int cmp = strncmp(prefix,it->first.c_str(),len); if (cmp == 0){ v.push_back(NINAC_VAR(it->first,it->second)); ret++; }else if (cmp < 0){ // un map est trié break; } } #if 0 fprintf (stderr,"prefix %d %s\n",ret,prefix); for (unsigned i=0; i<(unsigned)ret; i++){ fprintf (stderr,"%u: %s\n",i,v[i].c_str()); } #endif return ret; } /* Obtient la date de revision d'une variables */ time_t ninac_getvmtime(const char *var) { return ninac_getvmtime(var,(time_t)0) ; } time_t ninac_getvmtime(const char *var, time_t defval) { time_t ret = defval; if (vars.count(var)>0){ ret = vars[var].getmtime(); } return ret; } time_t ninac_getvmtime(const char *var, const char *subvar) { string tmp = string(var) + '.' + subvar; return ninac_getvmtime (tmp.c_str()); } time_t ninac_getvmtime(const char *var, const char *subvar, time_t defval) { string tmp = string(var) + '.' + subvar; return ninac_getvmtime (tmp.c_str(),defval); } int ninac_getvalnum(const char *var, int defval) { int ret = defval; const char *val = ninac_getval(var); if (val != NULL) ret = atoi(val); return ret; } int ninac_getvalnum(const char *var, const char *subvar, int defval) { string tmp = string(var) + '.' + subvar; return ninac_getvalnum(tmp.c_str(), defval) ; } void ninac_dumpvars(SSTREAM &ss) { for (map::iterator it=vars.begin(); it != vars.end(); it++){ ss.printf ("%s=\"%s\" %lu\n",it->first.c_str(),it->second.c_str() ,it->second.getmtime()); } } /* Erase all variables */ void ninac_clearvars() { vars.clear(); } /* Erase all tasks */ void ninac_cleartasks() { tasks.clear(); } /* Send a refresh to the journal */ void ninac_checkpoint() { ninac_journal_print ("clearvars\n"); for (map::iterator it=vars.begin(); it != vars.end(); it++){ if (strncmp(it->first.c_str(),"ninac.status",12)!=0){ ninac_journal_print ("setvar %s \"%s\" %lu\n",it->first.c_str(),it->second.c_str() ,it->second.getmtime()); } } } int ninac_locatetask (const char *task, const char *target) { int ret = -1; for (vector::iterator it=tasks.begin(); it != tasks.end(); it++){ if (strcmp(it->getname(),task)==0 && strcmp(it->gettarget(),target)==0){ ret = it - tasks.begin(); break; } } return ret; } static NINAC_FUNCTION *first_func; PUBLIC NINAC_FUNCTION::NINAC_FUNCTION ( const char *_name, bool (*_f)(const char *, const char *, const strings &), unsigned _nbarg) { name = _name; feval = _f; fexec = NULL; nbarg = _nbarg; ftype = NINAC_EVALFUNC; next = first_func; first_func = this; } PUBLIC NINAC_FUNCTION::NINAC_FUNCTION ( const char *_name, void (*_f)(const char *, const char *, bool pre_exec, const strings &), unsigned _nbarg) { name = _name; feval = NULL; fexec = _f; nbarg = _nbarg; ftype = NINAC_EXECFUNC; next = first_func; first_func = this; } PUBLIC NINAC_FUNCTION *NINAC_FUNCTION::getnext () const { return next; } PUBLIC const char *NINAC_FUNCTION::getname() const { return name.c_str(); } PUBLIC unsigned NINAC_FUNCTION::getnbarg() const { return nbarg; } PUBLIC NINAC_FUNC_TYPE NINAC_FUNCTION::gettype() const { return ftype; } void ninac_listfunc (SSTREAM &ss) { NINAC_FUNCTION *p = first_func; while (p != NULL){ char prefix = ':'; if (p->gettype() == NINAC_EXECFUNC) prefix = '@'; ss.printf ("%c%s\t%u\n",prefix,p->getname(),p->getnbarg()); p = p->getnext(); } } int ninac_settask ( const SSTRINGS &words, SSTRING &errors) { int ret = -1; int n = words.size(); if (n < 3){ errors.setfrom ("Requires a task name, a target and one or more state/function\n"); }else{ const char *task = words.getitem(0)->get(); const char *target = words.getitem(1)->get(); if (ninac_locatetask (task,target)!=-1){ errors.setfrom ("Task already defined\n"); }else{ NINAC_TASKBASE tmp (task); bool allok = true; for (int i=2; iget(); bool nottrue = false; if (s[0] == '!'){ nottrue = true; s++; } if (s[0] == ':' || s[0] == '@'){ bool execfunc = s[0] == '@'; s++; // Verify arguments const char *ptpar = strchr(s,'('); string fname; strings args; if (ptpar != NULL){ fname = string(s,ptpar); ptpar++; const char *ptend = strchr(ptpar,')'); if (ptend == NULL){ errors.appendf ("Missing closing parenthese: %s\n",s); }else{ string argline = string(ptpar,ptend); SSTRINGS tb; int nb = str_splitline(argline.c_str(),',',tb); for (int i=0; iget()); } } }else{ fname = s; } NINAC_FUNCTION *p = first_func; while (p != NULL){ if (strcmp(p->getname(),fname.c_str())==0){ NINAC_FUNC_TYPE ftype = p->gettype(); if (p->getnbarg() != args.size()){ errors.appendf ("Function %s requires %d arguments: %s\n" ,fname.c_str(),p->getnbarg(),s); allok = false; }else if (execfunc){ if (ftype != NINAC_EXECFUNC){ errors.appendf ("Function %s is not a pre/post-exec function\n" ,fname.c_str()); allok = false; }else{ tmp.addexec (p->getname(),p->fexec,args); } }else{ if (ftype != NINAC_EVALFUNC){ errors.appendf ("Function %s can't be used as a condition\n" ,fname.c_str()); allok = false; }else{ tmp.addcond (p->getname(),p->feval,args,nottrue); } } break; } p = p->getnext(); } if (p == NULL){ errors.appendf ("Function %s does not exist\n",fname.c_str()); allok = false; } }else{ tmp.addcond (s,nottrue); } } if (allok){ ninac_addtask (tmp,target); ret = 0; } } } return ret; } int ninac_unsettask ( const SSTRINGS &words, SSTRING &errors) { int ret = -1; if (words.size() != 2){ errors.setfrom ("Requires task name and target name\n"); }else{ const char *task = words.getitem(0)->get(); const char *target = words.getitem(1)->get(); int pos = ninac_locatetask (task,target); if (pos != -1){ tasks.erase(tasks.begin()+pos); ret = 0; }else{ errors.setfrom ("Unknown task\n"); } } return ret; } int ninac_runtask ( const SSTRINGS &words, SSTRING &errors) { int ret = -1; if (words.size() != 2){ errors.setfrom ("Requires task name and target name\n"); }else{ const char *task = words.getitem(0)->get(); const char *target = words.getitem(1)->get(); int pos = ninac_locatetask (task,target); if (pos != -1){ tasks[pos].set_exec(true) ; // make sure we can run it tasks[pos].exec(true) ; // run it with force ret = 0 ; }else{ errors.setfrom ("Unknown task\n"); } } return ret; } struct WAITVAR{ string var; time_t date; int handle; WAITVAR(const char *_var, time_t _date, int _handle){ var = _var; date = _date; handle = _handle; } }; static vector waitvars; /* Ajoute une variable a monitorer */ void ninac_addwait (int no, const char *var, time_t date) { waitvars.push_back(WAITVAR(var,date,no)); } /* Élimine tous les monitoring associés a un handle */ void ninac_delwait (int no) { for (unsigned i=0; i::iterator wit=waitvars.begin(); wit != waitvars.end(); wit++){ const char *var = wit->var.c_str(); regex_t reg; if (regcomp (®,var,REG_ICASE|REG_EXTENDED) != 0){ tlmp_error ("ninac_checkwait: Invalid regex %s",var); }else{ bool found = false; for (NINAC_VARS::iterator it=vars.begin(); it != vars.end(); it++){ if (regexec (®,it->first.c_str(),0,NULL,0)==0 && it->second.getmtime() >= wit->date){ char tmp[10000]; int n = snprintf (tmp,sizeof(tmp)-1,"%s=\"%s\" %lu\n" ,it->first.c_str(),it->second.c_str() ,it->second.getmtime()); write (wit->handle,tmp,n); found = true; } } regfree (®); if (found){ write (wit->handle,"## 0\n",5); waitvars.erase (wit); wit--; } } } }