1 /*-
2 * Coda: an Experimental Distributed File System
3 * Release 3.1
4 *
5 * Copyright (c) 1987-1998 Carnegie Mellon University
6 * All Rights Reserved
7 *
8 * Permission to use, copy, modify and distribute this software and its
9 * documentation is hereby granted, provided that both the copyright
10 * notice and this permission notice appear in all copies of the
11 * software, derivative works or modified versions, and any portions
12 * thereof, and that both notices appear in supporting documentation, and
13 * that credit is given to Carnegie Mellon University in all documents
14 * and publicity pertaining to direct or indirect use of this code or its
15 * derivatives.
16 *
17 * CODA IS AN EXPERIMENTAL SOFTWARE SYSTEM AND IS KNOWN TO HAVE BUGS,
18 * SOME OF WHICH MAY HAVE SERIOUS CONSEQUENCES. CARNEGIE MELLON ALLOWS
19 * FREE USE OF THIS SOFTWARE IN ITS "AS IS" CONDITION. CARNEGIE MELLON
20 * DISCLAIMS ANY LIABILITY OF ANY KIND FOR ANY DAMAGES WHATSOEVER
21 * RESULTING DIRECTLY OR INDIRECTLY FROM THE USE OF THIS SOFTWARE OR OF
22 * ANY DERIVATIVE WORK.
23 *
24 * Carnegie Mellon encourages users of this software to return any
25 * improvements or extensions that they make, and to grant Carnegie
26 * Mellon the rights to redistribute these changes without encumbrance.
27 *
28 * @(#) src/sys/coda/coda_psdev.c,v 1.1.1.1 1998/08/29 21:14:52 rvb Exp $
29 */
30 /*-
31 * Mach Operating System
32 * Copyright (c) 1989 Carnegie-Mellon University
33 * All rights reserved. The CMU software License Agreement specifies
34 * the terms and conditions for use and redistribution.
35 */
36
37 /*
38 * This code was written for the Coda filesystem at Carnegie Mellon
39 * University. Contributers include David Steere, James Kistler, and
40 * M. Satyanarayanan. */
41
42 /*
43 * These routines define the psuedo device for communication between
44 * Coda's Venus and Minicache in Mach 2.6. They used to be in cfs_subr.c,
45 * but I moved them to make it easier to port the Minicache without
46 * porting coda. -- DCS 10/12/94
47 */
48
49 /* These routines are the device entry points for Venus. */
50
51 #include <sys/cdefs.h>
52 __FBSDID("$FreeBSD$");
53
54 #include <sys/param.h>
55 #include <sys/systm.h>
56 #include <sys/ioccom.h>
57 #include <sys/kernel.h>
58 #include <sys/lock.h>
59 #include <sys/malloc.h>
60 #include <sys/file.h> /* must come after sys/malloc.h */
61 #include <sys/mount.h>
62 #include <sys/mutex.h>
63 #include <sys/poll.h>
64 #include <sys/proc.h>
65 #include <sys/filedesc.h>
66
67 #include <fs/coda/coda.h>
68 #include <fs/coda/cnode.h>
69 #include <fs/coda/coda_namecache.h>
70 #include <fs/coda/coda_io.h>
71 #include <fs/coda/coda_psdev.h>
72
73 #define CTL_C
74
75 #ifdef CTL_C
76 #include <sys/signalvar.h>
77 #endif
78
79 int coda_psdev_print_entry = 0;
80 static
81 int outstanding_upcalls = 0;
82 int coda_call_sleep = PZERO - 1;
83 #ifdef CTL_C
84 int coda_pcatch = PCATCH;
85 #else
86 #endif
87
88 #define ENTRY if(coda_psdev_print_entry) myprintf(("Entered %s\n",__func__))
89
90 void vcodaattach(int n);
91
92 struct vmsg {
93 struct queue vm_chain;
94 caddr_t vm_data;
95 u_short vm_flags;
96 u_short vm_inSize; /* Size is at most 5000 bytes */
97 u_short vm_outSize;
98 u_short vm_opcode; /* copied from data to save ptr lookup */
99 int vm_unique;
100 caddr_t vm_sleep; /* Not used by Mach. */
101 };
102
103 #define VM_READ 1
104 #define VM_WRITE 2
105 #define VM_INTR 4
106
107 /* vcodaattach: do nothing */
108 void
109 vcodaattach(n)
110 int n;
111 {
112 }
113
114 int
115 vc_nb_open(dev, flag, mode, td)
116 struct cdev *dev;
117 int flag;
118 int mode;
119 struct thread *td; /* NetBSD only */
120 {
121 struct vcomm *vcp;
122 struct coda_mntinfo *mnt;
123
124 ENTRY;
125
126 if (!coda_nc_initialized)
127 coda_nc_init();
128
129 mnt = dev2coda_mntinfo(dev);
130 KASSERT(mnt, ("Coda: tried to open uninitialized cfs device"));
131
132 vcp = &mnt->mi_vcomm;
133 if (VC_OPEN(vcp))
134 return(EBUSY);
135
136 bzero(&(vcp->vc_selproc), sizeof (struct selinfo));
137 INIT_QUEUE(vcp->vc_requests);
138 INIT_QUEUE(vcp->vc_replys);
139 MARK_VC_OPEN(vcp);
140
141 mnt->mi_vfsp = NULL;
142 mnt->mi_rootvp = NULL;
143
144 return(0);
145 }
146
147 int
148 vc_nb_close (dev, flag, mode, td)
149 struct cdev *dev;
150 int flag;
151 int mode;
152 struct thread *td;
153 {
154 register struct vcomm *vcp;
155 register struct vmsg *vmp, *nvmp = NULL;
156 struct coda_mntinfo *mi;
157 int err;
158
159 ENTRY;
160
161 mi = dev2coda_mntinfo(dev);
162 KASSERT(mi, ("Coda: closing unknown cfs device"));
163
164 vcp = &mi->mi_vcomm;
165 KASSERT(VC_OPEN(vcp), ("Coda: closing unopened cfs device"));
166
167 /* prevent future operations on this vfs from succeeding by auto-
168 * unmounting any vfs mounted via this device. This frees user or
169 * sysadm from having to remember where all mount points are located.
170 * Put this before WAKEUPs to avoid queuing new messages between
171 * the WAKEUP and the unmount (which can happen if we're unlucky)
172 */
173 if (!mi->mi_rootvp) {
174 /* just a simple open/close w no mount */
175 MARK_VC_CLOSED(vcp);
176 return 0;
177 }
178
179 /* Let unmount know this is for real */
180 VTOC(mi->mi_rootvp)->c_flags |= C_UNMOUNTING;
181 coda_unmounting(mi->mi_vfsp);
182
183 outstanding_upcalls = 0;
184 /* Wakeup clients so they can return. */
185 for (vmp = (struct vmsg *)GETNEXT(vcp->vc_requests);
186 !EOQ(vmp, vcp->vc_requests);
187 vmp = nvmp)
188 {
189 nvmp = (struct vmsg *)GETNEXT(vmp->vm_chain);
190 /* Free signal request messages and don't wakeup cause
191 no one is waiting. */
192 if (vmp->vm_opcode == CODA_SIGNAL) {
193 CODA_FREE((caddr_t)vmp->vm_data, (u_int)VC_IN_NO_DATA);
194 CODA_FREE((caddr_t)vmp, (u_int)sizeof(struct vmsg));
195 continue;
196 }
197 outstanding_upcalls++;
198 wakeup(&vmp->vm_sleep);
199 }
200
201 for (vmp = (struct vmsg *)GETNEXT(vcp->vc_replys);
202 !EOQ(vmp, vcp->vc_replys);
203 vmp = (struct vmsg *)GETNEXT(vmp->vm_chain))
204 {
205 outstanding_upcalls++;
206 wakeup(&vmp->vm_sleep);
207 }
208
209 MARK_VC_CLOSED(vcp);
210
211 if (outstanding_upcalls) {
212 #ifdef CODA_VERBOSE
213 printf("presleep: outstanding_upcalls = %d\n", outstanding_upcalls);
214 (void) tsleep(&outstanding_upcalls, coda_call_sleep, "coda_umount", 0);
215 printf("postsleep: outstanding_upcalls = %d\n", outstanding_upcalls);
216 #else
217 (void) tsleep(&outstanding_upcalls, coda_call_sleep, "coda_umount", 0);
218 #endif
219 }
220
221 err = dounmount(mi->mi_vfsp, flag, td);
222 if (err)
223 myprintf(("Error %d unmounting vfs in vcclose(%s)\n",
224 err, devtoname(dev)));
225 return 0;
226 }
227
228 int
229 vc_nb_read(dev, uiop, flag)
230 struct cdev *dev;
231 struct uio *uiop;
232 int flag;
233 {
234 register struct vcomm * vcp;
235 register struct vmsg *vmp;
236 int error = 0;
237
238 ENTRY;
239
240 vcp = &dev2coda_mntinfo(dev)->mi_vcomm;
241 /* Get message at head of request queue. */
242 if (EMPTY(vcp->vc_requests))
243 return(0); /* Nothing to read */
244
245 vmp = (struct vmsg *)GETNEXT(vcp->vc_requests);
246
247 /* Move the input args into userspace */
248 uiop->uio_rw = UIO_READ;
249 error = uiomove(vmp->vm_data, vmp->vm_inSize, uiop);
250 if (error) {
251 myprintf(("vcread: error (%d) on uiomove\n", error));
252 error = EINVAL;
253 }
254
255 #ifdef OLD_DIAGNOSTIC
256 if (vmp->vm_chain.forw == 0 || vmp->vm_chain.back == 0)
257 panic("vc_nb_read: bad chain");
258 #endif
259
260 REMQUE(vmp->vm_chain);
261
262 /* If request was a signal, free up the message and don't
263 enqueue it in the reply queue. */
264 if (vmp->vm_opcode == CODA_SIGNAL) {
265 if (codadebug)
266 myprintf(("vcread: signal msg (%d, %d)\n",
267 vmp->vm_opcode, vmp->vm_unique));
268 CODA_FREE((caddr_t)vmp->vm_data, (u_int)VC_IN_NO_DATA);
269 CODA_FREE((caddr_t)vmp, (u_int)sizeof(struct vmsg));
270 return(error);
271 }
272
273 vmp->vm_flags |= VM_READ;
274 INSQUE(vmp->vm_chain, vcp->vc_replys);
275
276 return(error);
277 }
278
279 int
280 vc_nb_write(dev, uiop, flag)
281 struct cdev *dev;
282 struct uio *uiop;
283 int flag;
284 {
285 register struct vcomm * vcp;
286 register struct vmsg *vmp;
287 struct coda_out_hdr *out;
288 u_long seq;
289 u_long opcode;
290 int buf[2];
291 int error = 0;
292
293 ENTRY;
294
295 vcp = &dev2coda_mntinfo(dev)->mi_vcomm;
296
297 /* Peek at the opcode, unique without transfering the data. */
298 uiop->uio_rw = UIO_WRITE;
299 error = uiomove((caddr_t)buf, sizeof(int) * 2, uiop);
300 if (error) {
301 myprintf(("vcwrite: error (%d) on uiomove\n", error));
302 return(EINVAL);
303 }
304
305 opcode = buf[0];
306 seq = buf[1];
307
308 if (codadebug)
309 myprintf(("vcwrite got a call for %ld.%ld\n", opcode, seq));
310
311 if (DOWNCALL(opcode)) {
312 union outputArgs pbuf;
313
314 /* get the rest of the data. */
315 uiop->uio_rw = UIO_WRITE;
316 error = uiomove((caddr_t)&pbuf.coda_purgeuser.oh.result, sizeof(pbuf) - (sizeof(int)*2), uiop);
317 if (error) {
318 myprintf(("vcwrite: error (%d) on uiomove (Op %ld seq %ld)\n",
319 error, opcode, seq));
320 return(EINVAL);
321 }
322
323 return handleDownCall(opcode, &pbuf);
324 }
325
326 /* Look for the message on the (waiting for) reply queue. */
327 for (vmp = (struct vmsg *)GETNEXT(vcp->vc_replys);
328 !EOQ(vmp, vcp->vc_replys);
329 vmp = (struct vmsg *)GETNEXT(vmp->vm_chain))
330 {
331 if (vmp->vm_unique == seq) break;
332 }
333
334 if (EOQ(vmp, vcp->vc_replys)) {
335 if (codadebug)
336 myprintf(("vcwrite: msg (%ld, %ld) not found\n", opcode, seq));
337
338 return(ESRCH);
339 }
340
341 /* Remove the message from the reply queue */
342 REMQUE(vmp->vm_chain);
343
344 /* move data into response buffer. */
345 out = (struct coda_out_hdr *)vmp->vm_data;
346 /* Don't need to copy opcode and uniquifier. */
347
348 /* get the rest of the data. */
349 if (vmp->vm_outSize < uiop->uio_resid) {
350 myprintf(("vcwrite: more data than asked for (%d < %d)\n",
351 vmp->vm_outSize, uiop->uio_resid));
352 wakeup(&vmp->vm_sleep); /* Notify caller of the error. */
353 return(EINVAL);
354 }
355
356 buf[0] = uiop->uio_resid; /* Save this value. */
357 uiop->uio_rw = UIO_WRITE;
358 error = uiomove((caddr_t) &out->result, vmp->vm_outSize - (sizeof(int) * 2), uiop);
359 if (error) {
360 myprintf(("vcwrite: error (%d) on uiomove (op %ld seq %ld)\n",
361 error, opcode, seq));
362 return(EINVAL);
363 }
364
365 /* I don't think these are used, but just in case. */
366 /* XXX - aren't these two already correct? -bnoble */
367 out->opcode = opcode;
368 out->unique = seq;
369 vmp->vm_outSize = buf[0]; /* Amount of data transferred? */
370 vmp->vm_flags |= VM_WRITE;
371
372 error = 0;
373 if (opcode == CODA_OPEN_BY_FD) {
374 struct coda_open_by_fd_out *tmp = (struct coda_open_by_fd_out *)out;
375 struct file *fp;
376 struct vnode *vp = NULL;
377
378 if (tmp->oh.result == 0) {
379 error = getvnode(uiop->uio_td->td_proc->p_fd, tmp->fd, &fp);
380 if (!error) {
381 mtx_lock(&Giant);
382 vp = fp->f_vnode;
383 VREF(vp);
384 fdrop(fp, uiop->uio_td);
385 mtx_unlock(&Giant);
386 }
387 }
388 tmp->vp = vp;
389 }
390
391 wakeup(&vmp->vm_sleep);
392
393 return(error);
394 }
395
396 int
397 vc_nb_ioctl(dev, cmd, addr, flag, td)
398 struct cdev *dev;
399 u_long cmd;
400 caddr_t addr;
401 int flag;
402 struct thread *td;
403 {
404 ENTRY;
405
406 switch(cmd) {
407 case CODARESIZE: {
408 struct coda_resize *data = (struct coda_resize *)addr;
409 return(coda_nc_resize(data->hashsize, data->heapsize, IS_DOWNCALL));
410 break;
411 }
412 case CODASTATS:
413 if (coda_nc_use) {
414 coda_nc_gather_stats();
415 return(0);
416 } else {
417 return(ENODEV);
418 }
419 break;
420 case CODAPRINT:
421 if (coda_nc_use) {
422 print_coda_nc();
423 return(0);
424 } else {
425 return(ENODEV);
426 }
427 break;
428 case CIOC_KERNEL_VERSION:
429 switch (*(u_int *)addr) {
430 case 0:
431 *(u_int *)addr = coda_kernel_version;
432 return 0;
433 break;
434 case 1:
435 case 2:
436 if (coda_kernel_version != *(u_int *)addr)
437 return ENOENT;
438 else
439 return 0;
440 default:
441 return ENOENT;
442 }
443 break;
444 default :
445 return(EINVAL);
446 break;
447 }
448 }
449
450 int
451 vc_nb_poll(dev, events, td)
452 struct cdev *dev;
453 int events;
454 struct thread *td;
455 {
456 register struct vcomm *vcp;
457 int event_msk = 0;
458
459 ENTRY;
460
461 vcp = &dev2coda_mntinfo(dev)->mi_vcomm;
462
463 event_msk = events & (POLLIN|POLLRDNORM);
464 if (!event_msk)
465 return(0);
466
467 if (!EMPTY(vcp->vc_requests))
468 return(events & (POLLIN|POLLRDNORM));
469
470 selrecord(td, &(vcp->vc_selproc));
471
472 return(0);
473 }
474
475 /*
476 * Statistics
477 */
478 struct coda_clstat coda_clstat;
479
480 /*
481 * Key question: whether to sleep interuptably or uninteruptably when
482 * waiting for Venus. The former seems better (cause you can ^C a
483 * job), but then GNU-EMACS completion breaks. Use tsleep with no
484 * timeout, and no longjmp happens. But, when sleeping
485 * "uninterruptibly", we don't get told if it returns abnormally
486 * (e.g. kill -9).
487 */
488
489 int
490 coda_call(mntinfo, inSize, outSize, buffer)
491 struct coda_mntinfo *mntinfo; int inSize; int *outSize; caddr_t buffer;
492 {
493 struct vcomm *vcp;
494 struct vmsg *vmp;
495 int error;
496 #ifdef CTL_C
497 struct thread *td = curthread;
498 struct proc *p = td->td_proc;
499 sigset_t psig_omask;
500 sigset_t tempset;
501 int i;
502 #endif
503 if (mntinfo == NULL) {
504 /* Unlikely, but could be a race condition with a dying warden */
505 return ENODEV;
506 }
507
508 vcp = &(mntinfo->mi_vcomm);
509
510 coda_clstat.ncalls++;
511 coda_clstat.reqs[((struct coda_in_hdr *)buffer)->opcode]++;
512
513 if (!VC_OPEN(vcp))
514 return(ENODEV);
515
516 CODA_ALLOC(vmp,struct vmsg *,sizeof(struct vmsg));
517 /* Format the request message. */
518 vmp->vm_data = buffer;
519 vmp->vm_flags = 0;
520 vmp->vm_inSize = inSize;
521 vmp->vm_outSize
522 = *outSize ? *outSize : inSize; /* |buffer| >= inSize */
523 vmp->vm_opcode = ((struct coda_in_hdr *)buffer)->opcode;
524 vmp->vm_unique = ++vcp->vc_seq;
525 if (codadebug)
526 myprintf(("Doing a call for %d.%d\n",
527 vmp->vm_opcode, vmp->vm_unique));
528
529 /* Fill in the common input args. */
530 ((struct coda_in_hdr *)buffer)->unique = vmp->vm_unique;
531
532 /* Append msg to request queue and poke Venus. */
533 INSQUE(vmp->vm_chain, vcp->vc_requests);
534 selwakeuppri(&(vcp->vc_selproc), coda_call_sleep);
535
536 /* We can be interrupted while we wait for Venus to process
537 * our request. If the interrupt occurs before Venus has read
538 * the request, we dequeue and return. If it occurs after the
539 * read but before the reply, we dequeue, send a signal
540 * message, and return. If it occurs after the reply we ignore
541 * it. In no case do we want to restart the syscall. If it
542 * was interrupted by a venus shutdown (vcclose), return
543 * ENODEV. */
544
545 /* Ignore return, We have to check anyway */
546 #ifdef CTL_C
547 /* This is work in progress. Setting coda_pcatch lets tsleep reawaken
548 on a ^c or ^z. The problem is that emacs sets certain interrupts
549 as SA_RESTART. This means that we should exit sleep handle the
550 "signal" and then go to sleep again. Mostly this is done by letting
551 the syscall complete and be restarted. We are not idempotent and
552 can not do this. A better solution is necessary.
553 */
554 i = 0;
555 PROC_LOCK(p);
556 psig_omask = td->td_sigmask;
557 do {
558 error = msleep(&vmp->vm_sleep, &p->p_mtx,
559 (coda_call_sleep|coda_pcatch), "coda_call",
560 hz*2);
561 if (error == 0)
562 break;
563 else if (error == EWOULDBLOCK) {
564 #ifdef CODA_VERBOSE
565 printf("coda_call: tsleep TIMEOUT %d sec\n", 2+2*i);
566 #endif
567 }
568 else {
569 SIGEMPTYSET(tempset);
570 SIGADDSET(tempset, SIGIO);
571 if (SIGSETEQ(td->td_siglist, tempset)) {
572 SIGADDSET(td->td_sigmask, SIGIO);
573 #ifdef CODA_VERBOSE
574 printf("coda_call: tsleep returns %d SIGIO, cnt %d\n",
575 error, i);
576 #endif
577 } else {
578 SIGDELSET(tempset, SIGIO);
579 SIGADDSET(tempset, SIGALRM);
580 if (SIGSETEQ(td->td_siglist, tempset)) {
581 SIGADDSET(td->td_sigmask, SIGALRM);
582 #ifdef CODA_VERBOSE
583 printf("coda_call: tsleep returns %d SIGALRM, cnt %d\n",
584 error, i);
585 #endif
586 }
587 else {
588 #ifdef CODA_VERBOSE
589 printf("coda_call: tsleep returns %d, cnt %d\n",
590 error, i);
591 #endif
592
593 #ifdef notyet
594 tempset = td->td_siglist;
595 SIGSETNAND(tempset, td->td_sigmask);
596 printf("coda_call: siglist = %p, sigmask = %p, mask %p\n",
597 td->td_siglist, td->td_sigmask,
598 tempset);
599 break;
600 SIGSETOR(td->td_sigmask, td->td_siglist);
601 tempset = td->td_siglist;
602 SIGSETNAND(tempset, td->td_sigmask);
603 printf("coda_call: new mask, siglist = %p, sigmask = %p, mask %p\n",
604 td->td_siglist, td->td_sigmask,
605 tempset);
606 #endif
607 }
608 }
609 }
610 } while (error && i++ < 128 && VC_OPEN(vcp));
611 td->td_sigmask = psig_omask;
612 signotify(td);
613 PROC_UNLOCK(p);
614 #else
615 (void) tsleep(&vmp->vm_sleep, coda_call_sleep, "coda_call", 0);
616 #endif
617 if (VC_OPEN(vcp)) { /* Venus is still alive */
618 /* Op went through, interrupt or not... */
619 if (vmp->vm_flags & VM_WRITE) {
620 error = 0;
621 *outSize = vmp->vm_outSize;
622 }
623
624 else if (!(vmp->vm_flags & VM_READ)) {
625 /* Interrupted before venus read it. */
626 #ifdef CODA_VERBOSE
627 if (1)
628 #else
629 if (codadebug)
630 #endif
631 myprintf(("interrupted before read: op = %d.%d, flags = %x\n",
632 vmp->vm_opcode, vmp->vm_unique, vmp->vm_flags));
633 REMQUE(vmp->vm_chain);
634 error = EINTR;
635 }
636
637 else {
638 /* (!(vmp->vm_flags & VM_WRITE)) means interrupted after
639 upcall started */
640 /* Interrupted after start of upcall, send venus a signal */
641 struct coda_in_hdr *dog;
642 struct vmsg *svmp;
643
644 #ifdef CODA_VERBOSE
645 if (1)
646 #else
647 if (codadebug)
648 #endif
649 myprintf(("Sending Venus a signal: op = %d.%d, flags = %x\n",
650 vmp->vm_opcode, vmp->vm_unique, vmp->vm_flags));
651
652 REMQUE(vmp->vm_chain);
653 error = EINTR;
654
655 CODA_ALLOC(svmp, struct vmsg *, sizeof (struct vmsg));
656
657 CODA_ALLOC((svmp->vm_data), char *, sizeof (struct coda_in_hdr));
658 dog = (struct coda_in_hdr *)svmp->vm_data;
659
660 svmp->vm_flags = 0;
661 dog->opcode = svmp->vm_opcode = CODA_SIGNAL;
662 dog->unique = svmp->vm_unique = vmp->vm_unique;
663 svmp->vm_inSize = sizeof (struct coda_in_hdr);
664 /*??? rvb */ svmp->vm_outSize = sizeof (struct coda_in_hdr);
665
666 if (codadebug)
667 myprintf(("coda_call: enqueing signal msg (%d, %d)\n",
668 svmp->vm_opcode, svmp->vm_unique));
669
670 /* insert at head of queue! */
671 INSQUE(svmp->vm_chain, vcp->vc_requests);
672 selwakeuppri(&(vcp->vc_selproc), coda_call_sleep);
673 }
674 }
675
676 else { /* If venus died (!VC_OPEN(vcp)) */
677 if (codadebug)
678 myprintf(("vcclose woke op %d.%d flags %d\n",
679 vmp->vm_opcode, vmp->vm_unique, vmp->vm_flags));
680
681 error = ENODEV;
682 }
683
684 CODA_FREE(vmp, sizeof(struct vmsg));
685
686 if (outstanding_upcalls > 0 && (--outstanding_upcalls == 0))
687 wakeup(&outstanding_upcalls);
688
689 if (!error)
690 error = ((struct coda_out_hdr *)buffer)->result;
691 return(error);
692 }
Cache object: 3c6dbc72008a3c12e31878d8715b52b3
|