1 /*
   2  * Copyright (c) 2015, 2017, 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 import java.io.ByteArrayInputStream;
  24 import java.io.ByteArrayOutputStream;
  25 import java.io.FilePermission;
  26 import java.io.IOException;
  27 import java.lang.ref.Reference;
  28 import java.lang.ref.ReferenceQueue;
  29 import java.lang.ref.WeakReference;
  30 import java.lang.reflect.Field;
  31 import java.nio.file.Files;
  32 import java.nio.file.Paths;
  33 import java.security.CodeSource;
  34 import java.security.Permission;
  35 import java.security.PermissionCollection;
  36 import java.security.Permissions;
  37 import java.security.Policy;
  38 import java.security.ProtectionDomain;
  39 import java.util.Arrays;
  40 import java.util.Collections;
  41 import java.util.Enumeration;
  42 import java.util.HashSet;
  43 import java.util.List;
  44 import java.util.Objects;
  45 import java.util.Properties;
  46 import java.util.Set;
  47 import java.util.TreeSet;
  48 import java.util.UUID;
  49 import java.util.concurrent.Callable;
  50 import java.util.concurrent.atomic.AtomicBoolean;
  51 import java.util.function.BiFunction;
  52 import java.util.function.Function;
  53 import java.util.logging.FileHandler;
  54 import java.util.logging.LogManager;
  55 import java.util.logging.Logger;
  56 import java.util.logging.LoggingPermission;
  57 import java.util.stream.Collectors;
  58 import java.util.stream.Stream;
  59 
  60 /**
  61  * @test
  62  * @bug 8033661 8189291
  63  * @summary tests LogManager.updateConfiguration(bin)
  64  * @modules java.logging/java.util.logging:open
  65  * @run main/othervm UpdateConfigurationTest UNSECURE
  66  * @run main/othervm UpdateConfigurationTest SECURE
  67  * @author danielfuchs
  68  */
  69 public class UpdateConfigurationTest {
  70 
  71     static final Policy DEFAULT_POLICY = Policy.getPolicy();
  72 
  73     /**
  74      * We will test the handling of abstract logger nodes with file handlers in
  75      * two configurations:
  76      * UNSECURE: No security manager.
  77      * SECURE: With the security manager present - and the required
  78      *         permissions granted.
  79      */
  80     public static enum TestCase {
  81         UNSECURE, SECURE;
  82         public void run(Properties propertyFile, boolean last) throws Exception {
  83             System.out.println("Running test case: " + name());
  84             Configure.setUp(this);
  85             test(this.name() + " " + propertyFile.getProperty("test.name"),
  86                     propertyFile, last);
  87         }
  88     }
  89 
  90 
  91     private static final String PREFIX =
  92             "FileHandler-" + UUID.randomUUID() + ".log";
  93     private static final String userDir = System.getProperty("user.dir", ".");
  94     private static final boolean userDirWritable = Files.isWritable(Paths.get(userDir));
  95 
  96     static enum ConfigMode { APPEND, REPLACE, DEFAULT;
  97         boolean append() { return this == APPEND; }
  98         Function<String, BiFunction<String,String,String>> remapper() {
  99             switch(this) {
 100                 case APPEND:
 101                     return (k) -> ((o,n) -> (n == null ? o : n));
 102                 case REPLACE:
 103                     return (k) -> ((o,n) -> n);
 104             }
 105             return null;
 106         }
 107     }
 108 
 109     private static final List<Properties> properties;
 110     static {
 111         // The test will be run with each of the configurations below.
 112         // The 'child' logger is forgotten after each test
 113 
 114         Properties props1 = new Properties();
 115         props1.setProperty("test.name", "props1");
 116         props1.setProperty("test.config.mode", ConfigMode.REPLACE.name());
 117         props1.setProperty(FileHandler.class.getName() + ".pattern", PREFIX);
 118         props1.setProperty(FileHandler.class.getName() + ".limit", String.valueOf(Integer.MAX_VALUE));
 119         props1.setProperty(FileHandler.class.getName() + ".level", "ALL");
 120         props1.setProperty(FileHandler.class.getName() + ".formatter", "java.util.logging.SimpleFormatter");
 121         props1.setProperty("com.foo.handlers", FileHandler.class.getName());
 122         props1.setProperty("com.bar.level", "FINEST");
 123 
 124         Properties props2 = new Properties();
 125         props2.setProperty("test.name", "props2");
 126         props2.setProperty("test.config.mode", ConfigMode.DEFAULT.name());
 127         props2.setProperty(FileHandler.class.getName() + ".pattern", PREFIX);
 128         props2.setProperty(FileHandler.class.getName() + ".limit", String.valueOf(Integer.MAX_VALUE));
 129         props2.setProperty(FileHandler.class.getName() + ".level", "ALL");
 130         props2.setProperty(FileHandler.class.getName() + ".formatter", "java.util.logging.SimpleFormatter");
 131         props2.setProperty("com.foo.handlers", FileHandler.class.getName());
 132         props2.setProperty("com.foo.handlers.ensureCloseOnReset", "true");
 133         props2.setProperty("com.level", "FINE");
 134 
 135         Properties props3 = new Properties();
 136         props3.setProperty("test.name", "props3");
 137         props3.setProperty("test.config.mode", ConfigMode.APPEND.name());
 138         props3.setProperty(FileHandler.class.getName() + ".pattern", PREFIX);
 139         props3.setProperty(FileHandler.class.getName() + ".limit", String.valueOf(Integer.MAX_VALUE));
 140         props3.setProperty(FileHandler.class.getName() + ".level", "ALL");
 141         props3.setProperty(FileHandler.class.getName() + ".formatter", "java.util.logging.SimpleFormatter");
 142         props3.setProperty("com.foo.handlers", ""); // specify "" to override the value in the previous conf
 143         props3.setProperty("com.foo.handlers.ensureCloseOnReset", "false");
 144         props3.setProperty("com.bar.level", "FINER");
 145 
 146 
 147         properties = Collections.unmodifiableList(Arrays.asList(
 148                     props1, props2, props3, props1));
 149     }
 150 
 151     static Properties previous;
 152     static Properties current;
 153     static final Field propsField;
 154     static {
 155         LogManager manager = LogManager.getLogManager();
 156         try {
 157             propsField = LogManager.class.getDeclaredField("props");
 158             propsField.setAccessible(true);
 159             previous = current = (Properties) propsField.get(manager);
 160         } catch (NoSuchFieldException | IllegalAccessException ex) {
 161             throw new ExceptionInInitializerError(ex);
 162         }
 163     }
 164 
 165     static Properties getProperties() {
 166         try {
 167             return (Properties) propsField.get(LogManager.getLogManager());
 168         } catch (IllegalAccessException x) {
 169             throw new RuntimeException(x);
 170         }
 171     }
 172 
 173     static String trim(String value) {
 174         return value == null ? null : value.trim();
 175     }
 176 
 177 
 178     /**
 179      * Tests one of the configuration defined above.
 180      * <p>
 181      * This is the main test method (the rest is infrastructure).
 182      * <p>
 183      * Creates a child of the com.foo logger (com.foo.child), resets
 184      * the configuration, and verifies that com.foo has no handler.
 185      * Then reapplies the configuration and verifies that the handler
 186      * for com.foo has been reestablished, depending on whether
 187      * java.util.logging.LogManager.reconfigureHandlers is present and
 188      * true.
 189      * <p>
 190      * Finally releases the logger com.foo.child, so that com.foo can
 191      * be garbage collected, and the next configuration can be
 192      * tested.
 193      */
 194     static void test(ConfigMode mode, String name, Properties props, boolean last)
 195             throws Exception {
 196 
 197         // Then create a child of the com.foo logger.
 198         Logger fooChild = Logger.getLogger("com.foo.child");
 199         fooChild.info("hello world");
 200         Logger barChild = Logger.getLogger("com.bar.child");
 201         barChild.info("hello world");
 202 
 203         ReferenceQueue<Logger> queue = new ReferenceQueue();
 204         WeakReference<Logger> fooRef = new WeakReference<>(Logger.getLogger("com.foo"), queue);
 205         if (fooRef.get() != fooChild.getParent()) {
 206             throw new RuntimeException("Unexpected parent logger: "
 207                     + fooChild.getParent() +"\n\texpected: " + fooRef.get());
 208         }
 209         WeakReference<Logger> barRef = new WeakReference<>(Logger.getLogger("com.bar"), queue);
 210         if (barRef.get() != barChild.getParent()) {
 211             throw new RuntimeException("Unexpected parent logger: "
 212                     + barChild.getParent() +"\n\texpected: " + barRef.get());
 213         }
 214         Reference<? extends Logger> ref2;
 215         int max = 10;
 216         barChild = null;
 217         System.gc();
 218         while ((ref2 = queue.poll()) == null) {
 219             System.gc();
 220             Thread.sleep(100);
 221             if (--max == 0) break;
 222         }
 223 
 224         Throwable failed = null;
 225         try {
 226             if (ref2 != null) {
 227                 String refName = ref2 == fooRef ? "fooRef" : ref2 == barRef ? "barRef" : "unknown";
 228                 if (ref2 != barRef) {
 229                     throw new RuntimeException("Unexpected logger reference cleared: " + refName);
 230                 } else {
 231                     System.out.println("Reference " + refName + " cleared as expected");
 232                 }
 233             } else if (ref2 == null) {
 234                 throw new RuntimeException("Expected 'barRef' to be cleared");
 235             }
 236             // Now lets try to  check that ref2 has expected handlers, and
 237             // attempt to configure again.
 238             String p = current.getProperty("com.foo.handlers", "").trim();
 239             assertEquals(p.isEmpty() ? 0 : 1, fooChild.getParent().getHandlers().length,
 240                     "["+name+"] fooChild.getParent().getHandlers().length");
 241             Configure.doPrivileged(() -> Configure.updateConfigurationWith(props, mode.remapper()));
 242             String p2 = previous.getProperty("com.foo.handlers", "").trim();
 243             assertEquals(p, p2, "["+name+"] com.foo.handlers");
 244             String n = trim(props.getProperty("com.foo.handlers", null));
 245             boolean hasHandlers = mode.append()
 246                     ? (n == null ? !p.isEmpty() : !n.isEmpty())
 247                     : n != null && !n.isEmpty();
 248             assertEquals( hasHandlers ? 1 : 0,
 249                     fooChild.getParent().getHandlers().length,
 250                     "["+name+"] fooChild.getParent().getHandlers().length"
 251                     + "[p=\""+p+"\", n=" + (n==null?null:"\""+n+"\"") + "]");
 252 
 253             checkProperties(mode, previous, current, props);
 254 
 255         } catch (Throwable t) {
 256             failed = t;
 257         } finally {
 258             if (last || failed != null) {
 259                 final Throwable suppressed = failed;
 260                 Configure.doPrivileged(LogManager.getLogManager()::reset);
 261                 Configure.doPrivileged(() -> {
 262                     try {
 263                         StringBuilder builder = new StringBuilder();
 264                         Files.list(Paths.get(userDir))
 265                             .filter((f) -> f.toString().contains(PREFIX))
 266                             .filter((f) -> f.toString().endsWith(".lck"))
 267                             .forEach((f) -> {
 268                                     builder.append(f.toString()).append('\n');
 269                             });
 270                         if (!builder.toString().isEmpty()) {
 271                             throw new RuntimeException("Lock files not cleaned:\n"
 272                                     + builder.toString());
 273                         }
 274                     } catch(RuntimeException | Error x) {
 275                         if (suppressed != null) x.addSuppressed(suppressed);
 276                         throw x;
 277                     } catch(Exception x) {
 278                         if (suppressed != null) x.addSuppressed(suppressed);
 279                         throw new RuntimeException(x);
 280                     }
 281                 });
 282 
 283                 if (suppressed == null) {
 284                     // Now we need to forget the child, so that loggers are released,
 285                     // and so that we can run the test with the next configuration...
 286                     // No need to do that if failed!=null however, as the first
 287                     // ref might not have been cleared yet and failing here would
 288                     // hide the original failure.
 289                     fooChild = null;
 290                     System.out.println("Setting fooChild to: " + fooChild);
 291                     while ((ref2 = queue.poll()) == null) {
 292                         System.gc();
 293                         Thread.sleep(1000);
 294                     }
 295                     if (ref2 != fooRef) {
 296                         throw new RuntimeException("Unexpected reference: "
 297                                 + ref2 +"\n\texpected: " + fooRef);
 298                     }
 299                     if (ref2.get() != null) {
 300                         throw new RuntimeException("Referent not cleared: " + ref2.get());
 301                     }
 302                     System.out.println("Got fooRef after reset(), fooChild is " + fooChild);
 303                 }
 304             }
 305         }
 306         if (failed != null) {
 307             // should rarely happen...
 308             throw new RuntimeException(failed);
 309         }
 310 
 311     }
 312 
 313     private static void checkProperties(ConfigMode mode,
 314             Properties previous, Properties current, Properties props) {
 315         Set<String> set = new HashSet<>();
 316 
 317         // Check that all property names from 'props' are in current.
 318         set.addAll(props.stringPropertyNames());
 319         set.removeAll(current.keySet());
 320         if (!set.isEmpty()) {
 321             throw new RuntimeException("Missing properties in current: " + set);
 322         }
 323         set.clear();
 324         set.addAll(current.stringPropertyNames());
 325         set.removeAll(previous.keySet());
 326         set.removeAll(props.keySet());
 327         if (!set.isEmpty()) {
 328             throw new RuntimeException("Superfluous properties in current: " + set);
 329         }
 330         set.clear();
 331         Stream<String> allnames =
 332                 Stream.concat(
 333                     Stream.concat(previous.stringPropertyNames().stream(),
 334                                   props.stringPropertyNames().stream()),
 335                     current.stringPropertyNames().stream())
 336                         .collect(Collectors.toCollection(TreeSet::new))
 337                         .stream();
 338         if (mode.append()) {
 339             // Check that all previous property names are in current.
 340             set.addAll(previous.stringPropertyNames());
 341             set.removeAll(current.keySet());
 342             if (!set.isEmpty()) {
 343                 throw new RuntimeException("Missing properties in current: " + set
 344                     + "\n\tprevious: " + previous
 345                     + "\n\tcurrent:  " + current
 346                     + "\n\tprops:    " + props);
 347 
 348             }
 349             allnames.forEach((k) -> {
 350                     String p = previous.getProperty(k, "").trim();
 351                     String n = current.getProperty(k, "").trim();
 352                     if (props.containsKey(k)) {
 353                         assertEquals(props.getProperty(k), n, k);
 354                     } else {
 355                         assertEquals(p, n, k);
 356                     }
 357                 });
 358         } else {
 359             // Check that only properties from 'props' are in current.
 360             set.addAll(current.stringPropertyNames());
 361             set.removeAll(props.keySet());
 362             if (!set.isEmpty()) {
 363                 throw new RuntimeException("Superfluous properties in current: " + set);
 364             }
 365             allnames.forEach((k) -> {
 366                     String p = previous.getProperty(k, "");
 367                     String n = current.getProperty(k, "");
 368                     if (props.containsKey(k)) {
 369                         assertEquals(props.getProperty(k), n, k);
 370                     } else {
 371                         assertEquals("", n, k);
 372                     }
 373                 });
 374         }
 375 
 376     }
 377 
 378     public static void main(String... args) throws Exception {
 379 
 380 
 381         if (args == null || args.length == 0) {
 382             args = new String[] {
 383                 TestCase.UNSECURE.name(),
 384                 TestCase.SECURE.name(),
 385             };
 386         }
 387 
 388         try {
 389             for (String testName : args) {
 390                 TestCase test = TestCase.valueOf(testName);
 391                 for (int i=0; i<properties.size();i++) {
 392                     Properties propertyFile = properties.get(i);
 393                     test.run(propertyFile, i == properties.size() - 1);
 394                 }
 395             }
 396         } finally {
 397             if (userDirWritable) {
 398                 Configure.doPrivileged(() -> {
 399                     // cleanup - delete files that have been created
 400                     try {
 401                         Files.list(Paths.get(userDir))
 402                             .filter((f) -> f.toString().contains(PREFIX))
 403                             .forEach((f) -> {
 404                                 try {
 405                                     System.out.println("deleting " + f);
 406                                     Files.delete(f);
 407                                 } catch(Throwable t) {
 408                                     System.err.println("Failed to delete " + f + ": " + t);
 409                                 }
 410                             });
 411                     } catch(Throwable t) {
 412                         System.err.println("Cleanup failed to list files: " + t);
 413                         t.printStackTrace();
 414                     }
 415                 });
 416             }
 417         }
 418     }
 419 
 420     static class Configure {
 421         static Policy policy = null;
 422         static final ThreadLocal<AtomicBoolean> allowAll = new ThreadLocal<AtomicBoolean>() {
 423             @Override
 424             protected AtomicBoolean initialValue() {
 425                 return  new AtomicBoolean(false);
 426             }
 427         };
 428         static void setUp(TestCase test) {
 429             switch (test) {
 430                 case SECURE:
 431                     if (policy == null && System.getSecurityManager() != null) {
 432                         throw new IllegalStateException("SecurityManager already set");
 433                     } else if (policy == null) {
 434                         policy = new SimplePolicy(TestCase.SECURE, allowAll);
 435                         Policy.setPolicy(policy);
 436                         System.setSecurityManager(new SecurityManager());
 437                     }
 438                     if (System.getSecurityManager() == null) {
 439                         throw new IllegalStateException("No SecurityManager.");
 440                     }
 441                     if (policy == null) {
 442                         throw new IllegalStateException("policy not configured");
 443                     }
 444                     break;
 445                 case UNSECURE:
 446                     if (System.getSecurityManager() != null) {
 447                         throw new IllegalStateException("SecurityManager already set");
 448                     }
 449                     break;
 450                 default:
 451                     new InternalError("No such testcase: " + test);
 452             }
 453         }
 454 
 455         static void updateConfigurationWith(Properties propertyFile,
 456                 Function<String,BiFunction<String,String,String>> remapper) {
 457             try {
 458                 ByteArrayOutputStream bytes = new ByteArrayOutputStream();
 459                 propertyFile.store(bytes, propertyFile.getProperty("test.name"));
 460                 ByteArrayInputStream bais = new ByteArrayInputStream(bytes.toByteArray());
 461                 LogManager.getLogManager().updateConfiguration(bais, remapper);
 462             } catch (IOException ex) {
 463                 throw new RuntimeException(ex);
 464             }
 465         }
 466 
 467         static void doPrivileged(Runnable run) {
 468             final boolean old = allowAll.get().getAndSet(true);
 469             try {
 470                 Properties before = getProperties();
 471                 try {
 472                     run.run();
 473                 } finally {
 474                     Properties after = getProperties();
 475                     if (before != after) {
 476                         previous = before;
 477                         current = after;
 478                     }
 479                 }
 480             } finally {
 481                 allowAll.get().set(old);
 482             }
 483         }
 484         static <T> T callPrivileged(Callable<T> call) throws Exception {
 485             final boolean old = allowAll.get().getAndSet(true);
 486             try {
 487                 Properties before = getProperties();
 488                 try {
 489                     return call.call();
 490                 } finally {
 491                     Properties after = getProperties();
 492                     if (before != after) {
 493                         previous = before;
 494                         current = after;
 495                     }
 496                 }
 497             } finally {
 498                 allowAll.get().set(old);
 499             }
 500         }
 501     }
 502 
 503     @FunctionalInterface
 504     public static interface FileHandlerSupplier {
 505         public FileHandler test() throws Exception;
 506     }
 507 
 508     static final class TestAssertException extends RuntimeException {
 509         TestAssertException(String msg) {
 510             super(msg);
 511         }
 512     }
 513 
 514     private static void assertEquals(long expected, long received, String msg) {
 515         if (expected != received) {
 516             throw new TestAssertException("Unexpected result for " + msg
 517                     + ".\n\texpected: " + expected
 518                     +  "\n\tactual:   " + received);
 519         } else {
 520             System.out.println("Got expected " + msg + ": " + received);
 521         }
 522     }
 523 
 524     private static void assertEquals(String expected, String received, String msg) {
 525         if (!Objects.equals(expected, received)) {
 526             throw new TestAssertException("Unexpected result for " + msg
 527                     + ".\n\texpected: " + expected
 528                     +  "\n\tactual:   " + received);
 529         } else {
 530             System.out.println("Got expected " + msg + ": " + received);
 531         }
 532     }
 533 
 534 
 535     public static void test(String name, Properties props, boolean last) throws Exception {
 536         ConfigMode configMode = ConfigMode.valueOf(props.getProperty("test.config.mode"));
 537         System.out.println("\nTesting: " + name + " mode=" + configMode);
 538         if (!userDirWritable) {
 539             throw new RuntimeException("Not writable: "+userDir);
 540         }
 541         switch(configMode) {
 542             case REPLACE:
 543             case APPEND:
 544             case DEFAULT:
 545                 test(configMode, name, props, last); break;
 546             default:
 547                 throw new RuntimeException("Unknwown mode: " + configMode);
 548         }
 549     }
 550 
 551     final static class PermissionsBuilder {
 552         final Permissions perms;
 553         public PermissionsBuilder() {
 554             this(new Permissions());
 555         }
 556         public PermissionsBuilder(Permissions perms) {
 557             this.perms = perms;
 558         }
 559         public PermissionsBuilder add(Permission p) {
 560             perms.add(p);
 561             return this;
 562         }
 563         public PermissionsBuilder addAll(PermissionCollection col) {
 564             if (col != null) {
 565                 for (Enumeration<Permission> e = col.elements(); e.hasMoreElements(); ) {
 566                     perms.add(e.nextElement());
 567                 }
 568             }
 569             return this;
 570         }
 571         public Permissions toPermissions() {
 572             final PermissionsBuilder builder = new PermissionsBuilder();
 573             builder.addAll(perms);
 574             return builder.perms;
 575         }
 576     }
 577 
 578     public static class SimplePolicy extends Policy {
 579 
 580         final Permissions permissions;
 581         final Permissions allPermissions;
 582         final ThreadLocal<AtomicBoolean> allowAll; // actually: this should be in a thread locale
 583         public SimplePolicy(TestCase test, ThreadLocal<AtomicBoolean> allowAll) {
 584             this.allowAll = allowAll;
 585             permissions = new Permissions();
 586             permissions.add(new LoggingPermission("control", null));
 587             permissions.add(new FilePermission(PREFIX+".lck", "read,write,delete"));
 588             permissions.add(new FilePermission(PREFIX, "read,write"));
 589 
 590             // these are used for configuring the test itself...
 591             allPermissions = new Permissions();
 592             allPermissions.add(new java.security.AllPermission());
 593 
 594         }
 595 
 596         @Override
 597         public boolean implies(ProtectionDomain domain, Permission permission) {
 598             if (allowAll.get().get()) return allPermissions.implies(permission);
 599             return permissions.implies(permission) ||
 600                    DEFAULT_POLICY.implies(domain, permission);
 601         }
 602 
 603         @Override
 604         public PermissionCollection getPermissions(CodeSource codesource) {
 605             return new PermissionsBuilder().addAll(allowAll.get().get()
 606                     ? allPermissions : permissions).toPermissions();
 607         }
 608 
 609         @Override
 610         public PermissionCollection getPermissions(ProtectionDomain domain) {
 611             return new PermissionsBuilder().addAll(allowAll.get().get()
 612                     ? allPermissions : permissions).toPermissions();
 613         }
 614     }
 615 
 616 }