+ * If the {@link ProtocolFamily} is of an {@link AFProtocolFamily}, or {@code UNIX}, the
+ * corresponding junixsocket implementation is used. In all other cases, the call is delegated to
+ * {@link DatagramChannel#open()}.
+ *
+ * @param family The protocol family.
+ * @return The new {@link DatagramChannel}.
+ * @throws IOException on error.
+ */
+ public static DatagramChannel open(ProtocolFamily family) throws IOException {
+ requireNonNull(family);
+
+ if (family instanceof AFProtocolFamily) {
+ return ((AFProtocolFamily) family).openDatagramChannel();
+ } else if ("UNIX".equals(family.name())) {
+ return AFUNIXDatagramChannel.open();
+ } else if (family instanceof StandardProtocolFamily) {
+ return DatagramChannel.open();
+ } else {
+ throw new UnsupportedOperationException("Protocol family not supported");
+ }
+ }
}
diff --git a/junixsocket-common/src/main/java/org/newsclub/net/unix/AFDatagramSocket.java b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFDatagramSocket.java
index d35194d23..c24123fd1 100644
--- a/junixsocket-common/src/main/java/org/newsclub/net/unix/AFDatagramSocket.java
+++ b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFDatagramSocket.java
@@ -1,7 +1,7 @@
/*
* junixsocket
*
- * Copyright 2009-2022 Christian Kohlschütter
+ * Copyright 2009-2024 Christian Kohlschütter
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@
import java.net.SocketImpl;
import java.nio.channels.AlreadyBoundException;
import java.nio.channels.DatagramChannel;
+import java.nio.channels.IllegalBlockingModeException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jdt.annotation.Nullable;
@@ -36,27 +37,28 @@
/**
* A {@link DatagramSocket} implementation that works with junixsocket.
- *
+ *
* @param The concrete {@link AFSocketAddress} that is supported by this type.
* @author Christian Kohlschütter
*/
-public abstract class AFDatagramSocket extends DatagramSocket implements
- AFSomeSocket, AFSocketExtensions {
+public abstract class AFDatagramSocket extends DatagramSocketShim
+ implements AFSomeSocket, AFSocketExtensions {
private static final InetSocketAddress WILDCARD_ADDRESS = new InetSocketAddress(0);
private final AFDatagramSocketImpl impl;
private final AncillaryDataSupport ancillaryDataSupport;
private final AtomicBoolean created = new AtomicBoolean(false);
private final AtomicBoolean deleteOnClose = new AtomicBoolean(true);
+
+ @SuppressWarnings("this-escape")
private final AFDatagramChannel channel = newChannel();
/**
* Creates a new {@link AFDatagramSocket} instance.
- *
+ *
* @param impl The corresponding {@link SocketImpl} class.
- * @throws IOException on error.
*/
- protected AFDatagramSocket(final AFDatagramSocketImpl impl) throws IOException {
+ protected AFDatagramSocket(final AFDatagramSocketImpl impl) {
super(impl);
this.impl = impl;
this.ancillaryDataSupport = impl.ancillaryDataSupport;
@@ -64,30 +66,30 @@ protected AFDatagramSocket(final AFDatagramSocketImpl impl) throws IOExceptio
/**
* Creates a new {@link DatagramChannel} that is associated with this socket.
- *
+ *
* @return The channel.
*/
protected abstract AFDatagramChannel newChannel();
/**
- * Returns the {@link AncillaryDataSupport} instance.
- *
+ * Returns the {@code AncillaryDataSupport} instance.
+ *
* @return The instance.
*/
- protected final AncillaryDataSupport getAncillaryDataSupport() {
+ final AncillaryDataSupport getAncillaryDataSupport() {
return ancillaryDataSupport;
}
/**
* A reference to the constructor of an {@link AFDatagramSocket} subclass.
- *
+ *
* @param The concrete {@link AFSocketAddress} that is supported by this type.
*/
@FunctionalInterface
public interface Constructor {
/**
* Constructs a new {@link DatagramSocket} instance.
- *
+ *
* @param fd The file descriptor.
* @return The new instance.
* @throws IOException on error.
@@ -97,7 +99,7 @@ public interface Constructor {
/**
* Returns the {@link AFSocketAddress} type supported by this socket.
- *
+ *
* @return The supported {@link AFSocketAddress}.
*/
protected final Class extends AFSocketAddress> socketAddressClass() {
@@ -119,7 +121,7 @@ protected static final AFDatagramSocket newInstan
/**
* Creates a new {@link AFDatagramSocket}.
- *
+ *
* @param The concrete {@link AFSocketAddress} that is supported by this type.
* @param constructor The supplying constructor.
* @param fdObj The file descriptor.
@@ -169,12 +171,12 @@ public final void connect(InetAddress address, int port) {
/**
* Reads the next received packet without actually removing it from the queue.
- *
+ *
* In other words, once a packet is received, calling this method multiple times in a row will not
* have further effects on the packet contents.
- *
+ *
* This call still blocks until at least one packet has been received and added to the queue.
- *
+ *
* @param p The packet.
* @throws IOException on error.
*/
@@ -328,10 +330,10 @@ public final synchronized void bind(SocketAddress addr) throws SocketException {
/**
* Checks if this {@link AFDatagramSocket}'s bound filename should be removed upon
* {@link #close()}.
- *
+ *
* Deletion is not guaranteed, especially when not supported (e.g., addresses in the abstract
* namespace).
- *
+ *
* @return {@code true} if an attempt is made to delete the socket file upon {@link #close()}.
*/
public final boolean isDeleteOnClose() {
@@ -340,10 +342,10 @@ public final boolean isDeleteOnClose() {
/**
* Enables/disables deleting this {@link AFDatagramSocket}'s bound filename upon {@link #close()}.
- *
+ *
* Deletion is not guaranteed, especially when not supported (e.g., addresses in the abstract
* namespace).
- *
+ *
* @param b Enabled if {@code true}.
*/
public final void setDeleteOnClose(boolean b) {
@@ -407,7 +409,7 @@ public final void receive(DatagramPacket p) throws IOException {
/**
* Returns the address family supported by this implementation.
- *
+ *
* @return The family.
*/
protected final AFAddressFamily addressFamily() {
@@ -416,7 +418,7 @@ protected final AFAddressFamily addressFamily() {
/**
* Returns the internal helper instance for address-specific extensions.
- *
+ *
* @return The helper instance.
* @throws UnsupportedOperationException if such extensions are not supported for this address
* type.
@@ -424,4 +426,108 @@ protected final AFAddressFamily addressFamily() {
protected AFSocketImplExtensions getImplExtensions() {
return getAFImpl(false).getImplExtensions();
}
+
+ /**
+ * Returns the value of a junixsocket socket option.
+ *
+ * @param AFSocketPair newSocketPair(P s1, P s2) {
+ return new AFGenericSocketPair<>(s1, s2);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public AFGenericSocketPair
+ * The address contains the sa_family identifier as the first byte, and, on some platforms only,
+ * the address length, as the second byte.
+ *
+ * @return A new byte array containing the system-specific representation of that address.
+ */
+ public byte[] toBytes() {
+ byte[] bytes = getBytes();
+ return Arrays.copyOf(bytes, bytes.length);
+ }
+
+ @Override
+ public boolean hasFilename() {
+ return false;
+ }
+
+ @Override
+ public File getFile() throws FileNotFoundException {
+ throw new FileNotFoundException("no file");
+ }
+
+ /**
+ * Checks if an {@link InetAddress} can be unwrapped to an {@link AFGenericSocketAddress}.
+ *
+ * @param addr The instance to check.
+ * @return {@code true} if so.
+ * @see #wrapAddress()
+ * @see #unwrap(InetAddress, int)
+ */
+ public static boolean isSupportedAddress(InetAddress addr) {
+ return AFSocketAddress.isSupportedAddress(addr, addressFamily());
+ }
+
+ /**
+ * Checks if a {@link SocketAddress} can be unwrapped to an {@link AFGenericSocketAddress}.
+ *
+ * @param addr The instance to check.
+ * @return {@code true} if so.
+ * @see #unwrap(InetAddress, int)
+ */
+ public static boolean isSupportedAddress(SocketAddress addr) {
+ return (addr instanceof AFGenericSocketAddress);
+ }
+
+ /**
+ * Returns the corresponding {@link AFAddressFamily}.
+ *
+ * @return The address family instance.
+ */
+ @SuppressWarnings("null")
+ public static synchronized AFAddressFamily
+ * If the {@link ProtocolFamily} is of an {@link AFProtocolFamily}, or {@code UNIX}, the
+ * corresponding junixsocket implementation is used. In all other cases, the call is delegated to
+ * {@link ServerSocketChannel#open()}.
+ *
+ * @param family The protocol family.
+ * @return The new {@link ServerSocketChannel}.
+ * @throws IOException on error.
+ */
+ public static ServerSocketChannel open(ProtocolFamily family) throws IOException {
+ requireNonNull(family);
+
+ if (family instanceof AFProtocolFamily) {
+ return ((AFProtocolFamily) family).openServerSocketChannel();
+ } else if ("UNIX".equals(family.name())) {
+ return AFUNIXServerSocketChannel.open();
+ } else if (family instanceof StandardProtocolFamily) {
+ return ServerSocketChannel.open();
+ } else {
+ throw new UnsupportedOperationException("Protocol family not supported");
+ }
+ }
}
diff --git a/junixsocket-common/src/main/java/org/newsclub/net/unix/AFServerSocketConnector.java b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFServerSocketConnector.java
new file mode 100644
index 000000000..094ce191b
--- /dev/null
+++ b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFServerSocketConnector.java
@@ -0,0 +1,41 @@
+/*
+ * junixsocket
+ *
+ * Copyright 2009-2024 Christian Kohlschütter
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.newsclub.net.unix;
+
+import java.io.IOException;
+
+/**
+ * Some connector that is able to create {@link AFServerSocket}s bound to a given
+ * {@link AFSocketAddress}.
+ *
+ * @param The address type to bind to.
+ * @param
+ * Works like {@link Integer#toUnsignedString(int)}; added to allow execution on Java 1.7.
+ *
+ * @param i The value.
+ * @return The string.
+ */
+ static String toUnsignedString(int i) {
+ return Long.toString(toUnsignedLong(i));
+ }
+
+ /**
+ * Returns a string representation of the first argument as an unsigned integer value in the radix
+ * specified by the second argument; added to allow execution on Java 1.7.
+ *
+ * @param i The value.
+ * @param radix The radix.
+ * @return The string.
+ */
+ static String toUnsignedString(int i, int radix) {
+ return Long.toUnsignedString(toUnsignedLong(i), radix);
+ }
+
+ private static long toUnsignedLong(long x) {
+ return x & 0xffffffffL;
+ }
+
+ /**
+ * Parses the string argument as an unsigned integer in the radix specified by the second
+ * argument. Works like {@link Integer#parseUnsignedInt(String, int)}; added to allow execution on
+ * Java 1.7.
+ *
+ * @param s The string.
+ * @param radix The radix.
+ * @return The integer.
+ * @throws NumberFormatException on parse error.
+ */
+ protected static int parseUnsignedInt(String s, int radix) throws NumberFormatException {
+ if (s == null || s.isEmpty()) {
+ throw new NumberFormatException("Cannot parse null or empty string");
+ }
+
+ int len = s.length();
+ if (s.startsWith("-")) {
+ throw new NumberFormatException("Illegal leading minus sign on unsigned string " + s);
+ }
+
+ if (len <= 5 || (radix == 10 && len <= 9)) {
+ return Integer.parseInt(s, radix);
+ } else {
+ long ell = Long.parseLong(s, radix);
+ if ((ell & 0xffff_ffff_0000_0000L) == 0) {
+ return (int) ell;
+ } else {
+ throw new NumberFormatException("String value exceeds " + "range of unsigned int: " + s);
+ }
+ }
+ }
+
+ /**
+ * Checks if the given {@link SocketAddress} can be mapped to an {@link AFSocketAddress}. This is
+ * the case if the address either already is an {@link AFSocketAddress}, {@code null}, or
+ * something that has an equivalent representation, such as {@code UnixDomainSocketAddress}.
+ *
+ * @param addr The address.
+ * @return {@code true} if mappable.
+ */
+ public static boolean canMap(SocketAddress addr) {
+ return canMap(addr, AFSocketAddress.class);
+ }
+
+ /**
+ * Checks if the given {@link SocketAddress} can be mapped to a specific {@link AFSocketAddress}
+ * subclass. This is the case if the address either already is such an {@link AFSocketAddress},
+ * {@code null}, or something that has an equivalent representation, such as
+ * {@code UnixDomainSocketAddress}.
+ *
+ * @param addr The address.
+ * @param targetAddressClass The target address class to map to.
+ * @return {@code true} if mappable.
+ */
+ public static boolean canMap(SocketAddress addr,
+ Class extends AFSocketAddress> targetAddressClass) {
+ if (addr == null) {
+ return true;
+ } else if (targetAddressClass.isAssignableFrom(addr.getClass())) {
+ return true;
+ }
+ AFSupplier extends AFSocketAddress> supplier = SocketAddressUtil.supplyAFSocketAddress(addr);
+ if (supplier == null) {
+ return false;
+ }
+ AFSocketAddress afAddr = supplier.get();
+ if (afAddr == null) {
+ return false;
+ }
+ return (targetAddressClass.isAssignableFrom(afAddr.getClass()));
+ }
+
+ /**
+ * Maps the given address to an {@link AFSocketAddress}.
+ *
+ * @param addr The address.
+ * @return The {@link AFSocketAddress}.
+ * @throws IllegalArgumentException if the address could not be mapped.
+ * @see #canMap(SocketAddress,Class)
+ */
+ public static AFSocketAddress mapOrFail(SocketAddress addr) {
+ return mapOrFail(addr, AFSocketAddress.class);
+ }
+
+ /**
+ * Maps the given address to a specific {@link AFSocketAddress} type.
+ *
+ * @param addr The address.
+ * @param targetAddressClass The target address class.
+ * @param The target address type.
+ * @return The {@link AFSocketAddress}.
+ * @throws IllegalArgumentException if the address could not be mapped.
+ * @see #canMap(SocketAddress,Class)
+ */
+ @SuppressWarnings("null")
+ public static A mapOrFail(SocketAddress addr,
+ Class targetAddressClass) {
+ if (addr == null) {
+ return null;
+ } else if (targetAddressClass.isAssignableFrom(addr.getClass())) {
+ return targetAddressClass.cast(addr);
+ }
+
+ AFSupplier extends AFSocketAddress> supplier = SocketAddressUtil.supplyAFSocketAddress(addr);
+ if (supplier == null) {
+ throw new IllegalArgumentException("Can only bind to endpoints of type "
+ + AFSocketAddress.class.getName() + ": " + addr);
+ }
+ AFSocketAddress afAddr = supplier.get();
+ if (afAddr == null || !targetAddressClass.isAssignableFrom(afAddr.getClass())) {
+ throw new IllegalArgumentException("Can only bind to endpoints of type "
+ + AFSocketAddress.class.getName() + ", and this specific address is unsupported: "
+ + addr);
+ }
+ return targetAddressClass.cast(afAddr);
+ }
}
diff --git a/junixsocket-common/src/main/java/org/newsclub/net/unix/AFSocketAddressConfig.java b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFSocketAddressConfig.java
index f4b6bc194..d71cc0a67 100644
--- a/junixsocket-common/src/main/java/org/newsclub/net/unix/AFSocketAddressConfig.java
+++ b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFSocketAddressConfig.java
@@ -1,7 +1,7 @@
/*
* junixsocket
*
- * Copyright 2009-2022 Christian Kohlschütter
+ * Copyright 2009-2024 Christian Kohlschütter
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,8 @@
*/
package org.newsclub.net.unix;
+import java.io.IOException;
+import java.net.SocketAddress;
import java.net.SocketException;
import java.net.URI;
import java.util.Set;
@@ -25,7 +27,7 @@
/**
* The implementation-specifics for a given {@link AFSocketAddress} subclass implementation.
- *
+ *
* @param The supported address type.
* @author Christian Kohlschütter
* @see AFAddressFamilyConfig
@@ -40,7 +42,7 @@ protected AFSocketAddressConfig() {
/**
* Tries to parse the given address-specific URI.
- *
+ *
* @param u The URI.
* @param port The port to use, or {@code -1} for "unspecified".
* @return The address.
@@ -50,24 +52,34 @@ protected AFSocketAddressConfig() {
/**
* Returns the implementation's address constructor.
- *
+ *
* @return The implementation's address constructor.
*/
protected abstract AFSocketAddressConstructor addressConstructor();
/**
* Returns the name of the implementation's selector provider class.
- *
+ *
* @return The name of the implementation's selector provider class.
*/
protected abstract String selectorProviderClassname();
/**
* Returns the set of supported URI schemes that can be parsed via {@link #parseURI(URI,int)}.
- *
+ *
* These schemes must be unique to this {@link AFSocketAddress} type.
- *
+ *
* @return The set of supported URI schemes.
*/
protected abstract Set
+ * If the {@link ProtocolFamily} is of an {@link AFProtocolFamily}, or {@code UNIX}, the
+ * corresponding junixsocket implementation is used. In all other cases, the call is delegated to
+ * {@link SocketChannel#open()}.
+ *
+ * @param family The protocol family.
+ * @return The new {@link SocketChannel}.
+ * @throws IOException on error.
+ */
+ public static SocketChannel open(ProtocolFamily family) throws IOException {
+ requireNonNull(family);
+
+ if (family instanceof AFProtocolFamily) {
+ return ((AFProtocolFamily) family).openSocketChannel();
+ } else if ("UNIX".equals(family.name())) {
+ return AFUNIXSocketChannel.open();
+ } else if (family instanceof StandardProtocolFamily) {
+ return SocketChannel.open();
+ } else {
+ throw new UnsupportedOperationException("Protocol family not supported");
+ }
+ }
}
diff --git a/junixsocket-common/src/main/java/org/newsclub/net/unix/AFSocketConnector.java b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFSocketConnector.java
new file mode 100644
index 000000000..94ace570b
--- /dev/null
+++ b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFSocketConnector.java
@@ -0,0 +1,40 @@
+/*
+ * junixsocket
+ *
+ * Copyright 2009-2024 Christian Kohlschütter
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.newsclub.net.unix;
+
+import java.io.IOException;
+
+/**
+ * Some connector that is able to connect to a given {@link AFSocketAddress}.
+ *
+ * @param The address type to connect to.
+ * @param
+ * If the given blocking mode is different from the currently cached blocking mode then this
+ * method native code to change it.
+ * AFSocketPair newSocketPair(P s1, P s2) {
- return new AFUNIXSocketPair (s1, s2);
+ return new AFUNIXSocketPair<>(s1, s2);
}
@SuppressWarnings("unchecked")
@@ -122,6 +122,13 @@ public AFUNIXSocketPair
* Usage:
*
- * IMPORTANT: On some platforms (e.g., Solaris, Illumos) you may need to re-apply a read timeout
- * (e.g., using {@link Socket#setSoTimeout(int)}) after obtaining the socket.
- *
- * Note that you may also lose Java port information for {@link AFSocketAddress} implementations
- * that do not encode this information directly (such as {@link AFUNIXSocketAddress} and
- * {@link AFTIPCSocketAddress}).
- *
+ * The issue can be prevented by keeping a reference to the original object, such as keeping it in
+ * an enclosing try-with-resources block or as a member variable, for example. Alternatively, using
+ * a "duplicate" file descriptor (via {@link #duplicating(FileDescriptor)}) circumvents this
+ * problem, at the cost of using additional system resources.
+ * Note that if any resource that also references this {@link FileDescriptor} is
+ * garbage-collected, the cleanup for that object may close the referenced {@link FileDescriptor},
+ * thereby resulting in premature connection losses, etc. See {@link #duplicating(FileDescriptor)}
+ * for a solution to this problem.
+ *
* @param fdObj The file descriptor.
* @return The {@link FileDescriptorCast} instance.
* @throws IOException on error, especially if the given file descriptor is invalid or
@@ -280,6 +404,7 @@ public static FileDescriptorCast using(FileDescriptor fdObj) throws IOException
if (!fdObj.valid()) {
throw new IOException("Not a valid file descriptor");
}
+
Class> primaryType = NativeUnixSocket.isLoaded() ? NativeUnixSocket.primaryType(fdObj) : null;
if (primaryType == null) {
primaryType = FileDescriptor.class;
@@ -291,17 +416,100 @@ public static FileDescriptorCast using(FileDescriptor fdObj) throws IOException
return new FileDescriptorCast(fdObj, map == null ? GLOBAL_PROVIDERS : map);
}
+ /**
+ * Creates a {@link FileDescriptorCast} using a duplicate of the given file descriptor.
+ *
+ * Duplicating a file descriptor is performed at the system-level, which means an additional file
+ * descriptor pointing to the same resource as the original is created by the operating system.
+ *
+ * The advantage of using {@link #duplicating(FileDescriptor)} over {@link #using(FileDescriptor)}
+ * is that neither implicit garbage collection nor an explicit call to {@link Closeable#close()}
+ * on a resource owning the original {@link FileDescriptor} affects the availability of the
+ * resource from the target of the cast.
+ *
+ * @param fdObj The file descriptor to duplicate.
+ * @return The {@link FileDescriptorCast} instance.
+ * @throws IOException on error, especially if the given file descriptor is invalid or
+ * unsupported, or if duplicating fails or is unsupported.
+ */
+ public static FileDescriptorCast duplicating(FileDescriptor fdObj) throws IOException {
+ if (!fdObj.valid()) {
+ throw new IOException("Not a valid file descriptor");
+ }
+
+ FileDescriptor duplicate = NativeUnixSocket.duplicate(fdObj, new FileDescriptor());
+ if (duplicate == null) {
+ throw new IOException("Could not duplicate file descriptor");
+ }
+ return using(duplicate);
+ }
+
+ /**
+ * Creates a {@link FileDescriptorCast} using the given native file descriptor value.
+ *
+ * This method is inherently unsafe as it may
+ *
+ * Note that attempts are made to reuse {@link FileDescriptor#in}, {@link FileDescriptor#out}, and
+ * {@link FileDescriptor#err}, respectively.
+ *
+ * @param fd The system-native file descriptor value.
+ * @return The {@link FileDescriptorCast} instance.
+ * @throws IOException on error, especially if the given file descriptor is invalid or
+ * unsupported, or when "unsafe" operations are unavailable or manually disabled for the
+ * current environment.
+ */
+ @Unsafe
+ public static FileDescriptorCast unsafeUsing(int fd) throws IOException {
+ AFSocket.ensureUnsafeSupported();
+
+ FileDescriptor fdObj;
+ if (fd == -1) {
+ throw new IOException("Not a valid file descriptor");
+ } else if (fd == FD_IN) {
+ fdObj = FileDescriptor.in;
+ } else if (fd == FD_OUT) {
+ fdObj = FileDescriptor.out;
+ } else if (fd == FD_ERR) {
+ fdObj = FileDescriptor.err;
+ } else {
+ fdObj = null;
+ }
+
+ if (fdObj != null) {
+ int check = getFdIfPossible(fdObj);
+ if (fd == check) {
+ return using(fdObj);
+ }
+ }
+
+ fdObj = new FileDescriptor();
+ NativeUnixSocket.initFD(fdObj, fd);
+
+ return using(fdObj);
+ }
+
private static void triggerInit() {
- AFUNIXSocketAddress.addressFamily().getClass(); // trigger registration
- AFTIPCSocketAddress.addressFamily().getClass(); // trigger registration
+ for (AFAddressFamily> family : new AFAddressFamily>[] {
+ AFUNIXSocketAddress.addressFamily(), //
+ AFTIPCSocketAddress.addressFamily(), //
+ AFVSOCKSocketAddress.addressFamily(), //
+ AFSYSTEMSocketAddress.addressFamily(), //
+ }) {
+ Objects.requireNonNull(family.getClass()); // trigger init
+ }
}
/**
* Registers the given port number as the "local port" for this file descriptor.
- *
+ *
* Important: This only changes the state of this instance. The actual file descriptor is not
* affected.
- *
+ *
* @param port The port to assign to (must be >= 0).
* @return This instance.
*/
@@ -315,10 +523,10 @@ public FileDescriptorCast withLocalPort(int port) {
/**
* Registers the given port number as the "remote port" for this file descriptor.
- *
+ *
* Important: This only changes the state of this instance. The actual file descriptor is not
* affected.
- *
+ *
* @param port The port to assign to (must be >= 0).
* @return This instance.
*/
@@ -332,7 +540,7 @@ public FileDescriptorCast withRemotePort(int port) {
/**
* Casts this instance to the desired type.
- *
+ *
* @param
+ * If {@code isChannel} is false, then we want to cast to a {@link Socket}, {@link DatagramSocket}
+ * or {@link ServerSocket}, which means blocking I/O is desired. If the underlying native socket
+ * is configured non-blocking, we need to reset the state to "blocking" accordingly.
+ *
+ * If {@code isChannel} is true, then we want to cast to a {@link SocketChannel},
+ * {@link DatagramChannel} or {@link ServerSocketChannel}, in which case the blocking state should
+ * be preserved, if possible. It is then up to the user to check blocking state via
+ * {@link AbstractSelectableChannel#isBlocking()} prior to using the socket.
+ *
+ * Note that on Windows, it may be impossible to query the blocking state from an external socket,
+ * so the state is always forcibly set to "blocking".
+ *
+ * @param
+ * The value is controlled by the concrete subclass ({@link #getValue()}). It can, for example, be a
+ * boolean or a counter, depending on the use case. If the value is equal to a "removed" sentinel
+ * value.
+ *
+ * @param
+ * Depending on the "removed" sentinel, the key may be added (if value is non-null but the map
+ * does not yet contain the key), modified (value is non-null, and the map has a different value
+ * for the key), or removed (if value is null).
+ *
+ * @param elem The element to remove.
+ */
+ public void markRemoved(T elem) {
+ if (removedSentinel == null) {
+ map.remove(elem);
+ } else {
+ map.put(elem, removedSentinel);
+ }
+ }
+
+ /**
+ * Sets all entries in the backing map to the "removed" sentinel, or removes them all if that
+ * value is {@code null}.
+ */
+ public void markAllRemoved() {
+ if (removedSentinel == null) {
+ map.clear();
+ } else {
+ for (Map.Entrybind
.
+ *
+ * @param forceAddr The address to use.
+ * @return The new, yet unbound {@link AFServerSocket}.
+ * @throws IOException if an exception occurs.
+ */
+ public static AFGenericServerSocket forceBindOn(final AFGenericSocketAddress forceAddr)
+ throws IOException {
+ return (AFGenericServerSocket) AFServerSocket.forceBindOn(AFGenericServerSocket::new,
+ forceAddr);
+ }
+
+ @Override
+ protected AFSocketImpltrue
iff {@link AFGenericSocket}s (sockets of unknown/otherwise
+ * unsupported type) are supported by the current Java VM and the kernel.
+ *
+ * To support {@link AFGenericSocket}s, a custom JNI library must be loaded that is supplied with
+ * junixsocket.
+ *
+ * This call is equivalent to checking {@link AFSocket#isSupported()}.
+ *
+ * @return {@code true} iff supported.
+ */
+ public static boolean isSupported() {
+ return AFSocket.isSupported();
+ }
+
+ @Override
+ protected AFGenericSocketChannel newChannel() {
+ return new AFGenericSocketChannel(this);
+ }
+
+ /**
+ * Creates a new, unbound {@link AFSocket}.
+ *
+ * This "default" implementation is a bit "lenient" with respect to the specification.
+ *
+ * In particular, we ignore calls to {@link Socket#getTcpNoDelay()} and
+ * {@link Socket#setTcpNoDelay(boolean)}.
+ *
+ * @return A new, unbound socket.
+ * @throws IOException if the operation fails.
+ */
+ public static AFGenericSocket newInstance() throws IOException {
+ return (AFGenericSocket) AFSocket.newInstance(AFGenericSocket::new,
+ (AFGenericSocketFactory) null);
+ }
+
+ static AFGenericSocket newInstance(AFGenericSocketFactory factory) throws SocketException {
+ return (AFGenericSocket) AFSocket.newInstance(AFGenericSocket::new, factory);
+ }
+
+ /**
+ * Creates a new, unbound, "strict" {@link AFSocket}.
+ *
+ * This call uses an implementation that tries to be closer to the specification than
+ * {@link #newInstance()}, at least for some cases.
+ *
+ * @return A new, unbound socket.
+ * @throws IOException if the operation fails.
+ */
+ public static AFGenericSocket newStrictInstance() throws IOException {
+ return (AFGenericSocket) AFSocket.newInstance(AFGenericSocket::new,
+ (AFGenericSocketFactory) null);
+ }
+
+ /**
+ * Creates a new {@link AFSocket} and connects it to the given {@link AFGenericSocketAddress}.
+ *
+ * @param addr The address to connect to.
+ * @return A new, connected socket.
+ * @throws IOException if the operation fails.
+ */
+ public static AFGenericSocket connectTo(AFGenericSocketAddress addr) throws IOException {
+ return (AFGenericSocket) AFSocket.connectTo(AFGenericSocket::new, addr);
+ }
+
+ @Override
+ public AFGenericSocketChannel getChannel() {
+ return (AFGenericSocketChannel) super.getChannel();
+ }
+}
diff --git a/junixsocket-common/src/main/java/org/newsclub/net/unix/AFGenericSocketAddress.java b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFGenericSocketAddress.java
new file mode 100644
index 000000000..93c3f41bb
--- /dev/null
+++ b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFGenericSocketAddress.java
@@ -0,0 +1,234 @@
+/*
+ * junixsocket
+ *
+ * Copyright 2009-2024 Christian Kohlschütter
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.newsclub.net.unix;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+
+import org.newsclub.net.unix.pool.ObjectPool.Lease;
+
+/**
+ * An {@link AFSocketAddress} for unknown socket types.
+ *
+ * @author Christian Kohlschütter
+ */
+public final class AFGenericSocketAddress extends AFSocketAddress {
+ private static final long serialVersionUID = 1L; // do not change!
+
+ private static AFAddressFamilybind
.
- *
+ *
* @param instanceSupplier The constructor of the concrete subclass.
* @param forceAddr The address to use.
* @param The concrete {@link AFSocketAddress} that is supported by this type.
@@ -216,7 +230,7 @@ protected static AFServerSocket forceBindOn(
* Forces the address to be used for any subsequent call to {@link #bind(SocketAddress)} to be the
* given one, regardless of what'll be passed to {@link #bind(SocketAddress, int)}, but doesn't
* bind yet.
- *
+ *
* @param endpoint The forced endpoint address.
* @return This {@link AFServerSocket}.
*/
@@ -226,6 +240,11 @@ public final AFServerSocket forceBindAddress(SocketAddress endpoint) {
});
}
+ @Override
+ public final void bind(SocketAddress endpoint) throws IOException {
+ bind(endpoint, 50);
+ }
+
@SuppressWarnings("unchecked")
@Override
public final void bind(SocketAddress endpoint, int backlog) throws IOException {
@@ -241,10 +260,7 @@ public final void bind(SocketAddress endpoint, int backlog) throws IOException {
bindErrorOk = false;
}
- if (!(endpoint instanceof AFSocketAddress)) {
- throw new IllegalArgumentException("Can only bind to endpoints of type "
- + AFSocketAddress.class.getName() + ": " + endpoint);
- }
+ endpoint = AFSocketAddress.mapOrFail(endpoint);
A endpointCast;
try {
@@ -264,7 +280,7 @@ public final void bind(SocketAddress endpoint, int backlog) throws IOException {
}
}
setBoundEndpoint(getAFImpl().getLocalSocketAddress());
- if (boundEndpoint == null) {
+ if (boundEndpoint0() == null) {
setBoundEndpoint(endpointCast);
}
@@ -277,7 +293,7 @@ public final void bind(SocketAddress endpoint, int backlog) throws IOException {
@Override
public final boolean isBound() {
- return boundEndpoint != null && implementation.getFD().valid();
+ return boundEndpoint0() != null && implementation.getFD().valid();
}
@Override
@@ -297,7 +313,7 @@ AFSocket accept1(boolean throwOnFail) throws IOException {
boolean success = implementation.accept0(as.getAFImpl(false));
if (isClosed()) {
// We may have connected to the socket to unblock it
- throw new SocketClosedException("Socket is closed");
+ throw new BrokenPipeSocketException("Socket is closed");
}
if (!success) {
@@ -323,7 +339,7 @@ AFSocket accept1(boolean throwOnFail) throws IOException {
/**
* Returns a new {@link AFSocket} instance.
- *
+ *
* @return The new instance.
* @throws IOException on error.
*/
@@ -331,11 +347,14 @@ AFSocket accept1(boolean throwOnFail) throws IOException {
@Override
public String toString() {
- return getClass().getSimpleName() + "[" + (isBound() ? boundEndpoint : "unbound") + "]";
+ return getClass().getSimpleName() + "[" + (isBound() ? boundEndpoint0() : "unbound") + "]";
}
@Override
- public synchronized void close() throws IOException {
+ public void close() throws IOException {
+ if (!closed.compareAndSet(false, true)) {
+ return;
+ }
if (isClosed()) {
return;
}
@@ -381,7 +400,7 @@ && isDeleteOnClose()) {
/**
* Registers a {@link Closeable} that should be closed when this socket is closed.
- *
+ *
* @param closeable The closeable.
*/
public final void addCloseable(Closeable closeable) {
@@ -390,7 +409,7 @@ public final void addCloseable(Closeable closeable) {
/**
* Unregisters a previously registered {@link Closeable}.
- *
+ *
* @param closeable The closeable.
*/
public final void removeCloseable(Closeable closeable) {
@@ -399,7 +418,7 @@ public final void removeCloseable(Closeable closeable) {
/**
* Checks whether everything is setup to support junixsocket sockets.
- *
+ *
* @return {@code true} if supported.
*/
public static boolean isSupported() {
@@ -409,18 +428,25 @@ public static boolean isSupported() {
@Override
@SuppressFBWarnings("EI_EXPOSE_REP")
public final @Nullable A getLocalSocketAddress() {
- if (boundEndpoint == null) {
- setBoundEndpoint(getAFImpl().getLocalSocketAddress());
+ @Nullable
+ A ep = boundEndpoint0();
+ if (ep == null) {
+ ep = getAFImpl().getLocalSocketAddress();
+ setBoundEndpoint(ep);
}
+ return ep;
+ }
+
+ private synchronized @Nullable A boundEndpoint0() {
return boundEndpoint;
}
/**
* Checks if the local socket address returned by {@link #getLocalSocketAddress()} is still valid.
- *
+ *
* The address is no longer valid if the server socket has been closed, {@code null}, or another
* server socket has been bound on that address.
- *
+ *
* @return {@code true} iff still valid.
*/
public boolean isLocalSocketAddressValid() {
@@ -435,7 +461,7 @@ public boolean isLocalSocketAddressValid() {
return addr.equals(getAFImpl().getLocalSocketAddress());
}
- final void setBoundEndpoint(@Nullable A addr) {
+ final synchronized void setBoundEndpoint(@Nullable A addr) {
this.boundEndpoint = addr;
int port;
if (addr == null) {
@@ -448,10 +474,10 @@ final void setBoundEndpoint(@Nullable A addr) {
@Override
public final int getLocalPort() {
- if (boundEndpoint == null) {
+ if (boundEndpoint0() == null) {
setBoundEndpoint(getAFImpl().getLocalSocketAddress());
}
- if (boundEndpoint == null) {
+ if (boundEndpoint0() == null) {
return -1;
} else {
return getAFImpl().getLocalPort1();
@@ -460,10 +486,10 @@ public final int getLocalPort() {
/**
* Checks if this {@link AFServerSocket}'s file should be removed upon {@link #close()}.
- *
+ *
* Deletion is not guaranteed, especially when not supported (e.g., addresses in the abstract
* namespace).
- *
+ *
* @return {@code true} if an attempt is made to delete the socket file upon {@link #close()}.
*/
public final boolean isDeleteOnClose() {
@@ -473,10 +499,10 @@ public final boolean isDeleteOnClose() {
/**
* Enables/disables deleting this {@link AFServerSocket}'s file (or other resource type) upon
* {@link #close()}.
- *
+ *
* Deletion is not guaranteed, especially when not supported (e.g., addresses in the abstract
* namespace).
- *
+ *
* @param b Enabled if {@code true}.
*/
public final void setDeleteOnClose(boolean b) {
@@ -486,6 +512,7 @@ public final void setDeleteOnClose(boolean b) {
final AFSocketImpl getAFImpl() {
if (created.compareAndSet(false, true)) {
try {
+ getAFImpl().create(true);
getSoTimeout(); // trigger create via java.net.Socket
} catch (IOException e) {
// ignore
@@ -496,7 +523,7 @@ final AFSocketImpl getAFImpl() {
@SuppressFBWarnings("EI_EXPOSE_REP")
@Override
- public AFServerSocketChannel> getChannel() {
+ public AFServerSocketChannel getChannel() {
return channel;
}
@@ -507,7 +534,7 @@ public final FileDescriptor getFileDescriptor() throws IOException {
/**
* Returns the address family supported by this implementation.
- *
+ *
* @return The family.
*/
protected final AFAddressFamily addressFamily() {
@@ -517,9 +544,9 @@ protected final AFAddressFamily addressFamily() {
/**
* Sets the hook for any subsequent call to {@link #bind(SocketAddress)} and
* {@link #bind(SocketAddress, int)} to be the given function.
- *
+ *
* The function can monitor calls or even alter the endpoint address.
- *
+ *
* @param hook The function that gets called for each {@code bind} call.
* @return This instance.
*/
@@ -527,4 +554,117 @@ public final AFServerSocket bindHook(SocketAddressFilter hook) {
this.bindFilter = hook;
return this;
}
+
+ @Override
+ public InetAddress getInetAddress() {
+ if (!isBound()) {
+ return null;
+ } else {
+ return getAFImpl().getInetAddress();
+ }
+ }
+
+ @Override
+ public synchronized void setReceiveBufferSize(int size) throws SocketException {
+ if (size <= 0) {
+ throw new IllegalArgumentException("receive buffer size must be a positive number");
+ }
+ if (isClosed()) {
+ throw new SocketException("Socket is closed");
+ }
+ getAFImpl().setOption(SocketOptions.SO_RCVBUF, size);
+ }
+
+ @Override
+ public synchronized int getReceiveBufferSize() throws SocketException {
+ if (isClosed()) {
+ throw new SocketException("Socket is closed");
+ }
+ int result = 0;
+ Object o = getAFImpl().getOption(SocketOptions.SO_RCVBUF);
+ if (o instanceof Number) {
+ result = ((Number) o).intValue();
+ }
+ return result;
+ }
+
+ @Override
+ @SuppressWarnings("UnsynchronizedOverridesSynchronized" /* errorprone */)
+ public void setSoTimeout(int timeout) throws SocketException {
+ if (isClosed()) {
+ throw new SocketException("Socket is closed");
+ }
+ if (timeout < 0) {
+ throw new IllegalArgumentException("timeout < 0");
+ }
+ getAFImpl().setOption(SocketOptions.SO_TIMEOUT, timeout);
+ }
+
+ @Override
+ @SuppressWarnings("UnsynchronizedOverridesSynchronized" /* errorprone */)
+ public int getSoTimeout() throws IOException {
+ if (isClosed()) {
+ throw new SocketException("Socket is closed");
+ }
+ Object o = getAFImpl().getOption(SocketOptions.SO_TIMEOUT);
+ /* extra type safety */
+ if (o instanceof Number) {
+ return ((Number) o).intValue();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public void setReuseAddress(boolean on) throws SocketException {
+ if (isClosed()) {
+ throw new SocketException("Socket is closed");
+ }
+ getAFImpl().setOption(SocketOptions.SO_REUSEADDR, on);
+ }
+
+ @Override
+ public boolean getReuseAddress() throws SocketException {
+ if (isClosed()) {
+ throw new SocketException("Socket is closed");
+ }
+ return ((Boolean) (getAFImpl().getOption(SocketOptions.SO_REUSEADDR)));
+ }
+
+ @Override
+ public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) {
+ }
+
+ @SuppressWarnings({"all", "MissingOverride" /* errorprone */})
+ public true
iff {@link AFSocket}s are supported by the current Java VM.
- *
+ *
* To support {@link AFSocket}s, a custom JNI library must be loaded that is supplied with
* junixsocket.
- *
+ *
* @return {@code true} iff supported.
*/
public static boolean isSupported() {
@@ -314,9 +318,9 @@ public static boolean isSupported() {
/**
* Checks if {@link AFSocket}s are supported by the current Java VM.
- *
+ *
* If not, an {@link UnsupportedOperationException} is thrown.
- *
+ *
* @throws UnsupportedOperationException if not supported.
*/
public static void ensureSupported() throws UnsupportedOperationException {
@@ -325,13 +329,18 @@ public static void ensureSupported() throws UnsupportedOperationException {
/**
* Returns the version of the junixsocket library, as a string, for debugging purposes.
- *
+ *
* NOTE: Do not rely on the format of the version identifier, use socket capabilities instead.
- *
+ *
* @return String The version identifier, or {@code null} if it could not be determined.
* @see #supports(AFSocketCapability)
*/
public static final String getVersion() {
+ String v = BuildProperties.getBuildProperties().get("git.build.version");
+ if (v != null && !v.startsWith("$")) {
+ return v;
+ }
+
try {
return NativeLibraryLoader.getJunixsocketVersion();
} catch (IOException e) {
@@ -342,9 +351,9 @@ public static final String getVersion() {
/**
* Returns an identifier of the loaded native library, or {@code null} if the library hasn't been
* loaded yet.
- *
+ *
* The identifier is useful mainly for debugging purposes.
- *
+ *
* @return The identifier of the loaded junixsocket-native library, or {@code null}.
*/
public static final String getLoadedLibrary() {
@@ -371,6 +380,11 @@ public final void ensureAncillaryReceiveBufferSize(int minSize) {
impl.ensureAncillaryReceiveBufferSize(minSize);
}
+ private static boolean isCapDisabled(AFSocketCapability cap) {
+ return Boolean.parseBoolean(System.getProperty(PROP_LIBRARY_DISABLE_CAPABILITY_PREFIX + cap
+ .name(), "false"));
+ }
+
private static int initCapabilities() {
if (!isSupported()) {
return 0;
@@ -392,41 +406,58 @@ private static int initCapabilities() {
}
}
- private static boolean isCapDisabled(AFSocketCapability cap) {
- return Boolean.valueOf(System.getProperty(PROP_LIBRARY_DISABLE_CAPABILITY_PREFIX + cap.name(),
- "false"));
+ private static synchronized int capabilities() {
+ if (capabilitiesValue == null) {
+ capabilitiesValue = initCapabilities();
+ }
+ return capabilitiesValue;
}
/**
* Checks if the current environment (system platform, native library, etc.) supports a given
* junixsocket capability.
- *
+ *
* Deprecated. Please use {@link #supports(AFSocketCapability)} instead.
- *
+ *
* NOTE: The result may or may not be cached from a previous call or from a check upon
* initialization.
- *
+ *
* @param capability The capability.
* @return true if supported.
* @see #supports(AFSocketCapability)
*/
@Deprecated
public static final boolean supports(AFUNIXSocketCapability capability) {
- return (CAPABILITIES & capability.getBitmask()) != 0;
+ return (capabilities() & capability.getBitmask()) != 0;
}
/**
* Checks if the current environment (system platform, native library, etc.) supports a given
* junixsocket capability.
- *
+ *
* NOTE: The result may or may not be cached from a previous call or from a check upon
* initialization.
- *
+ *
* @param capability The capability.
* @return true if supported.
*/
public static final boolean supports(AFSocketCapability capability) {
- return (CAPABILITIES & capability.getBitmask()) != 0;
+ return (capabilities() & capability.getBitmask()) != 0;
+ }
+
+ /**
+ * Checks if the current environment (system platform, native library, etc.) supports "unsafe"
+ * operations (as controlled via the {@link AFSocketCapability#CAPABILITY_UNSAFE} capability).
+ *
+ * If supported, the method returns normally. If not supported, an {@link IOException} is thrown.
+ *
+ * @throws IOException if "unsafe" operations are not supported.
+ * @see Unsafe
+ */
+ public static final void ensureUnsafeSupported() throws IOException {
+ if (!AFSocket.supports(AFSocketCapability.CAPABILITY_UNSAFE)) {
+ throw new IOException("Unsafe operations are not supported in this environment");
+ }
}
@Override
@@ -442,7 +473,7 @@ public final synchronized void close() throws IOException {
/**
* Registers a {@link Closeable} that should be closed when this socket is closed.
- *
+ *
* @param closeable The closeable.
*/
public final void addCloseable(Closeable closeable) {
@@ -451,7 +482,7 @@ public final void addCloseable(Closeable closeable) {
/**
* Unregisters a previously registered {@link Closeable}.
- *
+ *
* @param closeable The closeable.
*/
public final void removeCloseable(Closeable closeable) {
@@ -514,7 +545,7 @@ public final AFOutputStream getOutputStream() throws IOException {
/**
* Returns the internal helper instance for address-specific extensions.
- *
+ *
* @return The helper instance.
* @throws UnsupportedOperationException if such extensions are not supported for this address
* type.
@@ -526,7 +557,7 @@ protected final AFSocketImplExtensions getImplExtensions() {
/**
* Forces the address to be used for any subsequent call to {@link #connect(SocketAddress)} to be
* the given one, regardless of what'll be passed there.
- *
+ *
* @param endpoint The forced endpoint address.
* @return This instance.
*/
@@ -539,9 +570,9 @@ public final AFSocket forceConnectAddress(SocketAddress endpoint) {
/**
* Sets the hook for any subsequent call to {@link #connect(SocketAddress)} or
* {@link #connect(SocketAddress, int)} to be the given function.
- *
+ *
* The function can monitor events or even alter the target address.
- *
+ *
* @param hook The function that gets called for each connect call.
* @return This instance.
*/
@@ -552,10 +583,10 @@ public final AFSocket connectHook(SocketAddressFilter hook) {
/**
* Probes the status of the socket connection.
- *
+ *
* This usually involves checking for {@link #isConnected()}, and if assumed connected, also
* sending a zero-length message to the remote.
- *
+ *
* @return {@code true} if the connection is known to be closed, {@code false} if the connection
* is open/not closed or the condition is unknown.
* @throws IOException on an unexpected error.
@@ -580,4 +611,18 @@ public boolean checkConnectionClosed() throws IOException {
}
}
}
+
+ /**
+ * Checks if we're running on Android (as far as junixsocket is concerned).
+ *
+ * @return {@code true} if running on Android.
+ */
+ public static boolean isRunningOnAndroid() {
+ return NativeLibraryLoader.isAndroid();
+ }
+
+ @Override
+ public void setShutdownOnClose(boolean enabled) {
+ getAFImpl().getCore().setShutdownOnClose(enabled);
+ }
}
diff --git a/junixsocket-common/src/main/java/org/newsclub/net/unix/AFSocketAddress.java b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFSocketAddress.java
index 13ff3a541..989a78176 100644
--- a/junixsocket-common/src/main/java/org/newsclub/net/unix/AFSocketAddress.java
+++ b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFSocketAddress.java
@@ -1,7 +1,7 @@
/*
* junixsocket
*
- * Copyright 2009-2022 Christian Kohlschütter
+ * Copyright 2009-2024 Christian Kohlschütter
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,10 +17,13 @@
*/
package org.newsclub.net.unix;
+import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@@ -29,6 +32,7 @@
import java.net.SocketException;
import java.net.URI;
import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -36,14 +40,21 @@
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
+import org.newsclub.net.unix.pool.ObjectPool;
+import org.newsclub.net.unix.pool.ObjectPool.Lease;
+
+import com.google.errorprone.annotations.Immutable;
+import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;
/**
* Some {@link SocketAddress} that is supported by junixsocket, such as {@link AFUNIXSocketAddress}.
- *
+ *
* @author Christian Kohlschütter
*/
+@Immutable
+@SuppressWarnings({"PMD.CouplingBetweenObjects", "PMD.CyclomaticComplexity"})
public abstract class AFSocketAddress extends InetSocketAddress {
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 1L; // do not change!
/**
* Just a marker for "don't actually bind" (checked with "=="). Used in combination with a
@@ -65,102 +76,256 @@ public abstract class AFSocketAddress extends InetSocketAddress {
private static final Maporg.newsclub.net.unix.library.disable.CAPABILITY_SOMETHING_SOMETHING=true
+ * when invoking the JVM (make sure this property is set before junixsocket is accessed).
+ *
+ * A simple way to check which capabilities are supported in an environment is to run the
+ * `junixsocket-selftest` jar.
*/
public enum AFSocketCapability {
// see org_newsclub_net_unix_NativeUnixSocket.c in junixsocket-native
@@ -41,16 +48,16 @@ public enum AFSocketCapability {
/** Socket addressing supports the abstract namespace (Linux). */
CAPABILITY_ABSTRACT_NAMESPACE(3),
- /** Support for AF_UNIX datagrams (not on Windows yet). */
+ /** Support for AF_UNIX datagrams. */
CAPABILITY_UNIX_DATAGRAMS(4),
/**
* A pair of interconnected sockets can be created natively as AF_UNIX sockets.
- *
+ *
* This currently not possible on Windows, but instead emulated via anonymous AF_INET ports when
* you use {@link AFSocketPair}. Other systems may provide partial implementations of pipe-based
* (i.e., non-socket) pairs.
- *
+ *
* This capability is specific to AF_UNIX sockets. Other sockets, such as AF_VSOCK, may not
* implement socketpair natively even if this capability is set, but would work-around that
* limitation in a similar fashion but maybe without resorting to AF_INET.
@@ -59,14 +66,14 @@ public enum AFSocketCapability {
/**
* A file descriptor can be converted to {@link Redirect}.
- *
+ *
* This feature currently uses Java SDK internals that may change/disappear.
*/
CAPABILITY_FD_AS_REDIRECT(6),
/**
* Support for AF_TIPC.
- *
+ *
* Availability of this feature is checked upon launch and therefore loading the "tipc" kernel
* module at a later point may not be properly reflected.
*/
@@ -74,10 +81,10 @@ public enum AFSocketCapability {
/**
* Support for AF_UNIX.
- *
+ *
* Availability of this feature is checked upon launch and therefore, on systems adding support at
* a later point, may not be properly reflected when checking at a later point.
- *
+ *
* NOTE: While this capability is typically supported on most systems that can actually load a
* junixsocket JNI library, it is unavailable for older Windows versions (such as 8.1, 10 before
* AFUNIX.SYS was included, etc.) and on systems where support for UNIX domain sockets is actively
@@ -90,7 +97,7 @@ public enum AFSocketCapability {
*
* Availability of this feature is checked upon launch and therefore enabling vsock at a later
* point may not be properly reflected.
- *
+ *
* @see #CAPABILITY_VSOCK_DGRAM
*/
CAPABILITY_VSOCK(9),
@@ -106,16 +113,39 @@ public enum AFSocketCapability {
/**
* Support for zero-length send(2).
- *
+ *
* This can be used to perform a connection check, but not all operating systems support this or
* behave correctly (particularly, IBM AIX, IBM i, and IBM z/OS) at the moment.
- *
+ *
* If not supported, junixsocket will simply ignore writes of zero-length, and connection checking
* with {@link AFSocket#checkConnectionClosed()} may return {@code false} regardless of the actual
* condition.
*/
CAPABILITY_ZERO_LENGTH_SEND(11),
+ /**
+ * Support for "unsafe" operations.
+ *
+ * Trading-in safety for speed or simplicity may be justified sometimes.
+ *
+ * @see Unsafe
+ * @see AFSocket#ensureUnsafeSupported()
+ */
+ CAPABILITY_UNSAFE(12),
+
+ /**
+ * Support for port numbers larger than 65535 (0xffff).
+ *
+ * Not all systems allow setting port numbers beyond the default TCP range (we use JNI tricks for
+ * that). This capability is required for RMI support.
+ */
+ CAPABILITY_LARGE_PORTS(13),
+
+ /**
+ * Support for certain Darwin (macOS Kernel)-specific features, such as the AF_SYSTEM domain.
+ */
+ CAPABILITY_DARWIN(14),
+
; // end of list
private final int bitmask;
diff --git a/junixsocket-common/src/main/java/org/newsclub/net/unix/AFSocketChannel.java b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFSocketChannel.java
index 5c05353df..e971bc759 100644
--- a/junixsocket-common/src/main/java/org/newsclub/net/unix/AFSocketChannel.java
+++ b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFSocketChannel.java
@@ -1,7 +1,7 @@
/*
* junixsocket
*
- * Copyright 2009-2022 Christian Kohlschütter
+ * Copyright 2009-2024 Christian Kohlschütter
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,10 +17,14 @@
*/
package org.newsclub.net.unix;
+import static java.util.Objects.requireNonNull;
+
import java.io.FileDescriptor;
import java.io.IOException;
+import java.net.ProtocolFamily;
import java.net.SocketAddress;
import java.net.SocketOption;
+import java.net.StandardProtocolFamily;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
@@ -34,22 +38,22 @@
/**
* A selectable channel for stream-oriented connecting sockets.
- *
+ *
* @param The concrete {@link AFSocketAddress} that is supported by this type.
* @author Christian Kohlschütter
*/
public abstract class AFSocketChannel extends SocketChannel implements
- AFSomeSocket, AFSocketExtensions {
+ AFSomeSocket, AFSocketExtensions, AFSomeSocketChannel {
private final @NonNull AFSocket afSocket;
private final AtomicBoolean connectPending = new AtomicBoolean(false);
/**
* Creates a new socket channel for the given socket, using the given {@link SelectorProvider}.
- *
+ *
* @param socket The socket.
* @param sp The {@link SelectorProvider}.
*/
- @SuppressWarnings("null")
+ @SuppressWarnings("all")
protected AFSocketChannel(AFSocket socket, AFSelectorProvider sp) {
super(sp);
this.afSocket = Objects.requireNonNull(socket);
@@ -57,7 +61,7 @@ protected AFSocketChannel(AFSocket socket, AFSelectorProvider sp) {
/**
* Returns the corresponding {@link AFSocket}.
- *
+ *
* @return The corresponding socket.
*/
protected final AFSocket getAFSocket() {
@@ -66,14 +70,14 @@ protected final AFSocket getAFSocket() {
/**
* A reference to a method that provides an {@link AFSocket} instance.
- *
+ *
* @param The concrete {@link AFSocketAddress} that is supported by this type.
*/
@FunctionalInterface
protected interface AFSocketSupplier {
/**
* Returns a new {@link AFSocket} instance.
- *
+ *
* @return The instance.
* @throws IOException on error.
*/
@@ -85,7 +89,7 @@ protected interface AFSocketSupplier {
*
* @param The concrete {@link AFSocketAddress} that is supported by this type.
* @param supplier The AFSocketChannel constructor.
- *
+ *
* @return The new channel
* @throws IOException on error.
*/
@@ -131,7 +135,7 @@ public final
*
bind
.
- *
+ *
* @param forceAddr The address to use.
* @return The new, yet unbound {@link AFServerSocket}.
* @throws IOException if an exception occurs.
@@ -142,13 +142,13 @@ public static AFUNIXServerSocket forceBindOn(final AFUNIXSocketAddress forceAddr
}
@Override
- protected AFUNIXSocketImpl newImpl(FileDescriptor fdObj) throws SocketException {
+ protected AFSocketImpltrue
iff {@link AFUNIXSocket}s are supported by the current Java VM.
- *
+ *
* To support {@link AFSocket}s, a custom JNI library must be loaded that is supplied with
* junixsocket, and the system must support AF_UNIX sockets.
- *
+ *
* This call is equivalent to checking {@link AFSocket#isSupported()} and
* {@link AFSocket#supports(AFSocketCapability)} with
* {@link AFSocketCapability#CAPABILITY_UNIX_DOMAIN}.
- *
+ *
* @return {@code true} iff supported.
*/
public static boolean isSupported() {
@@ -163,13 +169,14 @@ public static boolean isSupported() {
/**
* Very basic self-test function.
- *
+ *
* Prints "supported" and "capabilities" status to System.out.
- *
+ *
* @param args ignored.
*/
public static void main(String[] args) {
- // If you want to run this directly from within Eclipse, see AFUNIXSocketTest#testMain.
+ // If you want to run this directly from within Eclipse, see
+ // org.newsclub.net.unix.domain.SocketTest#testMain.
System.out.print(AFUNIXSocket.class.getName() + ".isSupported(): ");
System.out.flush();
System.out.println(AFUNIXSocket.isSupported());
@@ -179,5 +186,75 @@ public static void main(String[] args) {
System.out.flush();
System.out.println(AFSocket.supports(cap));
}
+ System.out.println();
+ if (AFSocket.supports(AFSocketCapability.CAPABILITY_UNIX_DOMAIN)) {
+ System.out.println("Starting mini selftest...");
+ miniSelftest();
+ } else {
+ System.out.println(
+ "Skipping mini selftest; AFSocketCapability.CAPABILITY_UNIX_DOMAIN is missing");
+ }
+ }
+
+ private static void miniSelftest() {
+ AtomicBoolean success = new AtomicBoolean(true);
+ try {
+ AFUNIXSocketAddress addr = AFUNIXSocketAddress.ofNewTempFile();
+ System.out.println("Using temporary address: " + addr);
+ try (AFUNIXServerSocket server = addr.newBoundServerSocket()) {
+ Thread t = new Thread(() -> {
+ try {
+ try (AFUNIXSocket client = server.accept()) { // NOPMD.UseTryWithResources
+ System.out.println("Server accepted client connection");
+ try (SocketChannel chann = client.getChannel()) {
+ ByteBuffer bb = ByteBuffer.allocate(64).order(ByteOrder.BIG_ENDIAN);
+
+ int numRead = 0;
+ while (bb.position() != 4 && numRead != -1) {
+ numRead = chann.read(bb);
+ }
+ if (bb.position() != 4) {
+ throw new IOException("Unexpected number of bytes read: " + bb.position());
+ }
+ bb.flip();
+ int v;
+ if ((v = bb.getInt()) != 0xABCDEF12) {
+ throw new IOException("Received unexpected data from client: 0x" + Integer
+ .toHexString(v));
+ }
+ bb.clear();
+ bb.putLong(0x00112233456789L);
+ bb.flip();
+ chann.write(bb);
+ }
+ } finally {
+ server.close(); // NOPMD
+ }
+ } catch (Exception e) { // NOPMD
+ success.set(false);
+ e.printStackTrace();
+ }
+ });
+ t.start();
+
+ try (AFUNIXSocket socket = addr.newConnectedSocket();
+ DataInputStream in = new DataInputStream(socket.getInputStream());
+ DataOutputStream out = new DataOutputStream(socket.getOutputStream());) {
+ out.writeInt(0xABCDEF12);
+ out.flush();
+ long v = in.readLong();
+ if (v != 0x00112233456789L) {
+ throw new IOException("Received unexpected data from server: 0x" + Long.toHexString(v));
+ }
+ }
+ System.out.println("Data exchange succeeded");
+ }
+ } catch (Exception e) { // NOPMD
+ success.set(false);
+ e.printStackTrace();
+ return;
+ } finally {
+ System.out.println("mini selftest " + (success.get() ? "passed" : "failed"));
+ }
}
}
diff --git a/junixsocket-common/src/main/java/org/newsclub/net/unix/AFUNIXSocketAddress.java b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFUNIXSocketAddress.java
index 4325b2a56..8b1d073bf 100644
--- a/junixsocket-common/src/main/java/org/newsclub/net/unix/AFUNIXSocketAddress.java
+++ b/junixsocket-common/src/main/java/org/newsclub/net/unix/AFUNIXSocketAddress.java
@@ -1,7 +1,7 @@
/*
* junixsocket
*
- * Copyright 2009-2022 Christian Kohlschütter
+ * Copyright 2009-2024 Christian Kohlschütter
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,18 +37,19 @@
import java.util.Set;
import org.eclipse.jdt.annotation.NonNull;
+import org.newsclub.net.unix.pool.ObjectPool.Lease;
/**
* Describes an {@link InetSocketAddress} that actually uses AF_UNIX sockets instead of AF_INET.
- *
+ *
* The ability to specify a port number is not specified by AF_UNIX sockets, but we need it
* sometimes, for example for RMI-over-AF_UNIX.
- *
+ *
* @author Christian Kohlschütter
*/
@SuppressWarnings("PMD.ShortMethodName")
public final class AFUNIXSocketAddress extends AFSocketAddress {
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 1L; // do not change!
private static final Charset ADDRESS_CHARSET = Charset.defaultCharset();
@@ -57,6 +58,10 @@ public final class AFUNIXSocketAddress extends AFSocketAddress {
.registerAddressFamily("un", //
AFUNIXSocketAddress.class, new AFSocketAddressConfig
@@ -46,9 +48,10 @@
*
*
* FileDescriptor fd;
- *
+ *
* // succeeds if fd refers to an AF_UNIX stream socket
- * AFUNIXSocket socket = FileDescriptorCast.using(fd).as(AFUNIXSocket.class);
- *
+ * AFUNIXSocket socket = FileDescriptorCast.using(fd).as(AFUNIXSocket.class);
+ *
* // succeeds if fd refers to an AF_UNIX datagram socket
- * AFUNIXDatagramChannel channel = FileDescriptorCast.using(fd).as(AFUNIXDatagramChannel.class);
- *
+ * AFUNIXDatagramChannel channel = FileDescriptorCast.using(fd).as(AFUNIXDatagramChannel.class);
+ *
* // always succeeds
- * InputStream in = FileDescriptorCast.using(fd).as(InputStream.class);
- * OutputStream in = FileDescriptorCast.using(fd).as(OutputStream.class);
+ * InputStream in = FileDescriptorCast.using(fd).as(InputStream.class);
+ * OutputStream in = FileDescriptorCast.using(fd).as(OutputStream.class);
*
+ *
+ *
* @author Christian Kohlschütter
*/
+@SuppressWarnings("PMD.CouplingBetweenObjects")
public final class FileDescriptorCast implements FileDescriptorAccess {
private static final Map
+ *
+ * > S reconfigure(boolean isChannel, S socket)
+ throws IOException {
+ reconfigure(isChannel, socket.getChannel());
+ socket.getAFImpl().getCore().disableCleanFd();
+ return socket;
+ }
+
+ @SuppressWarnings("null")
+ private static > S reconfigure(boolean isChannel, S socket)
+ throws IOException {
+ reconfigure(isChannel, socket.getChannel());
+ socket.getAFImpl().getCore().disableCleanFd();
+ return socket;
+ }
+
+ @SuppressWarnings("null")
+ private static > S reconfigure(boolean isChannel, S socket)
+ throws IOException {
+ reconfigure(isChannel, socket.getChannel());
+ socket.getAFImpl().getCore().disableCleanFd();
+ return socket;
+ }
+
+ /**
+ * Reconfigures the Java-side of the socket/socket channel such that its state is compatible with
+ * the native socket's state. This is necessary to properly configure blocking/non-blocking state,
+ * as that is cached on the Java side.
+ * The type.
+ * @param isChannel The desired cast type (socket=set to blocking, or channel=preserve state).
+ * @param socketChannel The channel.
+ * @throws IOException on error.
+ */
+ private static <@NonNull S extends AFSomeSocketChannel> void reconfigure(boolean isChannel,
+ S socketChannel) throws IOException {
+ if (isChannel) {
+ reconfigureKeepBlockingState(socketChannel);
+ } else {
+ reconfigureSetBlocking(socketChannel);
+ }
+ }
+
+ private static <@NonNull S extends AFSomeSocketChannel> void reconfigureKeepBlockingState(
+ S socketChannel) throws IOException {
+ int result = NativeUnixSocket.checkBlocking(socketChannel.getFileDescriptor());
+
+ boolean blocking;
+ switch (result) {
+ case 0:
+ blocking = false;
+ break;
+ case 1:
+ blocking = true;
+ break;
+ case 2:
+ // need to reconfigure/forcibly override any cached result -> set to blocking by default
+ socketChannel.configureBlocking(false);
+ socketChannel.configureBlocking(true);
+ return;
+ default:
+ throw new OperationNotSupportedSocketException("Invalid blocking state");
+ }
+
+ socketChannel.configureBlocking(blocking);
+ }
+
+ private static <@NonNull S extends AFSomeSocketChannel> void reconfigureSetBlocking(
+ S socketChannel) throws IOException {
+ int result = NativeUnixSocket.checkBlocking(socketChannel.getFileDescriptor());
+
+ switch (result) {
+ case 0:
+ // see below
+ break;
+ case 1:
+ // already blocking, nothing to do
+ return;
+ case 2:
+ // need to reconfigure/forcibly override any cached result -> set to blocking by default
+ // see below
+ break;
+ default:
+ throw new OperationNotSupportedSocketException("Invalid blocking state");
+ }
+
+ socketChannel.configureBlocking(false);
+ socketChannel.configureBlocking(true);
+ }
}
diff --git a/junixsocket-common/src/main/java/org/newsclub/net/unix/HostAndPort.java b/junixsocket-common/src/main/java/org/newsclub/net/unix/HostAndPort.java
index ba1782759..c3b0d7c76 100644
--- a/junixsocket-common/src/main/java/org/newsclub/net/unix/HostAndPort.java
+++ b/junixsocket-common/src/main/java/org/newsclub/net/unix/HostAndPort.java
@@ -1,7 +1,7 @@
/*
* junixsocket
*
- * Copyright 2009-2022 Christian Kohlschütter
+ * Copyright 2009-2024 Christian Kohlschütter
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,7 +28,7 @@
/**
* Hostname and port.
- *
+ *
* @author Christian Kohlschütter
*/
public final class HostAndPort {
@@ -39,7 +39,7 @@ public final class HostAndPort {
/**
* Creates a new hostname and port combination.
- *
+ *
* @param hostname The hostname.
* @param port The port, or {@code -1} for "no port".
*/
@@ -88,7 +88,7 @@ public String toString() {
/**
* Tries to extract hostname and port information from the given URI.
- *
+ *
* @param u The URI to extract from.
* @return The parsed {@link HostAndPort} instance.
* @throws SocketException on error.
@@ -130,7 +130,7 @@ private static String urlEncode(String s) {
/**
* Returns the hostname.
- *
+ *
* @return The hostname.
*/
public String getHostname() {
@@ -139,7 +139,7 @@ public String getHostname() {
/**
* Returns the port, or {@code -1} for "no port specified".
- *
+ *
* @return The port.
*/
public int getPort() {
@@ -148,7 +148,7 @@ public int getPort() {
/**
* Returns a URI with this hostname and port.
- *
+ *
* @param scheme The scheme to use.
* @return The URI.
*/
@@ -159,7 +159,7 @@ public URI toURI(String scheme) {
/**
* Returns a URI with this hostname and port, potentially reusing other URI parameters from the
* given template URI (authority, path, query, fragment).
- *
+ *
* @param scheme The scheme to use.
* @param template The template. or {@code null}.
* @return The URI.
@@ -192,7 +192,7 @@ public URI toURI(String scheme, URI template) {
/**
* Returns a URI with this hostname and port, potentially using other URI parameters from the
* given set of parameters.
- *
+ *
* @param scheme The scheme to use.
* @param rawAuthority The raw authority field, or {@code null}.
* @param rawPath The raw path field, or {@code null}.
diff --git a/junixsocket-common/src/main/java/org/newsclub/net/unix/InterruptibleChannelUtil.java b/junixsocket-common/src/main/java/org/newsclub/net/unix/InterruptibleChannelUtil.java
new file mode 100644
index 000000000..aea10314f
--- /dev/null
+++ b/junixsocket-common/src/main/java/org/newsclub/net/unix/InterruptibleChannelUtil.java
@@ -0,0 +1,149 @@
+/*
+ * junixsocket
+ *
+ * Copyright 2009-2024 Christian Kohlschütter
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.newsclub.net.unix;
+
+import java.io.IOException;
+import java.nio.channels.AsynchronousCloseException;
+import java.nio.channels.ClosedByInterruptException;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.NotYetBoundException;
+import java.nio.channels.NotYetConnectedException;
+import java.nio.channels.spi.AbstractInterruptibleChannel;
+import java.util.Objects;
+
+/**
+ * Helper methods when working with {@link AbstractInterruptibleChannel} subclasses.
+ *
+ * @author Christian Kohlschütter
+ */
+final class InterruptibleChannelUtil {
+ /**
+ * Reference to the protected {@code AbstractInterruptibleChannel#end(boolean)} method.
+ */
+ @FunctionalInterface
+ interface EndMethod {
+ void end(boolean completed) throws AsynchronousCloseException;
+ }
+
+ /**
+ * Wrapper method that calls {@code AbstractInterruptibleChannel#end(boolean)}, making sure the
+ * socket is closed and the {@link Thread#interrupted()} state is set correctly upon error.
+ *
+ * @param channel The channel.
+ * @param end The reference to the protected {@code AbstractInterruptibleChannel#end(boolean)}
+ * method.
+ * @param complete {@code true} if the block started with {@code begin} succeeded without an
+ * exception.
+ * @param exception An optional exception that was caught in the try-catch-finally block.
+ * @throws AsynchronousCloseException on error.
+ */
+ static void endInterruptable(AFSomeSocketChannel channel, EndMethod end, boolean complete,
+ Exception exception) throws AsynchronousCloseException {
+ if (!complete) {
+ if (exception instanceof ClosedChannelException) {
+ // we already have caught a valid exception; we don't need to throw one from within "end"
+ complete = true;
+ }
+ }
+ try {
+ end.end(complete);
+ } catch (AsynchronousCloseException e) {
+ throw closeAndThrow(channel, e);
+ }
+ }
+
+ private static