Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
122f8bb
core: loopback server sockets, so MCP can serve any platform
shai-almog Jul 26, 2026
91949e3
iOS: serve accepted sockets with raw descriptor I/O
shai-almog Jul 26, 2026
075bb33
Address review: port 0 key collision, fail-fast listen, iOS leak
shai-almog Jul 26, 2026
c14cec6
Keep the new sources ASCII-only
shai-almog Jul 26, 2026
3f4b104
Block the MCP socket server on release builds
shai-almog Jul 26, 2026
af9f460
Address review round 2: fd and signal handling, honest failures
shai-almog Jul 26, 2026
6b5c6f3
Treat a frame cut short by a disconnect as a disconnect
shai-almog Jul 26, 2026
9837873
Only treat a carriage return as delimiter punctuation before a newline
shai-almog Jul 26, 2026
8a187a2
Do not strand the MCP registration when the listen fails
shai-almog Jul 26, 2026
1e01584
Read get-task-allow by parsing the profile, not by proximity
shai-almog Jul 26, 2026
2217b7e
Make stopping a listener actually stop it, and stop overclaiming on d…
shai-almog Jul 26, 2026
8c89c8d
Two more documentation overclaims about what counts as development
shai-almog Jul 26, 2026
9e312fd
Publish the listener stop flag across threads with an AtomicBoolean
shai-almog Jul 26, 2026
2ac902d
Bound the size of an incoming MCP frame
shai-almog Jul 27, 2026
79c91cc
Stop logging a stack trace for a deliberate listener stop
shai-almog Jul 27, 2026
7812b51
Review round: dropped clients, loopback family, EINTR, frame boundary…
shai-almog Jul 27, 2026
e902cc5
Retry accept on EINTR, back off a failing listener, make the ceiling …
shai-almog Jul 27, 2026
a51f97a
Let a caller preflight the build gate, not just platform capability
shai-almog Jul 27, 2026
f8d3ec8
Close the client's output stream too, and fix the guide anchor
shai-almog Jul 27, 2026
ad7e037
Read the provisioning profile once, and decode frames without a copy
shai-almog Jul 28, 2026
8ad47e8
Revert the frame decode: core compiles against CLDC11, not the JDK
shai-almog Jul 28, 2026
6734cb2
Do not announce a fault when a listener is deliberately stopped
shai-almog Jul 28, 2026
870b62e
Read frames in blocks, and close the visibility gap on the transport …
shai-almog Jul 28, 2026
9a21ba9
Report a listener that cannot bind, and back off exponentially when i…
shai-almog Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line number Diff line number Diff line change
Expand Up @@ -9917,6 +9917,69 @@ public Object listenSocket(int port) {
throw new RuntimeException("Not supported");
}

/// Whether this port can bind a server socket to the LOOPBACK interface only.
///
/// Deliberately separate from [#isServerSocketAvailable()] and defaulting to false:
/// a port must implement loopback binding explicitly. Answering this with the
/// wildcard-binding implementation would publish on every network interface a
/// channel the caller asked to keep local.
///
/// #### Returns
///
/// true if [#listenSocketLoopback(int)] is implemented here
public boolean isLoopbackServerSocketAvailable() {
return false;
}

/// Closes the listening socket for the given port so a thread parked in accept comes
/// back, which is what makes stopping a listener actually stop it. Setting a flag
/// alone leaves that thread blocked until some client happens to connect, and that
/// late connection is then served or dropped by a listener the caller has abandoned.
///
/// A port that does not implement this keeps the older behaviour, where stopping takes
/// effect on the next accept.
///
/// #### Parameters
///
/// - `port`: the port that was being listened on
///
/// - `loopbackOnly`: true if the listener was created by [#listenSocketLoopback(int)]
public void stopListeningSocket(int port, boolean loopbackOnly) {
}

/// Whether this build is a development build rather than a release build headed for
/// an app store: a debuggable Android package, a development provisioned iOS build,
/// or a JavaSE process, which covers the simulator, the designer, the desktop tooling
/// and a packaged desktop application alike - see [#isDebuggableBuild()] on the JavaSE
/// port for why it cannot tell them apart.
///
/// Facilities that are appropriate while developing but not in a shipped app can gate
/// themselves on this. A port that cannot tell should leave the default in place:
/// answering "release" is the safe direction, because it withholds a development
/// facility rather than exposing one in production.
///
/// #### Returns
///
/// true if this is a development build
public boolean isDebuggableBuild() {
return false;
}

/// Listens on the given port, bound to the loopback interface only, and blocks until
/// a connection arrives - the accept semantics of [#listenSocket(int)], with a
/// narrower bind.
///
/// #### Parameters
///
/// - `port`: the port to listen on
///
/// #### Returns
///
/// connected socket instance, or null when the accept failed
public Object listenSocketLoopback(int port) {
throw new RuntimeException("Loopback server sockets are not supported on this platform");
}

/// Returns the device host or ip address if available
///
/// #### Returns
Expand Down
125 changes: 121 additions & 4 deletions CodenameOne/src/com/codename1/io/Socket.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicBoolean;

/// Class implementing the socket API
///
Expand Down Expand Up @@ -61,6 +62,22 @@ public static boolean isServerSocketSupported() {
return Util.getImplementation().isServerSocketAvailable();
}

/// Returns true if this port can listen on the LOOPBACK interface only, so the
/// channel is reachable from the device itself but not from the network.
///
/// This is a strictly narrower capability than [#isServerSocketSupported()], which
/// binds the wildcard address and therefore publishes the port on every network
/// interface. A port must implement loopback binding explicitly; there is no
/// fallback to a wildcard bind, because silently widening the reach of a channel a
/// caller asked to keep local would be the wrong failure.
///
/// #### Returns
///
/// true if [#listenLoopback(int, Class)] is usable on this port
public static boolean isLoopbackServerSocketSupported() {
return Util.getImplementation().isLoopbackServerSocketAvailable();
}

/// Connect to a remote host
///
/// #### Parameters
Expand Down Expand Up @@ -166,16 +183,88 @@ public void close() throws IOException {
/// @deprecated server sockets are only supported on Android and Desktop and as such
/// we recommend against using them
public static StopListening listen(final int port, final Class scClass) {
return listenImpl(port, scClass, false);
}

/// Listens on the given port on the LOOPBACK interface only, so the channel is
/// reachable from this device but not from the network. Otherwise identical to
/// [#listen(int, Class)]: `scClass` is instantiated per incoming connection and
/// must have a public no-argument constructor.
///
/// Use this for anything that is local by nature - a debug or automation channel,
/// an on-device tool talking to a companion process. Callers that genuinely want to
/// serve the network should use [#listen(int, Class)] and say so.
///
/// Fails (never falls back to a wildcard bind) when
/// [#isLoopbackServerSocketSupported()] is false.
///
/// #### Parameters
///
/// - `port`: the device port
///
/// - `scClass`: class of callback for each incoming connection
///
/// #### Returns
///
/// StopListening instance that allows the caller to stop listening
///
/// #### Throws
///
/// - `IllegalStateException`: if this platform cannot bind a loopback server socket.
/// Thrown here rather than on the listener thread so a caller cannot walk away
/// believing it is listening when nothing ever bound.
public static StopListening listenLoopback(final int port, final Class scClass) {
if (!isLoopbackServerSocketSupported()) {
throw new IllegalStateException("This platform cannot bind a loopback server "
+ "socket; check Socket.isLoopbackServerSocketSupported() first");
}
return listenImpl(port, scClass, true);
}

private static StopListening listenImpl(final int port, final Class scClass, final boolean loopbackOnly) {
class Listener implements StopListening, Runnable {
private boolean stopped;
/// Written by stop() on the caller's thread and read by the accept loop on
/// another, so it needs a memory barrier rather than a plain field. Without
/// one the loop can miss the write, and since closing the listening socket
/// makes the port's cache hand out a FRESH socket on the next call, the
/// listener would quietly resurrect itself instead of stopping.
private final AtomicBoolean stopped = new AtomicBoolean();

/// The backoff after a failed accept. It starts short, doubles while the
/// failure persists and is capped, so a port that can never be bound settles
/// into one attempt every few seconds instead of two a second forever. Reset
/// as soon as an accept succeeds, so a listener that saw one bad moment is not
/// left sluggish. Slept in slices so a stop is still noticed promptly.
private static final int BACKOFF_MIN_MS = 50;
private static final int BACKOFF_MAX_MS = 5000;
private static final int BACKOFF_SLICE_MS = 50;
private int backoffMs = BACKOFF_MIN_MS;

@Override
public void run() {
try {
while (!stopped) {
final Object connection = Util.getImplementation().listenSocket(port);
while (!stopped.get()) {
final Object connection = loopbackOnly
? Util.getImplementation().listenSocketLoopback(port)
: Util.getImplementation().listenSocket(port);
Comment thread
shai-almog marked this conversation as resolved.
if (stopped.get()) {
// stop() was called while this thread sat inside accept. The
// connection that unblocked it belongs to a listener the caller
// has already abandoned, so hand it back rather than serving it.
if (connection != null) {
Util.getImplementation().disconnectSocket(connection);
}
break;
Comment thread
shai-almog marked this conversation as resolved.
}
if (connection == null && stopped.get()) {
// Closing the listening socket is how a stop is delivered, and
// it surfaces here as a failed accept. Reporting that through
// connectionError would announce a fault that never happened.
break;
}
final SocketConnection sc = (SocketConnection) scClass.newInstance();
Comment thread
shai-almog marked this conversation as resolved.
Comment thread
shai-almog marked this conversation as resolved.
Comment thread
shai-almog marked this conversation as resolved.
Comment thread
shai-almog marked this conversation as resolved.
if (connection != null) {
Comment thread
shai-almog marked this conversation as resolved.
backoffMs = BACKOFF_MIN_MS; // healthy again
sc.setConnected(true);
Comment thread
shai-almog marked this conversation as resolved.
Display.getInstance().startThread(new Runnable() {
@Override
Expand All @@ -188,6 +277,28 @@ public void run() {
}, "Connection " + port).start();
} else {
sc.connectionError(Util.getImplementation().getSocketErrorCode(connection), Util.getImplementation().getSocketErrorMessage(connection));
Comment thread
shai-almog marked this conversation as resolved.
// A listener whose accept fails immediately and keeps failing --
Comment thread
shai-almog marked this conversation as resolved.
// a port that cannot be bound, say -- would otherwise spin at
// full speed, burning a core and filling the log. Pause so a
// persistent failure is slow and visible instead of hot. Only
// the failure path waits; a served connection never gets here.
//
// Slept in slices, re-testing the flag between them, so a stop
// arriving during the pause is acted on within a slice rather
// than after the whole backoff.
int waited = 0;
while (waited < backoffMs && !stopped.get()) {
int slice = Math.min(BACKOFF_SLICE_MS, backoffMs - waited);
try {
Thread.sleep(slice);
} catch (InterruptedException interrupted) {
break; // the loop re-tests stopped straight away
}
waited += slice;
}
if (backoffMs < BACKOFF_MAX_MS) {
backoffMs = Math.min(backoffMs * 2, BACKOFF_MAX_MS);
}
Comment thread
shai-almog marked this conversation as resolved.
}
}
} catch (InstantiationException err) {
Expand All @@ -204,7 +315,13 @@ public void run() {

@Override
public void stop() {
stopped = true;
stopped.set(true);
// Closing the listening socket is what brings the thread back out of
// accept. Without it the flag is only noticed the next time a client
// connects, so the thread lingers, and a listener restarted on the same
// port shares the cached socket -- letting this abandoned thread win the
// race for the first connection and drop it.
Util.getImplementation().stopListeningSocket(port, loopbackOnly);
}

}
Expand Down
Loading
Loading