diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c b/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c index 2aeca1aa06a..aeaa27dc2b4 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c @@ -80,6 +80,7 @@ #include #include #include +#include #include #include #include @@ -133,6 +134,19 @@ static int cn1Dpi = 0; static int cn1Initialized = 0; static int cn1HeadlessForced = 0; /* enableHeadlessScreenshot was called */ static volatile int cn1Running = 1; +/* Set by the atexit hook before it puts the console's CRTC back. The EDT keeps + * running until the process ends and must not flip its own buffer back on top + * of the restored console; both present paths read this before a page flip. + * A flip the EDT already decided on can still land, so this narrows the + * window rather than closing it -- closing it would mean the exiting thread + * waiting on the EDT, which may be the thread that called exit. */ +static atomic_int cn1Exiting = 0; + +int cn1EmbeddedExiting(void); /* cn1_linux_gpu.h, not included without the GPU */ + +int cn1EmbeddedExiting(void) { + return atomic_load(&cn1Exiting); +} static volatile int cn1CurrentModifiers = 0; static int cn1HasTouch = 0, cn1HasPointer = 0; @@ -695,7 +709,7 @@ static void cn1Present(CN1EmbRect damage) { cn1DrawCursor(b, cn1CursorX, cn1CursorY); } #ifdef CN1_EMBEDDED_HAS_DRM - if (cn1Output == OUT_KMS) { + if (cn1Output == OUT_KMS && !cn1EmbeddedExiting()) { if (drmModePageFlip(cn1DrmFd, cn1DrmCrtc, b->fbId, DRM_MODE_PAGE_FLIP_EVENT, 0) == 0) { cn1FlipPending = 1; cn1Front = target; @@ -711,7 +725,7 @@ static void cn1Present(CN1EmbRect damage) { static void cn1RestoreScreen(void) { #ifdef CN1_EMBEDDED_GPU - cn1GpuShutdown(); + cn1GpuShutdownAtExit(); #endif #ifdef CN1_EMBEDDED_HAS_DRM if (cn1DrmFd >= 0) { @@ -1188,6 +1202,7 @@ static void cn1StartInput(void) { /* ------------------------------------------------------------- natives */ static void cn1AtExit(void) { + atomic_store(&cn1Exiting, 1); cn1Running = 0; cn1RestoreScreen(); } diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_gpu.c b/Ports/LinuxPort/nativeSources/cn1_linux_gpu.c index 05b503856bb..e468fde7be4 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_gpu.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_gpu.c @@ -1039,6 +1039,10 @@ void cn1GpuPresent(void) { if (!eglSwapBuffers(cn1gDpy, cn1gSurface)) { return; } + if (cn1EmbeddedExiting()) { + /* The console's CRTC is being put back; see cn1EmbeddedExiting. */ + return; + } bo = gbm_surface_lock_front_buffer(cn1gGbmSurface); if (bo == 0) { return; @@ -1155,4 +1159,22 @@ void cn1GpuShutdown(void) { cn1gTearDown(); } +void cn1GpuShutdownAtExit(void) { + /* Runs from exit() on whichever thread called System.exit, while the EDT + * may still be in the middle of a frame. Tearing the context down here + * freed the glyph atlas and terminated EGL under a texture upload in + * flight -- a segfault after the application had already finished. The + * process is going away, so its buffers are released by the kernel; only + * the screen state outlives it and needs putting back. */ + /* Not even waiting out a pending flip: that runs the DRM event handler and + * swaps and releases the front and next buffers, which the EDT may be doing + * at this same moment in its own present -- the same race in a smaller + * window. Hiding the cursor plane is a single ioctl and touches no shared + * state; the display unit's drmModeSetCrtc then replaces the scanout + * whatever flip is still in flight. */ + if (cn1gActive && cn1gScanout) { + cn1GpuCursor(0, 0, 0); + } +} + #endif /* CN1_LINUX_EMBEDDED && CN1_EMBEDDED_GPU */ diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_gpu.h b/Ports/LinuxPort/nativeSources/cn1_linux_gpu.h index 4c8a0c5483b..fc787f3fcf9 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_gpu.h +++ b/Ports/LinuxPort/nativeSources/cn1_linux_gpu.h @@ -103,6 +103,14 @@ int cn1GpuCursor(int show, int px, int py); * puts the console's CRTC back. */ void cn1GpuShutdown(void); +/* cn1GpuShutdown for the atexit path: hides the cursor plane and releases + * nothing, because another thread may still be drawing with the context. */ +void cn1GpuShutdownAtExit(void); + +/* Nonzero once the process is exiting; the GPU stops page flipping then. + * Defined by the display unit, cn1_linux_embedded.c. */ +int cn1EmbeddedExiting(void); + #ifdef __cplusplus } #endif diff --git a/scripts/embedded/gpu-check/gpu_check.c b/scripts/embedded/gpu-check/gpu_check.c index 63241453aa8..3c3c59674d9 100644 --- a/scripts/embedded/gpu-check/gpu_check.c +++ b/scripts/embedded/gpu-check/gpu_check.c @@ -53,6 +53,12 @@ typedef void (*Scene)(cairo_t* cr, cairo_surface_t* image, cairo_surface_t* target); +/* The display unit (cn1_linux_embedded.c) is not linked here; this harness + * never exits mid-frame. */ +int cn1EmbeddedExiting(void) { + return 0; +} + static void fills(cairo_t* cr, cairo_surface_t* image, cairo_surface_t* target) { (void) target; (void) image; diff --git a/scripts/initializr/common/src/main/resources/savor-modules.zip b/scripts/initializr/common/src/main/resources/savor-modules.zip index 9b9e8080308..9f50eedc092 100644 Binary files a/scripts/initializr/common/src/main/resources/savor-modules.zip and b/scripts/initializr/common/src/main/resources/savor-modules.zip differ diff --git a/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveEndpoint.java b/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveEndpoint.java index 196a992e105..a782f38d2d3 100644 --- a/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveEndpoint.java +++ b/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveEndpoint.java @@ -90,11 +90,28 @@ public void onBinary(WebSocketSession session, byte[] message, int offset, int l @Override public void onClose(WebSocketSession session, int code, String reason) { Object username = session.getAttachment(); + // Only an admitted connection; a refused one has no user and its end is + // expected. 1000 and 1001 are the app leaving. Anything else -- 1006 + // above all, a connection lost with no Close -- is what to read first + // when pushes stop arriving, so it costs a line per dropped app. + if (username != null && code != 1000 && code != 1001) { + System.err.println("[live] websocket " + session.getId() + " of " + username + + " closed " + code + (reason == null || reason.isEmpty() ? "" : " " + reason)); + } if (username instanceof String) { hub.remove((String) username, session); } } + @Override + public void onError(WebSocketSession session, Exception error) { + // A read failure, a protocol error or a callback that threw: the backend + // reports it here and then closes, and without this line the only trace + // of why a channel ended would be the 1006 that onClose prints. + System.err.println("[live] websocket " + session.getId() + " of " + + session.getAttachment() + " failed: " + error); + } + /// The `ticket` parameter of a query string, or null. A ticket is hex, so /// there is nothing in it to decode. static String ticket(String query) { diff --git a/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveHub.java b/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveHub.java index a9e6f43657c..ebe54108f83 100644 --- a/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveHub.java +++ b/scripts/savor/backend/src/main/java/com/codenameone/examples/savor/live/LiveHub.java @@ -204,8 +204,26 @@ private static void send(List to, String text) { try { session.sendText(text); } catch (IOException gone) { + // Said out loud: a dropped channel is otherwise invisible -- the + // app just stops hearing about its order, and a test sees only + // the connection end with no frame. + System.err.println("[live] websocket " + session.getId() + " of " + + session.getAttachment() + " dropped; could not send " + + frameType(text) + ": " + gone); session.abort(); } } } + + /// The `type` of a frame this class wrote, for a log line; never the + /// payload, which carries positions and order details. + static String frameType(String text) { + int at = text.indexOf("\"type\":\""); + if (at < 0) { + return "a frame"; + } + at += 8; + int end = text.indexOf('"', at); + return end < 0 ? "a frame" : "a " + text.substring(at, end) + " frame"; + } } diff --git a/scripts/savor/backend/src/test/java/com/codenameone/examples/savor/LiveSocketTest.java b/scripts/savor/backend/src/test/java/com/codenameone/examples/savor/LiveSocketTest.java index 52c82e6d185..0d2154d18d4 100644 --- a/scripts/savor/backend/src/test/java/com/codenameone/examples/savor/LiveSocketTest.java +++ b/scripts/savor/backend/src/test/java/com/codenameone/examples/savor/LiveSocketTest.java @@ -59,6 +59,8 @@ class LiveSocketTest { /// The last frame read: its opcode, and its payload as text. private int opcode; private byte[] payload; + /// Frames read so far in this test, for the message when one never comes. + private int framesRead; @Test void aTicketOpensTheChannelOnceAndPushesArrive() throws Exception { @@ -192,7 +194,11 @@ private int closeCode() { private void readFrame(Tcp connection) throws IOException { int first = readByte(connection); if (first < 0) { - throw new IOException("the connection ended with no frame"); + // The server closed it. Why is in its own log, the "[live]" lines + // above this failure; say where the test had got to. + throw new IOException("the connection ended with no frame; " + framesRead + + " frames read in this test, the last opcode " + opcode + ": " + + (payload == null ? "none" : new String(payload, "UTF-8"))); } opcode = first & 0xf; int length = readByte(connection) & 0x7f; @@ -208,6 +214,7 @@ private void readFrame(Tcp connection) throws IOException { } got += read; } + framesRead++; } /// A client's frames are masked; a server refuses one that is not.