Delay sending debug events until process initialization is complete.
[wine] / loader / dos / dosvm.c
1 /*
2  * DOS Virtual Machine
3  *
4  * Copyright 1998 Ove Kåven
5  *
6  * This code hasn't been completely cleaned up yet.
7  */
8
9 #include "config.h"
10
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <string.h>
14 #include <errno.h>
15 #include <fcntl.h>
16 #include <signal.h>
17 #include <unistd.h>
18 #include <sys/time.h>
19 #include <sys/types.h>
20 #include <sys/stat.h>
21
22 #include "wine/winbase16.h"
23 #include "wine/exception.h"
24 #include "winuser.h"
25 #include "winnt.h"
26 #include "msdos.h"
27 #include "file.h"
28 #include "miscemu.h"
29 #include "dosexe.h"
30 #include "dosmod.h"
31 #include "stackframe.h"
32 #include "debugtools.h"
33
34 DECLARE_DEBUG_CHANNEL(int)
35 DECLARE_DEBUG_CHANNEL(module)
36 DECLARE_DEBUG_CHANNEL(relay)
37
38 #ifdef MZ_SUPPORTED
39
40 #ifdef HAVE_SYS_VM86_H
41 # include <sys/vm86.h>
42 #endif
43 #ifdef HAVE_SYS_MMAN_H
44 # include <sys/mman.h>
45 #endif
46
47 #define IF_CLR(ctx) EFL_reg(ctx) &= ~VIF_MASK
48 #define IF_ENABLED(ctx) (EFL_reg(ctx) & VIF_MASK)
49 #define SET_PEND(ctx) EFL_reg(ctx) |= VIP_MASK
50 #define CLR_PEND(ctx) EFL_reg(ctx) &= ~VIP_MASK
51 #define IS_PEND(ctx) (EFL_reg(ctx) & VIP_MASK)
52
53 #undef TRY_PICRETURN
54
55 static void do_exception( int signal, CONTEXT86 *context )
56 {
57     EXCEPTION_RECORD rec;
58     if ((signal == SIGTRAP) || (signal == SIGHUP))
59     {
60         rec.ExceptionCode  = EXCEPTION_BREAKPOINT;
61         rec.ExceptionFlags = EXCEPTION_CONTINUABLE;
62     }
63     else
64     {
65         rec.ExceptionCode  = EXCEPTION_ILLEGAL_INSTRUCTION;  /* generic error */
66         rec.ExceptionFlags = EH_NONCONTINUABLE;
67     }
68     rec.ExceptionRecord  = NULL;
69     rec.ExceptionAddress = (LPVOID)EIP_reg(context);
70     rec.NumberParameters = 0;
71     EXC_RtlRaiseException( &rec, context );
72 }
73
74 static void DOSVM_Dump( LPDOSTASK lpDosTask, int fn, int sig,
75                         struct vm86plus_struct*VM86 )
76 {
77  unsigned iofs;
78  BYTE*inst;
79  int x;
80
81  switch (VM86_TYPE(fn)) {
82   case VM86_SIGNAL:
83    printf("Trapped signal %d\n",sig); break;
84   case VM86_UNKNOWN:
85    printf("Trapped unhandled GPF\n"); break;
86   case VM86_INTx:
87    printf("Trapped INT %02x\n",VM86_ARG(fn)); break;
88   case VM86_STI:
89    printf("Trapped STI\n"); break;
90   case VM86_PICRETURN:
91    printf("Trapped due to pending PIC request\n"); break;
92   case VM86_TRAP:
93    printf("Trapped debug request\n"); break;
94   default:
95    printf("Trapped unknown VM86 type %d arg %d\n",VM86_TYPE(fn),VM86_ARG(fn)); break;
96  }
97 #define REGS VM86->regs
98  fprintf(stderr,"AX=%04lX CX=%04lX DX=%04lX BX=%04lX\n",REGS.eax,REGS.ecx,REGS.edx,REGS.ebx);
99  fprintf(stderr,"SI=%04lX DI=%04lX SP=%04lX BP=%04lX\n",REGS.esi,REGS.edi,REGS.esp,REGS.ebp);
100  fprintf(stderr,"CS=%04X DS=%04X ES=%04X SS=%04X\n",REGS.cs,REGS.ds,REGS.es,REGS.ss);
101  fprintf(stderr,"IP=%04lX EFLAGS=%08lX\n",REGS.eip,REGS.eflags);
102
103  iofs=((DWORD)REGS.cs<<4)+REGS.eip;
104 #undef REGS
105  inst=(BYTE*)lpDosTask->img+iofs;
106  printf("Opcodes:");
107  for (x=0; x<8; x++) printf(" %02x",inst[x]);
108  printf("\n");
109 }
110
111 static int DOSVM_Int( int vect, CONTEXT86 *context, LPDOSTASK lpDosTask )
112 {
113  extern UINT16 DPMI_wrap_seg;
114
115  if (vect==0x31) {
116   if (CS_reg(context)==DPMI_wrap_seg) {
117    /* exit from real-mode wrapper */
118    return -1;
119   }
120   /* we could probably move some other dodgy stuff here too from dpmi.c */
121  }
122  INT_RealModeInterrupt(vect,context);
123  return 0;
124 }
125
126 static void DOSVM_SimulateInt( int vect, CONTEXT86 *context, LPDOSTASK lpDosTask )
127 {
128   FARPROC16 handler=INT_GetRMHandler(vect);
129
130   if (SELECTOROF(handler)==0xf000) {
131     /* if internal interrupt, call it directly */
132     INT_RealModeInterrupt(vect,context);
133   } else {
134     WORD*stack=(WORD*)(V86BASE(context)+(((DWORD)SS_reg(context))<<4)+LOWORD(ESP_reg(context)));
135     WORD flag=LOWORD(EFL_reg(context));
136
137     if (IF_ENABLED(context)) flag|=IF_MASK;
138     else flag&=~IF_MASK;
139
140     *(--stack)=flag;
141     *(--stack)=CS_reg(context);
142     *(--stack)=LOWORD(EIP_reg(context));
143     ESP_reg(context)-=6;
144     CS_reg(context)=SELECTOROF(handler);
145     EIP_reg(context)=OFFSETOF(handler);
146     IF_CLR(context);
147   }
148 }
149
150 #define SHOULD_PEND(x) \
151   (x && ((!lpDosTask->current) || (x->priority < lpDosTask->current->priority)))
152
153 static void DOSVM_SendQueuedEvent(CONTEXT86 *context, LPDOSTASK lpDosTask)
154 {
155   LPDOSEVENT event = lpDosTask->pending;
156
157   if (SHOULD_PEND(event)) {
158     /* remove from "pending" list */
159     lpDosTask->pending = event->next;
160     /* process event */
161     if (event->irq>=0) {
162       /* it's an IRQ, move it to "current" list */
163       event->next = lpDosTask->current;
164       lpDosTask->current = event;
165       TRACE_(int)("dispatching IRQ %d\n",event->irq);
166       /* note that if DOSVM_SimulateInt calls an internal interrupt directly,
167        * lpDosTask->current might be cleared (and event freed) in this very call! */
168       DOSVM_SimulateInt((event->irq<8)?(event->irq+8):(event->irq-8+0x70),context,lpDosTask);
169     } else {
170       /* callback event */
171       TRACE_(int)("dispatching callback event\n");
172       (*event->relay)(lpDosTask,context,event->data);
173       free(event);
174     }
175   }
176   if (!SHOULD_PEND(lpDosTask->pending)) {
177     TRACE_(int)("clearing Pending flag\n");
178     CLR_PEND(context);
179   }
180 }
181
182 static void DOSVM_SendQueuedEvents(CONTEXT86 *context, LPDOSTASK lpDosTask)
183 {
184   /* we will send all queued events as long as interrupts are enabled,
185    * but IRQ events will disable interrupts again */
186   while (IS_PEND(context) && IF_ENABLED(context))
187     DOSVM_SendQueuedEvent(context,lpDosTask);
188 }
189
190 void DOSVM_QueueEvent( int irq, int priority, void (*relay)(LPDOSTASK,CONTEXT86*,void*), void *data)
191 {
192   LPDOSTASK lpDosTask = MZ_Current();
193   LPDOSEVENT event, cur, prev;
194
195   if (lpDosTask) {
196     event = malloc(sizeof(DOSEVENT));
197     if (!event) {
198       ERR_(int)("out of memory allocating event entry\n");
199       return;
200     }
201     event->irq = irq; event->priority = priority;
202     event->relay = relay; event->data = data;
203
204     /* insert event into linked list, in order *after*
205      * all earlier events of higher or equal priority */
206     cur = lpDosTask->pending; prev = NULL;
207     while (cur && cur->priority<=priority) {
208       prev = cur;
209       cur = cur->next;
210     }
211     event->next = cur;
212     if (prev) prev->next = event;
213     else lpDosTask->pending = event;
214     
215     /* get dosmod's attention to the new event, except for irq==0 where we already have it */
216     if (irq && !lpDosTask->sig_sent) {
217       TRACE_(int)("new event queued, signalling dosmod\n");
218       kill(lpDosTask->task,SIGUSR2);
219       lpDosTask->sig_sent++;
220     } else {
221       TRACE_(int)("new event queued\n");
222     }
223   }
224 }
225
226 #define CV CP(eax,EAX); CP(ecx,ECX); CP(edx,EDX); CP(ebx,EBX); \
227            CP(esi,ESI); CP(edi,EDI); CP(esp,ESP); CP(ebp,EBP); \
228            CP(cs,CS); CP(ds,DS); CP(es,ES); \
229            CP(ss,SS); CP(fs,FS); CP(gs,GS); \
230            CP(eip,EIP); CP(eflags,EFL)
231
232 static int DOSVM_Process( LPDOSTASK lpDosTask, int fn, int sig,
233                           struct vm86plus_struct*VM86 )
234 {
235  CONTEXT86 context;
236  int ret=0;
237
238 #define CP(x,y) y##_reg(&context) = VM86->regs.x
239   CV;
240 #undef CP
241  if (VM86_TYPE(fn)==VM86_UNKNOWN) {
242   ret=INSTR_EmulateInstruction(&context);
243 #define CP(x,y) VM86->regs.x = y##_reg(&context)
244   CV;
245 #undef CP
246   if (ret) return 0;
247   ret=0;
248  }
249  (void*)V86BASE(&context)=lpDosTask->img;
250 #ifdef TRY_PICRETURN
251  if (VM86->vm86plus.force_return_for_pic) {
252    SET_PEND(&context);
253  }
254 #else
255  /* linux doesn't preserve pending flag on return */
256  if (SHOULD_PEND(lpDosTask->pending)) {
257    SET_PEND(&context);
258  }
259 #endif
260
261  switch (VM86_TYPE(fn)) {
262   case VM86_SIGNAL:
263    TRACE_(int)("DOS module caught signal %d\n",sig);
264    if ((sig==SIGALRM) || (sig==SIGUSR2)) {
265      if (sig==SIGALRM) {
266        DOSVM_QueueEvent(0,DOS_PRIORITY_REALTIME,NULL,NULL);
267      }
268      if (lpDosTask->pending) {
269        TRACE_(int)("setting Pending flag, interrupts are currently %s\n",
270                  IF_ENABLED(&context) ? "enabled" : "disabled");
271        SET_PEND(&context);
272        DOSVM_SendQueuedEvents(&context,lpDosTask);
273      } else {
274        TRACE_(int)("no events are pending, clearing Pending flag\n");
275        CLR_PEND(&context);
276      }
277      if (sig==SIGUSR2) lpDosTask->sig_sent--;
278    }
279    else if ((sig==SIGHUP) || (sig==SIGILL) || (sig==SIGSEGV)) {
280        do_exception( sig, &context );
281    } else {
282     DOSVM_Dump(lpDosTask,fn,sig,VM86);
283     ret=-1;
284    }
285    break;
286   case VM86_UNKNOWN: /* unhandled GPF */
287    DOSVM_Dump(lpDosTask,fn,sig,VM86);
288    do_exception( SIGSEGV, &context );
289    break;
290   case VM86_INTx:
291    if (TRACE_ON(relay))
292     DPRINTF("Call DOS int 0x%02x (EAX=%08lx) ret=%04lx:%04lx\n",VM86_ARG(fn),context.Eax,context.SegCs,context.Eip);
293    ret=DOSVM_Int(VM86_ARG(fn),&context,lpDosTask);
294    if (TRACE_ON(relay))
295     DPRINTF("Ret  DOS int 0x%02x (EAX=%08lx) ret=%04lx:%04lx\n",VM86_ARG(fn),context.Eax,context.SegCs,context.Eip);
296    break;
297   case VM86_STI:
298   case VM86_PICRETURN:
299     TRACE_(int)("DOS task enabled interrupts with events pending, sending events\n");
300     DOSVM_SendQueuedEvents(&context,lpDosTask);
301     break;
302   case VM86_TRAP:
303    do_exception( SIGTRAP, &context );
304    break;
305   default:
306    DOSVM_Dump(lpDosTask,fn,sig,VM86);
307    ret=-1;
308  }
309
310 #define CP(x,y) VM86->regs.x = y##_reg(&context)
311  CV;
312 #undef CP
313 #ifdef TRY_PICRETURN
314  VM86->vm86plus.force_return_for_pic = IS_PEND(&context) ? 1 : 0;
315  CLR_PEND(&context);
316 #endif
317  return ret;
318 }
319
320 void DOSVM_ProcessMessage(LPDOSTASK lpDosTask,MSG *msg)
321 {
322   BYTE scan = 0;
323
324   fprintf(stderr,"got message %04x, wparam=%08x, lparam=%08lx\n",msg->message,msg->wParam,msg->lParam);
325   if ((msg->message>=WM_MOUSEFIRST)&&
326       (msg->message<=WM_MOUSELAST)) {
327     INT_Int33Message(msg->message,msg->wParam,msg->lParam);
328   } else {
329     switch (msg->message) {
330     case WM_KEYUP:
331       scan = 0x80;
332     case WM_KEYDOWN:
333       scan |= (msg->lParam >> 16) & 0x7f;
334
335       /* check whether extended bit is set,
336        * and if so, queue the extension prefix */
337       if (msg->lParam & 0x1000000) {
338         /* FIXME: some keys (function keys) have
339          * extended bit set even when they shouldn't,
340          * should check for them */
341         INT_Int09SendScan(0xE0);
342       }
343       INT_Int09SendScan(scan);
344       break;
345     }
346   }
347 }
348
349 void DOSVM_Wait( int read_pipe, HANDLE hObject )
350 {
351   LPDOSTASK lpDosTask = MZ_Current();
352   MSG msg;
353   DWORD waitret;
354   BOOL got_msg = FALSE;
355
356   do {
357     /* check for messages (waste time before the response check below) */
358     while (PeekMessageA(&msg,0,0,0,PM_REMOVE|PM_NOYIELD)) {
359       /* got a message */
360       DOSVM_ProcessMessage(lpDosTask,&msg);
361       /* we don't need a TranslateMessage here */
362       DispatchMessageA(&msg);
363       got_msg = TRUE;
364     }
365     if (read_pipe == -1) {
366       if (got_msg) break;
367     } else {
368       fd_set readfds;
369       struct timeval timeout={0,0};
370       /* quick check for response from dosmod
371        * (faster than doing the full blocking wait, if data already available) */
372       FD_ZERO(&readfds); FD_SET(read_pipe,&readfds);
373       if (select(read_pipe+1,&readfds,NULL,NULL,&timeout)>0)
374         break;
375     }
376     /* check for data from win32 console device */
377
378     /* nothing yet, block while waiting for something to do */
379     waitret=MsgWaitForMultipleObjects(1,&hObject,FALSE,INFINITE,QS_ALLINPUT);
380     if (waitret==(DWORD)-1) {
381       ERR_(module)("dosvm wait error=%ld\n",GetLastError());
382     }
383     if (read_pipe != -1) {
384       if (waitret==WAIT_OBJECT_0) break;
385     }
386   } while (TRUE);
387 }
388
389 int DOSVM_Enter( CONTEXT86 *context )
390 {
391  LPDOSTASK lpDosTask = MZ_Current();
392  struct vm86plus_struct VM86;
393  int stat,len,sig;
394
395  if (!lpDosTask) {
396   /* MZ_CreateProcess or MZ_AllocDPMITask should have been called first */
397   ERR_(module)("dosmod has not been initialized!");
398   return -1;
399  }
400
401  if (context) {
402 #define CP(x,y) VM86.regs.x = y##_reg(context)
403   CV;
404 #undef CP
405   if (VM86.regs.eflags & IF_MASK)
406     VM86.regs.eflags |= VIF_MASK;
407  } else {
408 /* initial setup */
409   /* allocate standard DOS handles */
410   FILE_InitProcessDosHandles(); 
411   /* registers */
412   memset(&VM86,0,sizeof(VM86));
413   VM86.regs.cs=lpDosTask->init_cs;
414   VM86.regs.eip=lpDosTask->init_ip;
415   VM86.regs.ss=lpDosTask->init_ss;
416   VM86.regs.esp=lpDosTask->init_sp;
417   VM86.regs.ds=lpDosTask->psp_seg;
418   VM86.regs.es=lpDosTask->psp_seg;
419   VM86.regs.eflags=VIF_MASK;
420   /* hmm, what else do we need? */
421  }
422
423  /* main exchange loop */
424  do {
425   stat = VM86_ENTER;
426   errno = 0;
427   /* transmit VM86 structure to dosmod task */
428   if (write(lpDosTask->write_pipe,&stat,sizeof(stat))!=sizeof(stat)) {
429    ERR_(module)("dosmod sync lost, errno=%d, fd=%d, pid=%d\n",errno,lpDosTask->write_pipe,getpid());
430    return -1;
431   }
432   if (write(lpDosTask->write_pipe,&VM86,sizeof(VM86))!=sizeof(VM86)) {
433    ERR_(module)("dosmod sync lost, errno=%d\n",errno);
434    return -1;
435   }
436   /* wait for response, doing other things in the meantime */
437   DOSVM_Wait(lpDosTask->read_pipe, lpDosTask->hReadPipe);
438   /* read response */
439   while (1) {
440     if ((len=read(lpDosTask->read_pipe,&stat,sizeof(stat)))==sizeof(stat)) break;
441     if (((errno==EINTR)||(errno==EAGAIN))&&(len<=0)) {
442      WARN_(module)("rereading dosmod return code due to errno=%d, result=%d\n",errno,len);
443      continue;
444     }
445     ERR_(module)("dosmod sync lost reading return code, errno=%d, result=%d\n",errno,len);
446     return -1;
447   }
448   TRACE_(module)("dosmod return code=%d\n",stat);
449   while (1) {
450     if ((len=read(lpDosTask->read_pipe,&VM86,sizeof(VM86)))==sizeof(VM86)) break;
451     if (((errno==EINTR)||(errno==EAGAIN))&&(len<=0)) {
452      WARN_(module)("rereading dosmod VM86 structure due to errno=%d, result=%d\n",errno,len);
453      continue;
454     }
455     ERR_(module)("dosmod sync lost reading VM86 structure, errno=%d, result=%d\n",errno,len);
456     return -1;
457   }
458   if ((stat&0xff)==DOSMOD_SIGNAL) {
459     while (1) {
460       if ((len=read(lpDosTask->read_pipe,&sig,sizeof(sig)))==sizeof(sig)) break;
461       if (((errno==EINTR)||(errno==EAGAIN))&&(len<=0)) {
462         WARN_(module)("rereading dosmod signal due to errno=%d, result=%d\n",errno,len);
463         continue;
464       }
465       ERR_(module)("dosmod sync lost reading signal, errno=%d, result=%d\n",errno,len);
466       return -1;
467     } while (0);
468   } else sig=0;
469   /* got response */
470  } while (DOSVM_Process(lpDosTask,stat,sig,&VM86)>=0);
471
472  if (context) {
473 #define CP(x,y) y##_reg(context) = VM86.regs.x
474   CV;
475 #undef CP
476  }
477  return 0;
478 }
479
480 void DOSVM_PIC_ioport_out( WORD port, BYTE val)
481 {
482   LPDOSTASK lpDosTask = MZ_Current();
483   LPDOSEVENT event;
484
485   if (lpDosTask) {
486     if ((port==0x20) && (val==0x20)) {
487       if (lpDosTask->current) {
488         /* EOI (End Of Interrupt) */
489         TRACE_(int)("received EOI for current IRQ, clearing\n");
490         event = lpDosTask->current;
491         lpDosTask->current = event->next;
492         if (event->relay)
493         (*event->relay)(lpDosTask,NULL,event->data);
494         free(event);
495
496         if (lpDosTask->pending &&
497             !lpDosTask->sig_sent) {
498           /* another event is pending, which we should probably
499            * be able to process now, so tell dosmod about it */
500           TRACE_(int)("another event pending, signalling dosmod\n");
501           kill(lpDosTask->task,SIGUSR2);
502           lpDosTask->sig_sent++;
503         }
504       } else {
505         WARN_(int)("EOI without active IRQ\n");
506       }
507     } else {
508       FIXME_(int)("unrecognized PIC command %02x\n",val);
509     }
510   }
511 }
512
513 void DOSVM_SetTimer( unsigned ticks )
514 {
515   LPDOSTASK lpDosTask = MZ_Current();
516   int stat=DOSMOD_SET_TIMER;
517   struct timeval tim;
518
519   if (lpDosTask) {
520     /* the PC clocks ticks at 1193180 Hz */
521     tim.tv_sec=0;
522     tim.tv_usec=((unsigned long long)ticks*1000000)/1193180;
523     /* sanity check */
524     if (!tim.tv_usec) tim.tv_usec=1;
525
526     if (write(lpDosTask->write_pipe,&stat,sizeof(stat))!=sizeof(stat)) {
527       ERR_(module)("dosmod sync lost, errno=%d\n",errno);
528       return;
529     }
530     if (write(lpDosTask->write_pipe,&tim,sizeof(tim))!=sizeof(tim)) {
531       ERR_(module)("dosmod sync lost, errno=%d\n",errno);
532       return;
533     }
534     /* there's no return */
535   }
536 }
537
538 unsigned DOSVM_GetTimer( void )
539 {
540   LPDOSTASK lpDosTask = MZ_Current();
541   int stat=DOSMOD_GET_TIMER;
542   struct timeval tim;
543
544   if (lpDosTask) {
545     if (write(lpDosTask->write_pipe,&stat,sizeof(stat))!=sizeof(stat)) {
546       ERR_(module)("dosmod sync lost, errno=%d\n",errno);
547       return 0;
548     }
549     /* read response */
550     while (1) {
551       if (read(lpDosTask->read_pipe,&tim,sizeof(tim))==sizeof(tim)) break;
552       if ((errno==EINTR)||(errno==EAGAIN)) continue;
553       ERR_(module)("dosmod sync lost, errno=%d\n",errno);
554       return 0;
555     }
556     return ((unsigned long long)tim.tv_usec*1193180)/1000000;
557   }
558   return 0;
559 }
560
561 void DOSVM_SetSystemData( int id, void *data )
562 {
563   LPDOSTASK lpDosTask = MZ_Current();
564   DOSSYSTEM *sys, *prev;
565
566   if (lpDosTask) {
567     sys = lpDosTask->sys;
568     prev = NULL;
569     while (sys && (sys->id != id)) {
570       prev = sys;
571       sys = sys->next;
572     }
573     if (sys) {
574       free(sys->data);
575       sys->data = data;
576     } else {
577       sys = malloc(sizeof(DOSSYSTEM));
578       sys->id = id;
579       sys->data = data;
580       sys->next = NULL;
581       if (prev) prev->next = sys;
582       else lpDosTask->sys = sys;
583     }
584   } else free(data);
585 }
586
587 void* DOSVM_GetSystemData( int id )
588 {
589   LPDOSTASK lpDosTask = MZ_Current();
590   DOSSYSTEM *sys;
591
592   if (lpDosTask) {
593     sys = lpDosTask->sys;
594     while (sys && (sys->id != id))
595       sys = sys->next;
596     if (sys)
597       return sys->data;
598   }
599   return NULL;
600 }
601
602 #else /* !MZ_SUPPORTED */
603
604 int DOSVM_Enter( CONTEXT86 *context )
605 {
606  ERR_(module)("DOS realmode not supported on this architecture!\n");
607  return -1;
608 }
609
610 void DOSVM_Wait( int read_pipe, HANDLE hObject) {}
611 void DOSVM_PIC_ioport_out( WORD port, BYTE val) {}
612 void DOSVM_SetTimer( unsigned ticks ) {}
613 unsigned DOSVM_GetTimer( void ) { return 0; }
614 void DOSVM_SetSystemData( int id, void *data ) { free(data); }
615 void* DOSVM_GetSystemData( int id ) { return NULL; }
616 void DOSVM_QueueEvent( int irq, int priority, void (*relay)(LPDOSTASK,CONTEXT86*,void*), void *data) {}
617
618 #endif