Gratuitious prototype refinements.
[polipo.git] / local.c
blob081303f4f39fce8efb1ad1f22c0d533c090cdd33
1 /*
2 Copyright (c) 2003-2006 by Juliusz Chroboczek
4 Permission is hereby granted, free of charge, to any person obtaining a copy
5 of this software and associated documentation files (the "Software"), to deal
6 in the Software without restriction, including without limitation the rights
7 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 copies of the Software, and to permit persons to whom the Software is
9 furnished to do so, subject to the following conditions:
11 The above copyright notice and this permission notice shall be included in
12 all copies or substantial portions of the Software.
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 THE SOFTWARE.
23 #include "polipo.h"
25 int disableLocalInterface = 0;
26 int disableConfiguration = 0;
27 int disableIndexing = 1;
29 AtomPtr atomInitForbidden;
30 AtomPtr atomReopenLog;
31 AtomPtr atomDiscardObjects;
32 AtomPtr atomWriteoutObjects;
33 AtomPtr atomFreeChunkArenas;
35 void
36 preinitLocal()
38 atomInitForbidden = internAtom("init-forbidden");
39 atomReopenLog = internAtom("reopen-log");
40 atomDiscardObjects = internAtom("discard-objects");
41 atomWriteoutObjects = internAtom("writeout-objects");
42 atomFreeChunkArenas = internAtom("free-chunk-arenas");
44 /* These should not be settable for obvious reasons */
45 CONFIG_VARIABLE(disableLocalInterface, CONFIG_BOOLEAN,
46 "Disable the local configuration pages.");
47 CONFIG_VARIABLE(disableConfiguration, CONFIG_BOOLEAN,
48 "Disable reconfiguring Polipo at runtime.");
49 CONFIG_VARIABLE(disableIndexing, CONFIG_BOOLEAN,
50 "Disable indexing of the local cache.");
53 static void fillSpecialObject(ObjectPtr, void (*)(FILE*, char*), void*);
55 int
56 httpLocalRequest(ObjectPtr object, int method, int from, int to,
57 HTTPRequestPtr requestor, void *closure)
59 if(object->requestor == NULL)
60 object->requestor = requestor;
62 if(!disableLocalInterface && urlIsSpecial(object->key, object->key_size))
63 return httpSpecialRequest(object, method, from, to,
64 requestor, closure);
66 if(method >= METHOD_POST) {
67 httpClientError(requestor, 405, internAtom("Method not allowed"));
68 requestor->connection->flags &= ~CONN_READER;
69 return 1;
72 /* objectFillFromDisk already did the real work but we have to
73 make sure we don't get into an infinite loop. */
74 if(object->flags & OBJECT_INITIAL) {
75 abortObject(object, 404, internAtom("Not found"));
77 object->age = current_time.tv_sec;
78 object->date = current_time.tv_sec;
80 object->flags &= ~OBJECT_VALIDATING;
81 notifyObject(object);
82 return 1;
85 void
86 alternatingHttpStyle(FILE *out, char *id)
88 fprintf(out,
89 "<style type=\"text/css\">\n"
90 "#%s tbody tr.even td { background-color: #eee; }\n"
91 "#%s tbody tr.odd td { background-color: #fff; }\n"
92 "</style>\n", id, id);
95 static void
96 printConfig(FILE *out, char *dummy)
98 fprintf(out,
99 "<!DOCTYPE HTML PUBLIC "
100 "\"-//W3C//DTD HTML 4.01 Transitional//EN\" "
101 "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
102 "<html><head>\n"
103 "<title>Polipo configuration</title>\n"
104 "</head><body>\n"
105 "<h1>Polipo configuration</h1>\n");
106 printConfigVariables(out, 1);
107 fprintf(out, "<p><a href=\"/polipo/\">back</a></p>");
108 fprintf(out, "</body></html>\n");
111 #ifndef NO_DISK_CACHE
113 static void
114 recursiveIndexDiskObjects(FILE *out, char *root)
116 indexDiskObjects(out, root, 1);
119 static void
120 plainIndexDiskObjects(FILE *out, char *root)
122 indexDiskObjects(out, root, 0);
124 #endif
126 static void
127 serversList(FILE *out, char *dummy)
129 listServers(out);
132 static int
133 matchUrl(char *base, ObjectPtr object)
135 int n = strlen(base);
136 if(object->key_size < n)
137 return 0;
138 if(memcmp(base, object->key, n) != 0)
139 return 0;
140 return (object->key_size == n) || (((char*)object->key)[n] == '?');
143 int
144 httpSpecialRequest(ObjectPtr object, int method, int from, int to,
145 HTTPRequestPtr requestor, void *closure)
147 char buffer[1024];
148 int hlen;
150 if(method >= METHOD_POST) {
151 return httpSpecialSideRequest(object, method, from, to,
152 requestor, closure);
155 if(!(object->flags & OBJECT_INITIAL)) {
156 privatiseObject(object, 0);
157 supersedeObject(object);
158 object->flags &= ~(OBJECT_VALIDATING | OBJECT_INPROGRESS);
159 notifyObject(object);
160 return 1;
163 hlen = snnprintf(buffer, 0, 1024,
164 "\r\nServer: polipo"
165 "\r\nContent-Type: text/html");
166 object->date = current_time.tv_sec;
167 object->age = current_time.tv_sec;
168 object->headers = internAtomN(buffer, hlen);
169 object->code = 200;
170 object->message = internAtom("Okay");
171 object->flags &= ~OBJECT_INITIAL;
172 object->flags |= OBJECT_DYNAMIC;
174 if(object->key_size == 8 && memcmp(object->key, "/polipo/", 8) == 0) {
175 objectPrintf(object, 0,
176 "<!DOCTYPE HTML PUBLIC "
177 "\"-//W3C//DTD HTML 4.01 Transitional//EN\" "
178 "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
179 "<html><head>\n"
180 "<title>Polipo</title>\n"
181 "</head><body>\n"
182 "<h1>Polipo</h1>\n"
183 "<p><a href=\"status?\">Status report</a>.</p>\n"
184 "<p><a href=\"config?\">Current configuration</a>.</p>\n"
185 "<p><a href=\"servers?\">Known servers</a>.</p>\n"
186 #ifndef NO_DISK_CACHE
187 "<p><a href=\"index?\">Disk cache index</a>.</p>\n"
188 #endif
189 "</body></html>\n");
190 object->length = object->size;
191 } else if(matchUrl("/polipo/status", object)) {
192 objectPrintf(object, 0,
193 "<!DOCTYPE HTML PUBLIC "
194 "\"-//W3C//DTD HTML 4.01 Transitional//EN\" "
195 "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
196 "<html><head>\n"
197 "<title>Polipo status report</title>\n"
198 "</head><body>\n"
199 "<h1>Polipo proxy on %s:%d: status report</h1>\n"
200 "<p>The %s proxy on %s:%d is %s.</p>\n"
201 "<p>There are %d public and %d private objects "
202 "currently in memory using %d KB in %d chunks "
203 "(%d KB allocated).</p>\n"
204 "<p>There are %d atoms.</p>"
205 "<p><form method=POST action=\"/polipo/status?\">"
206 "<input type=submit name=\"init-forbidden\" "
207 "value=\"Read forbidden file\"></form>\n"
208 "<form method=POST action=\"/polipo/status?\">"
209 "<input type=submit name=\"writeout-objects\" "
210 "value=\"Write out in-memory cache\"></form>\n"
211 "<form method=POST action=\"/polipo/status?\">"
212 "<input type=submit name=\"discard-objects\" "
213 "value=\"Discard in-memory cache\"></form>\n"
214 "<form method=POST action=\"/polipo/status?\">"
215 "<input type=submit name=\"reopen-log\" "
216 "value=\"Reopen log file\"></form>\n"
217 "<form method=POST action=\"/polipo/status?\">"
218 "<input type=submit name=\"free-chunk-arenas\" "
219 "value=\"Free chunk arenas\"></form></p>\n"
220 "<p><a href=\"/polipo/\">back</a></p>"
221 "</body></html>\n",
222 proxyName->string, proxyPort,
223 cacheIsShared ? "shared" : "private",
224 proxyName->string, proxyPort,
225 proxyOffline ? "off line" :
226 (relaxTransparency ?
227 "on line (transparency relaxed)" :
228 "on line"),
229 publicObjectCount, privateObjectCount,
230 used_chunks * CHUNK_SIZE / 1024, used_chunks,
231 totalChunkArenaSize() / 1024,
232 used_atoms);
233 object->expires = current_time.tv_sec;
234 object->length = object->size;
235 } else if(matchUrl("/polipo/config", object)) {
236 fillSpecialObject(object, printConfig, NULL);
237 object->expires = current_time.tv_sec + 5;
238 #ifndef NO_DISK_CACHE
239 } else if(matchUrl("/polipo/index", object)) {
240 int len;
241 char *root;
242 if(disableIndexing) {
243 abortObject(object, 403, internAtom("Action not allowed"));
244 notifyObject(object);
245 return 1;
247 len = MAX(0, object->key_size - 14);
248 root = strdup_n((char*)object->key + 14, len);
249 if(root == NULL) {
250 abortObject(object, 503, internAtom("Couldn't allocate root"));
251 notifyObject(object);
252 return 1;
254 writeoutObjects(1);
255 fillSpecialObject(object, plainIndexDiskObjects, root);
256 free(root);
257 object->expires = current_time.tv_sec + 5;
258 } else if(matchUrl("/polipo/recursive-index", object)) {
259 int len;
260 char *root;
261 if(disableIndexing) {
262 abortObject(object, 403, internAtom("Action not allowed"));
263 notifyObject(object);
264 return 1;
266 len = MAX(0, object->key_size - 24);
267 root = strdup_n((char*)object->key + 24, len);
268 if(root == NULL) {
269 abortObject(object, 503, internAtom("Couldn't allocate root"));
270 notifyObject(object);
271 return 1;
273 writeoutObjects(1);
274 fillSpecialObject(object, recursiveIndexDiskObjects, root);
275 free(root);
276 object->expires = current_time.tv_sec + 20;
277 #endif
278 } else if(matchUrl("/polipo/servers", object)) {
279 fillSpecialObject(object, serversList, NULL);
280 object->expires = current_time.tv_sec + 2;
281 } else {
282 abortObject(object, 404, internAtom("Not found"));
285 object->flags &= ~OBJECT_VALIDATING;
286 notifyObject(object);
287 return 1;
290 int
291 httpSpecialSideRequest(ObjectPtr object, int method, int from, int to,
292 HTTPRequestPtr requestor, void *closure)
294 HTTPConnectionPtr client = requestor->connection;
296 assert(client->request == requestor);
298 if(method != METHOD_POST) {
299 httpClientError(requestor, 405, internAtom("Method not allowed"));
300 requestor->connection->flags &= ~CONN_READER;
301 return 1;
304 return httpSpecialDoSide(requestor);
308 httpSpecialDoSide(HTTPRequestPtr requestor)
310 HTTPConnectionPtr client = requestor->connection;
312 if(client->reqlen - client->reqbegin >= client->bodylen) {
313 AtomPtr data;
314 data = internAtomN(client->reqbuf + client->reqbegin,
315 client->reqlen - client->reqbegin);
316 client->reqbegin = 0;
317 client->reqlen = 0;
318 if(data == NULL) {
319 do_log(L_ERROR, "Couldn't allocate data.\n");
320 httpClientError(requestor, 500,
321 internAtom("Couldn't allocate data"));
322 return 1;
324 httpSpecialDoSideFinish(data, requestor);
325 return 1;
328 if(client->reqlen - client->reqbegin >= CHUNK_SIZE) {
329 httpClientError(requestor, 500, internAtom("POST too large"));
330 return 1;
333 if(client->reqbegin > 0 && client->reqlen > client->reqbegin) {
334 memmove(client->reqbuf, client->reqbuf + client->reqbegin,
335 client->reqlen - client->reqbegin);
337 client->reqlen -= client->reqbegin;
338 client->reqbegin = 0;
340 do_stream(IO_READ | IO_NOTNOW, client->fd,
341 client->reqlen, client->reqbuf, CHUNK_SIZE,
342 httpSpecialClientSideHandler, client);
343 return 1;
347 httpSpecialClientSideHandler(int status,
348 FdEventHandlerPtr event,
349 StreamRequestPtr srequest)
351 HTTPConnectionPtr connection = srequest->data;
352 HTTPRequestPtr request = connection->request;
353 int push;
355 if((request->object->flags & OBJECT_ABORTED) ||
356 !(request->object->flags & OBJECT_INPROGRESS)) {
357 httpClientDiscardBody(connection);
358 httpClientError(request, 503, internAtom("Post aborted"));
359 return 1;
362 if(status < 0) {
363 do_log_error(L_ERROR, -status, "Reading from client");
364 if(status == -EDOGRACEFUL)
365 httpClientFinish(connection, 1);
366 else
367 httpClientFinish(connection, 2);
368 return 1;
371 push = MIN(srequest->offset - connection->reqlen,
372 connection->bodylen - connection->reqoffset);
373 if(push > 0) {
374 connection->reqlen += push;
375 httpSpecialDoSide(request);
378 do_log(L_ERROR, "Incomplete client request.\n");
379 connection->flags &= ~CONN_READER;
380 httpClientRawError(connection, 502,
381 internAtom("Incomplete client request"), 1);
382 return 1;
386 httpSpecialDoSideFinish(AtomPtr data, HTTPRequestPtr requestor)
388 ObjectPtr object = requestor->object;
390 if(matchUrl("/polipo/config", object)) {
391 AtomListPtr list = NULL;
392 int i, rc;
394 if(disableConfiguration) {
395 abortObject(object, 403, internAtom("Action not allowed"));
396 goto out;
399 list = urlDecode(data->string, data->length);
400 if(list == NULL) {
401 abortObject(object, 400,
402 internAtom("Couldn't parse variable to set"));
403 goto out;
405 for(i = 0; i < list->length; i++) {
406 rc = parseConfigLine(list->list[i]->string, NULL, 0, 1);
407 if(rc < 0) {
408 abortObject(object, 400,
409 rc == -1 ?
410 internAtom("Couldn't parse variable to set") :
411 internAtom("Variable is not settable"));
412 destroyAtomList(list);
413 goto out;
416 destroyAtomList(list);
417 object->date = current_time.tv_sec;
418 object->age = current_time.tv_sec;
419 object->headers = internAtom("\r\nLocation: /polipo/config?");
420 object->code = 303;
421 object->message = internAtom("Done");
422 object->flags &= ~OBJECT_INITIAL;
423 object->length = 0;
424 } else if(matchUrl("/polipo/status", object)) {
425 AtomListPtr list = NULL;
426 int i;
428 if(disableConfiguration) {
429 abortObject(object, 403, internAtom("Action not allowed"));
430 goto out;
433 list = urlDecode(data->string, data->length);
434 if(list == NULL) {
435 abortObject(object, 400,
436 internAtom("Couldn't parse action"));
437 goto out;
439 for(i = 0; i < list->length; i++) {
440 char *equals =
441 memchr(list->list[i]->string, '=', list->list[i]->length);
442 AtomPtr name =
443 equals ?
444 internAtomN(list->list[i]->string,
445 equals - list->list[i]->string) :
446 retainAtom(list->list[i]);
447 if(name == atomInitForbidden)
448 initForbidden();
449 else if(name == atomReopenLog)
450 reopenLog();
451 else if(name == atomDiscardObjects)
452 discardObjects(1, 0);
453 else if(name == atomWriteoutObjects)
454 writeoutObjects(1);
455 else if(name == atomFreeChunkArenas)
456 free_chunk_arenas();
457 else {
458 abortObject(object, 400, internAtomF("Unknown action %s",
459 name->string));
460 releaseAtom(name);
461 destroyAtomList(list);
462 goto out;
464 releaseAtom(name);
466 destroyAtomList(list);
467 object->date = current_time.tv_sec;
468 object->age = current_time.tv_sec;
469 object->headers = internAtom("\r\nLocation: /polipo/status?");
470 object->code = 303;
471 object->message = internAtom("Done");
472 object->flags &= ~OBJECT_INITIAL;
473 object->length = 0;
474 } else {
475 abortObject(object, 405, internAtom("Method not allowed"));
478 out:
479 releaseAtom(data);
480 notifyObject(object);
481 requestor->connection->flags &= ~CONN_READER;
482 return 1;
485 #ifdef HAVE_FORK
486 static void
487 fillSpecialObject(ObjectPtr object, void (*fn)(FILE*, char*), void* closure)
489 int rc;
490 int filedes[2];
491 pid_t pid;
492 sigset_t ss, old_mask;
494 if(object->flags & OBJECT_INPROGRESS)
495 return;
497 rc = pipe(filedes);
498 if(rc < 0) {
499 do_log_error(L_ERROR, errno, "Couldn't create pipe");
500 abortObject(object, 503,
501 internAtomError(errno, "Couldn't create pipe"));
502 return;
505 fflush(stdout);
506 fflush(stderr);
507 fflush(logF);
509 /* Block signals that we handle specially until the child can
510 disable the handlers. */
511 interestingSignals(&ss);
512 /* I'm a little confused. POSIX doesn't allow EINTR here, but I
513 think that both Linux and SVR4 do. */
514 do {
515 rc = sigprocmask(SIG_BLOCK, &ss, &old_mask);
516 } while (rc < 0 && errno == EINTR);
517 if(rc < 0) {
518 do_log_error(L_ERROR, errno, "Sigprocmask failed");
519 abortObject(object, 503, internAtomError(errno, "Sigprocmask failed"));
520 close(filedes[0]);
521 close(filedes[1]);
522 return;
525 pid = fork();
526 if(pid < 0) {
527 do_log_error(L_ERROR, errno, "Couldn't fork");
528 abortObject(object, 503, internAtomError(errno, "Couldn't fork"));
529 close(filedes[0]);
530 close(filedes[1]);
531 do {
532 rc = sigprocmask(SIG_SETMASK, &old_mask, NULL);
533 } while (rc < 0 && errno == EINTR);
534 if(rc < 0) {
535 do_log_error(L_ERROR, errno, "Couldn't restore signal mask");
536 polipoExit();
538 return;
541 if(pid > 0) {
542 SpecialRequestPtr request;
543 close(filedes[1]);
544 do {
545 rc = sigprocmask(SIG_SETMASK, &old_mask, NULL);
546 } while (rc < 0 && errno == EINTR);
547 if(rc < 0) {
548 do_log_error(L_ERROR, errno, "Couldn't restore signal mask");
549 polipoExit();
550 return;
553 request = malloc(sizeof(SpecialRequestRec));
554 if(request == NULL) {
555 kill(pid, SIGTERM);
556 close(filedes[0]);
557 abortObject(object, 503,
558 internAtom("Couldn't allocate request\n"));
559 notifyObject(object);
560 /* specialRequestHandler will take care of the rest. */
561 } else {
562 request->buf = get_chunk();
563 if(request->buf == NULL) {
564 kill(pid, SIGTERM);
565 close(filedes[0]);
566 free(request);
567 abortObject(object, 503,
568 internAtom("Couldn't allocate request\n"));
569 notifyObject(object);
572 object->flags |= OBJECT_INPROGRESS;
573 retainObject(object);
574 request->object = object;
575 request->fd = filedes[0];
576 request->pid = pid;
577 request->offset = 0;
578 /* Under any sensible scheduler, the child will run before the
579 parent. So no need for IO_NOTNOW. */
580 do_stream(IO_READ, filedes[0], 0, request->buf, CHUNK_SIZE,
581 specialRequestHandler, request);
582 } else {
583 /* child */
584 close(filedes[0]);
585 uninitEvents();
586 do {
587 rc = sigprocmask(SIG_SETMASK, &old_mask, NULL);
588 } while (rc < 0 && errno == EINTR);
589 if(rc < 0)
590 exit(1);
592 if(filedes[1] != 1)
593 dup2(filedes[1], 1);
595 (*fn)(stdout, closure);
596 exit(0);
601 specialRequestHandler(int status,
602 FdEventHandlerPtr event, StreamRequestPtr srequest)
604 SpecialRequestPtr request = srequest->data;
605 int rc;
606 int killed = 0;
608 if(status < 0) {
609 kill(request->pid, SIGTERM);
610 killed = 1;
611 request->object->flags &= ~OBJECT_INPROGRESS;
612 abortObject(request->object, 502,
613 internAtomError(-status, "Couldn't read from client"));
614 goto done;
617 if(srequest->offset > 0) {
618 rc = objectAddData(request->object, request->buf,
619 request->offset, srequest->offset);
620 if(rc < 0) {
621 kill(request->pid, SIGTERM);
622 killed = 1;
623 request->object->flags &= ~OBJECT_INPROGRESS;
624 abortObject(request->object, 503,
625 internAtom("Couldn't add data to connection"));
626 goto done;
628 request->offset += srequest->offset;
630 if(status) {
631 request->object->flags &= ~OBJECT_INPROGRESS;
632 request->object->length = request->object->size;
633 goto done;
636 /* If we're the only person interested in this object, let's abort
637 it now. */
638 if(request->object->refcount <= 1) {
639 kill(request->pid, SIGTERM);
640 killed = 1;
641 request->object->flags &= ~OBJECT_INPROGRESS;
642 abortObject(request->object, 500, internAtom("Aborted"));
643 goto done;
645 notifyObject(request->object);
646 do_stream(IO_READ | IO_NOTNOW, request->fd, 0, request->buf, CHUNK_SIZE,
647 specialRequestHandler, request);
648 return 1;
650 done:
651 close(request->fd);
652 dispose_chunk(request->buf);
653 releaseNotifyObject(request->object);
654 /* That's a blocking wait. It shouldn't block for long, as we've
655 either already killed the child, or else we got EOF from it. */
656 do {
657 rc = waitpid(request->pid, &status, 0);
658 } while(rc < 0 && errno == EINTR);
659 if(rc < 0) {
660 do_log(L_ERROR, "Wait for %d: %d\n", (int)request->pid, errno);
661 } else {
662 int normal =
663 (WIFEXITED(status) && WEXITSTATUS(status) == 0) ||
664 (killed && WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM);
665 char *reason =
666 WIFEXITED(status) ? "with status" :
667 WIFSIGNALED(status) ? "on signal" :
668 "with unknown status";
669 int value =
670 WIFEXITED(status) ? WEXITSTATUS(status) :
671 WIFSIGNALED(status) ? WTERMSIG(status) :
672 status;
673 do_log(normal ? D_CHILD : L_ERROR,
674 "Child %d exited %s %d.\n",
675 (int)request->pid, reason, value);
677 free(request);
678 return 1;
680 #else
681 static void
682 fillSpecialObject(ObjectPtr object, void (*fn)(FILE*, char*), void* closure)
684 FILE *tmp = NULL;
685 char *buf = NULL;
686 int rc, len, offset;
688 if(object->flags & OBJECT_INPROGRESS)
689 return;
691 buf = get_chunk();
692 if(buf == NULL) {
693 abortObject(object, 503, internAtom("Couldn't allocate chunk"));
694 goto done;
697 tmp = tmpfile();
698 if(tmp == NULL) {
699 abortObject(object, 503, internAtom(pstrerror(errno)));
700 goto done;
703 (*fn)(tmp, closure);
704 fflush(tmp);
706 rewind(tmp);
707 offset = 0;
708 while(1) {
709 len = fread(buf, 1, CHUNK_SIZE, tmp);
710 if(len <= 0 && ferror(tmp)) {
711 abortObject(object, 503, internAtom(pstrerror(errno)));
712 goto done;
714 if(len <= 0)
715 break;
717 rc = objectAddData(object, buf, offset, len);
718 if(rc < 0) {
719 abortObject(object, 503, internAtom("Couldn't add data to object"));
720 goto done;
723 offset += len;
726 object->length = offset;
728 done:
729 if(buf)
730 dispose_chunk(buf);
731 if(tmp)
732 fclose(tmp);
733 notifyObject(object);
735 #endif