Refactor ZygoteInit to support a WebView-specific zygote.
This is a non-functional change that separates out functionality
that should be shared between the system zygote and the WebView
zygote from that which is system zygote specific.
* Move MethodAndArgsCaller to Zygote.
* Split out server socket functions into ZygoteServer.
* Add a new (stub, for now) WebViewZygoteInit class.
Bug: 22084679
Bug: 21643067
(cherry picked from commit ba816e0c9efd8cd2aeef618a819a2ad46b742f87)
Merged-In: I4c508a42af7ab7b53d10570ad53b846df7782cc4
Change-Id: I54f04c03443d10dabe6426697d1ff8a0cc66b985
diff --git a/core/java/com/android/internal/os/RuntimeInit.java b/core/java/com/android/internal/os/RuntimeInit.java
index de671b1..c851e4e 100644
--- a/core/java/com/android/internal/os/RuntimeInit.java
+++ b/core/java/com/android/internal/os/RuntimeInit.java
@@ -230,7 +230,7 @@
* @param classLoader the classLoader to load {@className} with
*/
private static void invokeStaticMain(String className, String[] argv, ClassLoader classLoader)
- throws ZygoteInit.MethodAndArgsCaller {
+ throws Zygote.MethodAndArgsCaller {
Class<?> cl;
try {
@@ -264,7 +264,7 @@
* clears up all the stack frames that were required in setting
* up the process.
*/
- throw new ZygoteInit.MethodAndArgsCaller(m, argv);
+ throw new Zygote.MethodAndArgsCaller(m, argv);
}
public static final void main(String[] argv) {
@@ -301,7 +301,7 @@
* @param argv arg strings
*/
public static final void zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader)
- throws ZygoteInit.MethodAndArgsCaller {
+ throws Zygote.MethodAndArgsCaller {
if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application from zygote");
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "RuntimeInit");
@@ -324,14 +324,14 @@
* @param argv arg strings
*/
public static void wrapperInit(int targetSdkVersion, String[] argv)
- throws ZygoteInit.MethodAndArgsCaller {
+ throws Zygote.MethodAndArgsCaller {
if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application from wrapper");
applicationInit(targetSdkVersion, argv, null);
}
private static void applicationInit(int targetSdkVersion, String[] argv, ClassLoader classLoader)
- throws ZygoteInit.MethodAndArgsCaller {
+ throws Zygote.MethodAndArgsCaller {
// If the application calls System.exit(), terminate the process
// immediately without running any shutdown hooks. It is not possible to
// shutdown an Android application gracefully. Among other things, the
diff --git a/core/java/com/android/internal/os/WebViewZygoteInit.java b/core/java/com/android/internal/os/WebViewZygoteInit.java
new file mode 100644
index 0000000..2ed7aa2
--- /dev/null
+++ b/core/java/com/android/internal/os/WebViewZygoteInit.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 com.android.internal.os;
+
+/**
+ * Startup class for the WebView zygote process.
+ *
+ * See {@link ZygoteInit} for generic zygote startup documentation.
+ *
+ * @hide
+ */
+class WebViewZygoteInit {
+ public static final String TAG = "WebViewZygoteInit";
+
+ public static void main(String argv[]) {
+ throw new RuntimeException("Not implemented yet");
+ }
+}
diff --git a/core/java/com/android/internal/os/WrapperInit.java b/core/java/com/android/internal/os/WrapperInit.java
index c558cf8..594b6ab 100644
--- a/core/java/com/android/internal/os/WrapperInit.java
+++ b/core/java/com/android/internal/os/WrapperInit.java
@@ -74,14 +74,14 @@
}
}
- // Mimic Zygote preloading.
+ // Mimic system Zygote preloading.
ZygoteInit.preload();
// Launch the application.
String[] runtimeArgs = new String[args.length - 2];
System.arraycopy(args, 2, runtimeArgs, 0, runtimeArgs.length);
RuntimeInit.wrapperInit(targetSdkVersion, runtimeArgs);
- } catch (ZygoteInit.MethodAndArgsCaller caller) {
+ } catch (Zygote.MethodAndArgsCaller caller) {
caller.run();
}
}
diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java
index 66cc975..fc0ccb7 100644
--- a/core/java/com/android/internal/os/Zygote.java
+++ b/core/java/com/android/internal/os/Zygote.java
@@ -22,6 +22,9 @@
import android.system.ErrnoException;
import android.system.Os;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
/** @hide */
public final class Zygote {
/*
@@ -191,4 +194,39 @@
command.append(" '").append(arg.replace("'", "'\\''")).append("'");
}
}
+
+ /**
+ * Helper exception class which holds a method and arguments and
+ * can call them. This is used as part of a trampoline to get rid of
+ * the initial process setup stack frames.
+ */
+ public static class MethodAndArgsCaller extends Exception
+ implements Runnable {
+ /** method to call */
+ private final Method mMethod;
+
+ /** argument array */
+ private final String[] mArgs;
+
+ public MethodAndArgsCaller(Method method, String[] args) {
+ mMethod = method;
+ mArgs = args;
+ }
+
+ public void run() {
+ try {
+ mMethod.invoke(null, new Object[] { mArgs });
+ } catch (IllegalAccessException ex) {
+ throw new RuntimeException(ex);
+ } catch (InvocationTargetException ex) {
+ Throwable cause = ex.getCause();
+ if (cause instanceof RuntimeException) {
+ throw (RuntimeException) cause;
+ } else if (cause instanceof Error) {
+ throw (Error) cause;
+ }
+ throw new RuntimeException(ex);
+ }
+ }
+ }
}
diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java
index 85d84bb..132b022 100644
--- a/core/java/com/android/internal/os/ZygoteConnection.java
+++ b/core/java/com/android/internal/os/ZygoteConnection.java
@@ -117,7 +117,7 @@
/**
* Reads one start command from the command socket. If successful,
- * a child is forked and a {@link ZygoteInit.MethodAndArgsCaller}
+ * a child is forked and a {@link Zygote.MethodAndArgsCaller}
* exception is thrown in that child while in the parent process,
* the method returns normally. On failure, the child is not
* spawned and messages are printed to the log and stderr. Returns
@@ -126,10 +126,10 @@
*
* @return false if command socket should continue to be read from, or
* true if an end-of-file has been encountered.
- * @throws ZygoteInit.MethodAndArgsCaller trampoline to invoke main()
+ * @throws Zygote.MethodAndArgsCaller trampoline to invoke main()
* method in child process
*/
- boolean runOnce() throws ZygoteInit.MethodAndArgsCaller {
+ boolean runOnce(ZygoteServer zygoteServer) throws Zygote.MethodAndArgsCaller {
String args[];
Arguments parsedArgs = null;
@@ -214,7 +214,7 @@
fdsToClose[0] = fd.getInt$();
}
- fd = ZygoteInit.getServerSocketFileDescriptor();
+ fd = zygoteServer.getServerSocketFileDescriptor();
if (fd != null) {
fdsToClose[1] = fd.getInt$();
@@ -238,12 +238,13 @@
try {
if (pid == 0) {
// in child
+ zygoteServer.closeServerSocket();
IoUtils.closeQuietly(serverPipeFd);
serverPipeFd = null;
handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr);
// should never get here, the child is expected to either
- // throw ZygoteInit.MethodAndArgsCaller or exec().
+ // throw Zygote.MethodAndArgsCaller or exec().
return true;
} else {
// in parent...pid of < 0 means failure
@@ -712,12 +713,12 @@
* @param newStderr null-ok; stream to use for stderr until stdio
* is reopened.
*
- * @throws ZygoteInit.MethodAndArgsCaller on success to
+ * @throws Zygote.MethodAndArgsCaller on success to
* trampoline to code that invokes static main.
*/
private void handleChildProc(Arguments parsedArgs,
FileDescriptor[] descriptors, FileDescriptor pipeFd, PrintStream newStderr)
- throws ZygoteInit.MethodAndArgsCaller {
+ throws Zygote.MethodAndArgsCaller {
/**
* By the time we get here, the native code has closed the two actual Zygote
* socket connections, and substituted /dev/null in their place. The LocalSocket
@@ -725,8 +726,6 @@
*/
closeSocket();
- ZygoteInit.closeServerSocket();
-
if (descriptors != null) {
try {
Os.dup2(descriptors[0], STDIN_FILENO);
diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java
index b57aea6..2e5b550 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -16,7 +16,6 @@
package com.android.internal.os;
-import static android.system.OsConstants.POLLIN;
import static android.system.OsConstants.S_IRWXG;
import static android.system.OsConstants.S_IRWXO;
@@ -35,7 +34,6 @@
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
-import android.system.StructPollfd;
import android.text.Hyphenator;
import android.util.EventLog;
import android.util.Log;
@@ -52,17 +50,13 @@
import libcore.io.IoUtils;
import java.io.BufferedReader;
-import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
import java.security.Security;
import java.security.Provider;
-import java.util.ArrayList;
/**
* Startup class for the zygote process.
@@ -82,8 +76,6 @@
private static final String PROPERTY_DISABLE_OPENGL_PRELOADING = "ro.zygote.disable_gl_preload";
private static final String PROPERTY_RUNNING_IN_CONTAINER = "ro.boot.container";
- private static final String ANDROID_SOCKET_PREFIX = "ANDROID_SOCKET_";
-
private static final int LOG_BOOT_PROGRESS_PRELOAD_START = 3020;
private static final int LOG_BOOT_PROGRESS_PRELOAD_END = 3030;
@@ -94,11 +86,8 @@
private static final String SOCKET_NAME_ARG = "--socket-name=";
- private static LocalServerSocket sServerSocket;
-
/**
- * Used to pre-load resources. We hold a global reference on it so it
- * never gets destroyed.
+ * Used to pre-load resources.
*/
private static Resources mResources;
@@ -110,78 +99,6 @@
/** Controls whether we should preload resources during zygote init. */
public static final boolean PRELOAD_RESOURCES = true;
- /**
- * Registers a server socket for zygote command connections
- *
- * @throws RuntimeException when open fails
- */
- private static void registerZygoteSocket(String socketName) {
- if (sServerSocket == null) {
- int fileDesc;
- final String fullSocketName = ANDROID_SOCKET_PREFIX + socketName;
- try {
- String env = System.getenv(fullSocketName);
- fileDesc = Integer.parseInt(env);
- } catch (RuntimeException ex) {
- throw new RuntimeException(fullSocketName + " unset or invalid", ex);
- }
-
- try {
- FileDescriptor fd = new FileDescriptor();
- fd.setInt$(fileDesc);
- sServerSocket = new LocalServerSocket(fd);
- } catch (IOException ex) {
- throw new RuntimeException(
- "Error binding to local socket '" + fileDesc + "'", ex);
- }
- }
- }
-
- /**
- * Waits for and accepts a single command connection. Throws
- * RuntimeException on failure.
- */
- private static ZygoteConnection acceptCommandPeer(String abiList) {
- try {
- return new ZygoteConnection(sServerSocket.accept(), abiList);
- } catch (IOException ex) {
- throw new RuntimeException(
- "IOException during accept()", ex);
- }
- }
-
- /**
- * Close and clean up zygote sockets. Called on shutdown and on the
- * child's exit path.
- */
- static void closeServerSocket() {
- try {
- if (sServerSocket != null) {
- FileDescriptor fd = sServerSocket.getFileDescriptor();
- sServerSocket.close();
- if (fd != null) {
- Os.close(fd);
- }
- }
- } catch (IOException ex) {
- Log.e(TAG, "Zygote: error closing sockets", ex);
- } catch (ErrnoException ex) {
- Log.e(TAG, "Zygote: error closing descriptor", ex);
- }
-
- sServerSocket = null;
- }
-
- /**
- * Return the server socket's underlying file descriptor, so that
- * ZygoteConnection can pass it to the native code for proper
- * closure after a child process is forked off.
- */
-
- static FileDescriptor getServerSocketFileDescriptor() {
- return sServerSocket.getFileDescriptor();
- }
-
private static final int UNPRIVILEGED_UID = 9999;
private static final int UNPRIVILEGED_GID = 9999;
@@ -504,9 +421,7 @@
*/
private static void handleSystemServerProcess(
ZygoteConnection.Arguments parsedArgs)
- throws ZygoteInit.MethodAndArgsCaller {
-
- closeServerSocket();
+ throws Zygote.MethodAndArgsCaller {
// set umask to 0077 so new files and directories will default to owner-only permissions.
Os.umask(S_IRWXG | S_IRWXO);
@@ -609,8 +524,8 @@
/**
* Prepare the arguments and fork for the system server process.
*/
- private static boolean startSystemServer(String abiList, String socketName)
- throws MethodAndArgsCaller, RuntimeException {
+ private static boolean startSystemServer(String abiList, String socketName, ZygoteServer zygoteServer)
+ throws Zygote.MethodAndArgsCaller, RuntimeException {
long capabilities = posixCapabilitiesAsBits(
OsConstants.CAP_IPC_LOCK,
OsConstants.CAP_KILL,
@@ -666,6 +581,7 @@
waitForSecondaryZygote(socketName);
}
+ zygoteServer.closeServerSocket();
handleSystemServerProcess(parsedArgs);
}
@@ -687,6 +603,8 @@
}
public static void main(String argv[]) {
+ ZygoteServer zygoteServer = new ZygoteServer();
+
// Mark zygote start. This ensures that thread creation will throw
// an error.
ZygoteHooks.startZygoteNoThreadCreation();
@@ -716,7 +634,7 @@
throw new RuntimeException("No ABI list supplied.");
}
- registerZygoteSocket(socketName);
+ zygoteServer.registerServerSocket(socketName);
Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "ZygotePreload");
EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,
SystemClock.uptimeMillis());
@@ -733,8 +651,6 @@
gcAndFinalize();
Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
- Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
-
// Disable tracing so that forked processes do not inherit stale tracing tags from
// Zygote.
Trace.setTracingEnabled(false);
@@ -745,18 +661,18 @@
ZygoteHooks.stopZygoteNoThreadCreation();
if (startSystemServer) {
- startSystemServer(abiList, socketName);
+ startSystemServer(abiList, socketName, zygoteServer);
}
Log.i(TAG, "Accepting command socket connections");
- runSelectLoop(abiList);
+ zygoteServer.runSelectLoop(abiList);
- closeServerSocket();
- } catch (MethodAndArgsCaller caller) {
+ zygoteServer.closeServerSocket();
+ } catch (Zygote.MethodAndArgsCaller caller) {
caller.run();
} catch (RuntimeException ex) {
- Log.e(TAG, "Zygote died with exception", ex);
- closeServerSocket();
+ Log.e(TAG, "System zygote died with exception", ex);
+ zygoteServer.closeServerSocket();
throw ex;
}
}
@@ -792,89 +708,8 @@
}
/**
- * Runs the zygote process's select loop. Accepts new connections as
- * they happen, and reads commands from connections one spawn-request's
- * worth at a time.
- *
- * @throws MethodAndArgsCaller in a child process when a main() should
- * be executed.
- */
- private static void runSelectLoop(String abiList) throws MethodAndArgsCaller {
- ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
- ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();
-
- fds.add(sServerSocket.getFileDescriptor());
- peers.add(null);
-
- while (true) {
- StructPollfd[] pollFds = new StructPollfd[fds.size()];
- for (int i = 0; i < pollFds.length; ++i) {
- pollFds[i] = new StructPollfd();
- pollFds[i].fd = fds.get(i);
- pollFds[i].events = (short) POLLIN;
- }
- try {
- Os.poll(pollFds, -1);
- } catch (ErrnoException ex) {
- throw new RuntimeException("poll failed", ex);
- }
- for (int i = pollFds.length - 1; i >= 0; --i) {
- if ((pollFds[i].revents & POLLIN) == 0) {
- continue;
- }
- if (i == 0) {
- ZygoteConnection newPeer = acceptCommandPeer(abiList);
- peers.add(newPeer);
- fds.add(newPeer.getFileDesciptor());
- } else {
- boolean done = peers.get(i).runOnce();
- if (done) {
- peers.remove(i);
- fds.remove(i);
- }
- }
- }
- }
- }
-
- /**
* Class not instantiable.
*/
private ZygoteInit() {
}
-
- /**
- * Helper exception class which holds a method and arguments and
- * can call them. This is used as part of a trampoline to get rid of
- * the initial process setup stack frames.
- */
- public static class MethodAndArgsCaller extends Exception
- implements Runnable {
- /** method to call */
- private final Method mMethod;
-
- /** argument array */
- private final String[] mArgs;
-
- public MethodAndArgsCaller(Method method, String[] args) {
- mMethod = method;
- mArgs = args;
- }
-
- public void run() {
- try {
- mMethod.invoke(null, new Object[] { mArgs });
- } catch (IllegalAccessException ex) {
- throw new RuntimeException(ex);
- } catch (InvocationTargetException ex) {
- Throwable cause = ex.getCause();
- if (cause instanceof RuntimeException) {
- throw (RuntimeException) cause;
- } else if (cause instanceof Error) {
- throw (Error) cause;
- }
- throw new RuntimeException(ex);
- }
- }
- }
}
diff --git a/core/java/com/android/internal/os/ZygoteServer.java b/core/java/com/android/internal/os/ZygoteServer.java
new file mode 100644
index 0000000..ab876410
--- /dev/null
+++ b/core/java/com/android/internal/os/ZygoteServer.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * 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 com.android.internal.os;
+
+import static android.system.OsConstants.POLLIN;
+
+import android.net.LocalServerSocket;
+import android.system.Os;
+import android.system.ErrnoException;
+import android.system.StructPollfd;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.FileDescriptor;
+import java.util.ArrayList;
+
+/**
+ * Server socket class for zygote processes.
+ *
+ * Provides functions to wait for commands on a UNIX domain socket, and fork
+ * off child processes that inherit the initial state of the VM.%
+ *
+ * Please see {@link ZygoteConnection.Arguments} for documentation on the
+ * client protocol.
+ */
+class ZygoteServer {
+ public static final String TAG = "ZygoteServer";
+
+ private static final String ANDROID_SOCKET_PREFIX = "ANDROID_SOCKET_";
+
+ private LocalServerSocket mServerSocket;
+
+ ZygoteServer() {
+ }
+
+ /**
+ * Registers a server socket for zygote command connections
+ *
+ * @throws RuntimeException when open fails
+ */
+ void registerServerSocket(String socketName) {
+ if (mServerSocket == null) {
+ int fileDesc;
+ final String fullSocketName = ANDROID_SOCKET_PREFIX + socketName;
+ try {
+ String env = System.getenv(fullSocketName);
+ fileDesc = Integer.parseInt(env);
+ } catch (RuntimeException ex) {
+ throw new RuntimeException(fullSocketName + " unset or invalid", ex);
+ }
+
+ try {
+ FileDescriptor fd = new FileDescriptor();
+ fd.setInt$(fileDesc);
+ mServerSocket = new LocalServerSocket(fd);
+ } catch (IOException ex) {
+ throw new RuntimeException(
+ "Error binding to local socket '" + fileDesc + "'", ex);
+ }
+ }
+ }
+
+ /**
+ * Waits for and accepts a single command connection. Throws
+ * RuntimeException on failure.
+ */
+ private ZygoteConnection acceptCommandPeer(String abiList) {
+ try {
+ return new ZygoteConnection(mServerSocket.accept(), abiList);
+ } catch (IOException ex) {
+ throw new RuntimeException(
+ "IOException during accept()", ex);
+ }
+ }
+
+ /**
+ * Close and clean up zygote sockets. Called on shutdown and on the
+ * child's exit path.
+ */
+ void closeServerSocket() {
+ try {
+ if (mServerSocket != null) {
+ FileDescriptor fd = mServerSocket.getFileDescriptor();
+ mServerSocket.close();
+ if (fd != null) {
+ Os.close(fd);
+ }
+ }
+ } catch (IOException ex) {
+ Log.e(TAG, "Zygote: error closing sockets", ex);
+ } catch (ErrnoException ex) {
+ Log.e(TAG, "Zygote: error closing descriptor", ex);
+ }
+
+ mServerSocket = null;
+ }
+
+ /**
+ * Return the server socket's underlying file descriptor, so that
+ * ZygoteConnection can pass it to the native code for proper
+ * closure after a child process is forked off.
+ */
+
+ FileDescriptor getServerSocketFileDescriptor() {
+ return mServerSocket.getFileDescriptor();
+ }
+
+ /**
+ * Runs the zygote process's select loop. Accepts new connections as
+ * they happen, and reads commands from connections one spawn-request's
+ * worth at a time.
+ *
+ * @throws Zygote.MethodAndArgsCaller in a child process when a main()
+ * should be executed.
+ */
+ void runSelectLoop(String abiList) throws Zygote.MethodAndArgsCaller {
+ ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
+ ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();
+
+ fds.add(mServerSocket.getFileDescriptor());
+ peers.add(null);
+
+ while (true) {
+ StructPollfd[] pollFds = new StructPollfd[fds.size()];
+ for (int i = 0; i < pollFds.length; ++i) {
+ pollFds[i] = new StructPollfd();
+ pollFds[i].fd = fds.get(i);
+ pollFds[i].events = (short) POLLIN;
+ }
+ try {
+ Os.poll(pollFds, -1);
+ } catch (ErrnoException ex) {
+ throw new RuntimeException("poll failed", ex);
+ }
+ for (int i = pollFds.length - 1; i >= 0; --i) {
+ if ((pollFds[i].revents & POLLIN) == 0) {
+ continue;
+ }
+ if (i == 0) {
+ ZygoteConnection newPeer = acceptCommandPeer(abiList);
+ peers.add(newPeer);
+ fds.add(newPeer.getFileDesciptor());
+ } else {
+ boolean done = peers.get(i).runOnce(this);
+ if (done) {
+ peers.remove(i);
+ fds.remove(i);
+ }
+ }
+ }
+ }
+ }
+}