diff --git a/src/xrt/ipc/CMakeLists.txt b/src/xrt/ipc/CMakeLists.txt index 8610b1c96..55943dc28 100644 --- a/src/xrt/ipc/CMakeLists.txt +++ b/src/xrt/ipc/CMakeLists.txt @@ -114,6 +114,9 @@ if(ANDROID) PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON) + target_sources(ipc_server PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/server/ipc_server_mainloop_android.c + ) target_link_libraries(ipc_server PUBLIC ${ANDROID_LIBRARY} PRIVATE diff --git a/src/xrt/ipc/android/src/main/java/org/freedesktop/monado/ipc/MonadoImpl.java b/src/xrt/ipc/android/src/main/java/org/freedesktop/monado/ipc/MonadoImpl.java index 330661dbe..1995904f2 100644 --- a/src/xrt/ipc/android/src/main/java/org/freedesktop/monado/ipc/MonadoImpl.java +++ b/src/xrt/ipc/android/src/main/java/org/freedesktop/monado/ipc/MonadoImpl.java @@ -19,6 +19,8 @@ import androidx.annotation.NonNull; import org.jetbrains.annotations.NotNull; +import java.io.IOException; + /** * Java implementation of the IMonado IPC interface. *

@@ -39,10 +41,38 @@ public class MonadoImpl extends IMonado.Stub { System.loadLibrary("monado-service"); } + private final Thread compositorThread = new Thread( + this::threadEntry, + "CompositorThread"); + private boolean started = false; + + private void launchThreadIfNeeded() { + synchronized (compositorThread) { + if (!started) { + compositorThread.start(); + nativeWaitForServerStartup(); + started = true; + } + } + } + @Override public void connect(@NotNull ParcelFileDescriptor parcelFileDescriptor) { - Log.i(TAG, "connect"); - nativeAddClient(parcelFileDescriptor.detachFd()); + /// @todo launch this thread earlier/elsewhere + launchThreadIfNeeded(); + int fd = parcelFileDescriptor.getFd(); + Log.i(TAG, "connect: given fd " + fd); + if (nativeAddClient(fd) != 0) { + Log.e(TAG, "Failed to transfer client fd ownership!"); + try { + parcelFileDescriptor.close(); + } catch (IOException e) { + // do nothing, probably already closed. + } + } else { + Log.i(TAG, "connect: fd ownership transferred"); + parcelFileDescriptor.detachFd(); + } } @Override @@ -55,6 +85,24 @@ public class MonadoImpl extends IMonado.Stub { nativeAppSurface(surface); } + private void threadEntry() { + Log.i(TAG, "threadEntry"); + nativeThreadEntry(); + Log.i(TAG, "native thread has exited"); + } + + /** + * Native thread entry point. + */ + @SuppressWarnings("JavaJniMissingFunction") + private native void nativeThreadEntry(); + + /** + * Native method that waits until the server reports that it is, in fact, started up. + */ + @SuppressWarnings("JavaJniMissingFunction") + private native void nativeWaitForServerStartup(); + /** * Native handling of receiving a surface: should convert it to an ANativeWindow then do stuff * with it. @@ -63,7 +111,7 @@ public class MonadoImpl extends IMonado.Stub { * See `src/xrt/targets/service-lib/service_target.cpp` for the implementation. * * @param surface The surface to pass to native code - * @todo figure out a good way to make the MonadoImpl pointer a client ID + * @todo figure out a good way to have a client ID */ @SuppressWarnings("JavaJniMissingFunction") private native void nativeAppSurface(@NonNull Surface surface); @@ -80,8 +128,9 @@ public class MonadoImpl extends IMonado.Stub { * See `src/xrt/targets/service-lib/service_target.cpp` for the implementation. * * @param fd The incoming file descriptor: ownership is transferred to native code here. - * @todo figure out a good way to make the MonadoImpl pointer a client ID + * @return 0 on success, anything else means the fd wasn't sent and ownership not transferred. + * @todo figure out a good way to have a client ID */ @SuppressWarnings("JavaJniMissingFunction") - private native void nativeAddClient(int fd); + private native int nativeAddClient(int fd); } diff --git a/src/xrt/ipc/server/ipc_server.h b/src/xrt/ipc/server/ipc_server.h index f510a1543..24215999a 100644 --- a/src/xrt/ipc/server/ipc_server.h +++ b/src/xrt/ipc/server/ipc_server.h @@ -159,7 +159,24 @@ struct ipc_device struct ipc_server_mainloop { #if defined(XRT_OS_ANDROID) - int _unused; + //! For waiting on various events in the main thread. + int epoll_fd; + + //! File descriptor for the read end of our pipe for submitting new clients + int pipe_read; + + //! File descriptor for the write end of our pipe for submitting new clients + int pipe_write; + + //! The last client fd we accepted, to unblock the right client. + int last_accepted_fd; + + //! Mutex for accepting clients + pthread_mutex_t accept_mutex; + + //! Condition variable for accepting clients + pthread_cond_t accept_cond; + #elif defined(XRT_OS_LINUX) //! Socket that we accept connections on. @@ -258,14 +275,19 @@ int ipc_server_main(int argc, char **argv); #endif +#ifdef XRT_OS_ANDROID /*! - * Android entry point to the IPC server process. + * Main entrypoint to the server process. + * + * @param ps Pointer to populate with the server struct. + * @param startup_complete_callback Function to call upon completing startup and populating *ps, but before entering the + * mainloop. + * @param data user data to pass to your callback. * * @ingroup ipc_server */ -#ifdef XRT_OS_ANDROID int -ipc_server_main_android(int fd); +ipc_server_main_android(struct ipc_server **ps, void (*startup_complete_callback)(void *data), void *data); #endif /*! diff --git a/src/xrt/ipc/server/ipc_server_mainloop_android.c b/src/xrt/ipc/server/ipc_server_mainloop_android.c new file mode 100644 index 000000000..33c6dd270 --- /dev/null +++ b/src/xrt/ipc/server/ipc_server_mainloop_android.c @@ -0,0 +1,205 @@ +// Copyright 2020-2021, Collabora, Ltd. +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Server mainloop details on Android. + * @author Ryan Pavlik + * @author Pete Black + * @author Jakob Bornecrantz + * @ingroup ipc_server + */ + +#include "xrt/xrt_config_have.h" +#include "xrt/xrt_config_os.h" + +#include "os/os_time.h" +#include "util/u_var.h" +#include "util/u_misc.h" +#include "util/u_debug.h" + +#include "server/ipc_server.h" +#include "server/ipc_server_mainloop_android.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SHUTTING_DOWN (-1) + +/* + * + * Static functions. + * + */ + +static int +init_pipe(struct ipc_server_mainloop *ml) +{ + int pipefd[2]; + int ret = pipe(pipefd); + if (ret < 0) { + U_LOG_E("pipe2() failed '%i'", ret); + return ret; + } + ml->pipe_read = pipefd[0]; + ml->pipe_write = pipefd[1]; + return 0; +} + +static int +init_epoll(struct ipc_server_mainloop *ml) +{ + int ret = epoll_create1(EPOLL_CLOEXEC); + if (ret < 0) { + return ret; + } + + pthread_mutex_init(&ml->accept_mutex, NULL); + pthread_cond_init(&ml->accept_cond, NULL); + ml->epoll_fd = ret; + + struct epoll_event ev = {0}; + + + ev.events = EPOLLIN; + ev.data.fd = ml->pipe_read; + ret = epoll_ctl(ml->epoll_fd, EPOLL_CTL_ADD, ml->pipe_read, &ev); + if (ret < 0) { + U_LOG_E("epoll_ctl(pipe_read) failed '%i'", ret); + return ret; + } + + return 0; +} + + +static void +handle_listen(struct ipc_server *vs, struct ipc_server_mainloop *ml) +{ + int newfd = 0; + if (read(ml->pipe_read, &newfd, sizeof(newfd)) == sizeof(newfd)) { + ipc_server_start_client_listener_thread(vs, newfd); + + // Release the thread that gave us this fd. + pthread_mutex_lock(&ml->accept_mutex); + ml->last_accepted_fd = newfd; + pthread_cond_broadcast(&ml->accept_cond); + pthread_mutex_unlock(&ml->accept_mutex); + } else { + U_LOG_E("error on pipe read"); + ipc_server_handle_failure(vs); + return; + } +} + +#define NUM_POLL_EVENTS 8 +#define NO_SLEEP 0 + +/* + * + * Exported functions + * + */ +void +ipc_server_mainloop_poll(struct ipc_server *vs, struct ipc_server_mainloop *ml) +{ + int epoll_fd = ml->epoll_fd; + + struct epoll_event events[NUM_POLL_EVENTS] = {0}; + + // No sleeping, returns immediately. + int ret = epoll_wait(epoll_fd, events, NUM_POLL_EVENTS, NO_SLEEP); + if (ret < 0) { + U_LOG_E("epoll_wait failed with '%i'.", ret); + ipc_server_handle_failure(vs); + return; + } + + for (int i = 0; i < ret; i++) { + // Somebody new at the door. + if (events[i].data.fd == ml->pipe_read) { + handle_listen(vs, ml); + } + } +} + +int +ipc_server_mainloop_init(struct ipc_server_mainloop *ml) +{ + int ret = init_pipe(ml); + if (ret < 0) { + ipc_server_mainloop_deinit(ml); + return ret; + } + + ret = init_epoll(ml); + if (ret < 0) { + ipc_server_mainloop_deinit(ml); + return ret; + } + + return 0; +} + +void +ipc_server_mainloop_deinit(struct ipc_server_mainloop *ml) +{ + if (ml == NULL) { + return; + } + if (ml->pipe_read > 0) { + // Close pipe on exit + close(ml->pipe_read); + ml->pipe_read = -1; + } + //! @todo close pipe_write or epoll_fd? + + // Tell everybody we're done and they should go away. + pthread_mutex_lock(&ml->accept_mutex); + while (ml->last_accepted_fd != 0) { + // Don't accidentally intervene in somebody else's message, + // wait until there's no unblocks pending. + pthread_cond_wait(&ml->accept_cond, &ml->accept_mutex); + } + ml->last_accepted_fd = SHUTTING_DOWN; + pthread_cond_broadcast(&ml->accept_cond); + pthread_mutex_unlock(&ml->accept_mutex); +} + +int +ipc_server_mainloop_add_fd(struct ipc_server *vs, struct ipc_server_mainloop *ml, int newfd) +{ + // Take the lock here, so we don't accidentally miss our fd being accepted. + pthread_mutex_lock(&ml->accept_mutex); + + // Write our fd number: the other side of the pipe is in the same process, so passing just the number is OK. + int ret = write(ml->pipe_write, &newfd, sizeof(newfd)); + if (ret < 0) { + U_LOG_E("write to pipe failed with '%i'.", ret); + pthread_mutex_unlock(&ml->accept_mutex); + return ret; + } + + // Normal looping on the condition variable's condition. + while (ml->last_accepted_fd != newfd && ml->last_accepted_fd != SHUTTING_DOWN) { + ret = pthread_cond_wait(&ml->accept_cond, &ml->accept_mutex); + if (ret < 0) { + U_LOG_E("pthread_cond_wait failed with '%i'.", ret); + pthread_mutex_unlock(&ml->accept_mutex); + return ret; + } + } + // OK, we have now been accepted. Zero out the last accepted fd to avoid confusing any other thread. + ml->last_accepted_fd = 0; + pthread_mutex_unlock(&ml->accept_mutex); + return 0; +} diff --git a/src/xrt/ipc/server/ipc_server_mainloop_android.h b/src/xrt/ipc/server/ipc_server_mainloop_android.h new file mode 100644 index 000000000..5578f7ee4 --- /dev/null +++ b/src/xrt/ipc/server/ipc_server_mainloop_android.h @@ -0,0 +1,27 @@ +// Copyright 2020-2021, Collabora, Ltd. +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief Additional server entry points needed for Android. + * @author Ryan Pavlik + * @ingroup ipc_server + */ + +#pragma once + +#include "ipc_server.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/*! + * Pass an fd for a new client to the mainloop. + */ +int +ipc_server_mainloop_add_fd(struct ipc_server *vs, struct ipc_server_mainloop *ml, int newfd); + + +#ifdef __cplusplus +} +#endif diff --git a/src/xrt/ipc/server/ipc_server_process.c b/src/xrt/ipc/server/ipc_server_process.c index dad00185e..138b1312c 100644 --- a/src/xrt/ipc/server/ipc_server_process.c +++ b/src/xrt/ipc/server/ipc_server_process.c @@ -88,25 +88,6 @@ teardown_idev(struct ipc_device *idev) * Static functions. * */ - -#ifdef XRT_OS_ANDROID - -// Stub -void -ipc_server_mainloop_deinit(struct ipc_server_mainloop *ml) -{} -// Stub -int -ipc_server_mainloop_init(struct ipc_server_mainloop *ml) -{ - return 0; -} -// Stub -void -ipc_server_mainloop_poll(struct ipc_server *vs, struct ipc_server_mainloop *ml) -{} - -#endif static void teardown_all(struct ipc_server *s) { @@ -1110,10 +1091,10 @@ ipc_server_main(int argc, char **argv) #ifdef XRT_OS_ANDROID int -ipc_server_main_android(int fd) +ipc_server_main_android(struct ipc_server **ps, void (*startup_complete_callback)(void *data), void *data) { struct ipc_server *s = U_TYPED_CALLOC(struct ipc_server); - U_LOG_D("Created IPC server on fd '%d'!", fd); + U_LOG_D("Created IPC server!"); int ret = init_all(s); if (ret < 0) { @@ -1122,7 +1103,10 @@ ipc_server_main_android(int fd) } init_server_state(s); - ipc_server_start_client_listener_thread(s, fd); + + *ps = s; + startup_complete_callback(data); + ret = main_loop(s); teardown_all(s); @@ -1132,4 +1116,4 @@ ipc_server_main_android(int fd) return ret; } -#endif +#endif // XRT_OS_ANDROID diff --git a/src/xrt/targets/service-lib/service_target.cpp b/src/xrt/targets/service-lib/service_target.cpp index 937c70744..4e6b9a349 100644 --- a/src/xrt/targets/service-lib/service_target.cpp +++ b/src/xrt/targets/service-lib/service_target.cpp @@ -15,26 +15,101 @@ #include "wrap/android.view.h" #include "server/ipc_server.h" +#include "server/ipc_server_mainloop_android.h" #include "util/u_logging.h" #include #include #include "android/android_globals.h" +#include using wrap::android::view::Surface; +namespace { +struct Singleton +{ +public: + static Singleton & + instance() + { + static Singleton singleton{}; + return singleton; + } + + + void + waitForStartupComplete() + { + + std::unique_lock lock{running_mutex}; + running_cond.wait(lock, [&]() { return this->startup_complete; }); + } + + //! static trampoline for the startup complete callback + static void + signalStartupComplete() + { + instance().signalStartupCompleteNonstatic(); + } + +private: + void + signalStartupCompleteNonstatic() + { + std::unique_lock lock{running_mutex}; + startup_complete = true; + running_cond.notify_all(); + } + Singleton() {} + //! Mutex for starting thread + std::mutex running_mutex; + + //! Condition variable for starting thread + std::condition_variable running_cond; + bool startup_complete = false; +}; +} // namespace + +static struct ipc_server *server = NULL; + +static void +signalStartupCompleteTrampoline(void *data) +{ + static_cast(data)->signalStartupComplete(); +} extern "C" void +Java_org_freedesktop_monado_ipc_MonadoImpl_nativeThreadEntry(JNIEnv *env, jobject thiz) +{ + jni::init(env); + jni::Object monadoImpl(thiz); + U_LOG_D("service: Called nativeThreadEntry"); + auto &singleton = Singleton::instance(); + ipc_server_main_android(&server, signalStartupCompleteTrampoline, &singleton); +} + +extern "C" JNIEXPORT void JNICALL +Java_org_freedesktop_monado_ipc_MonadoImpl_nativeWaitForServerStartup(JNIEnv *env, jobject thiz) +{ + Singleton::instance().waitForStartupComplete(); +} + +extern "C" JNIEXPORT jint JNICALL Java_org_freedesktop_monado_ipc_MonadoImpl_nativeAddClient(JNIEnv *env, jobject thiz, int fd) { jni::init(env); jni::Object monadoImpl(thiz); U_LOG_D("service: Called nativeAddClient with fd %d", fd); - // "entry point" of the native code - ipc_server_main_android(fd); + if (server == nullptr) { + // Should not happen. + U_LOG_E("service: nativeAddClient called before service started up!"); + return -1; + } + // We try pushing the fd number to the server. If and only if we get a 0 return, has the server taken ownership. + return ipc_server_mainloop_add_fd(server, &server->ml, fd); } -extern "C" void +extern "C" JNIEXPORT void JNICALL Java_org_freedesktop_monado_ipc_MonadoImpl_nativeAppSurface(JNIEnv *env, jobject thiz, jobject surface) { jni::init(env); @@ -43,5 +118,5 @@ Java_org_freedesktop_monado_ipc_MonadoImpl_nativeAppSurface(JNIEnv *env, jobject ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface); android_globals_store_window((struct _ANativeWindow *)nativeWindow); - U_LOG_D("Stored ANativeWindow: %p", nativeWindow); + U_LOG_D("Stored ANativeWindow: %p", (void *)nativeWindow); }