1 /*
   2  * Copyright (c) 2014, 2015, 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.File;
  25 import java.io.BufferedReader;
  26 import java.io.IOException;
  27 import java.io.UncheckedIOException;
  28 import java.lang.ProcessBuilder;
  29 import java.nio.file.Files;
  30 import java.nio.file.Path;
  31 import java.nio.file.Paths;
  32 import java.nio.file.attribute.UserPrincipal;
  33 import java.time.Duration;
  34 import java.time.Instant;
  35 import java.util.ArrayList;
  36 import java.util.Arrays;
  37 import java.util.List;
  38 import java.util.Objects;
  39 import java.util.Optional;
  40 import java.util.Random;
  41 import java.util.concurrent.TimeUnit;
  42 
  43 import jdk.testlibrary.Platform;
  44 import jdk.testlibrary.Utils;
  45 
  46 import org.testng.Assert;
  47 import org.testng.annotations.Test;
  48 import org.testng.TestNG;
  49 
  50 /*
  51  * @test
  52  * @build jdk.testlibrary.*
  53  * @library /lib/testlibrary
  54  * @summary Functions of ProcessHandle.Info
  55  * @author Roger Riggs
  56  */
  57 
  58 public class InfoTest {
  59 
  60     static String whoami;
  61 
  62     static {
  63         try {
  64             // Create a file and take the username from the file
  65             Path p = Paths.get("OwnerName.tmp");
  66             Files.createFile(p);
  67             UserPrincipal owner = Files.getOwner(p);
  68             whoami = owner.getName();
  69             Files.delete(p);
  70         } catch (IOException ex) {
  71             ex.printStackTrace();
  72             throw new UncheckedIOException("tmp file", ex);
  73         }
  74     }
  75 
  76     // Main can be used to run the tests from the command line with only testng.jar.
  77     @SuppressWarnings("raw_types")
  78     public static void main(String[] args) {
  79         Class<?>[] testclass = {InfoTest.class};
  80         TestNG testng = new TestNG();
  81         testng.setTestClasses(testclass);
  82         testng.run();
  83     }
  84 
  85     /**
  86      * Test that cputime used shows up in ProcessHandle.info
  87      */
  88     @Test
  89     public static void test1() {
  90         System.out.println("Note: when run in samevm mode the cputime of the " +
  91                 "test runner is included.");
  92         ProcessHandle self = ProcessHandle.current();
  93 
  94         Duration someCPU = Duration.ofMillis(200L);
  95         Instant end = Instant.now().plus(someCPU);
  96         while (Instant.now().isBefore(end)) {
  97             // waste the cpu
  98         }
  99         ProcessHandle.Info info = self.info();
 100         System.out.printf(" info: %s%n", info);
 101         Optional<Duration> totalCpu = info.totalCpuDuration();
 102         if (totalCpu.isPresent() && (totalCpu.get().compareTo(someCPU) < 0)) {
 103             Assert.fail("reported cputime less than expected: " + someCPU + ", " +
 104                     "actual: " + info.totalCpuDuration());
 105         }
 106     }
 107 
 108     /**
 109      * Spawn a child with arguments and check they are visible via the ProcessHandle.
 110      */
 111     @Test
 112     public static void test2() {
 113         try {
 114             long cpuLoopTime = 100;             // 100 ms
 115             String[] extraArgs = {"pid", "parent", "stdin"};
 116             JavaChild p1 = JavaChild.spawnJavaChild((Object[])extraArgs);
 117             Instant afterStart = Instant.now();
 118 
 119             try (BufferedReader lines = p1.outputReader()) {
 120                 Duration lastCpu = Duration.ofMillis(0L);
 121                 for (int j = 0; j < 10; j++) {
 122 
 123                     p1.sendAction("cpuloop", cpuLoopTime);
 124                     p1.sendAction("cputime", "");
 125 
 126                     // Read cputime from child
 127                     Duration childCpuTime = null;
 128                     // Read lines from the child until the result from cputime is returned
 129                     String s;
 130                     while ((s = lines.readLine()) != null) {
 131                         String[] split = s.trim().split(" ");
 132                         if (split.length == 3 && split[1].equals("cputime")) {
 133                             long nanos = Long.valueOf(split[2]);
 134                             childCpuTime = Duration.ofNanos(nanos);
 135                             break;      // found the result we're looking for
 136                         }
 137                     }
 138 
 139                     if (Platform.isAix()) {
 140                         // Unfortunately, on AIX the usr/sys times reported through
 141                         // /proc/<pid>/status which are used by ProcessHandle.Info
 142                         // are running slow compared to the corresponding times reported
 143                         // by the times()/getrusage() system calls which are used by
 144                         // OperatingSystemMXBean.getProcessCpuTime() and returned by
 145                         // the JavaChild for the "cputime" command.
 146                         // So we better wait a little bit to get plausible values here.
 147                         Thread.sleep(1000);
 148                     }
 149                     ProcessHandle.Info info = p1.info();
 150                     System.out.printf(" info: %s%n", info);
 151 
 152                     if (info.user().isPresent()) {
 153                         String user = info.user().get();
 154                         Assert.assertNotNull(user, "User name");
 155                         Assert.assertEquals(user, whoami, "User name");
 156                     }
 157 
 158                     Optional<String> command = info.command();
 159                     if (command.isPresent()) {
 160                         String javaExe = System.getProperty("test.jdk") +
 161                                 File.separator + "bin" + File.separator + "java";
 162                         String expected = Platform.isWindows() ? javaExe + ".exe" : javaExe;
 163                         Path expectedPath = Paths.get(expected);
 164                         Path actualPath = Paths.get(command.get());
 165                         Assert.assertTrue(Files.isSameFile(expectedPath, actualPath),
 166                                 "Command: expected: " + javaExe + ", actual: " + command.get());
 167                     }
 168 
 169                     if (info.arguments().isPresent()) {
 170                         String[] args = info.arguments().get();
 171 
 172                         if (Platform.isLinux() || Platform.isOSX()) {
 173                             int offset = args.length - extraArgs.length;
 174                             for (int i = 0; i < extraArgs.length; i++) {
 175                                 Assert.assertEquals(args[offset + i], extraArgs[i],
 176                                         "Actual argument mismatch, index: " + i);
 177                             }
 178                         } else {
 179                             // Arguments on Solaris and AIX come from /proc/pid/psinfo and
 180                             // are usually truncated to 80 characters so there's not much
 181                             // we can check here.
 182                             System.out.printf("No argument test for OS: %s%n", Platform.getOsName());
 183                         }
 184 
 185                         // Now check that the first argument is not the same as the executed command
 186                         if (args.length > 0) {
 187                             Assert.assertNotEquals(args[0], command,
 188                                     "First argument should not be the executable: args[0]: "
 189                                             + args[0] + ", command: " + command);
 190                         }
 191                     }
 192 
 193                     if (info.totalCpuDuration().isPresent()) {
 194                         Duration totalCPU = info.totalCpuDuration().get();
 195                         Duration epsilon = Duration.ofMillis(200L);
 196                         if (childCpuTime != null) {
 197                             System.out.printf(" info.totalCPU: %s, childCpuTime: %s, diff: %s%n",
 198                                     totalCPU.toNanos(), childCpuTime.toNanos(),
 199                                     childCpuTime.toNanos() - totalCPU.toNanos());
 200                             Assert.assertTrue(checkEpsilon(childCpuTime, totalCPU, epsilon),
 201                                     childCpuTime + " should be within " +
 202                                             epsilon + " of " + totalCPU);
 203                         }
 204                         Assert.assertTrue(totalCPU.toNanos() > 0L,
 205                                 "total cpu time expected > 0ms, actual: " + totalCPU);
 206                         long t = Utils.adjustTimeout(10L);  // Adjusted timeout seconds
 207                         Assert.assertTrue(totalCPU.toNanos() < lastCpu.toNanos() + t * 1_000_000_000L,
 208                                 "total cpu time expected < " + t
 209                                         + " seconds more than previous iteration, actual: "
 210                                         + (totalCPU.toNanos() - lastCpu.toNanos()));
 211                         lastCpu = totalCPU;
 212                     }
 213 
 214                     if (info.startInstant().isPresent()) {
 215                         Instant startTime = info.startInstant().get();
 216                         Assert.assertTrue(startTime.isBefore(afterStart),
 217                                 "startTime after process spawn completed"
 218                                         + startTime + " + > " + afterStart);
 219                     }
 220                 }
 221             }
 222             p1.waitFor(Utils.adjustTimeout(5), TimeUnit.SECONDS);
 223         } catch (IOException | InterruptedException ie) {
 224             ie.printStackTrace(System.out);
 225             Assert.fail("unexpected exception", ie);
 226         }
 227     }
 228 
 229     /**
 230      * Spawn a child with arguments and check they are visible via the ProcessHandle.
 231      */
 232     @Test
 233     public static void test3() {
 234         try {
 235             for (int sleepTime : Arrays.asList(1, 2)) {
 236                 Process p = spawn("sleep", String.valueOf(sleepTime));
 237                 ProcessHandle.Info info = p.info();
 238                 System.out.printf(" info: %s%n", info);
 239 
 240                 if (info.user().isPresent()) {
 241                     String user = info.user().get();
 242                     Assert.assertNotNull(user);
 243                     Assert.assertEquals(user, whoami);
 244                 }
 245                 if (info.command().isPresent()) {
 246                     String command = info.command().get();
 247                     String expected = Platform.isWindows() ? "sleep.exe" : "sleep";
 248                     Assert.assertTrue(command.endsWith(expected), "Command: expected: \'" +
 249                             expected + "\', actual: " + command);
 250 
 251                     // Verify the command exists and is executable
 252                     if (!Platform.isAix()) {
 253                         // On Aix, Info.command() only returns arg[0] as command which
 254                         // doesn't necessarily contains the full path to the executable.
 255                         File exe = new File(command);
 256                         Assert.assertTrue(exe.exists(), "command must exist: " + exe);
 257                         Assert.assertTrue(exe.canExecute(), "command must be executable: " + exe);
 258                     }
 259                 }
 260                 if (info.arguments().isPresent()) {
 261                     String[] args = info.arguments().get();
 262                     if (args.length > 0) {
 263                         Assert.assertEquals(args[0], String.valueOf(sleepTime));
 264                     }
 265                 }
 266                 Assert.assertTrue(p.waitFor(15, TimeUnit.SECONDS));
 267             }
 268         } catch (IOException | InterruptedException ex) {
 269             ex.printStackTrace(System.out);
 270         } finally {
 271             // Destroy any children that still exist
 272             ProcessUtil.destroyProcessTree(ProcessHandle.current());
 273         }
 274     }
 275 
 276     /**
 277      * Cross check the cputime reported from java.management with that for the current process.
 278      */
 279     @Test
 280     public static void test4() {
 281         Duration myCputime1 = ProcessUtil.MXBeanCpuTime();
 282 
 283         if (Platform.isAix()) {
 284             // Unfortunately, on AIX the usr/sys times reported through
 285             // /proc/<pid>/status which are used by ProcessHandle.Info
 286             // are running slow compared to the corresponding times reported
 287             // by the times()/getrusage() system calls which are used by
 288             // OperatingSystemMXBean.getProcessCpuTime() and returned by
 289             // the JavaChild for the "cputime" command.
 290             // So we better wait a little bit to get plausible values here.
 291             try {
 292                 Thread.sleep(1000);
 293             } catch (InterruptedException ex) {}
 294         }
 295         Optional<Duration> dur1 = ProcessHandle.current().info().totalCpuDuration();
 296 
 297         Duration myCputime2 = ProcessUtil.MXBeanCpuTime();
 298 
 299         if (Platform.isAix()) {
 300             try {
 301                 Thread.sleep(1000);
 302             } catch (InterruptedException ex) {}
 303         }
 304         Optional<Duration> dur2 = ProcessHandle.current().info().totalCpuDuration();
 305 
 306         if (dur1.isPresent() && dur2.isPresent()) {
 307             Duration total1 = dur1.get();
 308             Duration total2 = dur2.get();
 309             System.out.printf(" total1 vs. mbean: %s, getProcessCpuTime: %s, diff: %s%n",
 310                     Objects.toString(total1), myCputime1, myCputime1.minus(total1));
 311             System.out.printf(" total2 vs. mbean: %s, getProcessCpuTime: %s, diff: %s%n",
 312                     Objects.toString(total2), myCputime2, myCputime2.minus(total2));
 313 
 314             Duration epsilon = Duration.ofMillis(200L);      // Epsilon is 200ms.
 315             Assert.assertTrue(checkEpsilon(myCputime1, myCputime2, epsilon),
 316                     myCputime1.toNanos() + " should be within " + epsilon
 317                             + " of " + myCputime2.toNanos());
 318             Assert.assertTrue(checkEpsilon(total1, total2, epsilon),
 319                     total1.toNanos() + " should be within " + epsilon
 320                             + " of " + total2.toNanos());
 321             Assert.assertTrue(checkEpsilon(myCputime1, total1, epsilon),
 322                     myCputime1.toNanos() + " should be within " + epsilon
 323                             + " of " + total1.toNanos());
 324             Assert.assertTrue(checkEpsilon(total1, myCputime2, epsilon),
 325                     total1.toNanos() + " should be within " + epsilon
 326                             + " of " + myCputime2.toNanos());
 327             Assert.assertTrue(checkEpsilon(myCputime2, total2, epsilon),
 328                     myCputime2.toNanos() + " should be within " + epsilon
 329                             + " of " + total2.toNanos());
 330         }
 331     }
 332 
 333     @Test
 334     public static void test5() {
 335         ProcessHandle self = ProcessHandle.current();
 336         Random r = new Random();
 337         for (int i = 0; i < 30; i++) {
 338             Instant end = Instant.now().plusMillis(500L);
 339             while (end.isBefore(Instant.now())) {
 340                 // burn the cpu time checking the time
 341                 long x = r.nextLong();
 342             }
 343             if (self.info().totalCpuDuration().isPresent()) {
 344                 Duration totalCpu = self.info().totalCpuDuration().get();
 345                 long infoTotalCputime = totalCpu.toNanos();
 346                 long beanCputime = ProcessUtil.MXBeanCpuTime().toNanos();
 347                 System.out.printf(" infoTotal: %12d, beanCpu: %12d, diff: %12d%n",
 348                         infoTotalCputime, beanCputime, beanCputime - infoTotalCputime);
 349             } else {
 350                 break;  // nothing to compare; continue
 351             }
 352         }
 353     }
 354     /**
 355      * Check two Durations, the second should be greater than the first or
 356      * within the supplied Epsilon.
 357      * @param d1 a Duration - presumed to be shorter
 358      * @param d2 a 2nd Duration - presumed to be greater (or within Epsilon)
 359      * @param epsilon Epsilon the amount of overlap allowed
 360      * @return true if d2 is greater than d1 or within epsilon, false otherwise
 361      */
 362     static boolean checkEpsilon(Duration d1, Duration d2, Duration epsilon) {
 363         if (d1.toNanos() <= d2.toNanos()) {
 364             return true;
 365         }
 366         Duration diff = d1.minus(d2).abs();
 367         return diff.compareTo(epsilon) <= 0;
 368     }
 369 
 370     /**
 371      * Spawn a native process with the provided arguments.
 372      * @param command the executable of native process
 373      * @param args to start a new process
 374      * @return the Process that was started
 375      * @throws IOException thrown by ProcessBuilder.start
 376      */
 377     static Process spawn(String command, String... args) throws IOException {
 378         ProcessBuilder pb = new ProcessBuilder();
 379         pb.inheritIO();
 380         List<String> list = new ArrayList<>();
 381         list.add(command);
 382         for (String arg : args)
 383             list.add(arg);
 384         pb.command(list);
 385         return pb.start();
 386     }
 387 }