1 /*
   2  * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import java.io.BufferedReader;
  25 import java.io.File;
  26 import java.io.IOException;
  27 import java.io.UncheckedIOException;
  28 import java.nio.file.Files;
  29 import java.nio.file.Path;
  30 import java.nio.file.Paths;
  31 import java.nio.file.attribute.UserPrincipal;
  32 import java.time.Duration;
  33 import java.time.Instant;
  34 import java.util.ArrayList;
  35 import java.util.Arrays;
  36 import java.util.List;
  37 import java.util.Objects;
  38 import java.util.Optional;
  39 import java.util.Random;
  40 import java.util.concurrent.TimeUnit;
  41 
  42 import jdk.test.lib.Platform;
  43 import jdk.test.lib.Utils;
  44 import org.testng.Assert;
  45 import org.testng.TestNG;
  46 import org.testng.annotations.Test;
  47 
  48 /*
  49  * @test
  50  * @bug 8077350 8081566 8081567 8098852 8136597
  51  * @library /test/lib
  52  * @modules java.base/jdk.internal.misc
  53  *          jdk.management
  54  * @build jdk.test.lib.Utils
  55  *        jdk.test.lib.Asserts
  56  *        jdk.test.lib.JDKToolFinder
  57  *        jdk.test.lib.JDKToolLauncher
  58  *        jdk.test.lib.Platform
  59  *        jdk.test.lib.process.*
  60  * @run testng InfoTest
  61  * @summary Functions of ProcessHandle.Info
  62  * @author Roger Riggs
  63  */
  64 
  65 public class InfoTest {
  66 
  67     static String whoami;
  68 
  69     static {
  70         try {
  71             // Create a file and take the username from the file
  72             Path p = Paths.get("OwnerName.tmp");
  73             Files.createFile(p);
  74             UserPrincipal owner = Files.getOwner(p);
  75             whoami = owner.getName();
  76             Files.delete(p);
  77         } catch (IOException ex) {
  78             ex.printStackTrace();
  79             throw new UncheckedIOException("tmp file", ex);
  80         }
  81     }
  82 
  83     // Main can be used to run the tests from the command line with only testng.jar.
  84     @SuppressWarnings("raw_types")
  85     public static void main(String[] args) {
  86         Class<?>[] testclass = {InfoTest.class};
  87         TestNG testng = new TestNG();
  88         testng.setTestClasses(testclass);
  89         testng.run();
  90     }
  91 
  92     /**
  93      * Test that cputime used shows up in ProcessHandle.info
  94      */
  95     @Test
  96     public static void test1() {
  97         System.out.println("Note: when run in samevm mode the cputime of the " +
  98                 "test runner is included.");
  99         ProcessHandle self = ProcessHandle.current();
 100 
 101         Duration someCPU = Duration.ofMillis(200L);
 102         Instant end = Instant.now().plus(someCPU);
 103         while (Instant.now().isBefore(end)) {
 104             // waste the cpu
 105         }
 106         ProcessHandle.Info info = self.info();
 107         System.out.printf(" info: %s%n", info);
 108         Optional<Duration> totalCpu = info.totalCpuDuration();
 109         if (totalCpu.isPresent() && (totalCpu.get().compareTo(someCPU) < 0)) {
 110             Assert.fail("reported cputime less than expected: " + someCPU + ", " +
 111                     "actual: " + info.totalCpuDuration());
 112         }
 113     }
 114 
 115     /**
 116      * Spawn a child with arguments and check they are visible via the ProcessHandle.
 117      */
 118     @Test
 119     public static void test2() {
 120         try {
 121             long cpuLoopTime = 100;             // 100 ms
 122             String[] extraArgs = {"pid", "parent", "stdin"};
 123             JavaChild p1 = JavaChild.spawnJavaChild((Object[])extraArgs);
 124             Instant afterStart = null;
 125 
 126             try (BufferedReader lines = p1.outputReader()) {
 127                 // Read the args line to know the subprocess has started
 128                 lines.readLine();
 129                 afterStart = Instant.now();
 130 
 131                 Duration lastCpu = Duration.ofMillis(0L);
 132                 for (int j = 0; j < 10; j++) {
 133 
 134                     p1.sendAction("cpuloop", cpuLoopTime);
 135                     p1.sendAction("cputime", "");
 136 
 137                     // Read cputime from child
 138                     Duration childCpuTime = null;
 139                     // Read lines from the child until the result from cputime is returned
 140                     for (String s; (s = lines.readLine()) != null;) {
 141                         String[] split = s.trim().split(" ");
 142                         if (split.length == 3 && split[1].equals("cputime")) {
 143                             long nanos = Long.valueOf(split[2]);
 144                             childCpuTime = Duration.ofNanos(nanos);
 145                             break;      // found the result we're looking for
 146                         }
 147                     }
 148 
 149                     if (Platform.isAix()) {
 150                         // Unfortunately, on AIX the usr/sys times reported through
 151                         // /proc/<pid>/psinfo which are used by ProcessHandle.Info
 152                         // are running slow compared to the corresponding times reported
 153                         // by the times()/getrusage() system calls which are used by
 154                         // OperatingSystemMXBean.getProcessCpuTime() and returned by
 155                         // the JavaChild for the "cputime" command.
 156                         // This is because /proc/<pid>/status is only updated once a second.
 157                         // So we better wait a little bit to get plausible values here.
 158                         Thread.sleep(1000);
 159                     }
 160                     ProcessHandle.Info info = p1.info();
 161                     System.out.printf(" info: %s%n", info);
 162 
 163                     if (info.user().isPresent()) {
 164                         String user = info.user().get();
 165                         Assert.assertNotNull(user, "User name");
 166                         Assert.assertEquals(user, whoami, "User name");
 167                     }
 168 
 169                     Optional<String> command = info.command();
 170                     if (command.isPresent()) {
 171                         String javaExe = System.getProperty("test.jdk") +
 172                                 File.separator + "bin" + File.separator + "java";
 173                         String expected = Platform.isWindows() ? javaExe + ".exe" : javaExe;
 174                         Path expectedPath = Paths.get(expected);
 175                         Path actualPath = Paths.get(command.get());
 176                         Assert.assertTrue(Files.isSameFile(expectedPath, actualPath),
 177                                 "Command: expected: " + javaExe + ", actual: " + command.get());
 178                     }
 179 
 180                     if (info.arguments().isPresent()) {
 181                         String[] args = info.arguments().get();
 182 
 183                         int offset = args.length - extraArgs.length;
 184                         for (int i = 0; i < extraArgs.length; i++) {
 185                             Assert.assertEquals(args[offset + i], extraArgs[i],
 186                                                 "Actual argument mismatch, index: " + i);
 187                         }
 188 
 189                         // Now check that the first argument is not the same as the executed command
 190                         if (args.length > 0) {
 191                             Assert.assertNotEquals(args[0], command,
 192                                     "First argument should not be the executable: args[0]: "
 193                                             + args[0] + ", command: " + command);
 194                         }
 195                     }
 196 
 197                     if (command.isPresent() && info.arguments().isPresent()) {
 198                         // If both, 'command' and 'arguments' are present,
 199                         // 'commandLine' is just the concatenation of the two.
 200                         Assert.assertTrue(info.commandLine().isPresent(),
 201                                           "commandLine() must be available");
 202 
 203                         String javaExe = System.getProperty("test.jdk") +
 204                                 File.separator + "bin" + File.separator + "java";
 205                         String expected = Platform.isWindows() ? javaExe + ".exe" : javaExe;
 206                         Path expectedPath = Paths.get(expected);
 207                         String commandLine = info.commandLine().get();
 208                         String commandLineCmd = commandLine.split(" ")[0];
 209                         Path commandLineCmdPath = Paths.get(commandLineCmd);
 210                         Assert.assertTrue(Files.isSameFile(commandLineCmdPath, expectedPath),
 211                                           "commandLine() should start with: " + expectedPath +
 212                                           " but starts with " + commandLineCmdPath);
 213 
 214                         Assert.assertTrue(commandLine.contains(command.get()),
 215                                 "commandLine() must contain the command: " + command.get());
 216                         List<String> allArgs = p1.getArgs();
 217                         for (int i = 1; i < allArgs.size(); i++) {
 218                             Assert.assertTrue(commandLine.contains(allArgs.get(i)),
 219                                               "commandLine() must contain argument: " + allArgs.get(i));
 220                         }
 221                     } else if (info.commandLine().isPresent() &&
 222                             command.isPresent() &&
 223                             command.get().length() < info.commandLine().get().length()) {
 224                         // If we only have the commandLine() we can only do some basic checks...
 225                         String commandLine = info.commandLine().get();
 226                         String javaExe = "java" + (Platform.isWindows() ? ".exe" : "");
 227                         int pos = commandLine.indexOf(javaExe);
 228                         Assert.assertTrue(pos > 0, "commandLine() should at least contain 'java'");
 229 
 230                         pos += javaExe.length() + 1; // +1 for the space after the command
 231                         List<String> allArgs = p1.getArgs();
 232                         // First argument is the command - skip it here as we've already checked that.
 233                         for (int i = 1; (i < allArgs.size()) &&
 234                                         (pos + allArgs.get(i).length() < commandLine.length()); i++) {
 235                             Assert.assertTrue(commandLine.contains(allArgs.get(i)),
 236                                               "commandLine() must contain argument: " + allArgs.get(i));
 237                             pos += allArgs.get(i).length() + 1;
 238                         }
 239                     }
 240 
 241                     if (info.totalCpuDuration().isPresent()) {
 242                         Duration totalCPU = info.totalCpuDuration().get();
 243                         Duration epsilon = Duration.ofMillis(200L);
 244                         if (childCpuTime != null) {
 245                             System.out.printf(" info.totalCPU: %s, childCpuTime: %s, diff: %s%n",
 246                                     totalCPU.toNanos(), childCpuTime.toNanos(),
 247                                     childCpuTime.toNanos() - totalCPU.toNanos());
 248                             Assert.assertTrue(checkEpsilon(childCpuTime, totalCPU, epsilon),
 249                                     childCpuTime + " should be within " +
 250                                             epsilon + " of " + totalCPU);
 251                         }
 252                         Assert.assertTrue(totalCPU.toNanos() > 0L,
 253                                 "total cpu time expected > 0ms, actual: " + totalCPU);
 254                         long t = Utils.adjustTimeout(10L);  // Adjusted timeout seconds
 255                         Assert.assertTrue(totalCPU.toNanos() < lastCpu.toNanos() + t * 1_000_000_000L,
 256                                 "total cpu time expected < " + t
 257                                         + " seconds more than previous iteration, actual: "
 258                                         + (totalCPU.toNanos() - lastCpu.toNanos()));
 259                         lastCpu = totalCPU;
 260                     }
 261 
 262                     if (info.startInstant().isPresent()) {
 263                         Instant startTime = info.startInstant().get();
 264                         Assert.assertTrue(startTime.isBefore(afterStart),
 265                                 "startTime after process spawn completed"
 266                                         + startTime + " + > " + afterStart);
 267                     }
 268                 }
 269             }
 270             p1.sendAction("exit");
 271             Assert.assertTrue(p1.waitFor(Utils.adjustTimeout(30L), TimeUnit.SECONDS),
 272                     "timeout waiting for process to terminate");
 273         } catch (IOException | InterruptedException ie) {
 274             ie.printStackTrace(System.out);
 275             Assert.fail("unexpected exception", ie);
 276         } finally {
 277             // Destroy any children that still exist
 278             ProcessUtil.destroyProcessTree(ProcessHandle.current());
 279         }
 280     }
 281 
 282     /**
 283      * Spawn a child with arguments and check they are visible via the ProcessHandle.
 284      */
 285     @Test
 286     public static void test3() {
 287         try {
 288             for (long sleepTime : Arrays.asList(Utils.adjustTimeout(30), Utils.adjustTimeout(32))) {
 289                 Process p = spawn("sleep", String.valueOf(sleepTime));
 290 
 291                 ProcessHandle.Info info = p.info();
 292                 System.out.printf(" info: %s%n", info);
 293 
 294                 if (info.user().isPresent()) {
 295                     String user = info.user().get();
 296                     Assert.assertNotNull(user);
 297                     Assert.assertEquals(user, whoami);
 298                 }
 299                 if (info.command().isPresent()) {
 300                     String command = info.command().get();
 301                     String expected = "sleep";
 302                     if (Platform.isWindows()) {
 303                         expected = "sleep.exe";
 304                     } else if (new File("/bin/busybox").exists()) {
 305                         // With busybox sleep is just a sym link to busybox.
 306                         // The busbox executable is seen as ProcessHandle.Info command.
 307                         expected = "busybox";
 308                     }
 309                     Assert.assertTrue(command.endsWith(expected), "Command: expected: \'" +
 310                             expected + "\', actual: " + command);
 311 
 312                     // Verify the command exists and is executable
 313                     File exe = new File(command);
 314                     Assert.assertTrue(exe.exists(), "command must exist: " + exe);
 315                     Assert.assertTrue(exe.canExecute(), "command must be executable: " + exe);
 316                 }
 317                 if (info.arguments().isPresent()) {
 318                     String[] args = info.arguments().get();
 319                     if (args.length > 0) {
 320                         Assert.assertEquals(args[0], String.valueOf(sleepTime));
 321                     }
 322                 }
 323                 p.destroy();
 324                 Assert.assertTrue(p.waitFor(Utils.adjustTimeout(30), TimeUnit.SECONDS),
 325                         "timeout waiting for process to terminate");
 326             }
 327         } catch (IOException | InterruptedException ex) {
 328             ex.printStackTrace(System.out);
 329         } finally {
 330             // Destroy any children that still exist
 331             ProcessUtil.destroyProcessTree(ProcessHandle.current());
 332         }
 333     }
 334 
 335     /**
 336      * Cross check the cputime reported from java.management with that for the current process.
 337      */
 338     @Test
 339     public static void test4() {
 340         Duration myCputime1 = ProcessUtil.MXBeanCpuTime();
 341 
 342         if (Platform.isAix()) {
 343             // Unfortunately, on AIX the usr/sys times reported through
 344             // /proc/<pid>/psinfo which are used by ProcessHandle.Info
 345             // are running slow compared to the corresponding times reported
 346             // by the times()/getrusage() system calls which are used by
 347             // OperatingSystemMXBean.getProcessCpuTime() and returned by
 348             // the JavaChild for the "cputime" command.
 349             // So we better wait a little bit to get plausible values here.
 350             try {
 351                 Thread.sleep(1000);
 352             } catch (InterruptedException ex) {}
 353         }
 354         Optional<Duration> dur1 = ProcessHandle.current().info().totalCpuDuration();
 355 
 356         Duration myCputime2 = ProcessUtil.MXBeanCpuTime();
 357 
 358         if (Platform.isAix()) {
 359             try {
 360                 Thread.sleep(1000);
 361             } catch (InterruptedException ex) {}
 362         }
 363         Optional<Duration> dur2 = ProcessHandle.current().info().totalCpuDuration();
 364 
 365         if (dur1.isPresent() && dur2.isPresent()) {
 366             Duration total1 = dur1.get();
 367             Duration total2 = dur2.get();
 368             System.out.printf(" total1 vs. mbean: %s, getProcessCpuTime: %s, diff: %s%n",
 369                     Objects.toString(total1), myCputime1, myCputime1.minus(total1));
 370             System.out.printf(" total2 vs. mbean: %s, getProcessCpuTime: %s, diff: %s%n",
 371                     Objects.toString(total2), myCputime2, myCputime2.minus(total2));
 372 
 373             Duration epsilon = Duration.ofMillis(200L);      // Epsilon is 200ms.
 374             Assert.assertTrue(checkEpsilon(myCputime1, myCputime2, epsilon),
 375                     myCputime1.toNanos() + " should be within " + epsilon
 376                             + " of " + myCputime2.toNanos());
 377             Assert.assertTrue(checkEpsilon(total1, total2, epsilon),
 378                     total1.toNanos() + " should be within " + epsilon
 379                             + " of " + total2.toNanos());
 380             Assert.assertTrue(checkEpsilon(myCputime1, total1, epsilon),
 381                     myCputime1.toNanos() + " should be within " + epsilon
 382                             + " of " + total1.toNanos());
 383             Assert.assertTrue(checkEpsilon(total1, myCputime2, epsilon),
 384                     total1.toNanos() + " should be within " + epsilon
 385                             + " of " + myCputime2.toNanos());
 386             Assert.assertTrue(checkEpsilon(myCputime2, total2, epsilon),
 387                     myCputime2.toNanos() + " should be within " + epsilon
 388                             + " of " + total2.toNanos());
 389         }
 390     }
 391 
 392     @Test
 393     public static void test5() {
 394         ProcessHandle self = ProcessHandle.current();
 395         Random r = new Random();
 396         for (int i = 0; i < 30; i++) {
 397             Instant end = Instant.now().plusMillis(500L);
 398             while (end.isBefore(Instant.now())) {
 399                 // burn the cpu time checking the time
 400                 long x = r.nextLong();
 401             }
 402             if (self.info().totalCpuDuration().isPresent()) {
 403                 Duration totalCpu = self.info().totalCpuDuration().get();
 404                 long infoTotalCputime = totalCpu.toNanos();
 405                 long beanCputime = ProcessUtil.MXBeanCpuTime().toNanos();
 406                 System.out.printf(" infoTotal: %12d, beanCpu: %12d, diff: %12d%n",
 407                         infoTotalCputime, beanCputime, beanCputime - infoTotalCputime);
 408             } else {
 409                 break;  // nothing to compare; continue
 410             }
 411         }
 412     }
 413     /**
 414      * Check two Durations, the second should be greater than the first or
 415      * within the supplied Epsilon.
 416      * @param d1 a Duration - presumed to be shorter
 417      * @param d2 a 2nd Duration - presumed to be greater (or within Epsilon)
 418      * @param epsilon Epsilon the amount of overlap allowed
 419      * @return true if d2 is greater than d1 or within epsilon, false otherwise
 420      */
 421     static boolean checkEpsilon(Duration d1, Duration d2, Duration epsilon) {
 422         if (d1.toNanos() <= d2.toNanos()) {
 423             return true;
 424         }
 425         Duration diff = d1.minus(d2).abs();
 426         return diff.compareTo(epsilon) <= 0;
 427     }
 428 
 429     /**
 430      * Spawn a native process with the provided arguments.
 431      * @param command the executable of native process
 432      * @param args to start a new process
 433      * @return the Process that was started
 434      * @throws IOException thrown by ProcessBuilder.start
 435      */
 436     static Process spawn(String command, String... args) throws IOException {
 437         ProcessBuilder pb = new ProcessBuilder();
 438         pb.inheritIO();
 439         List<String> list = new ArrayList<>();
 440         list.add(command);
 441         for (String arg : args)
 442             list.add(arg);
 443         pb.command(list);
 444         return pb.start();
 445     }
 446 }