8284490: Remove finalizer method in java.security.jgss

Reviewed-by: rriggs, dfuchs, weijun
This commit is contained in:
Xue-Lei Andrew Fan 2022-05-03 14:14:09 +00:00
parent 0f62cb6fcc
commit ffca23a531
11 changed files with 418 additions and 37 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -25,6 +25,7 @@
package sun.security.jgss.wrapper;
import org.ietf.jgss.*;
import java.lang.ref.Cleaner;
import java.security.Provider;
import sun.security.jgss.GSSUtil;
import sun.security.jgss.spi.GSSCredentialSpi;
@ -37,11 +38,12 @@ import sun.security.jgss.spi.GSSNameSpi;
* @since 1.6
*/
public class GSSCredElement implements GSSCredentialSpi {
private final Cleaner.Cleanable cleanable;
private int usage;
long pCred; // Pointer to the gss_cred_id_t structure
final long pCred; // Pointer to the gss_cred_id_t structure
private GSSNameElement name = null;
private GSSLibStub cStub;
private final GSSLibStub cStub;
// Perform the necessary ServicePermission check on this cred
@SuppressWarnings("removal")
@ -69,6 +71,7 @@ public class GSSCredElement implements GSSCredentialSpi {
cStub = GSSLibStub.getInstance(mech);
usage = GSSCredential.INITIATE_ONLY;
name = srcName;
cleanable = Krb5Util.cleaner.register(this, disposerFor(cStub, pCred));
}
GSSCredElement(GSSNameElement name, int lifetime, int usage,
@ -85,17 +88,23 @@ public class GSSCredElement implements GSSCredentialSpi {
this.name = new GSSNameElement(cStub.getCredName(pCred), cStub);
doServicePermCheck();
}
cleanable = Krb5Util.cleaner.register(this, disposerFor(cStub, pCred));
}
public Provider getProvider() {
return SunNativeProvider.INSTANCE;
}
public void dispose() throws GSSException {
public void dispose() {
name = null;
if (pCred != 0) {
pCred = cStub.releaseCred(pCred);
}
cleanable.clean();
}
private static Runnable disposerFor(GSSLibStub stub, long pCredentials) {
return () -> {
stub.releaseCred(pCredentials);
};
}
public GSSNameElement getName() throws GSSException {
@ -132,11 +141,6 @@ public class GSSCredElement implements GSSCredentialSpi {
return "N/A";
}
@SuppressWarnings("removal")
protected void finalize() throws Throwable {
dispose();
}
@Override
public GSSCredentialSpi impersonate(GSSNameSpi name) throws GSSException {
throw new GSSException(GSSException.FAILURE, -1,

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -26,6 +26,7 @@
package sun.security.jgss.wrapper;
import org.ietf.jgss.*;
import java.lang.ref.Cleaner;
import java.security.Provider;
import java.security.Security;
import java.io.IOException;
@ -48,11 +49,12 @@ import javax.security.auth.kerberos.ServicePermission;
*/
public class GSSNameElement implements GSSNameSpi {
private final Cleaner.Cleanable cleanable;
long pName = 0; // Pointer to the gss_name_t structure
final long pName; // Pointer to the gss_name_t structure
private String printableName;
private Oid printableType;
private GSSLibStub cStub;
final private GSSLibStub cStub;
static final GSSNameElement DEF_ACCEPTOR = new GSSNameElement();
@ -94,6 +96,9 @@ public class GSSNameElement implements GSSNameSpi {
private GSSNameElement() {
printableName = "<DEFAULT ACCEPTOR>";
pName = 0;
cleanable = null;
cStub = null;
}
// Warning: called by NativeUtil.c
@ -106,6 +111,8 @@ public class GSSNameElement implements GSSNameSpi {
pName = pNativeName;
cStub = stub;
setPrintables();
cleanable = Krb5Util.cleaner.register(this, disposerFor(stub, pName));
}
GSSNameElement(byte[] nameBytes, Oid nameType, GSSLibStub stub)
@ -151,6 +158,8 @@ public class GSSNameElement implements GSSNameSpi {
}
}
pName = cStub.importName(name, nameType);
cleanable = Krb5Util.cleaner.register(this, disposerFor(stub, pName));
setPrintables();
@SuppressWarnings("removal")
@ -284,14 +293,14 @@ public class GSSNameElement implements GSSNameSpi {
}
public void dispose() {
if (pName != 0) {
cStub.releaseName(pName);
pName = 0;
if (cleanable != null) {
cleanable.clean();
}
}
@SuppressWarnings("removal")
protected void finalize() throws Throwable {
dispose();
private static Runnable disposerFor(GSSLibStub stub, long pName) {
return () -> {
stub.releaseName(pName);
};
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -25,6 +25,7 @@
package sun.security.jgss.wrapper;
import org.ietf.jgss.*;
import java.lang.ref.Cleaner;
import javax.security.auth.kerberos.ServicePermission;
/**
@ -33,6 +34,8 @@ import javax.security.auth.kerberos.ServicePermission;
* @since 1.6
*/
class Krb5Util {
// A cleaner, shared within this module.
static final Cleaner cleaner = Cleaner.create();
// Return the Kerberos TGS principal name using the domain
// of the specified <code>name</code>

View File

@ -26,6 +26,7 @@
package sun.security.jgss.wrapper;
import org.ietf.jgss.*;
import java.lang.ref.Cleaner;
import java.security.Provider;
import sun.security.jgss.GSSHeader;
import sun.security.jgss.GSSUtil;
@ -46,6 +47,7 @@ import java.io.*;
* @since 1.6
*/
class NativeGSSContext implements GSSContextSpi {
private Cleaner.Cleanable cleanable;
private static final int GSS_C_DELEG_FLAG = 1;
private static final int GSS_C_MUTUAL_FLAG = 2;
@ -238,8 +240,8 @@ class NativeGSSContext implements GSSContextSpi {
// Warning: called by NativeUtil.c
NativeGSSContext(long pCtxt, GSSLibStub stub) throws GSSException {
assert(pCtxt != 0);
pContext = pCtxt;
cStub = stub;
setContext(pCtxt);
// Set everything except cred, cb, delegatedCred
long[] info = cStub.inquireContext(pContext);
@ -359,7 +361,7 @@ class NativeGSSContext implements GSSContextSpi {
return isEstablished;
}
public void dispose() throws GSSException {
public void dispose() {
if (disposeCred != null) {
disposeCred.dispose();
}
@ -370,12 +372,36 @@ class NativeGSSContext implements GSSContextSpi {
srcName = null;
targetName = null;
delegatedCred = null;
if (pContext != 0) {
pContext = cStub.deleteContext(pContext);
if (pContext != 0 && cleanable != null) {
pContext = 0;
cleanable.clean();
}
}
// Note: this method is also used in native code.
private void setContext(long pContext) {
// Dispose the existing context.
if (this.pContext != 0L && cleanable != null) {
cleanable.clean();
}
// Reset the context
this.pContext = pContext;
// Register the cleaner.
if (pContext != 0L) {
cleanable = Krb5Util.cleaner.register(this,
disposerFor(cStub, pContext));
}
}
private static Runnable disposerFor(GSSLibStub stub, long pContext) {
return () -> {
stub.deleteContext(pContext);
};
}
public int getWrapSizeLimit(int qop, boolean confReq,
int maxTokenSize)
throws GSSException {
@ -639,11 +665,6 @@ class NativeGSSContext implements GSSContextSpi {
return isInitiator;
}
@SuppressWarnings("removal")
protected void finalize() throws Throwable {
dispose();
}
public Object inquireSecContext(String type)
throws GSSException {
throw new GSSException(GSSException.UNAVAILABLE, -1,

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -937,7 +937,7 @@ Java_sun_security_jgss_wrapper_GSSLibStub_initContext(JNIEnv *env,
// this is to work with both MIT and Solaris. Former deletes half-built
// context if error occurs
if (contextHdl != contextHdlSave) {
(*env)->SetLongField(env, jcontextSpi, FID_NativeGSSContext_pContext,
(*env)->CallVoidMethod(env, jcontextSpi, MID_NativeGSSContext_setContext,
ptr_to_jlong(contextHdl));
TRACE1("[GSSLibStub_initContext] set pContext=%" PRIuPTR "", (uintptr_t)contextHdl);
}
@ -1057,7 +1057,7 @@ Java_sun_security_jgss_wrapper_GSSLibStub_acceptContext(JNIEnv *env,
// this is to work with both MIT and Solaris. Former deletes half-built
// context if error occurs
if (contextHdl != contextHdlSave) {
(*env)->SetLongField(env, jcontextSpi, FID_NativeGSSContext_pContext,
(*env)->CallVoidMethod(env, jcontextSpi, MID_NativeGSSContext_setContext,
ptr_to_jlong(contextHdl));
TRACE1("[GSSLibStub_acceptContext] set pContext=%" PRIuPTR "", (uintptr_t)contextHdl);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2018, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -81,6 +81,7 @@ jmethodID MID_InetAddress_getAddr;
jmethodID MID_GSSNameElement_ctor;
jmethodID MID_GSSCredElement_ctor;
jmethodID MID_NativeGSSContext_ctor;
jmethodID MID_NativeGSSContext_setContext;
jfieldID FID_GSSLibStub_pMech;
jfieldID FID_NativeGSSContext_pContext;
jfieldID FID_NativeGSSContext_srcName;
@ -290,6 +291,15 @@ DEF_JNI_OnLoad(JavaVM *jvm, void *reserved) {
printf("Couldn't find NativeGSSContext(long, GSSLibStub) constructor\n");
return JNI_ERR;
}
MID_NativeGSSContext_setContext =
(*env)->GetMethodID(env, CLS_NativeGSSContext, "setContext",
"(J)V");
if (MID_NativeGSSContext_setContext == NULL) {
printf("Couldn't find NativeGSSContext.setContext(long) method\n");
return JNI_ERR;
}
/* Compute and cache the field ID */
cls = (*env)->FindClass(env, "sun/security/jgss/wrapper/GSSLibStub");
if (cls == NULL) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -73,6 +73,7 @@ extern "C" {
extern jmethodID MID_GSSNameElement_ctor;
extern jmethodID MID_GSSCredElement_ctor;
extern jmethodID MID_NativeGSSContext_ctor;
extern jmethodID MID_NativeGSSContext_setContext;
extern jfieldID FID_GSSLibStub_pMech;
extern jfieldID FID_NativeGSSContext_pContext;
extern jfieldID FID_NativeGSSContext_srcName;

View File

@ -0,0 +1,66 @@
/*
* Copyright (C) 2022 THL A29 Limited, a Tencent company. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @bug 8284490
* @summary Remove finalizer method in java.security.jgss
* @key intermittent
* @run main/othervm GssContextCleanup
*/
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSManager;
import java.util.WeakHashMap;
public final class GssContextCleanup {
private final static WeakHashMap<GSSContext, ?> whm = new WeakHashMap<>();
public static void main(String[] args) throws Exception {
// Enable debug log so that the failure analysis could be easier.
System.setProperty("sun.security.nativegss.debug", "true");
// Use native provider
System.setProperty("sun.security.jgss.native", "true");
// Create an object
GSSManager manager = GSSManager.getInstance();
GSSContext context = manager.createContext((GSSCredential)null);
whm.put(context, null);
context = null;
// Wait to trigger the cleanup.
for (int i = 0; i < 10 && whm.size() > 0; i++) {
System.gc();
Thread.sleep(100);
}
// Check if the object has been collected.
if (whm.size() > 0) {
throw new RuntimeException("GSSContext object is not released");
}
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright (C) 2022 THL A29 Limited, a Tencent company. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @bug 8284490
* @summary Remove finalizer method in java.security.jgss
* @key intermittent
* @run main/othervm GssNameCleanup
*/
import java.util.WeakHashMap;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.GSSException;
public final class GssNameCleanup {
private final static WeakHashMap<GSSName, ?> whm = new WeakHashMap<>();
public static void main(String[] args) throws Exception {
// Enable debug log so that the failure analysis could be easier.
System.setProperty("sun.security.nativegss.debug", "true");
// Use native provider
System.setProperty("sun.security.jgss.native", "true");
// Create an object
GSSManager manager = GSSManager.getInstance();
try {
GSSName name =
manager.createName("u1", GSSName.NT_USER_NAME);
whm.put(name, null);
name = null;
} catch (GSSException gsse) {
// createName() could fail if the local default realm
// cannot be located. Just ignore the test case for
// such circumstances.
System.out.println("Ignore this test case: " + gsse);
}
// Wait to trigger the cleanup.
for (int i = 0; i < 10 && whm.size() > 0; i++) {
System.gc();
Thread.sleep(100);
}
// Check if the object has been collected.
if (whm.size() > 0) {
throw new RuntimeException("GSSName object is not released");
}
}
}

View File

@ -0,0 +1,180 @@
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @bug 8284490
* @summary Remove finalizer method in java.security.jgss
* @key intermittent
* @requires os.family != "windows"
* @library /test/lib
* @compile -XDignore.symbol.file Cleaners.java
* @run main/othervm Cleaners launcher
*/
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Arrays;
import java.util.Set;
import jdk.test.lib.Asserts;
import jdk.test.lib.process.Proc;
import org.ietf.jgss.Oid;
import sun.security.krb5.Config;
public class Cleaners {
private static final String CONF = "krb5.conf";
private static final String KTAB_S = "server.ktab";
private static final String KTAB_B = "backend.ktab";
private static final String HOST = "localhost";
private static final String SERVER = "server/" + HOST;
private static final String BACKEND = "backend/" + HOST;
private static final String USER = "user";
private static final char[] PASS = "password".toCharArray();
private static final String REALM = "REALM";
private static final byte[] MSG = "12345678".repeat(128)
.getBytes(StandardCharsets.UTF_8);
public static void main(String[] args) throws Exception {
Oid oid = new Oid("1.2.840.113554.1.2.2");
byte[] token, msg;
switch (args[0]) {
case "launcher" -> {
KDC kdc = KDC.create(REALM, HOST, 0, true);
kdc.addPrincipal(USER, PASS);
kdc.addPrincipalRandKey("krbtgt/" + REALM);
kdc.addPrincipalRandKey(SERVER);
kdc.addPrincipalRandKey(BACKEND);
// Native lib might do some name lookup
KDC.saveConfig(CONF, kdc,
"dns_lookup_kdc = no",
"ticket_lifetime = 1h",
"dns_lookup_realm = no",
"dns_canonicalize_hostname = false",
"forwardable = true");
System.setProperty("java.security.krb5.conf", CONF);
Config.refresh();
// Create kaytab and ccache files for native clients
kdc.writeKtab(KTAB_S, false, SERVER);
kdc.writeKtab(KTAB_B, false, BACKEND);
kdc.kinit(USER, "ccache");
Files.setPosixFilePermissions(Paths.get("ccache"),
Set.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE));
Proc pc = proc("client")
.env("KRB5CCNAME", "FILE:ccache")
.env("KRB5_KTNAME", "none") // Do not try system ktab if ccache fails
.start();
Proc ps = proc("server")
.env("KRB5_KTNAME", KTAB_S)
.start();
Proc pb = proc("backend")
.env("KRB5_KTNAME", KTAB_B)
.start();
// Client and server
ps.println(pc.readData()); // AP-REQ
pc.println(ps.readData()); // AP-REP, mutual auth
ps.println(pc.readData()); // wrap msg
ps.println(pc.readData()); // mic msg
// Server and backend
pb.println(ps.readData()); // AP-REQ
ps.println(pb.readData()); // wrap msg
ps.println(pb.readData()); // mic msg
ensureCleanersCalled(pc);
ensureCleanersCalled(ps);
ensureCleanersCalled(pb);
}
case "client" -> {
Context c = Context.fromThinAir();
c.startAsClient(SERVER, oid);
c.x().requestCredDeleg(true);
c.x().requestMutualAuth(true);
Proc.binOut(c.take(new byte[0])); // AP-REQ
c.take(Proc.binIn()); // AP-REP
Proc.binOut(c.wrap(MSG, true));
Proc.binOut(c.getMic(MSG));
}
case "server" -> {
Context s = Context.fromThinAir();
s.startAsServer(oid);
token = Proc.binIn(); // AP-REQ
Proc.binOut(s.take(token)); // AP-REP
msg = s.unwrap(Proc.binIn(), true);
Asserts.assertTrue(Arrays.equals(msg, MSG));
s.verifyMic(Proc.binIn(), msg);
Context s2 = s.delegated();
s2.startAsClient(BACKEND, oid);
s2.x().requestMutualAuth(false);
Proc.binOut(s2.take(new byte[0])); // AP-REQ
msg = s2.unwrap(Proc.binIn(), true);
Asserts.assertTrue(Arrays.equals(msg, MSG));
s2.verifyMic(Proc.binIn(), msg);
}
case "backend" -> {
Context b = Context.fromThinAir();
b.startAsServer(oid);
token = b.take(Proc.binIn()); // AP-REQ
Asserts.assertTrue(token == null);
Proc.binOut(b.wrap(MSG, true));
Proc.binOut(b.getMic(MSG));
}
}
System.out.println("Prepare for GC");
for (int i = 0; i < 10; i++) {
System.gc();
Thread.sleep(100);
}
}
private static void ensureCleanersCalled(Proc p) throws Exception {
p.output()
.shouldHaveExitValue(0)
.stdoutShouldMatch("Prepare for GC(.|\\n)*GSSLibStub_deleteContext")
.stdoutShouldMatch("Prepare for GC(.|\\n)*GSSLibStub_releaseName")
.stdoutShouldMatch("Prepare for GC(.|\\n)*GSSLibStub_releaseCred");
}
private static Proc proc(String type) throws Exception {
return Proc.create("Cleaners")
.args(type)
.debug(type)
.env("KRB5_CONFIG", CONF)
.env("KRB5_TRACE", "/dev/stderr")
.prop("sun.security.jgss.native", "true")
.prop("javax.security.auth.useSubjectCredsOnly", "false")
.prop("sun.security.nativegss.debug", "true");
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2013, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2013, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -123,6 +123,7 @@ public class Proc {
private String debug; // debug flag, controller will show data
// transfer between procs. If debug is set,
// it MUST be different between Procs.
private final StringBuilder stdout = new StringBuilder();
final private static String PREFIX = "PROCISFUN:";
@ -358,6 +359,9 @@ public class Proc {
// Reads a line from stdout of proc
public String readLine() throws IOException {
String s = br.readLine();
if (s != null) {
stdout.append(s).append('\n');
}
if (debug != null) {
System.out.println("PROC: " + debug + " readline: " +
(s == null ? "<EOF>" : s));
@ -402,6 +406,16 @@ public class Proc {
}
return p.waitFor();
}
// Returns an OutputAnalyzer
public OutputAnalyzer output() throws Exception {
int exitCode = waitFor();
Path stderr = Path.of(getId("stderr"));
return new OutputAnalyzer(stdout.toString(),
Files.exists(stderr) ? Files.readString(stderr) : "",
exitCode);
}
// Wait for process end with expected exit code
public void waitFor(int expected) throws Exception {
if (p.waitFor() != expected) {