88c9e11806876e16d11d668fdad7598990a86f97
[openafs.git] / src / xstat / xstat_cm.c
1 /*
2  * Copyright 2000, International Business Machines Corporation and others.
3  * All Rights Reserved.
4  * 
5  * This software has been released under the terms of the IBM Public
6  * License.  For details, see the LICENSE file in the top-level source
7  * directory or online at http://www.openafs.org/dl/license10.html
8  */
9
10 /*
11  * Description:
12  *      Implementation of the client side of the AFS Cache Manager
13  *      extended statistics facility.
14  *
15  *------------------------------------------------------------------------*/
16
17 #include <afsconfig.h>
18 #include <afs/param.h>
19
20
21 #include "xstat_cm.h"           /*Interface for this module */
22 #include <lwp.h>                /*Lightweight process package */
23
24 #include <afs/afsutil.h>
25 #include <string.h>
26
27 #define LWP_STACK_SIZE  (16 * 1024)
28
29 /*
30  * Exported variables.
31  */
32 int xstat_cm_numServers;        /*Num connected servers */
33 struct xstat_cm_ConnectionInfo
34  *xstat_cm_ConnInfo;            /*Ptr to connection array */
35 int numCollections;             /*Number of data collections */
36 struct xstat_cm_ProbeResults xstat_cm_Results;  /*Latest probe results */
37 char terminationEvent;          /*One-shot termination event */
38
39 afs_int32 xstat_cmData[AFSCB_MAX_XSTAT_LONGS];  /*Buffer for collected data */
40
41 /*
42  * Private globals.
43  */
44 static int xstat_cm_ProbeFreqInSecs;    /*Probe freq. in seconds */
45 static int xstat_cm_initflag = 0;       /*Was init routine called? */
46 static int xstat_cm_debug = 0;  /*Debugging output enabled? */
47 static int xstat_cm_oneShot = 0;        /*One-shot operation? */
48 static int (*xstat_cm_Handler) (void);  /*Probe handler routine */
49 static PROCESS probeLWP_ID;     /*Probe LWP process ID */
50 static int xstat_cm_numCollections;     /*Number of desired collections */
51 static afs_int32 *xstat_cm_collIDP;     /*Ptr to collection IDs desired */
52
53
54 /*------------------------------------------------------------------------
55  * [private] xstat_cm_CleanupInit
56  *
57  * Description:
58  *      Set up for recovery after an error in initialization (i.e.,
59  *      during a call to xstat_cm_Init.
60  *
61  * Arguments:
62  *      None.
63  *
64  * Returns:
65  *      0 on success,
66  *      Error value otherwise.
67  *
68  * Environment:
69  *      This routine is private to the module.
70  *
71  * Side Effects:
72  *      Zeros out basic data structures.
73  *------------------------------------------------------------------------*/
74
75 static int
76 xstat_cm_CleanupInit(void)
77 {
78     xstat_cm_ConnInfo = (struct xstat_cm_ConnectionInfo *)0;
79     xstat_cm_Results.probeNum = 0;
80     xstat_cm_Results.probeTime = 0;
81     xstat_cm_Results.connP = (struct xstat_cm_ConnectionInfo *)0;
82     xstat_cm_Results.collectionNumber = 0;
83     xstat_cm_Results.data.AFSCB_CollData_len = AFSCB_MAX_XSTAT_LONGS;
84     xstat_cm_Results.data.AFSCB_CollData_val = (afs_int32 *) xstat_cmData;
85     xstat_cm_Results.probeOK = 0;
86
87     return (0);
88 }
89
90
91 /*------------------------------------------------------------------------
92  * [exported] xstat_cm_Cleanup
93  *
94  * Description:
95  *      Clean up our memory and connection state.
96  *
97  * Arguments:
98  *      int a_releaseMem : Should we free up malloc'ed areas?
99  *
100  * Returns:
101  *      0 on total success,
102  *      -1 if the module was never initialized, or there was a problem
103  *              with the xstat_cm connection array.
104  *
105  * Environment:
106  *      xstat_cm_numServers should be properly set.  We don't do anything
107  *      unless xstat_cm_Init() has already been called.
108  *
109  * Side Effects:
110  *      Shuts down Rx connections gracefully, frees allocated space
111  *      (if so directed).
112  *------------------------------------------------------------------------*/
113
114 int
115 xstat_cm_Cleanup(int a_releaseMem)
116 {
117     static char rn[] = "xstat_cm_Cleanup";      /*Routine name */
118     int code;                   /*Return code */
119     int conn_idx;               /*Current connection index */
120     struct xstat_cm_ConnectionInfo *curr_conn;  /*Ptr to xstat_cm connection */
121
122     /*
123      * Assume the best, but check the worst.
124      */
125     if (!xstat_cm_initflag) {
126         fprintf(stderr, "[%s] Refused; module not initialized\n", rn);
127         return (-1);
128     } else
129         code = 0;
130
131     /*
132      * Take care of all Rx connections first.  Check to see that the
133      * server count is a legal value.
134      */
135     if (xstat_cm_numServers <= 0) {
136         fprintf(stderr,
137                 "[%s] Illegal number of servers (xstat_cm_numServers = %d)\n",
138                 rn, xstat_cm_numServers);
139         code = -1;
140     } else {
141         if (xstat_cm_ConnInfo != (struct xstat_cm_ConnectionInfo *)0) {
142             /*
143              * The xstat_cm connection structure array exists.  Go through
144              * it and close up any Rx connections it holds.
145              */
146             curr_conn = xstat_cm_ConnInfo;
147             for (conn_idx = 0; conn_idx < xstat_cm_numServers; conn_idx++) {
148                 if (curr_conn->rxconn != (struct rx_connection *)0) {
149                     rx_DestroyConnection(curr_conn->rxconn);
150                     curr_conn->rxconn = (struct rx_connection *)0;
151                 }
152                 curr_conn++;
153             }                   /*for each xstat_cm connection */
154         }                       /*xstat_cm connection structure exists */
155     }                           /*Legal number of servers */
156
157     /*
158      * If asked to, release the space we've allocated.
159      */
160     if (a_releaseMem) {
161         if (xstat_cm_ConnInfo != (struct xstat_cm_ConnectionInfo *)0)
162             free(xstat_cm_ConnInfo);
163     }
164
165     /*
166      * Return the news, whatever it is.
167      */
168     return (code);
169 }
170
171
172 /*------------------------------------------------------------------------
173  * [private] xstat_cm_LWP
174  *
175  * Description:
176  *      This LWP iterates over the server connections and gathers up
177  *      the desired statistics from each one on a regular basis, for
178  *      all known data collections.  The associated handler function
179  *      is called each time a new data collection is received.
180  *
181  * Arguments:
182  *      None.
183  *
184  * Returns:
185  *      Nothing.
186  *
187  * Environment:
188  *      Started by xstat_cm_Init(), uses global structures and the
189  *      global private xstat_cm_oneShot variable.
190  *
191  * Side Effects:
192  *      As advertised.
193  *------------------------------------------------------------------------*/
194 static void *
195 xstat_cm_LWP(void *unused)
196 {
197     static char rn[] = "xstat_cm_LWP";  /*Routine name */
198     register afs_int32 code;    /*Results of calls */
199     int oneShotCode;            /*Result of one-shot signal */
200     struct timeval tv;          /*Time structure */
201     int conn_idx;               /*Connection index */
202     struct xstat_cm_ConnectionInfo *curr_conn;  /*Current connection */
203     afs_int32 srvVersionNumber; /*Xstat version # */
204     afs_int32 clientVersionNumber;      /*Client xstat version */
205     afs_int32 numColls;         /*Number of collections to get */
206     afs_int32 *currCollIDP;     /*Curr collection ID desired */
207
208     /*
209      * Set up some numbers we'll need.
210      */
211     clientVersionNumber = AFSCB_XSTAT_VERSION;
212
213     while (1) {                 /*Service loop */
214         /*
215          * Iterate through the server connections, gathering data.
216          * Don't forget to bump the probe count and zero the statistics
217          * areas before calling the servers.
218          */
219         if (xstat_cm_debug)
220             printf("[%s] Waking up, getting data from %d server(s)\n", rn,
221                    xstat_cm_numServers);
222         curr_conn = xstat_cm_ConnInfo;
223         xstat_cm_Results.probeNum++;
224
225         for (conn_idx = 0; conn_idx < xstat_cm_numServers; conn_idx++) {
226             /*
227              * Grab the statistics for the current Cache Manager, if the
228              * connection is valid.
229              */
230             if (xstat_cm_debug)
231                 printf("[%s] Getting collections from Cache Manager '%s'\n",
232                        rn, curr_conn->hostName);
233             if (curr_conn->rxconn != (struct rx_connection *)0) {
234                 if (xstat_cm_debug)
235                     printf("[%s] Connection OK, calling RXAFSCB_GetXStats\n",
236                            rn);
237
238                 /*
239                  * Probe the given CM for each desired collection.
240                  */
241                 currCollIDP = xstat_cm_collIDP;
242                 for (numColls = 0; numColls < xstat_cm_numCollections;
243                      numColls++, currCollIDP++) {
244                     /*
245                      * Initialize the per-probe values.
246                      */
247                     if (xstat_cm_debug)
248                         printf("[%s] Asking for data collection %d\n", rn,
249                                *currCollIDP);
250                     xstat_cm_Results.collectionNumber = *currCollIDP;
251                     xstat_cm_Results.data.AFSCB_CollData_len =
252                         AFSCB_MAX_XSTAT_LONGS;
253                     memset(xstat_cm_Results.data.AFSCB_CollData_val, 0,
254                            AFSCB_MAX_XSTAT_LONGS * 4);
255
256                     xstat_cm_Results.connP = curr_conn;
257
258                     if (xstat_cm_debug) {
259                         printf
260                             ("%s: Calling RXAFSCB_GetXStats, conn=0x%" AFS_PTR_FMT ", clientVersionNumber=%d, collectionNumber=%d, srvVersionNumberP=0x%" AFS_PTR_FMT ", timeP=0x%" AFS_PTR_FMT ", dataP=0x%" AFS_PTR_FMT "\n",
261                              rn, curr_conn->rxconn, clientVersionNumber,
262                              *currCollIDP, &srvVersionNumber,
263                              &(xstat_cm_Results.probeTime),
264                              &(xstat_cm_Results.data));
265                         printf("%s: [bufflen=%d, buffer at 0x%" AFS_PTR_FMT "]\n", rn,
266                                xstat_cm_Results.data.AFSCB_CollData_len,
267                                xstat_cm_Results.data.AFSCB_CollData_val);
268                     }
269
270                     xstat_cm_Results.probeOK =
271                         RXAFSCB_GetXStats(curr_conn->rxconn,
272                                           clientVersionNumber, *currCollIDP,
273                                           &srvVersionNumber,
274                                           &(xstat_cm_Results.probeTime),
275                                           &(xstat_cm_Results.data));
276
277                     /*
278                      * Now that we (may) have the data for this connection,
279                      * call the associated handler function.  The handler
280                      * does not take any explicit parameters, but rather
281                      * gets to the goodies via some of the objects exported
282                      * by this module.
283                      */
284                     if (xstat_cm_debug)
285                         printf("[%s] Calling handler routine.\n", rn);
286                     code = xstat_cm_Handler();
287                     if (code)
288                         fprintf(stderr,
289                                 "[%s] Handler routine got error code %d\n",
290                                 rn, code);
291                 }               /*For each collection */
292             }
293
294             /*Valid Rx connection */
295             /*
296              * Advance the xstat_cm connection pointer.
297              */
298             curr_conn++;
299
300         }                       /*For each xstat_cm connection */
301
302         /*
303          * All (valid) connections have been probed.  Fall asleep for the
304          * prescribed number of seconds, unless we're a one-shot.  In
305          * that case, we need to signal our caller that we're done.
306          */
307         if (xstat_cm_debug)
308             printf("[%s] Polling complete for probe round %d.\n", rn,
309                    xstat_cm_Results.probeNum);
310
311         if (xstat_cm_oneShot) {
312             /*
313              * One-shot execution desired.  Signal our main procedure
314              * that we've finished our collection round.
315              */
316             if (xstat_cm_debug)
317                 printf("[%s] Signalling main process at 0x%" AFS_PTR_FMT "\n", rn,
318                        &terminationEvent);
319             oneShotCode = LWP_SignalProcess(&terminationEvent);
320             if (oneShotCode)
321                 fprintf(stderr, "[%s] Error %d from LWP_SignalProcess()", rn,
322                         oneShotCode);
323             break;              /*from the perpetual while loop */
324         } /*One-shot execution */
325         else {
326             /*
327              * Continuous execution desired.  Sleep for the required
328              * number of seconds.
329              */
330             tv.tv_sec = xstat_cm_ProbeFreqInSecs;
331             tv.tv_usec = 0;
332             if (xstat_cm_debug)
333                 printf("[%s] Falling asleep for %d seconds\n", rn,
334                        xstat_cm_ProbeFreqInSecs);
335             code = IOMGR_Select(0,      /*Num fids */
336                                 0,      /*Descs ready for reading */
337                                 0,      /*Descs ready for writing */
338                                 0,      /*Descs w/exceptional conditions */
339                                 &tv);   /*Ptr to timeout structure */
340             if (code)
341                 fprintf(stderr, "[%s] IOMGR_Select returned code %d\n", rn,
342                         code);
343         }                       /*Continuous execution */
344     }                           /*Service loop */
345     return NULL;
346 }
347
348
349 /*------------------------------------------------------------------------
350  * [exported] xstat_cm_Init
351  *
352  * Description:
353  *      Initialize the xstat_cm module: set up Rx connections to the
354  *      given set of Cache Managers, start up the probe LWP, and
355  *      associate the routine to be called when a probe completes.
356  *      Also, let it know which collections you're interested in.
357  *
358  * Arguments:
359  *      int a_numServers                  : Num. servers to connect to.
360  *      struct sockaddr_in *a_socketArray : Array of server sockets.
361  *      int a_ProbeFreqInSecs             : Probe frequency in seconds.
362  *      int (*a_ProbeHandler)()           : Ptr to probe handler fcn.
363  *      int a_flags;                      : Various flags.
364  *      int a_numCollections              : Number of collections desired.
365  *      afs_int32 *a_collIDP                      : Ptr to collection IDs.
366  *
367  * Returns:
368  *      0 on success,
369  *      -2 for (at least one) connection error,
370  *      LWP process creation code, if it failed,
371  *      -1 for other fatal errors.
372  *
373  * Environment:
374  *      *** MUST BE THE FIRST ROUTINE CALLED FROM THIS PACKAGE ***
375  *
376  * Side Effects:
377  *      Sets up just about everything.
378  *------------------------------------------------------------------------*/
379
380 int
381 xstat_cm_Init(int a_numServers, struct sockaddr_in *a_socketArray,
382               int a_ProbeFreqInSecs, int (*a_ProbeHandler) (), int a_flags,
383               int a_numCollections, afs_int32 * a_collIDP)
384 {
385
386     static char rn[] = "xstat_cm_Init"; /*Routine name */
387     register afs_int32 code;    /*Return value */
388     struct rx_securityClass *secobj;    /*Client security object */
389     int arg_errfound;           /*Argument error found? */
390     int curr_srv;               /*Current server idx */
391     struct xstat_cm_ConnectionInfo *curr_conn;  /*Ptr to current conn */
392     char *hostNameFound;        /*Ptr to returned host name */
393     int conn_err;               /*Connection error? */
394     int collIDBytes;            /*Num bytes in coll ID array */
395     char hoststr[16];
396
397     /*
398      * If we've already been called, snicker at the bozo, gently
399      * remind him of his doubtful heritage, and return success.
400      */
401     if (xstat_cm_initflag) {
402         fprintf(stderr, "[%s] Called multiple times!\n", rn);
403         return (0);
404     } else
405         xstat_cm_initflag = 1;
406
407     /*
408      * Check the parameters for bogosities.
409      */
410     arg_errfound = 0;
411     if (a_numServers <= 0) {
412         fprintf(stderr, "[%s] Illegal number of servers: %d\n", rn,
413                 a_numServers);
414         arg_errfound = 1;
415     }
416     if (a_socketArray == (struct sockaddr_in *)0) {
417         fprintf(stderr, "[%s] Null server socket array argument\n", rn);
418         arg_errfound = 1;
419     }
420     if (a_ProbeFreqInSecs <= 0) {
421         fprintf(stderr, "[%s] Illegal probe frequency: %d\n", rn,
422                 a_ProbeFreqInSecs);
423         arg_errfound = 1;
424     }
425     if (a_ProbeHandler == (int (*)())0) {
426         fprintf(stderr, "[%s] Null probe handler function argument\n", rn);
427         arg_errfound = 1;
428     }
429     if (a_numCollections <= 0) {
430         fprintf(stderr, "[%s] Illegal collection count argument: %d\n", rn,
431                 a_numServers);
432         arg_errfound = 1;
433     }
434     if (a_collIDP == NULL) {
435         fprintf(stderr, "[%s] Null collection ID array argument\n", rn);
436         arg_errfound = 1;
437     }
438     if (arg_errfound)
439         return (-1);
440
441     /*
442      * Record our passed-in info.
443      */
444     xstat_cm_debug = (a_flags & XSTAT_CM_INITFLAG_DEBUGGING);
445     xstat_cm_oneShot = (a_flags & XSTAT_CM_INITFLAG_ONE_SHOT);
446     xstat_cm_numServers = a_numServers;
447     xstat_cm_Handler = a_ProbeHandler;
448     xstat_cm_ProbeFreqInSecs = a_ProbeFreqInSecs;
449     xstat_cm_numCollections = a_numCollections;
450     collIDBytes = xstat_cm_numCollections * sizeof(afs_int32);
451     xstat_cm_collIDP = (afs_int32 *) (malloc(collIDBytes));
452     memcpy(xstat_cm_collIDP, a_collIDP, collIDBytes);
453     if (xstat_cm_debug) {
454         printf("[%s] Asking for %d collection(s): ", rn,
455                xstat_cm_numCollections);
456         for (curr_srv = 0; curr_srv < xstat_cm_numCollections; curr_srv++)
457             printf("%d ", *(xstat_cm_collIDP + curr_srv));
458         printf("\n");
459     }
460
461     /*
462      * Get ready in case we have to do a cleanup - basically, zero
463      * everything out.
464      */
465     code = xstat_cm_CleanupInit();
466     if (code)
467         return (code);
468
469     /*
470      * Allocate the necessary data structures and initialize everything
471      * else.
472      */
473     xstat_cm_ConnInfo = (struct xstat_cm_ConnectionInfo *)
474         malloc(a_numServers * sizeof(struct xstat_cm_ConnectionInfo));
475     if (xstat_cm_ConnInfo == (struct xstat_cm_ConnectionInfo *)0) {
476         fprintf(stderr,
477                 "[%s] Can't allocate %d connection info structs (%d bytes)\n",
478                 rn, a_numServers,
479                 (a_numServers * sizeof(struct xstat_cm_ConnectionInfo)));
480         return (-1);            /*No cleanup needs to be done yet */
481     }
482
483     /*
484      * Initialize the Rx subsystem, just in case nobody's done it.
485      */
486     if (xstat_cm_debug)
487         printf("[%s] Initializing Rx on port 0\n", rn);
488     code = rx_Init(htons(0));
489     if (code) {
490         fprintf(stderr, "[%s] Fatal error in rx_Init(), error=%d\n", rn,
491                 code);
492         return (-1);
493     }
494
495     if (xstat_cm_debug)
496         printf("[%s] Rx initialized on port 0\n", rn);
497
498     /*
499      * Create a null Rx client security object, to be used by the
500      * probe LWP.
501      */
502     secobj = rxnull_NewClientSecurityObject();
503     if (secobj == (struct rx_securityClass *)0) {
504         fprintf(stderr,
505                 "[%s] Can't create probe LWP client security object.\n", rn);
506         xstat_cm_Cleanup(1);    /*Delete already-malloc'ed areas */
507         return (-1);
508     }
509     if (xstat_cm_debug)
510         printf("[%s] Probe LWP client security object created\n", rn);
511
512     curr_conn = xstat_cm_ConnInfo;
513     conn_err = 0;
514     for (curr_srv = 0; curr_srv < a_numServers; curr_srv++) {
515         /*
516          * Copy in the socket info for the current server, resolve its
517          * printable name if possible.
518          */
519         if (xstat_cm_debug) {
520             printf("[%s] Copying in the following socket info:\n", rn);
521             printf("[%s] IP addr 0s, port %d\n",
522                    afs_inet_ntoa_r((a_socketArray + curr_srv)->sin_addr.s_addr,hoststr),
523                    ntohs((a_socketArray + curr_srv)->sin_port));
524         }
525         memcpy(&(curr_conn->skt), a_socketArray + curr_srv,
526                sizeof(struct sockaddr_in));
527
528         hostNameFound =
529             hostutil_GetNameByINet(curr_conn->skt.sin_addr.s_addr);
530         if (hostNameFound == NULL) {
531             fprintf(stderr,
532                     "[%s] Can't map Internet address %s to a string name\n",
533                     rn, afs_inet_ntoa_r(curr_conn->skt.sin_addr.s_addr,hoststr));
534             curr_conn->hostName[0] = '\0';
535         } else {
536             strcpy(curr_conn->hostName, hostNameFound);
537             if (xstat_cm_debug)
538                 printf("[%s] Host name for server index %d is %s\n", rn,
539                        curr_srv, curr_conn->hostName);
540         }
541
542         /*
543          * Make an Rx connection to the current server.
544          */
545         if (xstat_cm_debug)
546             printf
547                 ("[%s] Connecting to srv idx %d, IP addr %s, port %d, service 1\n",
548                  rn, curr_srv, afs_inet_ntoa_r(curr_conn->skt.sin_addr.s_addr,hoststr),
549                  ntohs(curr_conn->skt.sin_port));
550         curr_conn->rxconn = rx_NewConnection(curr_conn->skt.sin_addr.s_addr,    /*Server addr */
551                                              curr_conn->skt.sin_port,   /*Server port */
552                                              1, /*AFS service # */
553                                              secobj,    /*Security obj */
554                                              0);        /*# of above */
555         if (curr_conn->rxconn == (struct rx_connection *)0) {
556             fprintf(stderr,
557                     "[%s] Can't create Rx connection to server '%s' (%s)\n",
558                     rn, curr_conn->hostName, afs_inet_ntoa_r(curr_conn->skt.sin_addr.s_addr,hoststr));
559             conn_err = 1;
560         }
561         if (xstat_cm_debug)
562             printf("[%s] New connection at 0x%" AFS_PTR_FMT "\n", rn, curr_conn->rxconn);
563
564         /*
565          * Bump the current xstat_cm connection to set up.
566          */
567         curr_conn++;
568
569     }                           /*for curr_srv */
570
571     /*
572      * Start up the probe LWP.
573      */
574     if (xstat_cm_debug)
575         printf("[%s] Creating the probe LWP\n", rn);
576     code = LWP_CreateProcess(xstat_cm_LWP,      /*Function to start up */
577                              LWP_STACK_SIZE,    /*Stack size in bytes */
578                              1, /*Priority */
579                              (void *)0, /*Parameters */
580                              "xstat_cm Worker", /*Name to use */
581                              &probeLWP_ID);     /*Returned LWP process ID */
582     if (code) {
583         fprintf(stderr, "[%s] Can't create xstat_cm LWP!  Error is %d\n", rn,
584                 code);
585         xstat_cm_Cleanup(1);    /*Delete already-malloc'ed areas */
586         return (code);
587     }
588     if (xstat_cm_debug)
589         printf("[%s] Probe LWP process structure located at 0x%" AFS_PTR_FMT "\n", rn,
590                probeLWP_ID);
591
592     /*
593      * Return the final results.
594      */
595     if (conn_err)
596         return (-2);
597     else
598         return (0);
599 }
600
601
602 /*------------------------------------------------------------------------
603  * [exported] xstat_cm_ForceProbeNow
604  *
605  * Description:
606  *      Wake up the probe LWP, forcing it to execute a probe immediately.
607  *
608  * Arguments:
609  *      None.
610  *
611  * Returns:
612  *      0 on success,
613  *      Error value otherwise.
614  *
615  * Environment:
616  *      The module must have been initialized.
617  *
618  * Side Effects:
619  *      As advertised.
620  *------------------------------------------------------------------------*/
621
622 int
623 xstat_cm_ForceProbeNow(void)
624 {
625     static char rn[] = "xstat_cm_ForceProbeNow";        /*Routine name */
626
627     /*
628      * There isn't a prayer unless we've been initialized.
629      */
630     if (!xstat_cm_initflag) {
631         fprintf(stderr, "[%s] Must call xstat_cm_Init first!\n", rn);
632         return (-1);
633     }
634
635     /*
636      * Kick the sucker in the side.
637      */
638     IOMGR_Cancel(probeLWP_ID);
639
640     /*
641      * We did it, so report the happy news.
642      */
643     return (0);
644 }