1 /* 2 * Copyright (c) 2017, The Linux Foundation. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * * Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * * Redistributions in binary form must reproduce the above 10 * copyright notice, this list of conditions and the following 11 * disclaimer in the documentation and/or other materials provided 12 * with the distribution. 13 * * Neither the name of The Linux Foundation nor the names of its 14 * contributors may be used to endorse or promote products derived 15 * from this software without specific prior written permission. 16 * 17 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED 18 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT 20 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 21 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 24 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 25 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 26 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 27 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 */ 29 #ifndef DBG 30 #define DBG true 31 #endif /* DBG */ 32 #define LOG_TAG "IPAHALService" 33 34 /* HIDL Includes */ 35 #include <hwbinder/IPCThreadState.h> 36 #include <hwbinder/ProcessState.h> 37 38 /* Kernel Includes */ 39 #include <linux/netfilter/nfnetlink_compat.h> 40 41 /* External Includes */ 42 #include <cutils/log.h> 43 #include <string> 44 #include <sys/socket.h> 45 #include <sys/types.h> 46 #include <vector> 47 48 /* Internal Includes */ 49 #include "HAL.h" 50 #include "LocalLogBuffer.h" 51 #include "PrefixParser.h" 52 53 /* Namespace pollution avoidance */ 54 using ::android::hardware::Void; 55 using ::android::status_t; 56 57 using RET = ::IOffloadManager::RET; 58 using Prefix = ::IOffloadManager::Prefix; 59 60 using ::std::map; 61 using ::std::vector; 62 63 64 /* ------------------------------ PUBLIC ------------------------------------ */ 65 HAL* HAL::makeIPAHAL(int version, IOffloadManager* mgr) { 66 if (DBG) 67 ALOGI("makeIPAHAL(%d, %s)", version, 68 (mgr != nullptr) ? "provided" : "null"); 69 if (nullptr == mgr) return NULL; 70 else if (version != 1) return NULL; 71 HAL* ret = new HAL(mgr); 72 if (nullptr == ret) return NULL; 73 configureRpcThreadpool(1, false); 74 ret->registerAsSystemService(); 75 return ret; 76 } /* makeIPAHAL */ 77 78 79 /* ------------------------------ PRIVATE ----------------------------------- */ 80 HAL::HAL(IOffloadManager* mgr) : mLogs("HAL Function Calls", 100) { 81 mIPA = mgr; 82 mCb.clear(); 83 mCbIpa = nullptr; 84 mCbCt = nullptr; 85 } /* HAL */ 86 87 void HAL::registerAsSystemService() { 88 status_t ret = 0; 89 90 ret = IOffloadControl::registerAsService(); 91 if (ret != 0) ALOGE("Failed to register IOffloadControl (%d)", ret); 92 else if (DBG) { 93 ALOGI("Successfully registered IOffloadControl"); 94 } 95 96 IOffloadConfig::registerAsService(); 97 if (ret != 0) ALOGE("Failed to register IOffloadConfig (%d)", ret); 98 else if (DBG) { 99 ALOGI("Successfully registered IOffloadConfig"); 100 } 101 } /* registerAsSystemService */ 102 103 void HAL::doLogcatDump() { 104 ALOGD("mHandles"); 105 ALOGD("========"); 106 /* @TODO This will segfault if they aren't initialized and I don't currently 107 * care to check for initialization in a function that isn't used anyways 108 * ALOGD("fd1->%d", mHandle1->data[0]); 109 * ALOGD("fd2->%d", mHandle2->data[0]); 110 */ 111 ALOGD("========"); 112 } /* doLogcatDump */ 113 114 HAL::BoolResult HAL::makeInputCheckFailure(string customErr) { 115 BoolResult ret; 116 ret.success = false; 117 ret.errMsg = "Failed Input Checks: " + customErr; 118 return ret; 119 } /* makeInputCheckFailure */ 120 121 HAL::BoolResult HAL::ipaResultToBoolResult(RET in) { 122 BoolResult ret; 123 ret.success = (in >= RET::SUCCESS); 124 switch (in) { 125 case RET::FAIL_TOO_MANY_PREFIXES: 126 ret.errMsg = "Too Many Prefixes Provided"; 127 break; 128 case RET::FAIL_UNSUPPORTED: 129 ret.errMsg = "Unsupported by Hardware"; 130 break; 131 case RET::FAIL_INPUT_CHECK: 132 ret.errMsg = "Failed Input Checks"; 133 break; 134 case RET::FAIL_HARDWARE: 135 ret.errMsg = "Hardware did not accept"; 136 break; 137 case RET::FAIL_TRY_AGAIN: 138 ret.errMsg = "Try Again"; 139 break; 140 case RET::SUCCESS: 141 ret.errMsg = "Successful"; 142 break; 143 case RET::SUCCESS_DUPLICATE_CONFIG: 144 ret.errMsg = "Successful: Was a duplicate configuration"; 145 break; 146 case RET::SUCCESS_NO_OP: 147 ret.errMsg = "Successful: No action needed"; 148 break; 149 case RET::SUCCESS_OPTIMIZED: 150 ret.errMsg = "Successful: Performed optimized version of action"; 151 break; 152 default: 153 ret.errMsg = "Unknown Error"; 154 break; 155 } 156 return ret; 157 } /* ipaResultToBoolResult */ 158 159 /* This will likely always result in doubling the number of loops the execution 160 * goes through. Obviously that is suboptimal. But if we first translate 161 * away from all HIDL specific code, then we can avoid sprinkling HIDL 162 * dependencies everywhere. 163 */ 164 vector<string> HAL::convertHidlStrToStdStr(hidl_vec<hidl_string> in) { 165 vector<string> ret; 166 for (size_t i = 0; i < in.size(); i++) { 167 string add = in[i]; 168 ret.push_back(add); 169 } 170 return ret; 171 } /* convertHidlStrToStdStr */ 172 173 void HAL::registerEventListeners() { 174 registerIpaCb(); 175 registerCtCb(); 176 } /* registerEventListeners */ 177 178 void HAL::registerIpaCb() { 179 if (isInitialized() && mCbIpa == nullptr) { 180 LocalLogBuffer::FunctionLog fl("registerEventListener"); 181 mCbIpa = new IpaEventRelay(mCb); 182 mIPA->registerEventListener(mCbIpa); 183 mLogs.addLog(fl); 184 } else { 185 ALOGE("Failed to registerIpaCb (isInitialized()=%s, (mCbIpa == nullptr)=%s)", 186 isInitialized() ? "true" : "false", 187 (mCbIpa == nullptr) ? "true" : "false"); 188 } 189 } /* registerIpaCb */ 190 191 void HAL::registerCtCb() { 192 if (isInitialized() && mCbCt == nullptr) { 193 LocalLogBuffer::FunctionLog fl("registerCtTimeoutUpdater"); 194 mCbCt = new CtUpdateAmbassador(mCb); 195 mIPA->registerCtTimeoutUpdater(mCbCt); 196 mLogs.addLog(fl); 197 } else { 198 ALOGE("Failed to registerCtCb (isInitialized()=%s, (mCbCt == nullptr)=%s)", 199 isInitialized() ? "true" : "false", 200 (mCbCt == nullptr) ? "true" : "false"); 201 } 202 } /* registerCtCb */ 203 204 void HAL::unregisterEventListeners() { 205 unregisterIpaCb(); 206 unregisterCtCb(); 207 } /* unregisterEventListeners */ 208 209 void HAL::unregisterIpaCb() { 210 if (mCbIpa != nullptr) { 211 LocalLogBuffer::FunctionLog fl("unregisterEventListener"); 212 mIPA->unregisterEventListener(mCbIpa); 213 mCbIpa = nullptr; 214 mLogs.addLog(fl); 215 } else { 216 ALOGE("Failed to unregisterIpaCb"); 217 } 218 } /* unregisterIpaCb */ 219 220 void HAL::unregisterCtCb() { 221 if (mCbCt != nullptr) { 222 LocalLogBuffer::FunctionLog fl("unregisterCtTimeoutUpdater"); 223 mIPA->unregisterCtTimeoutUpdater(mCbCt); 224 mCbCt = nullptr; 225 mLogs.addLog(fl); 226 } else { 227 ALOGE("Failed to unregisterCtCb"); 228 } 229 } /* unregisterCtCb */ 230 231 void HAL::clearHandles() { 232 ALOGI("clearHandles()"); 233 /* @TODO handle this more gracefully... also remove the log 234 * 235 * Things that would be nice, but I can't do: 236 * [1] Destroy the object (it's on the stack) 237 * [2] Call freeHandle (it's private) 238 * 239 * Things I can do but are hacks: 240 * [1] Look at code and notice that setTo immediately calls freeHandle 241 */ 242 mHandle1.setTo(nullptr, true); 243 mHandle2.setTo(nullptr, true); 244 } /* clearHandles */ 245 246 bool HAL::isInitialized() { 247 return mCb.get() != nullptr; 248 } /* isInitialized */ 249 250 251 /* -------------------------- IOffloadConfig -------------------------------- */ 252 Return<void> HAL::setHandles( 253 const hidl_handle &fd1, 254 const hidl_handle &fd2, 255 setHandles_cb hidl_cb 256 ) { 257 LocalLogBuffer::FunctionLog fl(__func__); 258 259 if (fd1->numFds != 1) { 260 BoolResult res = makeInputCheckFailure("Must provide exactly one FD per handle (fd1)"); 261 hidl_cb(res.success, res.errMsg); 262 fl.setResult(res.success, res.errMsg); 263 264 mLogs.addLog(fl); 265 return Void(); 266 } 267 268 if (fd2->numFds != 1) { 269 BoolResult res = makeInputCheckFailure("Must provide exactly one FD per handle (fd2)"); 270 hidl_cb(res.success, res.errMsg); 271 fl.setResult(res.success, res.errMsg); 272 273 mLogs.addLog(fl); 274 return Void(); 275 } 276 277 /* The = operator calls freeHandle internally. Therefore, if we were using 278 * these handles previously, they're now gone... forever. But hopefully the 279 * new ones kick in very quickly. 280 * 281 * After freeing anything previously held, it will dup the FD so we have our 282 * own copy. 283 */ 284 mHandle1 = fd1; 285 mHandle2 = fd2; 286 287 /* Log the DUPed FD instead of the actual input FD so that we can lookup 288 * this value in ls -l /proc/<pid>/<fd> 289 */ 290 fl.addArg("fd1", mHandle1->data[0]); 291 fl.addArg("fd2", mHandle2->data[0]); 292 293 /* Try to provide each handle to IPACM. Destroy our DUPed hidl_handles if 294 * IPACM does not like either input. This keeps us from leaking FDs or 295 * providing half solutions. 296 * 297 * @TODO unfortunately, this does not cover duplicate configs where IPACM 298 * thinks it is still holding on to a handle that we would have freed above. 299 * It also probably means that IPACM would not know about the first FD being 300 * freed if it rejects the second FD. 301 */ 302 RET ipaReturn = mIPA->provideFd(mHandle1->data[0], UDP_SUBSCRIPTIONS); 303 if (ipaReturn == RET::SUCCESS) { 304 ipaReturn = mIPA->provideFd(mHandle2->data[0], TCP_SUBSCRIPTIONS); 305 } 306 307 if (ipaReturn != RET::SUCCESS) { 308 ALOGE("IPACM failed to accept the FDs (%d %d)", mHandle1->data[0], 309 mHandle2->data[0]); 310 clearHandles(); 311 } else { 312 /* @TODO remove logs after stabilization */ 313 ALOGI("IPACM was provided two FDs (%d, %d)", mHandle1->data[0], 314 mHandle2->data[0]); 315 } 316 317 BoolResult res = ipaResultToBoolResult(ipaReturn); 318 hidl_cb(res.success, res.errMsg); 319 320 fl.setResult(res.success, res.errMsg); 321 mLogs.addLog(fl); 322 return Void(); 323 } /* setHandles */ 324 325 326 /* -------------------------- IOffloadControl ------------------------------- */ 327 Return<void> HAL::initOffload 328 ( 329 const ::android::sp<ITetheringOffloadCallback>& cb, 330 initOffload_cb hidl_cb 331 ) { 332 LocalLogBuffer::FunctionLog fl(__func__); 333 334 if (isInitialized()) { 335 BoolResult res = makeInputCheckFailure("Already initialized"); 336 hidl_cb(res.success, res.errMsg); 337 fl.setResult(res.success, res.errMsg); 338 mLogs.addLog(fl); 339 } else { 340 /* Should storing the CB be a function? */ 341 mCb = cb; 342 registerEventListeners(); 343 BoolResult res = ipaResultToBoolResult(RET::SUCCESS); 344 hidl_cb(res.success, res.errMsg); 345 fl.setResult(res.success, res.errMsg); 346 mLogs.addLog(fl); 347 } 348 349 return Void(); 350 } /* initOffload */ 351 352 Return<void> HAL::stopOffload 353 ( 354 stopOffload_cb hidl_cb 355 ) { 356 LocalLogBuffer::FunctionLog fl(__func__); 357 358 if (!isInitialized()) { 359 BoolResult res = makeInputCheckFailure("Was never initialized"); 360 hidl_cb(res.success, res.errMsg); 361 fl.setResult(res.success, res.errMsg); 362 mLogs.addLog(fl); 363 } else { 364 /* Should removing the CB be a function? */ 365 mCb.clear(); 366 unregisterEventListeners(); 367 368 RET ipaReturn = mIPA->stopAllOffload(); 369 if (ipaReturn != RET::SUCCESS) { 370 /* Ignore IPAs return value here and provide why stopAllOffload 371 * failed. However, if IPA failed to clearAllFds, then we can't 372 * clear our map because they may still be in use. 373 */ 374 RET ret = mIPA->clearAllFds(); 375 if (ret == RET::SUCCESS) { 376 clearHandles(); 377 } 378 } else { 379 ipaReturn = mIPA->clearAllFds(); 380 /* If IPA fails, they may still be using these for some reason. */ 381 if (ipaReturn == RET::SUCCESS) { 382 clearHandles(); 383 } else { 384 ALOGE("IPACM failed to return success for clearAllFds so they will not be released..."); 385 } 386 } 387 388 BoolResult res = ipaResultToBoolResult(ipaReturn); 389 hidl_cb(res.success, res.errMsg); 390 391 fl.setResult(res.success, res.errMsg); 392 mLogs.addLog(fl); 393 } 394 395 return Void(); 396 } /* stopOffload */ 397 398 Return<void> HAL::setLocalPrefixes 399 ( 400 const hidl_vec<hidl_string>& prefixes, 401 setLocalPrefixes_cb hidl_cb 402 ) { 403 BoolResult res; 404 PrefixParser parser; 405 vector<string> prefixesStr = convertHidlStrToStdStr(prefixes); 406 407 LocalLogBuffer::FunctionLog fl(__func__); 408 fl.addArg("prefixes", prefixesStr); 409 410 if (!isInitialized()) { 411 res = makeInputCheckFailure("Not initialized"); 412 } else if(prefixesStr.size() < 1) { 413 res = ipaResultToBoolResult(RET::FAIL_INPUT_CHECK); 414 } else if (!parser.add(prefixesStr)) { 415 res = makeInputCheckFailure(parser.getLastErrAsStr()); 416 } else { 417 res = ipaResultToBoolResult(RET::SUCCESS); 418 } 419 420 hidl_cb(res.success, res.errMsg); 421 fl.setResult(res.success, res.errMsg); 422 mLogs.addLog(fl); 423 return Void(); 424 } /* setLocalPrefixes */ 425 426 Return<void> HAL::getForwardedStats 427 ( 428 const hidl_string& upstream, 429 getForwardedStats_cb hidl_cb 430 ) { 431 LocalLogBuffer::FunctionLog fl(__func__); 432 fl.addArg("upstream", upstream); 433 434 OffloadStatistics ret; 435 RET ipaReturn = mIPA->getStats(upstream.c_str(), true, ret); 436 if (ipaReturn == RET::SUCCESS) { 437 hidl_cb(ret.getTotalRxBytes(), ret.getTotalTxBytes()); 438 fl.setResult(ret.getTotalRxBytes(), ret.getTotalTxBytes()); 439 } else { 440 /* @TODO Ensure the output is zeroed, but this is probably not enough to 441 * tell Framework that an error has occurred. If, for example, they had 442 * not yet polled for statistics previously, they may incorrectly assume 443 * that simply no statistics have transpired on hardware path. 444 * 445 * Maybe ITetheringOffloadCallback:onEvent(OFFLOAD_STOPPED_ERROR) is 446 * enough to handle this case, time will tell. 447 */ 448 hidl_cb(0, 0); 449 fl.setResult(0, 0); 450 } 451 452 mLogs.addLog(fl); 453 return Void(); 454 } /* getForwardedStats */ 455 456 Return<void> HAL::setDataLimit 457 ( 458 const hidl_string& upstream, 459 uint64_t limit, 460 setDataLimit_cb hidl_cb 461 ) { 462 LocalLogBuffer::FunctionLog fl(__func__); 463 fl.addArg("upstream", upstream); 464 fl.addArg("limit", limit); 465 466 if (!isInitialized()) { 467 BoolResult res = makeInputCheckFailure("Not initialized (setDataLimit)"); 468 hidl_cb(res.success, res.errMsg); 469 fl.setResult(res.success, res.errMsg); 470 } else { 471 RET ipaReturn = mIPA->setQuota(upstream.c_str(), limit); 472 BoolResult res = ipaResultToBoolResult(ipaReturn); 473 hidl_cb(res.success, res.errMsg); 474 fl.setResult(res.success, res.errMsg); 475 } 476 477 mLogs.addLog(fl); 478 return Void(); 479 } /* setDataLimit */ 480 481 Return<void> HAL::setUpstreamParameters 482 ( 483 const hidl_string& iface, 484 const hidl_string& v4Addr, 485 const hidl_string& v4Gw, 486 const hidl_vec<hidl_string>& v6Gws, 487 setUpstreamParameters_cb hidl_cb 488 ) { 489 vector<string> v6GwStrs = convertHidlStrToStdStr(v6Gws); 490 491 LocalLogBuffer::FunctionLog fl(__func__); 492 fl.addArg("iface", iface); 493 fl.addArg("v4Addr", v4Addr); 494 fl.addArg("v4Gw", v4Gw); 495 fl.addArg("v6Gws", v6GwStrs); 496 497 PrefixParser v4AddrParser; 498 PrefixParser v4GwParser; 499 PrefixParser v6GwParser; 500 501 /* @TODO maybe we should enforce that these addresses and gateways are fully 502 * qualified here. But then, how do we allow them to be empty/null as well 503 * while still preserving a sane API on PrefixParser? 504 */ 505 if (!isInitialized()) { 506 BoolResult res = makeInputCheckFailure("Not initialized (setUpstreamParameters)"); 507 hidl_cb(res.success, res.errMsg); 508 fl.setResult(res.success, res.errMsg); 509 } else if (!v4AddrParser.addV4(v4Addr) && !v4Addr.empty()) { 510 BoolResult res = makeInputCheckFailure(v4AddrParser.getLastErrAsStr()); 511 hidl_cb(res.success, res.errMsg); 512 fl.setResult(res.success, res.errMsg); 513 } else if (!v4GwParser.addV4(v4Gw) && !v4Gw.empty()) { 514 BoolResult res = makeInputCheckFailure(v4GwParser.getLastErrAsStr()); 515 hidl_cb(res.success, res.errMsg); 516 fl.setResult(res.success, res.errMsg); 517 } else if (v6GwStrs.size() >= 1 && !v6GwParser.addV6(v6GwStrs)) { 518 BoolResult res = makeInputCheckFailure(v6GwParser.getLastErrAsStr()); 519 hidl_cb(res.success, res.errMsg); 520 fl.setResult(res.success, res.errMsg); 521 } else if (iface.size()>= 1) { 522 RET ipaReturn = mIPA->setUpstream( 523 iface.c_str(), 524 v4GwParser.getFirstPrefix(), 525 v6GwParser.getFirstPrefix()); 526 BoolResult res = ipaResultToBoolResult(ipaReturn); 527 hidl_cb(res.success, res.errMsg); 528 fl.setResult(res.success, res.errMsg); 529 } else { 530 /* send NULL iface string when upstream down */ 531 RET ipaReturn = mIPA->setUpstream( 532 NULL, 533 v4GwParser.getFirstPrefix(), 534 v6GwParser.getFirstPrefix()); 535 BoolResult res = ipaResultToBoolResult(ipaReturn); 536 hidl_cb(res.success, res.errMsg); 537 fl.setResult(res.success, res.errMsg); 538 } 539 540 mLogs.addLog(fl); 541 return Void(); 542 } /* setUpstreamParameters */ 543 544 Return<void> HAL::addDownstream 545 ( 546 const hidl_string& iface, 547 const hidl_string& prefix, 548 addDownstream_cb hidl_cb 549 ) { 550 LocalLogBuffer::FunctionLog fl(__func__); 551 fl.addArg("iface", iface); 552 fl.addArg("prefix", prefix); 553 554 PrefixParser prefixParser; 555 556 if (!isInitialized()) { 557 BoolResult res = makeInputCheckFailure("Not initialized (setUpstreamParameters)"); 558 hidl_cb(res.success, res.errMsg); 559 fl.setResult(res.success, res.errMsg); 560 } 561 else if (!prefixParser.add(prefix)) { 562 BoolResult res = makeInputCheckFailure(prefixParser.getLastErrAsStr()); 563 hidl_cb(res.success, res.errMsg); 564 fl.setResult(res.success, res.errMsg); 565 } else { 566 RET ipaReturn = mIPA->addDownstream( 567 iface.c_str(), 568 prefixParser.getFirstPrefix()); 569 BoolResult res = ipaResultToBoolResult(ipaReturn); 570 hidl_cb(res.success, res.errMsg); 571 fl.setResult(res.success, res.errMsg); 572 } 573 574 mLogs.addLog(fl); 575 return Void(); 576 } /* addDownstream */ 577 578 Return<void> HAL::removeDownstream 579 ( 580 const hidl_string& iface, 581 const hidl_string& prefix, 582 removeDownstream_cb hidl_cb 583 ) { 584 LocalLogBuffer::FunctionLog fl(__func__); 585 fl.addArg("iface", iface); 586 fl.addArg("prefix", prefix); 587 588 PrefixParser prefixParser; 589 590 if (!isInitialized()) { 591 BoolResult res = makeInputCheckFailure("Not initialized (setUpstreamParameters)"); 592 hidl_cb(res.success, res.errMsg); 593 fl.setResult(res.success, res.errMsg); 594 } 595 else if (!prefixParser.add(prefix)) { 596 BoolResult res = makeInputCheckFailure(prefixParser.getLastErrAsStr()); 597 hidl_cb(res.success, res.errMsg); 598 fl.setResult(res.success, res.errMsg); 599 } else { 600 RET ipaReturn = mIPA->removeDownstream( 601 iface.c_str(), 602 prefixParser.getFirstPrefix()); 603 BoolResult res = ipaResultToBoolResult(ipaReturn); 604 hidl_cb(res.success, res.errMsg); 605 fl.setResult(res.success, res.errMsg); 606 } 607 608 mLogs.addLog(fl); 609 return Void(); 610 } /* removeDownstream */ 611 612 Return<void> HAL::debug 613 ( 614 const hidl_handle& handle, 615 const hidl_vec<hidl_string>& /* options */ 616 ) { 617 if (handle != nullptr && handle->numFds >= 1) { 618 mLogs.toFd(handle->data[0]); 619 } 620 return Void(); 621 } /* debug */ 622