All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.newsclub.net.unix.FileDescriptorCast Maven / Gradle / Ivy

Go to download

junixsocket is a Java/JNI library that allows the use of Unix Domain Sockets (AF_UNIX sockets) and other socket types, such as AF_TIPC and AF_VSOCK, from Java, using the standard Socket API

The newest version!
/*
 * 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.Closeable;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ProcessBuilder.Redirect;
import java.net.DatagramSocket;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.channels.DatagramChannel;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.channels.spi.AbstractSelectableChannel;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNull;

import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;

/**
 * Provides object-oriented access to file descriptors via {@link InputStream}, {@link Socket},
 * etc., depending on the file descriptor type.
 * 

* Typical usage: *

*

 * FileDescriptor fd;
 *
 * // succeeds if fd refers to an AF_UNIX stream socket
 * 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);
 *
 * // always succeeds
 * InputStream in = FileDescriptorCast.using(fd).as(InputStream.class);
 * OutputStream in = FileDescriptorCast.using(fd).as(OutputStream.class);
 * 
*

* Important notes *

    *
  1. 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.
  2. *
  3. You may lose Java port information for {@link AFSocketAddress} implementations that do not * encode this information directly (such as {@link AFUNIXSocketAddress} and * {@link AFTIPCSocketAddress}).
  4. *
  5. The "blocking" state of a socket may be forcibly changed to "blocking" when performing the * cast, especially when casting to {@link Socket}, {@link DatagramSocket} or {@link ServerSocket} * and any of their subclasses where "blocking" is the expected state.
  6. *
  7. When calling {@link #using(FileDescriptor)} for a {@link FileDescriptor} obtained from * another socket or other resource in the same JVM (i.e., not from another process), especially for * sockets provided by junixsocket itself, there is a chance that the garbage collector may clean up * the original socket at an opportune moment, thereby closing the resource underlying the shared * {@link FileDescriptor} prematurely. *

    * This is considered an edge-case, and deliberately not handled automatically for performance and * portability reasons: We would have to do additional reference counting on all FileDescriptor * instances, either through patching {@code FileCleanable} or a shared data structure. *

    * 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.

  8. *
  9. As a consequence of the previous point: For {@link #using(FileDescriptor)}: when casting file * descriptors that belong to a junixsocket-controlled sockets, the target socket is configured in a * way such that garbage collection will not automatically close the target's underlying file * descriptor (but still potentially any file descriptors received from other processes via * ancillary messages).
  10. *
  11. The same restrictions as for {@link #using(FileDescriptor)} apply to * {@link #unsafeUsing(int)} as well.
  12. *
* * @author Christian Kohlschütter */ @SuppressWarnings("PMD.CouplingBetweenObjects") public final class FileDescriptorCast implements FileDescriptorAccess { private static final Map, CastingProviderMap> PRIMARY_TYPE_PROVIDERS_MAP = Collections .synchronizedMap(new HashMap<>()); private static final AFFunction FD_IS_PROVIDER = System .getProperty("osv.version") != null ? LenientFileInputStream::new : FileInputStream::new; private static final CastingProviderMap GLOBAL_PROVIDERS_FINAL = new CastingProviderMap() { @Override protected void addProviders() { // FileDescriptor and Object cannot be overridden addProvider(FileDescriptor.class, new CastingProvider() { @Override public FileDescriptor provideAs(FileDescriptorCast fdc, Class desiredType) throws IOException { return fdc.getFileDescriptor(); } }); } }; private static final CastingProviderMap GLOBAL_PROVIDERS = new CastingProviderMap() { @Override protected void addProviders() { addProvider(WritableByteChannel.class, new CastingProvider() { @SuppressWarnings("resource") @Override public WritableByteChannel provideAs(FileDescriptorCast fdc, Class desiredType) throws IOException { return new FileOutputStream(fdc.getFileDescriptor()).getChannel(); } }); addProvider(ReadableByteChannel.class, new CastingProvider() { @Override public ReadableByteChannel provideAs(FileDescriptorCast fdc, Class desiredType) throws IOException { return FD_IS_PROVIDER.apply(fdc.getFileDescriptor()).getChannel(); } }); addProvider(FileChannel.class, new CastingProvider() { @Override public FileChannel provideAs(FileDescriptorCast fdc, Class desiredType) throws IOException { return RAFChannelProvider.getFileChannel(fdc.getFileDescriptor()); } }); addProvider(FileOutputStream.class, new CastingProvider() { @Override public FileOutputStream provideAs(FileDescriptorCast fdc, Class desiredType) throws IOException { return new FileOutputStream(fdc.getFileDescriptor()); } }); addProvider(FileInputStream.class, new CastingProvider() { @Override public FileInputStream provideAs(FileDescriptorCast fdc, Class desiredType) throws IOException { return FD_IS_PROVIDER.apply(fdc.getFileDescriptor()); } }); addProvider(FileDescriptor.class, new CastingProvider() { @Override public FileDescriptor provideAs(FileDescriptorCast fdc, Class desiredType) throws IOException { return fdc.getFileDescriptor(); } }); addProvider(Integer.class, new CastingProvider() { @Override public Integer provideAs(FileDescriptorCast fdc, Class desiredType) throws IOException { FileDescriptor fd = fdc.getFileDescriptor(); int val = fd.valid() ? NativeUnixSocket.getFD(fd) : -1; if (val == -1) { throw new IOException("Not a valid file descriptor"); } return val; } }); if (AFSocket.supports(AFSocketCapability.CAPABILITY_FD_AS_REDIRECT)) { addProvider(Redirect.class, new CastingProvider() { @Override public Redirect provideAs(FileDescriptorCast fdc, Class desiredType) throws IOException { Redirect red = NativeUnixSocket.initRedirect(fdc.getFileDescriptor()); if (red == null) { throw new ClassCastException("Cannot access file descriptor as " + desiredType); } return red; } }); } } }; private static final int FD_IN = getFdIfPossible(FileDescriptor.in); private static final int FD_OUT = getFdIfPossible(FileDescriptor.out); private static final int FD_ERR = getFdIfPossible(FileDescriptor.err); static { registerGenericSocketSupport(); } private final FileDescriptor fdObj; private int localPort = 0; private int remotePort = 0; private final CastingProviderMap cpm; private FileDescriptorCast(FileDescriptor fdObj, CastingProviderMap cpm) { this.fdObj = Objects.requireNonNull(fdObj); this.cpm = Objects.requireNonNull(cpm); } private static int getFdIfPossible(FileDescriptor fd) { if (!NativeUnixSocket.isLoaded()) { return -1; } try { if (!fd.valid()) { return -1; } return NativeUnixSocket.getFD(fd); } catch (IOException e) { return -1; } } private static void registerCastingProviders(Class primaryType, CastingProviderMap cpm) { Objects.requireNonNull(primaryType); CastingProviderMap prev; if ((prev = PRIMARY_TYPE_PROVIDERS_MAP.put(primaryType, cpm)) != null) { PRIMARY_TYPE_PROVIDERS_MAP.put(primaryType, prev); throw new IllegalStateException("Already registered: " + primaryType); } } static void registerCastingProviders( AFAddressFamilyConfig config) { Class> socketClass = config.socketClass(); Class> datagramSocketClass = config.datagramSocketClass(); registerCastingProviders(socketClass, new CastingProviderMap() { @SuppressWarnings("null") @Override protected void addProviders() { addProviders(GLOBAL_PROVIDERS); final CastingProviderSocketOrChannel> cpSocketOrChannel = (fdc, desiredType, isChannel) -> reconfigure(isChannel, AFSocket.newInstance(config.socketConstructor(), (AFSocketFactory) null, fdc.getFileDescriptor(), fdc.localPort, fdc.remotePort)); final CastingProviderSocketOrChannel> cpServerSocketOrChannel = (fdc, desiredType, isChannel) -> reconfigure(isChannel, AFServerSocket.newInstance(config .serverSocketConstructor(), fdc.getFileDescriptor(), fdc.localPort, fdc.remotePort)); registerGenericSocketProviders(); addProvider(socketClass, (fdc, desiredType) -> cpSocketOrChannel.provideAs(fdc, desiredType, false)); addProvider(config.serverSocketClass(), (fdc, desiredType) -> cpServerSocketOrChannel .provideAs(fdc, desiredType, false)); addProvider(config.socketChannelClass(), (fdc, desiredType) -> cpSocketOrChannel.provideAs( fdc, AFSocket.class, true).getChannel()); addProvider(config.serverSocketChannelClass(), (fdc, desiredType) -> cpServerSocketOrChannel .provideAs(fdc, AFServerSocket.class, true).getChannel()); } }); registerCastingProviders(datagramSocketClass, new CastingProviderMap() { @SuppressWarnings("null") @Override protected void addProviders() { addProviders(GLOBAL_PROVIDERS); final CastingProviderSocketOrChannel> cpDatagramSocketOrChannel = (fdc, desiredType, isChannel) -> reconfigure(isChannel, AFDatagramSocket.newInstance(config .datagramSocketConstructor(), fdc.getFileDescriptor(), fdc.localPort, fdc.remotePort)); registerGenericDatagramSocketProviders(); addProvider(datagramSocketClass, (fdc, desiredType) -> cpDatagramSocketOrChannel.provideAs( fdc, desiredType, false)); addProvider(config.datagramChannelClass(), (fdc, desiredType) -> cpDatagramSocketOrChannel .provideAs(fdc, AFDatagramSocket.class, true).getChannel()); } }); } private abstract static class CastingProviderMap { private final Map, CastingProvider> providers = new HashMap<>(); private final Set> classes = Collections.unmodifiableSet(providers.keySet()); @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") protected CastingProviderMap() { addProviders(); addProviders(GLOBAL_PROVIDERS_FINAL); } @SuppressWarnings("null") protected void registerGenericSocketProviders() { final CastingProviderSocketOrChannel> cpSocketOrChannelGeneric = (fdc, desiredType, isChannel) -> reconfigure(isChannel, AFSocket.newInstance( AFGenericSocket::new, (AFSocketFactory) null, fdc .getFileDescriptor(), fdc.localPort, fdc.remotePort)); final CastingProviderSocketOrChannel> cpServerSocketOrChannelGeneric = (fdc, desiredType, isChannel) -> reconfigure(isChannel, AFServerSocket.newInstance( AFGenericServerSocket::new, fdc.getFileDescriptor(), fdc.localPort, fdc.remotePort)); addProvider(AFGenericSocket.class, (fdc, desiredType) -> cpSocketOrChannelGeneric.provideAs( fdc, desiredType, false)); addProvider(AFGenericServerSocket.class, (fdc, desiredType) -> cpServerSocketOrChannelGeneric .provideAs(fdc, desiredType, false)); addProvider(AFGenericSocketChannel.class, (fdc, desiredType) -> cpSocketOrChannelGeneric .provideAs(fdc, AFSocket.class, true).getChannel()); addProvider(AFGenericServerSocketChannel.class, (fdc, desiredType) -> cpServerSocketOrChannelGeneric.provideAs(fdc, AFServerSocket.class, true) .getChannel()); } @SuppressWarnings("null") protected void registerGenericDatagramSocketProviders() { final CastingProviderSocketOrChannel> cpDatagramSocketOrChannelGeneric = (fdc, desiredType, isChannel) -> reconfigure(isChannel, AFDatagramSocket.newInstance( AFGenericDatagramSocket::new, fdc.getFileDescriptor(), fdc.localPort, fdc.remotePort)); addProvider(AFDatagramSocket.class, (fdc, desiredType) -> cpDatagramSocketOrChannelGeneric .provideAs(fdc, desiredType, false)); addProvider(AFDatagramChannel.class, (fdc, desiredType) -> cpDatagramSocketOrChannelGeneric .provideAs(fdc, AFDatagramSocket.class, true).getChannel()); } protected abstract void addProviders(); protected final void addProvider(Class type, CastingProvider cp) { Objects.requireNonNull(type); addProvider0(type, cp); } private void addProvider0(Class type, CastingProvider cp) { if (providers.put(type, cp) != cp) { // NOPMD for (Class cl : type.getInterfaces()) { addProvider0(cl, cp); } Class scl = type.getSuperclass(); if (scl != null) { addProvider0(scl, cp); } } } protected final void addProviders(CastingProviderMap other) { if (other == null || other == this) { // NOPMD return; } this.providers.putAll(other.providers); } @SuppressWarnings("unchecked") public CastingProvider get(Class desiredType) { return (CastingProvider) providers.get(desiredType); } } @FunctionalInterface private interface CastingProvider { T provideAs(FileDescriptorCast fdc, Class desiredType) throws IOException; } @FunctionalInterface private interface CastingProviderSocketOrChannel { T provideAs(FileDescriptorCast fdc, Class desiredType, boolean isChannel) throws IOException; } /** * Creates a {@link FileDescriptorCast} using the given file descriptor. *

* 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 * unsupported. */ 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; } triggerInit(); CastingProviderMap map = PRIMARY_TYPE_PROVIDERS_MAP.get(primaryType); 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 *

    *
  1. make assumptions on the internal system representation of a file descriptor (which differs * between Windows and Unix, for example).
  2. *
  3. provide access to resources that are otherwise not accessible
  4. *
*

* 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() { 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. */ public FileDescriptorCast withLocalPort(int port) { if (port < 0) { throw new IllegalArgumentException(); } this.localPort = port; return this; } /** * 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. */ public FileDescriptorCast withRemotePort(int port) { if (port < 0) { throw new IllegalArgumentException(); } this.remotePort = port; return this; } /** * Casts this instance to the desired type. * * @param The desired type. * @param desiredType The class of the desired type. * @return s An instance of the desired type. * @throws IOException if there was a problem while casting. * @throws ClassCastException if the cast cannot be legally made. * @see #availableTypes() * @see #isAvailable(Class) */ @SuppressWarnings("PMD.ShortMethodName") public @NonNull K as(Class desiredType) throws IOException { Objects.requireNonNull(desiredType); CastingProvider provider = cpm.get(desiredType); if (provider != null) { K obj = desiredType.cast(provider.provideAs(this, desiredType)); Objects.requireNonNull(obj); return obj; } else { throw new ClassCastException("Cannot access file descriptor as " + desiredType); } } /** * Checks if the instance can be cast as the given desired type (using {@link #as(Class)}). * * @param desiredType The class of the desired type. * @return {@code true} if the cast can be made. * @throws IOException on error. * @see #as(Class) */ public boolean isAvailable(Class desiredType) throws IOException { return cpm.providers.containsKey(desiredType); } /** * Returns a collection of available types this instance can be cast to (using * {@link #as(Class)}). * * @return The collection of available types. */ public Set> availableTypes() { return cpm.classes; } @Override @SuppressFBWarnings("EI_EXPOSE_REP") public FileDescriptor getFileDescriptor() { return fdObj; } private static final class LenientFileInputStream extends FileInputStream { private LenientFileInputStream(FileDescriptor fdObj) { super(fdObj); } @Override public int available() throws IOException { try { return super.available(); } catch (IOException e) { String msg = e.getMessage(); if ("Invalid seek".equals(msg)) { // OSv may not like FileInputStream#availabe() on pipe fds. return 0; } throw e; } } } /** * Add support for otherwise unsupported sockets. */ private static void registerGenericSocketSupport() { registerCastingProviders(Socket.class, new CastingProviderMap() { @Override protected void addProviders() { addProviders(GLOBAL_PROVIDERS); registerGenericSocketProviders(); } }); registerCastingProviders(DatagramSocket.class, new CastingProviderMap() { @Override protected void addProviders() { addProviders(GLOBAL_PROVIDERS); registerGenericDatagramSocketProviders(); } }); } @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; } @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. *

* 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 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); } }