]>
Commit | Line | Data |
---|---|---|
774c051c | 1 | /* |
507d0a78 | 2 | * DEBUG: section 93 ICAP (RFC 3507) Client |
774c051c | 3 | */ |
4 | ||
5 | #include "squid.h" | |
3d93a84d AJ |
6 | #include "adaptation/icap/Config.h" |
7 | #include "adaptation/icap/ModXact.h" | |
26cc52cb AR |
8 | #include "adaptation/icap/Options.h" |
9 | #include "adaptation/icap/OptXact.h" | |
3d93a84d AJ |
10 | #include "adaptation/icap/ServiceRep.h" |
11 | #include "base/TextException.h" | |
774c051c | 12 | #include "ConfigParser.h" |
3d93a84d | 13 | #include "HttpReply.h" |
985c86bc | 14 | #include "SquidTime.h" |
774c051c | 15 | |
26cc52cb | 16 | CBDATA_NAMESPACED_CLASS_INIT(Adaptation::Icap, ServiceRep); |
774c051c | 17 | |
b0365bd9 FC |
18 | Adaptation::Icap::ServiceRep::ServiceRep(const Adaptation::ServiceConfig &svcCfg): |
19 | AsyncJob("Adaptation::Icap::ServiceRep"), Adaptation::Service(svcCfg), | |
98a74c05 | 20 | theOptions(NULL), theOptionsFetcher(0), theLastUpdate(0), |
8277060a | 21 | isSuspended(0), notifying(false), |
76fc7e57 AJ |
22 | updateScheduled(false), |
23 | wasAnnouncedUp(true), // do not announce an "up" service at startup | |
24 | isDetached(false) | |
774c051c | 25 | {} |
26 | ||
26cc52cb | 27 | Adaptation::Icap::ServiceRep::~ServiceRep() |
774c051c | 28 | { |
98a74c05 | 29 | Must(!theOptionsFetcher); |
76fc7e57 | 30 | delete theOptions; |
62c7f90e | 31 | } |
774c051c | 32 | |
62c7f90e | 33 | void |
26cc52cb | 34 | Adaptation::Icap::ServiceRep::finalize() |
62c7f90e | 35 | { |
9e008dda | 36 | Adaptation::Service::finalize(); |
774c051c | 37 | |
a68cf076 | 38 | // use /etc/services or default port if needed |
9e008dda | 39 | const bool have_port = cfg().port >= 0; |
a68cf076 | 40 | if (!have_port) { |
774c051c | 41 | struct servent *serv = getservbyname("icap", "tcp"); |
42 | ||
43 | if (serv) { | |
d81a31f1 | 44 | writeableCfg().port = htons(serv->s_port); |
774c051c | 45 | } else { |
d81a31f1 | 46 | writeableCfg().port = 1344; |
774c051c | 47 | } |
48 | } | |
8277060a CT |
49 | |
50 | theSessionFailures.configure(TheConfig.oldest_service_failure > 0 ? | |
a459e80a | 51 | TheConfig.oldest_service_failure : -1); |
a68cf076 | 52 | } |
774c051c | 53 | |
26cc52cb | 54 | void Adaptation::Icap::ServiceRep::noteFailure() |
9e008dda | 55 | { |
8277060a CT |
56 | const int failures = theSessionFailures.count(1); |
57 | debugs(93,4, HERE << " failure " << failures << " out of " << | |
58 | TheConfig.service_failure_limit << " allowed in " << | |
59 | TheConfig.oldest_service_failure << "sec " << status()); | |
1299ecbf | 60 | |
61 | if (isSuspended) | |
62 | return; | |
c99de607 | 63 | |
26cc52cb | 64 | if (TheConfig.service_failure_limit >= 0 && |
8277060a | 65 | failures > TheConfig.service_failure_limit) |
c99de607 | 66 | suspend("too many failures"); |
67 | ||
68 | // TODO: Should bypass setting affect how much Squid tries to talk to | |
9e008dda AJ |
69 | // the ICAP service that is currently unusable and is likely to remain |
70 | // so for some time? The current code says "no". Perhaps the answer | |
c99de607 | 71 | // should be configurable. |
72 | } | |
73 | ||
26cc52cb | 74 | void Adaptation::Icap::ServiceRep::suspend(const char *reason) |
9e008dda | 75 | { |
c99de607 | 76 | if (isSuspended) { |
192378eb | 77 | debugs(93,4, HERE << "keeping suspended, also for " << reason); |
c99de607 | 78 | } else { |
79 | isSuspended = reason; | |
192378eb | 80 | debugs(93,1, "suspending ICAP service for " << reason); |
26cc52cb | 81 | scheduleUpdate(squid_curtime + TheConfig.service_revival_delay); |
c99de607 | 82 | announceStatusChange("suspended", true); |
83 | } | |
84 | } | |
85 | ||
26cc52cb | 86 | bool Adaptation::Icap::ServiceRep::probed() const |
c99de607 | 87 | { |
88 | return theLastUpdate != 0; | |
89 | } | |
90 | ||
26cc52cb | 91 | bool Adaptation::Icap::ServiceRep::hasOptions() const |
9e008dda | 92 | { |
c99de607 | 93 | return theOptions && theOptions->valid() && theOptions->fresh(); |
94 | } | |
95 | ||
26cc52cb | 96 | bool Adaptation::Icap::ServiceRep::up() const |
774c051c | 97 | { |
76fc7e57 | 98 | return !isSuspended && hasOptions(); |
c99de607 | 99 | } |
100 | ||
26cc52cb | 101 | bool Adaptation::Icap::ServiceRep::wantsUrl(const String &urlPath) const |
c99de607 | 102 | { |
103 | Must(hasOptions()); | |
26cc52cb | 104 | return theOptions->transferKind(urlPath) != Adaptation::Icap::Options::xferIgnore; |
774c051c | 105 | } |
106 | ||
26cc52cb | 107 | bool Adaptation::Icap::ServiceRep::wantsPreview(const String &urlPath, size_t &wantedSize) const |
774c051c | 108 | { |
c99de607 | 109 | Must(hasOptions()); |
774c051c | 110 | |
111 | if (theOptions->preview < 0) | |
112 | return false; | |
113 | ||
26cc52cb | 114 | if (theOptions->transferKind(urlPath) != Adaptation::Icap::Options::xferPreview) |
c99de607 | 115 | return false; |
116 | ||
774c051c | 117 | wantedSize = theOptions->preview; |
118 | ||
119 | return true; | |
120 | } | |
121 | ||
26cc52cb | 122 | bool Adaptation::Icap::ServiceRep::allows204() const |
774c051c | 123 | { |
c99de607 | 124 | Must(hasOptions()); |
774c051c | 125 | return true; // in the future, we may have ACLs to prevent 204s |
126 | } | |
127 | ||
83c51da9 CT |
128 | bool Adaptation::Icap::ServiceRep::allows206() const |
129 | { | |
130 | Must(hasOptions()); | |
131 | if (theOptions->allow206) | |
132 | return true; // in the future, we may have ACLs to prevent 206s | |
133 | return false; | |
134 | } | |
135 | ||
774c051c | 136 | |
137 | static | |
26cc52cb | 138 | void ServiceRep_noteTimeToUpdate(void *data) |
774c051c | 139 | { |
26cc52cb | 140 | Adaptation::Icap::ServiceRep *service = static_cast<Adaptation::Icap::ServiceRep*>(data); |
774c051c | 141 | Must(service); |
142 | service->noteTimeToUpdate(); | |
143 | } | |
144 | ||
26cc52cb | 145 | void Adaptation::Icap::ServiceRep::noteTimeToUpdate() |
774c051c | 146 | { |
76fc7e57 | 147 | if (!detached()) |
c99de607 | 148 | updateScheduled = false; |
149 | ||
4299f876 | 150 | if (detached() || theOptionsFetcher.set()) { |
192378eb | 151 | debugs(93,5, HERE << "ignores options update " << status()); |
774c051c | 152 | return; |
153 | } | |
154 | ||
192378eb | 155 | debugs(93,5, HERE << "performs a regular options update " << status()); |
774c051c | 156 | startGettingOptions(); |
157 | } | |
158 | ||
bd7f2ede | 159 | #if 0 |
774c051c | 160 | static |
26cc52cb | 161 | void Adaptation::Icap::ServiceRep_noteTimeToNotify(void *data) |
774c051c | 162 | { |
26cc52cb | 163 | Adaptation::Icap::ServiceRep *service = static_cast<Adaptation::Icap::ServiceRep*>(data); |
774c051c | 164 | Must(service); |
165 | service->noteTimeToNotify(); | |
166 | } | |
bd7f2ede | 167 | #endif |
774c051c | 168 | |
26cc52cb | 169 | void Adaptation::Icap::ServiceRep::noteTimeToNotify() |
774c051c | 170 | { |
171 | Must(!notifying); | |
172 | notifying = true; | |
192378eb | 173 | debugs(93,7, HERE << "notifies " << theClients.size() << " clients " << |
774c051c | 174 | status()); |
175 | ||
176 | // note: we must notify even if we are invalidated | |
177 | ||
178 | Pointer us = NULL; | |
179 | ||
180 | while (!theClients.empty()) { | |
181 | Client i = theClients.pop_back(); | |
9e008dda AJ |
182 | ScheduleCallHere(i.callback); |
183 | i.callback = 0; | |
774c051c | 184 | } |
185 | ||
186 | notifying = false; | |
187 | } | |
188 | ||
26cc52cb | 189 | void Adaptation::Icap::ServiceRep::callWhenReady(AsyncCall::Pointer &cb) |
774c051c | 190 | { |
bd7f2ede | 191 | Must(cb!=NULL); |
192 | ||
26cc52cb | 193 | debugs(93,5, HERE << "Adaptation::Icap::Service is asked to call " << *cb << |
9e008dda | 194 | " when ready " << status()); |
5f8252d2 | 195 | |
c99de607 | 196 | Must(!broken()); // we do not wait for a broken service |
774c051c | 197 | |
198 | Client i; | |
76fc7e57 | 199 | i.service = Pointer(this); // TODO: is this really needed? |
774c051c | 200 | i.callback = cb; |
774c051c | 201 | theClients.push_back(i); |
202 | ||
4299f876 | 203 | if (theOptionsFetcher.set() || notifying) |
774c051c | 204 | return; // do nothing, we will be picked up in noteTimeToNotify() |
205 | ||
206 | if (needNewOptions()) | |
207 | startGettingOptions(); | |
208 | else | |
209 | scheduleNotification(); | |
210 | } | |
211 | ||
26cc52cb | 212 | void Adaptation::Icap::ServiceRep::scheduleNotification() |
774c051c | 213 | { |
192378eb | 214 | debugs(93,7, HERE << "will notify " << theClients.size() << " clients"); |
4299f876 | 215 | CallJobHere(93, 5, this, Adaptation::Icap::ServiceRep, noteTimeToNotify); |
774c051c | 216 | } |
217 | ||
26cc52cb | 218 | bool Adaptation::Icap::ServiceRep::needNewOptions() const |
774c051c | 219 | { |
76fc7e57 | 220 | return !detached() && !up(); |
774c051c | 221 | } |
222 | ||
26cc52cb | 223 | void Adaptation::Icap::ServiceRep::changeOptions(Adaptation::Icap::Options *newOptions) |
774c051c | 224 | { |
192378eb | 225 | debugs(93,8, HERE << "changes options from " << theOptions << " to " << |
5f8252d2 | 226 | newOptions << ' ' << status()); |
c99de607 | 227 | |
774c051c | 228 | delete theOptions; |
229 | theOptions = newOptions; | |
8277060a | 230 | theSessionFailures.clear(); |
c99de607 | 231 | isSuspended = 0; |
232 | theLastUpdate = squid_curtime; | |
233 | ||
234 | checkOptions(); | |
235 | announceStatusChange("down after an options fetch failure", true); | |
236 | } | |
ffddf96c | 237 | |
26cc52cb | 238 | void Adaptation::Icap::ServiceRep::checkOptions() |
c99de607 | 239 | { |
9d0cdbb9 | 240 | if (theOptions == NULL) |
241 | return; | |
242 | ||
eafe3f72 | 243 | if (!theOptions->valid()) { |
244 | debugs(93,1, "WARNING: Squid got an invalid ICAP OPTIONS response " << | |
9e008dda | 245 | "from service " << cfg().uri << "; error: " << theOptions->error); |
eafe3f72 | 246 | return; |
247 | } | |
248 | ||
ffddf96c | 249 | /* |
eadded2e | 250 | * Issue a warning if the ICAP server returned methods in the |
251 | * options response that don't match the method from squid.conf. | |
ffddf96c | 252 | */ |
253 | ||
eadded2e | 254 | if (!theOptions->methods.empty()) { |
255 | bool method_found = false; | |
30abd221 | 256 | String method_list; |
eadded2e | 257 | Vector <ICAP::Method>::iterator iter = theOptions->methods.begin(); |
eadded2e | 258 | |
259 | while (iter != theOptions->methods.end()) { | |
eadded2e | 260 | |
d81a31f1 | 261 | if (*iter == cfg().method) { |
eadded2e | 262 | method_found = true; |
263 | break; | |
264 | } | |
265 | ||
266 | method_list.append(ICAP::methodStr(*iter)); | |
267 | method_list.append(" ", 1); | |
268 | iter++; | |
eadded2e | 269 | } |
270 | ||
eadded2e | 271 | if (!method_found) { |
272 | debugs(93,1, "WARNING: Squid is configured to use ICAP method " << | |
d81a31f1 | 273 | cfg().methodStr() << |
a7a42b14 | 274 | " for service " << cfg().uri << |
5b4117d8 | 275 | " but OPTIONS response declares the methods are " << method_list); |
eadded2e | 276 | } |
277 | } | |
8eeb99bf | 278 | |
279 | ||
280 | /* | |
281 | * Check the ICAP server's date header for clock skew | |
282 | */ | |
5f8252d2 | 283 | const int skew = (int)(theOptions->timestamp() - squid_curtime); |
284 | if (abs(skew) > theOptions->ttl()) { | |
285 | // TODO: If skew is negative, the option will be considered down | |
286 | // because of stale options. We should probably change this. | |
287 | debugs(93, 1, "ICAP service's clock is skewed by " << skew << | |
a7a42b14 | 288 | " seconds: " << cfg().uri); |
5f8252d2 | 289 | } |
c99de607 | 290 | } |
8eeb99bf | 291 | |
26cc52cb | 292 | void Adaptation::Icap::ServiceRep::announceStatusChange(const char *downPhrase, bool important) const |
c99de607 | 293 | { |
294 | if (wasAnnouncedUp == up()) // no significant changes to announce | |
295 | return; | |
9d0cdbb9 | 296 | |
d81a31f1 | 297 | const char *what = cfg().bypass ? "optional" : "essential"; |
c99de607 | 298 | const char *state = wasAnnouncedUp ? downPhrase : "up"; |
9e008dda | 299 | const int level = important ? 1 :2; |
d81a31f1 | 300 | debugs(93,level, what << " ICAP service is " << state << ": " << |
9e008dda | 301 | cfg().uri << ' ' << status()); |
9d0cdbb9 | 302 | |
c99de607 | 303 | wasAnnouncedUp = !wasAnnouncedUp; |
774c051c | 304 | } |
305 | ||
c824c43b | 306 | // we are receiving ICAP OPTIONS response headers here or NULL on failures |
26cc52cb | 307 | void Adaptation::Icap::ServiceRep::noteAdaptationAnswer(HttpMsg *msg) |
774c051c | 308 | { |
4299f876 | 309 | Must(initiated(theOptionsFetcher)); |
d81a31f1 | 310 | clearAdaptation(theOptionsFetcher); |
774c051c | 311 | |
c824c43b | 312 | Must(msg); |
313 | ||
192378eb | 314 | debugs(93,5, HERE << "is interpreting new options " << status()); |
5f8252d2 | 315 | |
26cc52cb | 316 | Adaptation::Icap::Options *newOptions = NULL; |
c824c43b | 317 | if (HttpReply *r = dynamic_cast<HttpReply*>(msg)) { |
26cc52cb | 318 | newOptions = new Adaptation::Icap::Options; |
9e008dda | 319 | newOptions->configure(r); |
c824c43b | 320 | } else { |
192378eb | 321 | debugs(93,1, "ICAP service got wrong options message " << status()); |
c824c43b | 322 | } |
323 | ||
324 | handleNewOptions(newOptions); | |
325 | } | |
326 | ||
26cc52cb | 327 | void Adaptation::Icap::ServiceRep::noteAdaptationQueryAbort(bool) |
9e008dda | 328 | { |
4299f876 | 329 | Must(initiated(theOptionsFetcher)); |
d81a31f1 | 330 | clearAdaptation(theOptionsFetcher); |
774c051c | 331 | |
192378eb | 332 | debugs(93,3, HERE << "failed to fetch options " << status()); |
c824c43b | 333 | handleNewOptions(0); |
334 | } | |
335 | ||
4299f876 AR |
336 | // we (a) must keep trying to get OPTIONS and (b) are RefCounted so we |
337 | // must keep our job alive (XXX: until nobody needs us) | |
338 | void Adaptation::Icap::ServiceRep::callException(const std::exception &e) | |
339 | { | |
340 | clearAdaptation(theOptionsFetcher); | |
341 | debugs(93,2, "ICAP probably failed to fetch options (" << e.what() << | |
342 | ")" << status()); | |
343 | handleNewOptions(0); | |
344 | } | |
345 | ||
26cc52cb | 346 | void Adaptation::Icap::ServiceRep::handleNewOptions(Adaptation::Icap::Options *newOptions) |
c824c43b | 347 | { |
348 | // new options may be NULL | |
5f8252d2 | 349 | changeOptions(newOptions); |
774c051c | 350 | |
192378eb | 351 | debugs(93,3, HERE << "got new options and is now " << status()); |
774c051c | 352 | |
1299ecbf | 353 | scheduleUpdate(optionsFetchTime()); |
774c051c | 354 | scheduleNotification(); |
355 | } | |
356 | ||
26cc52cb | 357 | void Adaptation::Icap::ServiceRep::startGettingOptions() |
774c051c | 358 | { |
98a74c05 | 359 | Must(!theOptionsFetcher); |
192378eb | 360 | debugs(93,6, HERE << "will get new options " << status()); |
774c051c | 361 | |
4299f876 AR |
362 | // XXX: "this" here is "self"; works until refcounting API changes |
363 | theOptionsFetcher = initiateAdaptation( | |
364 | new Adaptation::Icap::OptXactLauncher(this)); | |
26cc52cb | 365 | // TODO: timeout in case Adaptation::Icap::OptXact never calls us back? |
c824c43b | 366 | // Such a timeout should probably be a generic AsyncStart feature. |
774c051c | 367 | } |
368 | ||
26cc52cb | 369 | void Adaptation::Icap::ServiceRep::scheduleUpdate(time_t when) |
774c051c | 370 | { |
1299ecbf | 371 | if (updateScheduled) { |
192378eb | 372 | debugs(93,7, HERE << "reschedules update"); |
1299ecbf | 373 | // XXX: check whether the event is there because AR saw |
04d76948 | 374 | // an unreproducible eventDelete assertion on 2007/06/18 |
26cc52cb AR |
375 | if (eventFind(&ServiceRep_noteTimeToUpdate, this)) |
376 | eventDelete(&ServiceRep_noteTimeToUpdate, this); | |
c99de607 | 377 | else |
192378eb | 378 | debugs(93,1, "XXX: ICAP service lost an update event."); |
1299ecbf | 379 | updateScheduled = false; |
774c051c | 380 | } |
381 | ||
1299ecbf | 382 | debugs(93,7, HERE << "raw OPTIONS fetch at " << when << " or in " << |
9e008dda | 383 | (when - squid_curtime) << " sec"); |
1299ecbf | 384 | debugs(93,9, HERE << "last fetched at " << theLastUpdate << " or " << |
9e008dda | 385 | (squid_curtime - theLastUpdate) << " sec ago"); |
5f8252d2 | 386 | |
387 | /* adjust update time to prevent too-frequent updates */ | |
388 | ||
c99de607 | 389 | if (when < squid_curtime) |
390 | when = squid_curtime; | |
391 | ||
26cc52cb | 392 | // XXX: move hard-coded constants from here to Adaptation::Icap::TheConfig |
1299ecbf | 393 | const int minUpdateGap = 30; // seconds |
c99de607 | 394 | if (when < theLastUpdate + minUpdateGap) |
395 | when = theLastUpdate + minUpdateGap; | |
774c051c | 396 | |
c99de607 | 397 | const int delay = when - squid_curtime; |
192378eb | 398 | debugs(93,5, HERE << "will fetch OPTIONS in " << delay << " sec"); |
1299ecbf | 399 | |
26cc52cb AR |
400 | eventAdd("Adaptation::Icap::ServiceRep::noteTimeToUpdate", |
401 | &ServiceRep_noteTimeToUpdate, this, delay, 0, true); | |
c99de607 | 402 | updateScheduled = true; |
774c051c | 403 | } |
404 | ||
1299ecbf | 405 | // returns absolute time when OPTIONS should be fetched |
406 | time_t | |
26cc52cb | 407 | Adaptation::Icap::ServiceRep::optionsFetchTime() const |
1299ecbf | 408 | { |
409 | if (theOptions && theOptions->valid()) { | |
410 | const time_t expire = theOptions->expire(); | |
192378eb | 411 | debugs(93,7, HERE << "options expire on " << expire << " >= " << squid_curtime); |
1299ecbf | 412 | |
413 | // conservative estimate of how long the OPTIONS transaction will take | |
26cc52cb | 414 | // XXX: move hard-coded constants from here to Adaptation::Icap::TheConfig |
1299ecbf | 415 | const int expectedWait = 20; // seconds |
416 | ||
417 | // Unknown or invalid (too small) expiration times should not happen. | |
26cc52cb | 418 | // Adaptation::Icap::Options should use the default TTL, and ICAP servers should not |
1299ecbf | 419 | // send invalid TTLs, but bugs and attacks happen. |
420 | if (expire < expectedWait) | |
421 | return squid_curtime; | |
422 | else | |
423 | return expire - expectedWait; // before the current options expire | |
424 | } | |
425 | ||
426 | // use revival delay as "expiration" time for a service w/o valid options | |
26cc52cb | 427 | return squid_curtime + TheConfig.service_revival_delay; |
1299ecbf | 428 | } |
429 | ||
d81a31f1 | 430 | Adaptation::Initiate * |
4299f876 AR |
431 | Adaptation::Icap::ServiceRep::makeXactLauncher(HttpMsg *virgin, |
432 | HttpRequest *cause) | |
d81a31f1 | 433 | { |
4299f876 | 434 | return new Adaptation::Icap::ModXactLauncher(virgin, cause, this); |
d81a31f1 AR |
435 | } |
436 | ||
c99de607 | 437 | // returns a temporary string depicting service status, for debugging |
26cc52cb | 438 | const char *Adaptation::Icap::ServiceRep::status() const |
774c051c | 439 | { |
c99de607 | 440 | static MemBuf buf; |
441 | ||
442 | buf.reset(); | |
443 | buf.append("[", 1); | |
444 | ||
445 | if (up()) | |
446 | buf.append("up", 2); | |
5f8252d2 | 447 | else { |
c99de607 | 448 | buf.append("down", 4); |
5f8252d2 | 449 | if (isSuspended) |
450 | buf.append(",susp", 5); | |
c99de607 | 451 | |
5f8252d2 | 452 | if (!theOptions) |
453 | buf.append(",!opt", 5); | |
e1381638 AJ |
454 | else if (!theOptions->valid()) |
455 | buf.append(",!valid", 7); | |
456 | else if (!theOptions->fresh()) | |
457 | buf.append(",stale", 6); | |
5f8252d2 | 458 | } |
774c051c | 459 | |
76fc7e57 AJ |
460 | if (detached()) |
461 | buf.append(",detached", 9); | |
462 | ||
4299f876 | 463 | if (theOptionsFetcher.set()) |
98a74c05 | 464 | buf.append(",fetch", 6); |
774c051c | 465 | |
c99de607 | 466 | if (notifying) |
467 | buf.append(",notif", 6); | |
774c051c | 468 | |
8277060a CT |
469 | if (const int failures = theSessionFailures.remembered()) |
470 | buf.Printf(",fail%d", failures); | |
774c051c | 471 | |
c99de607 | 472 | buf.append("]", 1); |
473 | buf.terminate(); | |
774c051c | 474 | |
c99de607 | 475 | return buf.content(); |
774c051c | 476 | } |
76fc7e57 AJ |
477 | |
478 | void Adaptation::Icap::ServiceRep::detach() | |
479 | { | |
480 | debugs(93,3, HERE << "detaching ICAP service: " << cfg().uri << | |
d090e020 | 481 | ' ' << status()); |
76fc7e57 AJ |
482 | isDetached = true; |
483 | } | |
484 | ||
485 | bool Adaptation::Icap::ServiceRep::detached() const | |
486 | { | |
487 | return isDetached; | |
488 | } |