1 /******************************************************************************* 2 3 Contains API to obtain various information about the running application 4 from /proc VFS. 5 6 Copyright: 7 Copyright (c) 2009-2017 dunnhumby Germany GmbH. 8 All rights reserved. 9 10 License: 11 Boost Software License Version 1.0. See LICENSE_BOOST.txt for details. 12 Alternatively, this file may be distributed under the terms of the Tango 13 3-Clause BSD License (see LICENSE_BSD.txt for details). 14 15 *******************************************************************************/ 16 17 module ocean.sys.stats.linux.ProcVFS; 18 19 import ocean.meta.types.Qualifiers; 20 import Path = ocean.io.Path; 21 import ocean.sys.ErrnoException; 22 import core.sys.posix.stdio; 23 import core.sys.posix.dirent; 24 import core.stdc.errno; 25 import ocean.core.Tuple; 26 import ocean.io.device.File; 27 import ocean.text.Search; 28 import ocean.text.convert.Integer; 29 import ocean.core.Buffer; 30 import ocean.core.Enforce; 31 import core.stdc.stdlib; 32 import ocean.meta.traits.Basic; 33 import ocean.meta.codegen.Identifier; 34 import ocean.text.util.StringC; 35 36 version (unittest) 37 { 38 import ocean.core.Test; 39 } 40 41 private 42 { 43 // TODO: use druntime bindings when available 44 extern (C) ssize_t getline (char** lineptr, size_t* n, FILE* stream); 45 } 46 47 /******************************************************************************* 48 49 Reusable procvfs_exception instance. 50 51 *******************************************************************************/ 52 53 private ErrnoException procvfs_exception; 54 55 /******************************************************************************* 56 57 Buffer for processing files. 58 59 *******************************************************************************/ 60 61 private Buffer!(char) procvfs_file_buf; 62 63 /*************************************************************************** 64 65 Gets the number of the fd open in a process. 66 67 Note: 68 In order to get the number of open files, this method iterates 69 through the directory entires in /proc VFS. This implies that it should 70 not be called multiple times a second, as there might be performance 71 implications. Ideally, it's called every 30 seconds, or so, just 72 to generate the stats.log as needed. 73 74 Returns: 75 number of the open file descriptors 76 77 ***************************************************************************/ 78 79 public int getOpenFdCount () 80 { 81 int count; 82 83 auto dir = opendir("/proc/self/fdinfo".ptr); 84 85 if (dir is null) 86 return 0; 87 88 scope (exit) 89 { 90 // ignore EBADF 91 closedir(dir); 92 } 93 94 /* 95 This uses `readdir` and not `readdir_r` since the latter 96 is de-facto deprecated. 97 98 From http://man7.org/linux/man-pages/man3/readdir_r.3.html 99 100 In the current POSIX.1 specification (POSIX.1-2008), readdir(3) is 101 not required to be thread-safe. However, in modern 102 implementations (including the glibc implementation), concurrent 103 calls to readdir(3) that specify different directory streams are 104 thread-safe. Therefore, the use of readdir_r() is generally 105 unnecessary in multithreaded programs. In cases where multiple 106 threads must read from the same directory stream, using readdir(3) 107 with external synchronization is still preferable to the use of 108 readdir_r(), for the reasons given in the points above. 109 */ 110 dirent* dp; 111 while ((dp = readdir(dir)) !is null) 112 { 113 auto entry_name = StringC.toDString(dp.d_name.ptr); 114 if (entry_name != "." && entry_name != "..") 115 { 116 count++; 117 } 118 } 119 120 return count; 121 } 122 123 /******************************************************************************* 124 125 Information reported by /proc/pid/stat file 126 127 Information about individual fields can be found in `man 5 proc`: 128 http://man7.org/linux/man-pages/man5/proc.5.html 129 130 *******************************************************************************/ 131 132 struct ProcStat 133 { 134 int pid; 135 mstring cmd; 136 char state; 137 int ppid; 138 int pgrp; 139 int session; 140 int tty_nr; 141 int tpgid; 142 uint flags; 143 ulong minflt; 144 ulong cminflt; 145 ulong majflt; 146 ulong cmajflt; 147 ulong utime; 148 ulong stime; 149 long cutime; 150 long cstime; 151 long priority; 152 long nice; 153 long num_threads; 154 long itrealvalue; 155 ulong starttime; 156 ulong vsize; 157 ulong rss; 158 ulong rsslim; 159 ulong startcode; 160 ulong endcode; 161 ulong startstack; 162 ulong kstkesp; 163 ulong kstkeip; 164 ulong signal; 165 ulong blocked; 166 ulong sigingore; 167 ulong sigcatch; 168 ulong wchan; 169 ulong nswap; 170 ulong cnswap; 171 int exit_signal; 172 int processor; 173 uint rt_priority; 174 uint policy; 175 ulong delayacct_blkio_ticks; 176 ulong guest_time; 177 long cguest_time; 178 ulong start_data; 179 ulong end_data; 180 ulong start_brk; 181 ulong arg_start; 182 ulong arg_end; 183 ulong env_start; 184 ulong env_end; 185 int exit_code; 186 } 187 188 189 /******************************************************************************* 190 191 Reads /proc/<pid>/stat and extracts the data. 192 193 Params: 194 path = path of the file to query 195 196 Returns: 197 filled ProcStat structure based on /proc/self/stat or empty 198 ProcStat instance in case parsing has failed. 199 200 *******************************************************************************/ 201 202 public ProcStat getProcStat (cstring path) 203 { 204 ProcStat s; 205 return getProcStat(path, s) ? s : ProcStat.init; 206 } 207 208 /******************************************************************************* 209 210 Reads /proc/<pid>/stat and extracts the data. 211 212 Params: 213 path = path of the file to query 214 stats = struct instance where to extract data 215 216 Returns: 217 'false' if parsing has failed, 'true' otherwise 218 219 *******************************************************************************/ 220 221 public bool getProcStat (cstring path, ref ProcStat stat) 222 { 223 // Get the data from file 224 scope file = new File(path); 225 226 // stat(2) generally on the special files returns 0, 227 // so we can't figure out in advance how much to allocate 228 procvfs_file_buf.length = 8192; // Should be enough to cover maximum path of the 229 // executable stored in the file 230 auto num_read = file.read(procvfs_file_buf[]); 231 enforce(*procvfs_file_buf[num_read-1] == '\n'); 232 233 char[] data = procvfs_file_buf[0..num_read]; 234 235 return parseProcStatData(data, stat); 236 } 237 238 /******************************************************************************* 239 240 Parses /proc/self/stat and extracts the data. 241 242 Returns: 243 filled ProcStat structure based on /proc/self/stat or empty 244 ProcStat instance in case parsing has failed. 245 246 *******************************************************************************/ 247 248 public ProcStat getProcSelfStat () 249 { 250 ProcStat s; 251 return getProcSelfStat(s) ? s : ProcStat.init; 252 } 253 254 /******************************************************************************* 255 256 Parses /proc/self/stat and extracts the data. 257 258 Params: 259 stat = reusable struct instance to extract data to 260 261 Returns: 262 'false' if parsing has failed, 'true' otherwise 263 264 *******************************************************************************/ 265 266 public bool getProcSelfStat (ref ProcStat stat) 267 { 268 return getProcStat("/proc/self/stat", stat); 269 } 270 271 /******************************************************************************* 272 273 Structure representing data found in /proc/uptime. 274 275 Contains the uptime of the system and the amount of time spent in idle 276 process, both in seconds. 277 278 *******************************************************************************/ 279 280 public struct ProcUptime 281 { 282 /*************************************************************************** 283 284 Helper struct containing whole and fractional part of the second. 285 286 ***************************************************************************/ 287 288 public struct Time 289 { 290 import ocean.math.Math: abs; 291 292 /// Whole part of seconds 293 public long seconds; 294 /// Fractional part of the second 295 public long cents; 296 297 /*********************************************************************** 298 299 Returns: 300 Floating point representation of the Time struct 301 302 ***********************************************************************/ 303 304 public double as_double () 305 { 306 return seconds + cents / 100.0; 307 } 308 309 310 /*********************************************************************** 311 312 Params: 313 rhs = Time value to subtract 314 315 Returns: 316 current time subtracted by rhs 317 318 ***********************************************************************/ 319 320 Time opBinary ( string op : "-" ) (Time rhs) 321 { 322 Time res_time; 323 324 auto t = (this.seconds * 100 + this.cents) - 325 (rhs.seconds * 100 + rhs.cents); 326 327 res_time.seconds = t / 100; 328 res_time.cents = abs(t) % 100; 329 330 return res_time; 331 } 332 333 unittest 334 { 335 auto t1 = Time(2, 0); 336 auto t2 = Time(1, 0); 337 test!("==")(t1 - t2, Time(1, 0)); 338 339 t1 = Time(2, 0); 340 t2 = Time(2, 0); 341 test!("==")(t1 - t2, Time(0, 0)); 342 343 t1 = Time(1, 0); 344 t2 = Time(2, 0); 345 test!("==")(t1 - t2, Time(-1, 0)); 346 347 t1 = Time(2, 50); 348 t2 = Time(2, 30); 349 test!("==")(t1 - t2, Time(0, 20)); 350 351 t1 = Time(2, 30); 352 t2 = Time(1, 50); 353 test!("==")(t1 - t2, Time(0, 80)); 354 355 t1 = Time(2, 50); 356 t2 = Time(1, 50); 357 test!("==")(t1 - t2, Time(1, 0)); 358 359 t1 = Time(1, 30); 360 t2 = Time(2, 50); 361 test!("==")(t1 - t2, Time(-1, 20)); 362 363 } 364 } 365 366 /// Uptime time 367 public Time uptime; 368 369 /// Idle time 370 public Time idle; 371 } 372 373 unittest 374 { 375 auto t = ProcUptime.Time(1, 50); 376 test!("==")(t.as_double(), 1.5); 377 t.cents = 25; 378 test!("==")(t.as_double(), 1.25); 379 t.cents = 0; 380 test!("==")(t.as_double, 1.0); 381 } 382 383 /******************************************************************************* 384 385 Parses and returns data found in /proc/uptime. 386 387 Returns: 388 ProcUptime instance representing /proc/uptime 389 390 Throws: 391 ErrnoException on failure to parse this file. 392 393 *******************************************************************************/ 394 395 public ProcUptime getProcUptime () 396 { 397 auto f = fopen("/proc/uptime".ptr, "r".ptr); 398 399 if (!f) 400 { 401 throwException("fopen"); 402 } 403 404 scope (exit) fclose(f); 405 406 ProcUptime t; 407 if (fscanf(f, "%lld.%lld %lld.%lld\n".ptr, 408 &t.uptime.seconds, 409 &t.uptime.cents, 410 &t.idle.seconds, 411 &t.idle.cents) != 4) 412 { 413 throwException("fscanf"); 414 } 415 416 return t; 417 } 418 419 /******************************************************************************* 420 421 Information reported by /proc/meminfo file 422 423 Information about individual fields can be found in `man 5 proc`: 424 http://man7.org/linux/man-pages/man5/proc.5.html 425 426 *******************************************************************************/ 427 428 struct ProcMemInfo 429 { 430 ulong MemTotal; 431 ulong MemFree; 432 ulong MemAvailable; 433 ulong Buffers; 434 ulong Cached; 435 ulong SwapCached; 436 ulong Active; 437 ulong Inactive; 438 ulong Unevictable; 439 ulong Mlocked; 440 ulong HighTotal; 441 ulong HighFree; 442 ulong LowTotal; 443 ulong LowFree; 444 ulong MmapCopy; 445 ulong SwapTotal; 446 ulong SwapFree; 447 ulong Dirty; 448 ulong Writeback; 449 ulong AnonPages; 450 ulong Mapped; 451 ulong Shmem; 452 ulong Slab; 453 ulong SReclaimable; 454 ulong SUnreclaim; 455 ulong KernelStack; 456 ulong PageTables; 457 ulong QuickList; 458 ulong NFS_Unstable; 459 ulong Bounce; 460 ulong WritebackTmp; 461 ulong CommitLimit; 462 ulong Committed_As; 463 ulong VmallocTotal; 464 ulong VmallocUsed; 465 ulong VmallocChunk; 466 } 467 468 /******************************************************************************* 469 470 Returns: 471 Filled in ProcMemInfo structure 472 473 Throws: 474 ErrnoException on failure to parse this file. 475 476 *******************************************************************************/ 477 478 public ProcMemInfo getProcMemInfo () 479 { 480 /*************************************************************************** 481 482 Buffer used for getline reading. Since getline may reallocate buffer, 483 this must be heap allocate and managed with malloc/free. 484 485 ***************************************************************************/ 486 487 static struct getline_buffer_t 488 { 489 /// Pointer to the buffer 490 char* ptr; 491 /// size of the buffer 492 size_t length; 493 } 494 495 // Instance of the getline_buffer_t. 496 static getline_buffer_t getline_buf; 497 498 auto f = fopen("/proc/meminfo".ptr, "r".ptr); 499 500 if (!f) 501 { 502 throwException("fopen"); 503 } 504 505 scope (exit) fclose(f); 506 507 cstring get_next_line () 508 { 509 ssize_t read = getline(&getline_buf.ptr, &getline_buf.length, f); 510 511 if (read <= 0) 512 { 513 return null; 514 } 515 516 // getline gets the last \n 517 auto data = getline_buf.ptr[0 .. read - 1]; 518 519 return data; 520 } 521 522 return parseProcMemInfoData(&get_next_line); 523 } 524 525 /******************************************************************************* 526 527 Returns: 528 size of machine's RAM in bytes, as reported in /proc/meminfo 529 530 Throws: 531 ErrnoException on failure to parse this file. 532 533 *******************************************************************************/ 534 535 public ulong getTotalMemoryInBytes () 536 { 537 return getProcMemInfo().MemTotal; 538 } 539 540 /******************************************************************************* 541 542 Initializes .exception object if necessary and throws it, carrying the 543 last errno. 544 545 Params: 546 name = name of the method that failed 547 548 *******************************************************************************/ 549 550 private void throwException( istring name ) 551 { 552 auto saved_errno = .errno; 553 554 if (.procvfs_exception is null) 555 { 556 .procvfs_exception = new ErrnoException(); 557 } 558 559 throw .procvfs_exception.set(saved_errno, name); 560 } 561 562 /******************************************************************************* 563 564 Provides parsing for /proc/meminfo data. 565 566 Params: 567 read_next_line = delegate providing next line, or `null` when 568 no more data is to be parsed 569 570 Returns: 571 Filled in ProcMemInfo structure 572 573 *******************************************************************************/ 574 575 private ProcMemInfo parseProcMemInfoData (scope cstring delegate() read_next_line) 576 { 577 /// Helper function to strip leading spaces 578 /// Params: 579 /// data: string to strip spaces from 580 /// Returns: 581 /// passed string with no leading spaces 582 cstring strip_spaces (cstring data) 583 { 584 while (data.length && data[0] == ' ') 585 { 586 data = data[1..$]; 587 } 588 589 return data; 590 } 591 592 ProcMemInfo meminfo; 593 594 cstring colon = ":"; 595 cstring space = " "; 596 auto colon_it = find(colon); 597 auto space_it = find(space); 598 599 while (true) 600 { 601 auto data = read_next_line(); 602 603 if (data.length == 0) 604 { 605 break; 606 } 607 608 // MemTotal: 32 kB 609 auto colon_pos = colon_it.forward(data); 610 auto field_name = data[0..colon_pos]; 611 612 // Swallow all spaces between colon and number 613 data = data[colon_pos+1..$]; 614 enforce(data.length); 615 data = strip_spaces(data); 616 617 // Find separator between value and units 618 auto space_pos = space_it.forward(data); 619 auto field_value = data[0..space_pos]; 620 auto field_units = space_pos < data.length ? 621 strip_spaces(data[space_pos + 1 .. $]) : ""; 622 623 /*********************************************************************** 624 625 Multiplies value by the power of two derived from units. 626 627 Params: 628 value = value to adjust 629 units = units to take in acount 630 631 ***********************************************************************/ 632 633 static void adjustUnits (ref ulong value, cstring units) 634 { 635 if (units.length == 0) 636 return; 637 638 switch (units[0]) 639 { 640 case 'k': 641 value *= 1024; 642 break; 643 case 'M': 644 value *= 1024 * 1024; 645 break; 646 case 'G': 647 value *= 1024 * 1024 * 1024; 648 break; 649 case 'T': 650 value *= 1024 * 1024 * 1024 * 1024; 651 break; 652 default: 653 break; 654 } 655 } 656 657 switch (field_name) 658 { 659 foreach (i, FieldT; typeof(meminfo.tupleof)) 660 { 661 case identifier!(typeof(meminfo.init).tupleof[i]): 662 // Using tupleof[i] instead of field 663 // as a workaround for 664 // https://issues.dlang.org/show_bug.cgi?id=16521 665 toInteger(field_value, meminfo.tupleof[i]); 666 adjustUnits(meminfo.tupleof[i], field_units); 667 break; 668 } 669 670 default: 671 break; 672 } 673 } 674 675 return meminfo; 676 } 677 678 version (unittest) 679 { 680 import ocean.io.Stdout; 681 } 682 683 unittest 684 { 685 // test empty string 686 cstring empty () { return null; } 687 test!("==")(parseProcMemInfoData(&empty), ProcMemInfo.init); 688 689 // test input in expected format, all lines supported 690 cstring[] data = 691 [ 692 "MemTotal: 8131024 kB", 693 "MemFree: 35664 MB", 694 "MemAvailable: 4 GB", 695 "Buffers: 1 " 696 ]; 697 698 int line_read; 699 cstring read_data(cstring[] data) { 700 if (line_read >= data.length) 701 return null; 702 return data[line_read++]; 703 } 704 705 auto res = parseProcMemInfoData({ return read_data(data); }); 706 707 ProcMemInfo expected; 708 expected.MemTotal = 8131024UL * 1024; 709 expected.MemFree = 35664UL * 1024 * 1024; 710 expected.MemAvailable = 4UL * 1024 * 1024 * 1024; 711 expected.Buffers = 1UL; 712 713 test!("==")(res, expected); 714 715 // test input in expected format, but add some 716 // lines that don't correspond ProcMemInfo fields (kernel update) 717 cstring[] new_data = 718 [ 719 "SomeNewStat: 11111 kB", 720 "MemTotal: 8131024 kB", 721 "MemFree: 35664 MB", 722 "MemAvailable: 4 GB", 723 "ExtraStats: 11111 kB", 724 "Buffers: 1 " 725 ]; 726 727 line_read = 0; 728 res = parseProcMemInfoData({ return read_data(new_data); }); 729 test!("==")(res, expected); 730 731 // Let's see if we can handle misalignment 732 // Separator between name and value is ':', 733 // and between value and units is ' '. Any number of spaces 734 // should be irrelevant 735 data = 736 [ 737 "MemTotal:8131024 kB", 738 "MemFree:35664 MB", 739 "MemAvailable: 4 GB", 740 "Buffers: 1 " 741 ]; 742 743 line_read = 0; 744 res = parseProcMemInfoData({ return read_data(data); }); 745 test!("==")(res, expected); 746 } 747 748 /******************************************************************************* 749 750 Parses /proc/<pid>/stat data and extracts the data. 751 752 Params: 753 path = path of the file to query 754 s = reused ProcStat struct instance, all its array/string field will 755 be reset to 0 length and overwritten 756 757 Returns: 758 'false' if parsing has failed, 'true' otherwise 759 760 *******************************************************************************/ 761 762 private bool parseProcStatData (cstring data, ref ProcStat s) 763 { 764 cstring space = " "; 765 cstring parenth = ")"; 766 767 auto space_it = find(space); 768 auto parenth_it = find(parenth); 769 770 // The /proc/self/stat file is based on the following format 771 // (consult the man proc(5) page for details): 772 // pid (<cmd>) C # # # # .... # 773 // Where pid is a number, cmd is a file name, with arbitrary amount of 774 // spaces, but always with parentheses, 775 // C being the character describing the status of the program 776 // and # being again numbers, for every of the entries in the ProcStat 777 778 // consume pid 779 auto pid_pos = space_it.forward(data); 780 if (pid_pos == data.length) 781 return false; 782 783 toInteger(data[0..pid_pos], s.pid); 784 785 // chop pid 786 data = data[pid_pos+1..$]; 787 788 // chop the left bracket from cmd name 789 data = data[1 .. $]; 790 791 // Find the last closing bracket (as the process name can contain bracket 792 // itself. 793 auto last_bracket = parenth_it.reverse(data); 794 // There should be space for the space and process status ("(cat) R ") 795 if (last_bracket >= data.length - 3) 796 return false; 797 798 s.cmd.length = last_bracket; 799 assumeSafeAppend(s.cmd); 800 s.cmd[] = data[0..last_bracket]; 801 802 // chop last bracket and space 803 data = data[last_bracket+2..$]; 804 805 s.state = data[0]; 806 807 // Cut the process status and the trailing space 808 data = data[2..$]; 809 810 // Now we have a list of numbers, parse one after another 811 foreach (i, ref field; s.tupleof) 812 { 813 static if (i > 2) 814 { 815 static assert (isIntegerType!(typeof(field))); 816 auto next_space = space_it.forward(data); 817 toInteger(data[0..next_space], field); 818 819 // Last field doesn't have space after it 820 if (next_space < data.length) 821 { 822 data = data[next_space+1..$]; 823 } 824 else 825 { 826 break; 827 } 828 } 829 } 830 831 return true; 832 } 833 834 /******************************************************************************* 835 836 Convenience wrapper over another `parseProcStatData` overload for cases when 837 repeated allocation is not a problem. 838 839 *******************************************************************************/ 840 841 private ProcStat parseProcStatData ( cstring data ) 842 { 843 ProcStat s; 844 return parseProcStatData(data, s) ? s : ProcStat.init; 845 } 846 847 unittest 848 { 849 // Test with invalid inputs, all should return empty struct 850 auto data = "13300 (cat R 32559 13300 32559 34817 13300 4194304 116 0 0 0 "[]; 851 test!("==")(parseProcStatData(data), ProcStat.init); 852 853 data = ""; 854 test!("==")(parseProcStatData(data), ProcStat.init); 855 856 data = "13300 (cat)"; 857 test!("==")(parseProcStatData(data), ProcStat.init); 858 859 // Test with no fields except process state 860 data = "13300 (cat) R"; 861 test!("==")(parseProcStatData(data), ProcStat.init); 862 863 // Test with just three first fields filled 864 data = "13300 (cat) R 32559 13300 32559"; 865 auto res = parseProcStatData(data); 866 867 // Compare the strings separatelly 868 test!("==")(res.cmd, "cat"); 869 870 // workaround for easy comparing structs without going into field by field 871 // comparison 872 char[] empty_string = "".dup; 873 res.cmd = empty_string; 874 test!("==")(res, ProcStat(13300, empty_string, 'R', 32559, 13300, 32559)); 875 876 // Test will full data 877 data = "13300 (cat) R 32559 13300 32559 34817 13300 4194304 116 0 0 0 " 878 ~ "0 0 0 0 20 0 1 0 28524130 9216000 173 18446744073709551615 4194304 " 879 ~ "4240332 140734453962304 140734453961656 139949142898304 0 0 0 0 0 " 880 ~ "0 0 17 1 0 0 0 0 0 6340112 6341364 21700608 140734453969735 " 881 ~ "140734453969755 140734453969755 140734453972975 0"; 882 883 res = parseProcStatData(data); 884 test!("==")(res.cmd, "cat"); 885 res.cmd = empty_string; 886 887 test!("==")(res, ProcStat(13300, empty_string, 'R', 32559, 13300, 32559, 34817, 13300, 888 4194304, 116, 0, 0, 0, 0, 0, 0, 0, 20, 0, 1, 0, 28524130, 889 9216000, 173, 18446744073709551615UL, 4194304, 4240332, 890 140734453962304, 140734453961656, 139949142898304, 0, 0, 0, 0, 891 0, 0, 0, 17, 1, 0, 0, 0, 0, 0, 6340112, 6341364, 21700608, 892 140734453969735, 140734453969755, 140734453969755, 893 140734453972975, 0)); 894 }