1 module hio.resolver.ares; 2 3 import core.sys.posix.sys.select; 4 import core.sys.posix.netdb; 5 6 import std.socket; 7 import std.string; 8 import std.typecons; 9 import std.algorithm; 10 import std.traits; 11 import std.bitmanip; 12 13 import std.experimental.logger; 14 15 import core.thread; 16 17 import hio.events; 18 import hio.loop; 19 import hio.common; 20 import hio.scheduler; 21 22 struct ares_channeldata; 23 alias ares_channel = ares_channeldata*; 24 alias ares_socket_t = int; 25 26 enum ARES_SOCKET_BAD = -1; 27 enum ARES_GETSOCK_MAXNUM = 16; 28 29 int ARES_SOCK_READABLE(uint bits,uint num) @safe @nogc nothrow 30 { 31 return (bits & (1<< (num))); 32 } 33 int ARES_SOCK_WRITABLE(uint bits, uint num) @safe @nogc nothrow 34 { 35 return (bits & (1 << ((num) + ARES_GETSOCK_MAXNUM))); 36 } 37 38 enum ARES_SUCCESS = 0; 39 enum ARES_ENODATA = 1; 40 enum ARES_EFORMERR = 2; 41 enum ARES_ESERVFAIL = 3; 42 enum ARES_ENOTFOUND = 4; 43 enum ARES_ENOTIMP = 5; 44 enum ARES_EREFUSED = 6; 45 46 /* Locally generated error codes */ 47 enum ARES_EBADQUERY = 7; 48 enum ARES_EBADNAME = 8; 49 enum ARES_EBADFAMILY = 9; 50 enum ARES_EBADRESP = 10; 51 enum ARES_ECONNREFUSED= 11; 52 enum ARES_ETIMEOUT = 12; 53 enum ARES_EOF = 13; 54 enum ARES_EFILE = 14; 55 enum ARES_ENOMEM = 15; 56 enum ARES_EDESTRUCTION= 16; 57 enum ARES_EBADSTR = 17; 58 59 /* ares_getnameinfo error codes */ 60 enum ARES_EBADFLAGS = 18; 61 62 /* ares_getaddrinfo error codes */ 63 enum ARES_ENONAME = 19; 64 enum ARES_EBADHINTS = 20; 65 66 extern(C) 67 { 68 alias ares_host_callback = void function(void *arg, int status, int timeouts, hostent *he); 69 int ares_init(ares_channel*); 70 void ares_destroy(ares_channel); 71 timeval* ares_timeout(ares_channel channel, timeval *maxtv, timeval *tv); 72 char* ares_strerror(int); 73 void ares_gethostbyname(ares_channel channel, const char *name, int family, ares_host_callback callback, void *arg); 74 75 int ares_fds(ares_channel, fd_set* reads_fds, fd_set* writes_fds); 76 int ares_getsock(ares_channel channel, ares_socket_t* socks, int numsocks) @trusted @nogc nothrow; 77 78 void ares_process(ares_channel channel, fd_set *read_fds, fd_set *write_fds); 79 void ares_process_fd(ares_channel channel, ares_socket_t read_fd, ares_socket_t write_fd) @trusted @nogc nothrow; 80 void ares_library_init(); 81 void ares_library_cleanup(); 82 } 83 84 alias ResolverCallbackFunction = void function(int status, uint[] addresses); 85 alias ResolverCallbackDelegate = void delegate(int status, uint[] addresses); 86 alias ResolverCallbackFunction6 = void function(int status, ubyte[16][]* addresses); 87 alias ResolverCallbackDelegate6 = void delegate(int status, ubyte[16][]* addresses); 88 89 alias ResolverResult4 = Tuple!(int, "status", InternetAddress[], "addresses"); 90 alias ResolverResult6 = Tuple!(int, "status", Internet6Address[], "addresses"); 91 alias ResolverResult = ResolverResult4; 92 93 shared static this() 94 { 95 ares_library_init(); 96 } 97 shared static ~this() 98 { 99 ares_library_cleanup(); 100 } 101 102 package static Resolver theResolver; 103 static this() { 104 theResolver = new Resolver(); 105 } 106 107 public auto hio_gethostbyname(string host, ushort port=InternetAddress.PORT_ANY) 108 { 109 return theResolver.gethostbyname(host, port); 110 } 111 112 public auto hio_gethostbyname6(string host, ushort port=Internet6Address.PORT_ANY) 113 { 114 return theResolver.gethostbyname6(host, port); 115 } 116 /// 117 auto ares_statusString(int status) 118 { 119 return fromStringz(ares_strerror(status)); 120 } 121 122 package class Resolver: FileEventHandler 123 { 124 private 125 { 126 ares_channel _ares_channel; 127 hlEvLoop _loop; 128 bool[ARES_GETSOCK_MAXNUM] _in_read; 129 bool[ARES_GETSOCK_MAXNUM] _in_write; 130 ares_socket_t[ARES_GETSOCK_MAXNUM] _sockets; 131 int _id; 132 ResolverCallbackFunction[int] _cbFunctions; 133 ResolverCallbackDelegate[int] _cbDelegates; 134 ResolverCallbackFunction6[int] _cb6Functions; 135 ResolverCallbackDelegate6[int] _cb6Delegates; 136 } 137 138 this() 139 { 140 immutable init_res = ares_init(&_ares_channel); 141 assert(init_res == ARES_SUCCESS, "Can't initialise ares."); 142 } 143 ~this() 144 { 145 close(); 146 } 147 void close() 148 { 149 if (_ares_channel) 150 { 151 ares_destroy(_ares_channel); 152 } 153 } 154 155 private ares_host_callback host_callback4 = (void *arg, int status, int timeouts, hostent* he) 156 { 157 int id = cast(int)arg; 158 uint[] result; 159 debug tracef("got callback from ares s:\"%s\" t:%d h:%s, id: %d", fromStringz(ares_strerror(status)), timeouts, he, id); 160 161 Resolver resolver = theResolver; 162 if (status == 0 && he !is null && he.h_addrtype == AF_INET) 163 { 164 debug tracef("he=%s", fromStringz(he.h_name)); 165 debug tracef("h_length=%X", he.h_length); 166 auto a = he.h_addr_list; 167 while( *a ) 168 { 169 uint addr; 170 for (int i; i < he.h_length; i++) 171 { 172 addr = addr << 8; 173 addr += (*a)[i]; 174 } 175 result ~= addr; 176 a++; 177 } 178 } 179 else 180 { 181 debug tracef("callback failed: %d", status); 182 } 183 auto cbf = id in resolver._cbFunctions; 184 auto cbd = id in resolver._cbDelegates; 185 debug tracef("cbf: %s, cbd: %s", cbd, cbf); 186 if (cbf !is null) 187 { 188 auto cb = *cbf; 189 resolver._cbFunctions.remove(id); 190 cb(status, result); 191 } 192 else if (cbd !is null) 193 { 194 auto cb = *cbd; 195 resolver._cbDelegates.remove(id); 196 cb(status, result); 197 } 198 }; 199 200 private ares_host_callback host_callback6 = (void *arg, int status, int timeouts, hostent* he) 201 { 202 int id = cast(int)arg; 203 ubyte[16][] result; 204 debug tracef("got callback from ares s:\"%s\" t:%d h:%s, id: %d", fromStringz(ares_strerror(status)), timeouts, he, id); 205 206 Resolver resolver = theResolver; 207 if (status == 0 && he !is null && he.h_addrtype == AF_INET6) 208 { 209 if (he is null) 210 { 211 return; 212 } 213 debug tracef("he=%s", fromStringz(he.h_name)); 214 debug tracef("h_length=%X", he.h_length); 215 auto a = he.h_addr_list; 216 while(*a) 217 { 218 //ubyte[16] *addr = cast(ubyte[16]*)*a; 219 result ~= *(cast(ubyte[16]*)*a);//new Internet6Address(*addr, Internet6Address.PORT_ANY); 220 a++; 221 } 222 } 223 else 224 { 225 debug tracef("callback failed: %d", status); 226 } 227 auto cbf = id in resolver._cb6Functions; 228 auto cbd = id in resolver._cb6Delegates; 229 debug tracef("cbf: %s, cbd: %s", cbd, cbf); 230 if (cbf !is null) 231 { 232 auto cb = *cbf; 233 resolver._cb6Functions.remove(id); 234 cb(status, &result); 235 } 236 else if (cbd !is null) 237 { 238 auto cb = *cbd; 239 resolver._cb6Delegates.remove(id); 240 cb(status, &result); 241 } 242 }; 243 /// 244 /// gethostbyname which wait for result (can be called w/o eventloop or inside of task) 245 /// 246 ResolverResult4 gethostbyname(string hostname, ushort port=InternetAddress.PORT_ANY) 247 { 248 int status, id; 249 InternetAddress[] adresses; 250 bool done; 251 252 _id++; 253 254 debug tracef("start resolving %s", hostname); 255 auto fiber = Fiber.getThis(); 256 if (fiber is null) 257 { 258 void cba(int s, uint[] a) 259 { 260 status = s; 261 foreach(ia;a) 262 { 263 adresses ~= new InternetAddress(ia, port); 264 } 265 done = true; 266 _cbDelegates.remove(id); 267 debug tracef("resolve for %s: %s, %s", hostname, ares_strerror(s), a); 268 } 269 id = _id; 270 _cbDelegates[_id] = &cba; 271 ares_gethostbyname(_ares_channel, toStringz(hostname), AF_INET, host_callback4, cast(void*)_id); 272 if ( done ) 273 { 274 // resolved from files 275 debug tracef("return ready result"); 276 return ResolverResult(status, adresses); 277 } 278 // called without loop/callback, we can and have to block 279 int nfds, count; 280 fd_set readers, writers; 281 timeval tv; 282 timeval *tvp; 283 284 while (!done) { 285 FD_ZERO(&readers); 286 FD_ZERO(&writers); 287 nfds = ares_fds(_ares_channel, &readers, &writers); 288 if (nfds == 0) 289 break; 290 tvp = ares_timeout(_ares_channel, null, &tv); 291 count = select(nfds, &readers, &writers, null, tvp); 292 ares_process(_ares_channel, &readers, &writers); 293 } 294 return ResolverResult(status, adresses); 295 } 296 else 297 { 298 bool yielded; 299 void cbb(int s, uint[] a) 300 { 301 status = s; 302 foreach (ia; a) { 303 adresses ~= new InternetAddress(ia, port); 304 } 305 done = true; 306 debug tracef("resolve for %s: %s, %s, yielded: %s", hostname, ares_strerror(s), a, yielded); 307 if (yielded) fiber.call(); 308 debug tracef("done"); 309 } 310 if (!_loop) 311 { 312 _loop = getDefaultLoop(); 313 } 314 id = _id; 315 _cbDelegates[_id] = &cbb; 316 ares_gethostbyname(_ares_channel, toStringz(hostname), AF_INET, host_callback4, cast(void*)_id); 317 if ( done ) 318 { 319 // resolved from files 320 debug tracef("return ready result"); 321 return ResolverResult(status, adresses); 322 } 323 auto rc = ares_getsock(_ares_channel, &_sockets[0], ARES_GETSOCK_MAXNUM); 324 debug tracef("getsocks: 0x%04X, %s", rc, _sockets); 325 // prepare listening for socket events 326 handleGetSocks(rc, &_sockets); 327 yielded = true; 328 Fiber.yield(); 329 return ResolverResult(status, adresses); 330 } 331 } 332 /// 333 /// gethostbyname which wait for result (can be called w/o eventloop or inside of task) 334 /// 335 ResolverResult6 gethostbyname6(string hostname, ushort port = Internet6Address.PORT_ANY) 336 { 337 int status, id; 338 Internet6Address[] addresses; 339 bool done; 340 341 _id++; 342 343 debug tracef("start resolving %s", hostname); 344 auto fiber = Fiber.getThis(); 345 if (fiber is null) 346 { 347 void cba(int s, ubyte[16][] *a) 348 { 349 status = s; 350 foreach(ia;*a) 351 { 352 addresses ~= new Internet6Address(ia, port); 353 } 354 done = true; 355 _cb6Delegates.remove(id); 356 debug tracef("resolve for %s: %s, %s", hostname, ares_strerror(s), a); 357 } 358 id = _id; 359 _cb6Delegates[_id] = &cba; 360 ares_gethostbyname(_ares_channel, toStringz(hostname), AF_INET6, host_callback6, cast(void*)_id); 361 if ( done ) 362 { 363 // resolved from files 364 debug tracef("return ready result"); 365 return ResolverResult6(status, addresses); 366 } 367 // called without loop/callback, we can and have to block 368 int nfds, count; 369 fd_set readers, writers; 370 timeval tv; 371 timeval *tvp; 372 373 while (!done) { 374 FD_ZERO(&readers); 375 FD_ZERO(&writers); 376 nfds = ares_fds(_ares_channel, &readers, &writers); 377 if (nfds == 0) 378 break; 379 tvp = ares_timeout(_ares_channel, null, &tv); 380 count = select(nfds, &readers, &writers, null, tvp); 381 ares_process(_ares_channel, &readers, &writers); 382 } 383 return ResolverResult6(status, addresses); 384 } 385 else 386 { 387 bool yielded; 388 void cbb(int s, ubyte[16][] *a) 389 { 390 status = s; 391 foreach (ia; *a) 392 { 393 addresses ~= new Internet6Address(ia, port); 394 } 395 done = true; 396 debug tracef("resolve for %s: %s, %s, yielded: %s", hostname, ares_strerror(s), a, yielded); 397 if (yielded) fiber.call(); 398 debug tracef("done"); 399 } 400 if (!_loop) 401 { 402 _loop = getDefaultLoop(); 403 } 404 id = _id; 405 _cb6Delegates[_id] = &cbb; 406 ares_gethostbyname(_ares_channel, toStringz(hostname), AF_INET6, host_callback6, cast(void*)_id); 407 if ( done ) 408 { 409 // resolved from files 410 debug tracef("return ready result"); 411 return ResolverResult6(status, addresses); 412 } 413 auto rc = ares_getsock(_ares_channel, &_sockets[0], ARES_GETSOCK_MAXNUM); 414 debug tracef("getsocks: 0x%04X, %s", rc, _sockets); 415 // prepare listening for socket events 416 handleGetSocks(rc, &_sockets); 417 yielded = true; 418 Fiber.yield(); 419 return ResolverResult6(status, addresses); 420 } 421 } 422 423 private void handleGetSocks(int rc, ares_socket_t[ARES_GETSOCK_MAXNUM] *s) @safe 424 { 425 for(int i; i < ARES_GETSOCK_MAXNUM;i++) 426 { 427 if (ARES_SOCK_READABLE(rc, i) && !_in_read[i]) 428 { 429 debug tracef("add ares socket %s to IN events", (*s)[i]); 430 _loop.startPoll((*s)[i], AppEvent.IN, this); 431 _in_read[i] = true; 432 } 433 else if (!ARES_SOCK_READABLE(rc, i) && _in_read[i]) 434 { 435 debug tracef("detach ares socket %s from IN events", (*s)[i]); 436 _loop.stopPoll((*s)[i], AppEvent.IN); 437 _in_read[i] = false; 438 } 439 if (ARES_SOCK_WRITABLE(rc, i) && !_in_write[i]) 440 { 441 debug tracef("add ares socket %s to OUT events", (*s)[i]); 442 _loop.startPoll((*s)[i], AppEvent.OUT, this); 443 _in_write[i] = true; 444 } 445 else if (!ARES_SOCK_WRITABLE(rc, i) && _in_write[i]) 446 { 447 debug tracef("detach ares socket %s from OUT events", (*s)[i]); 448 _loop.stopPoll((*s)[i], AppEvent.OUT); 449 _in_write[i] = true; 450 } 451 } 452 } 453 454 // 455 // call ares_process_fd for evented socket (may call completion callback), 456 // check againg for list of sockets to listen, 457 // prepare to listening. 458 // 459 override void eventHandler(int f, AppEvent ev) 460 { 461 debug tracef("handler: %d, %s", f, ev); 462 int socket_index; 463 ares_socket_t rs = ARES_SOCKET_BAD, ws = ARES_SOCKET_BAD; 464 465 if (f==_sockets[0]) 466 { 467 // 99.99 %% case 468 socket_index = 0; 469 } 470 else 471 { 472 for(socket_index=1;socket_index<ARES_GETSOCK_MAXNUM;socket_index++) 473 if (f==_sockets[socket_index]) 474 { 475 break; 476 } 477 } 478 // we'll got range violation in case we didn't find socket 479 // ares_process() can close socket for its own reasons 480 // so we have to detach descriptor. 481 // If we do not - we will have closed socket in polling system. 482 if (ev & AppEvent.OUT) 483 { 484 _loop.stopPoll(f, AppEvent.OUT); 485 _in_write[socket_index] = false; 486 ws = f; 487 } 488 if (ev & AppEvent.IN) 489 { 490 _loop.stopPoll(f, AppEvent.IN); 491 _in_read[socket_index] = false; 492 rs = f; 493 } 494 ares_process_fd(_ares_channel, rs, ws); 495 auto rc = ares_getsock(_ares_channel, &_sockets[0], ARES_GETSOCK_MAXNUM); 496 debug tracef("getsocks: 0x%04X, %s", rc, _sockets); 497 // prepare listening for socket events 498 handleGetSocks(rc, &_sockets); 499 } 500 501 /// 502 /// increment request id, 503 /// register callbacks in resolver, 504 /// start listening on sockets. 505 /// 506 auto gethostbyname(F)(string hostname, hlEvLoop loop, F cb) if (isCallable!F) 507 { 508 assert(!_loop || _loop == loop); 509 if (_loop is null) 510 { 511 _loop = loop; 512 } 513 _id++; 514 static if (isDelegate!F) 515 { 516 _cbDelegates[_id] = cb; 517 } 518 else 519 { 520 _cbFunctions[_id] = cb; 521 } 522 ares_gethostbyname(_ares_channel, toStringz(hostname), AF_INET, host_callback4, cast(void*)_id); 523 auto rc = ares_getsock(_ares_channel, &_sockets[0], ARES_GETSOCK_MAXNUM); 524 debug tracef("getsocks: 0x%04X, %s", rc, _sockets); 525 // prepare listening for socket events 526 handleGetSocks(rc, &_sockets); 527 } 528 auto gethostbyname6(F)(string hostname, hlEvLoop loop, F cb) if (isCallable!F) 529 { 530 assert(!_loop || _loop == loop); 531 if (_loop is null) 532 { 533 _loop = loop; 534 } 535 _id++; 536 static if (isDelegate!F) 537 { 538 _cb6Delegates[_id] = cb; 539 } 540 else 541 { 542 _cb6Functions[_id] = cb; 543 } 544 ares_gethostbyname(_ares_channel, toStringz(hostname), AF_INET6, host_callback6, cast(void*)_id); 545 auto rc = ares_getsock(_ares_channel, &_sockets[0], ARES_GETSOCK_MAXNUM); 546 debug tracef("getsocks: 0x%04X, %s", rc, _sockets); 547 // prepare listening for socket events 548 handleGetSocks(rc, &_sockets); 549 } 550 } 551 552 unittest 553 { 554 globalLogLevel = LogLevel.info; 555 info("=== Testing resolver ares/sync INET4 ==="); 556 auto resolver = theResolver; 557 auto r = resolver.gethostbyname("localhost"); 558 assert(r.status == 0); 559 debug tracef("%s", r); 560 r = resolver.gethostbyname("8.8.8.8"); 561 assert(r.status == 0); 562 debug tracef("%s", r); 563 r = resolver.gethostbyname("dlang.org"); 564 assert(r.status == 0); 565 debug tracef("%s", r); 566 r = resolver.gethostbyname("......."); 567 assert(r.status != 0); 568 debug tracef("%s", r); 569 if (r.status != 0) 570 { 571 tracef("status: %s", ares_statusString(r.status)); 572 } 573 r = resolver.gethostbyname("iuytkjhcxbvkjhgfaksdjf"); 574 assert(r.status != 0); 575 debug tracef("%s", r); 576 if (r.status != 0) 577 { 578 tracef("status: %s", ares_statusString(r.status)); 579 } 580 } 581 582 unittest 583 { 584 globalLogLevel = LogLevel.info; 585 info("=== Testing resolver ares/sync INET6 ==="); 586 auto resolver = theResolver; 587 auto r = resolver.gethostbyname6("localhost"); 588 assert(r.status == 0); 589 debug tracef("%s", r); 590 r = resolver.gethostbyname6("8.8.8.8"); 591 assert(r.status == 0); 592 debug tracef("%s", r); 593 r = resolver.gethostbyname6("dlang.org"); 594 assert(r.status == 0); 595 debug tracef("%s", r); 596 r = resolver.gethostbyname6("......."); 597 assert(r.status != 0); 598 debug tracef("%s", r); 599 if (r.status != 0) 600 { 601 tracef("status: %s", ares_statusString(r.status)); 602 } 603 } 604 605 unittest 606 { 607 import std.array: array; 608 globalLogLevel = LogLevel.info; 609 info("=== Testing resolver ares/async INET4 ==="); 610 auto resolver = theResolver; 611 auto app(string hostname) 612 { 613 int status; 614 InternetAddress[] adresses; 615 Fiber fiber = Fiber.getThis(); 616 bool done; 617 bool yielded; 618 619 void cb(int s, uint[] a) 620 { 621 status = s; 622 foreach (ia; a) { 623 adresses ~= new InternetAddress(ia, InternetAddress.PORT_ANY); 624 } 625 done = true; 626 debug tracef("resolve for %s: %s, %s", hostname, fromStringz(ares_strerror(s)), a); 627 if (yielded) 628 { 629 fiber.call(); 630 } 631 } 632 auto loop = getDefaultLoop(); 633 resolver.gethostbyname(hostname, loop, &cb); 634 if (!done) 635 { 636 yielded = true; 637 Fiber.yield(); 638 } 639 return adresses; 640 } 641 auto names = ["dlang.org", "google.com", ".."]; 642 auto tasks = names.map!(n => task(&app, n)).array; 643 try 644 { 645 tasks.each!(t => t.start); 646 getDefaultLoop.run(2.seconds); 647 assert(tasks.all!(t => t.ready)); 648 } 649 catch (Throwable e) 650 { 651 errorf("%s", e); 652 } 653 } 654 unittest 655 { 656 import std.array: array; 657 globalLogLevel = LogLevel.info; 658 info("=== Testing resolver ares/async INET6 ==="); 659 auto resolver = theResolver; 660 auto app(string hostname) 661 { 662 int status; 663 Internet6Address[] addresses; 664 Fiber fiber = Fiber.getThis(); 665 bool done; 666 bool yielded; 667 668 void cb(int s, ubyte[16][] *a) 669 { 670 status = s; 671 foreach (ia; *a) 672 { 673 addresses ~= new Internet6Address(ia, Internet6Address.PORT_ANY); 674 } 675 done = true; 676 debug tracef("resolve for %s: %s, %s", hostname, fromStringz(ares_strerror(s)), a); 677 if (yielded) 678 { 679 fiber.call(); 680 } 681 } 682 auto loop = getDefaultLoop(); 683 resolver.gethostbyname6(hostname, loop, &cb); 684 if (!done) 685 { 686 yielded = true; 687 Fiber.yield(); 688 } 689 return addresses; 690 } 691 auto names = ["dlang.org", "google.com", "cloudflare.com", ".."]; 692 auto tasks = names.map!(n => task(&app, n)).array; 693 try 694 { 695 tasks.each!(t => t.start); 696 getDefaultLoop.run(2.seconds); 697 assert(tasks.all!(t => t.ready)); 698 } 699 catch (Throwable e) 700 { 701 errorf("%s", e); 702 } 703 } 704 705 unittest 706 { 707 globalLogLevel = LogLevel.info; 708 info("=== Testing resolver ares/App INET4 ==="); 709 App({ 710 import std.array: array; 711 auto resolve(string name) 712 { 713 auto r = theResolver.gethostbyname(name); 714 debug tracef("app resolved %s=%s", name, r); 715 return r; 716 } 717 auto names = [ 718 "dlang.org", 719 "google.com", 720 "a.root-servers.net.", 721 "b.root-servers.net.", 722 "c.root-servers.net.", 723 "d.root-servers.net.", 724 "e.root-servers.net.", 725 "...", 726 ]; 727 auto tasks = names.map!(n => task(&resolve, n)).array; 728 tasks.each!(t => t.start); 729 tasks.each!(t => t.wait); 730 }); 731 } 732 unittest 733 { 734 globalLogLevel = LogLevel.info; 735 info("=== Testing resolver ares/App INET6 ==="); 736 App({ 737 import std.array: array; 738 auto resolve(string name) 739 { 740 auto r = theResolver.gethostbyname6(name); 741 debug tracef("app resolved %s=%s", name, r); 742 return r; 743 } 744 auto names = [ 745 "dlang.org", 746 "google.com", 747 "a.root-servers.net.", 748 "b.root-servers.net.", 749 "c.root-servers.net.", 750 "d.root-servers.net.", 751 "e.root-servers.net.", 752 ".....", 753 ]; 754 auto tasks = names.map!(n => task(&resolve, n)).array; 755 tasks.each!(t => t.start); 756 tasks.each!(t => t.wait); 757 //tasks.each!(t => writeln(t.result.status)); 758 }); 759 }