From 85926b80fe3eb39c0111ff3e814542ec77a131bb Mon Sep 17 00:00:00 2001 From: maxonfjvipon Date: Wed, 2 Oct 2024 21:24:29 +0300 Subject: [PATCH] feat(#3251): introduced socket.listen object for windows --- .codacy.yml | 10 +- .../src/main/eo/org/eolang/net/socket.eo | 240 +++++++++++---- .../EOeolang/EOsys/EOwin32$EO\317\206.java" | 10 + .../EOeolang/EOsys/Win32/AcceptFuncCall.java | 79 +++++ .../EOeolang/EOsys/Win32/BindFuncCall.java | 78 +++++ .../EOsys/Win32/InetAddrFuncCall.java | 2 +- .../EOeolang/EOsys/Win32/ListenFuncCall.java | 71 +++++ .../EOeolang/EOsys/Win32/RecvFuncCall.java | 71 +++++ .../EOeolang/EOsys/Win32/SendFuncCall.java | 73 +++++ .../EOorg/EOeolang/EOsys/Win32/Winsock.java | 54 +++- .../EOorg/EOeolang/EOnet/EOsocketTest.java | 278 +++++++++++++++--- 11 files changed, 866 insertions(+), 100 deletions(-) create mode 100644 eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/AcceptFuncCall.java create mode 100644 eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/BindFuncCall.java create mode 100644 eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/ListenFuncCall.java create mode 100644 eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/RecvFuncCall.java create mode 100644 eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/SendFuncCall.java diff --git a/.codacy.yml b/.codacy.yml index 292d9ae796..62d5fddcf7 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -25,8 +25,8 @@ # package name contains capital letter and such names are conventional. --- exclude_paths: - - "eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Posix/BindSyscall.java" - - "eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Posix/ListenSyscall.java" - - "eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Posix/AcceptSyscall.java" - - "eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Posix/SendSyscall.java" - - "eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Posix/RecvSyscall.java" + - "eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/BindFuncCall.java" + - "eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/ListenFuncCall.java" + - "eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/AcceptFuncCall.java" + - "eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/SendFuncCall.java" + - "eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/RecvFuncCall.java" diff --git a/eo-runtime/src/main/eo/org/eolang/net/socket.eo b/eo-runtime/src/main/eo/org/eolang/net/socket.eo index 82d468d28e..2fd926ff75 100644 --- a/eo-runtime/src/main/eo/org/eolang/net/socket.eo +++ b/eo-runtime/src/main/eo/org/eolang/net/socket.eo @@ -250,12 +250,8 @@ closed.eq -1 error sprintf - "Couldn't close a posix socket '%d' connected to '%s:%d', reason: '%s'" - * - sockfd - ^.address - ^.port - ^.strerror.code + "Couldn't close a posix socket '%d', reason: '%s'" + * sockfd ^.strerror.code true # Safe posix socket that ensures that socket is closed even if error is occurred. @@ -356,41 +352,110 @@ error ex > [ex] sock.closed-socket client-sockfd + # The win32 platform specified socket. + # + # Attention! The object is for internal usage only, please don't use + # the object programmatically outside of `socket` object. [address port] > win-socket - [scope] > connect - code. > started-up - win32 - "WSAStartup" - * win32.winsock-version-2-2 - code. > sd - win32 - "socket" - * win32.af-inet win32.sock-stream win32.ipproto-tcp - code. > inet-addr + code. > sd + win32 + "socket" + * win32.af-inet win32.sock-stream win32.ipproto-tcp + code. > inet-addr + win32 + "inet_addr" + * address + if. > inet-addr-as-int + inet-addr.eq win32.inaddr-none + error + sprintf + "Couldn't convert an IPv4 address '%s' into a 32-bit integer via 'inet_addr' win32 function call, WSA error code: %d" + * ^.address last-error-code + inet-addr.as-i32 + win32.sockaddr-in > sockaddr + win32.af-inet.as-i16 + ^.htons port + inet-addr-as-int + + # Scoped win32 socket that is passed as argument + # to scope of `win32-socket.connect`, `win32-socket.listen` and `scoped-socket.accept`. + # Here `sockfd` is the socket descriptor. + [sockfd] > scoped-socket + # Makes an `input` from win32 `socket`. + # The object allows to use `socket` as input stream and `read` from it sequentially. + ^.^.^.as-input recv > as-input + # Makes an `output` from win32 socket. + # The object allows to use `socket` as output stream and `write` to it sequentially. + ^.^.^.as-output send > as-output + + # Send bytes through the socket. + # Here `buffer` must be an object that can be dataized. + # On success the `number` of sent bytes is returned, otherwise `error` is returned. + [buffer] > send + buffer > buff! + code. > sent + win32 + "send" + * ^.sockfd buff buff.size 0 + if. > @ + sent.eq -1 + error + sprintf + "Failed to send message through the socket '%d', WSA error code: %d" + * ^.sockfd ^.^.last-error.code + sent + + # Receive bytes from the socket. + # Here `size` must be a `number` of desired bytes to receive. + # On success the received `bytes` are returned, otherwise `error` is returned. + [size] > recv + called. > received + win32 + "recv" + * ^.sockfd size 0 + if. > @ + received.code.eq -1 + error + sprintf + "Failed to receive data from the socket '%d', WSA error code: %d" + * ^.sockfd ^.^.last-error.code + received.output + + # Get last error. + # + # Attention! The object is for internal usage only, please don't use + # the object programmatically outside of `socket` object. + win32 "WSAGetLastError" * > [] > last-error + + # Close socket by given descriptor `sockfd`. + # Returns `true` on success, returns `error` otherwise. + # + # Attention! The object is for internal usage only, please don't use + # the object programmatically outside of `socket` object. + [sockfd] > closed-socket + code. > closed win32 - "inet_addr" - * ^.address - if. > inet-addr-as-int - inet-addr.eq win32.inaddr-none + "closesocket" + * sockfd + if. > @ + closed.eq win32.socket-error error sprintf - "Couldn't convert an IPv4 address '%s' into a 32-bit integer via 'inet_addr' win32 function call, WSA error code: %d" - * ^.address last-error-code - inet-addr.as-i32 - win32.sockaddr-in > sockaddr - win32.af-inet.as-i16 - ^.^.htons ^.port - inet-addr-as-int - code. > connected - win32 - "connect" - * sd sockaddr sockaddr.size - code. > closed + "Couldn't close a win32 socket '%d', WSA error code: %d" + * sockfd ^.last-error.code + true + + # Safe win32 socket that ensures that socket is closed and Winsock resources are + # cleaned even if error is occurred. + # + # Attention! The object is for internal usage only, please don't use + # the object programmatically outside of `socket` object. + [scope] > safe-socket + code. > started-up win32 - "closesocket" - * sd + "WSAStartup" + * win32.winsock-version-2-2 (win32 "WSACleanup" *).code > cleaned-up - (win32 "WSAGetLastError" *).code > last-error-code if. > @ (started-up.eq 0).not error @@ -399,34 +464,99 @@ * started-up try if. - sd.eq -1 + ^.sd.eq -1 error sprintf "Couldn't create a win32 socket, WSA error code: %d" - * last-error-code + * ^.last-error.code try - if. - connected.eq -1 - error - sprintf - "Couldn't connect to '%s:%d' on win32 socket '%d', WSA error code: %d" - * ^.address ^.port sd last-error-code - as-bytes. - dataized - scope - ^.scoped-win-socket sd + scope error ex > [ex] - if. - closed.eq win32.socket-error - error - sprintf - "Couldn't close a win32 socket '%d', WSA error code: %d" - * sd last-error-code - true + ^.closed-socket ^.sd error ex > [ex] if. cleaned-up.eq win32.socket-error error "Couldn't cleanup Winsock resources" true - [descriptor] > scoped-win-socket + # Initiate a connection on a socket. + # Here `scope` must be an abstract object with one free attribute which will be + # set to `scoped-socket` object and used as connector for socket messaging. + [scope] > connect + ^.safe-socket > @ + [] >> + ^.^ > sock + code. > connected + win32 + "connect" + * sock.sd sock.sockaddr sock.sockaddr.size + if. > @ + connected.eq -1 + error + sprintf + "Couldn't connect to '%s:%d' on win32 socket '%d', WSA error code: %d" + * sock.address sock.port sock.sd sock.last-error.code + as-bytes. + dataized + scope + sock.scoped-socket + sock.sd + + # Listen for connections on a socket. + # Here `scope` must be an abstract object with one free attribute, but unlike `connect`, + # this free attribute is set to abstract object which decorates `scoped-socket` and also + # has attribute `accept` to accept other socket connections. + [scope] > listen + ^.safe-socket > @ + [] >> + ^.^ > sock + code. > bound + win32 + "bind" + * sock.sd sock.sockaddr sock.sockaddr.size + code. > listened + win32 + "listen" + * sock.sd 2048 + if. > @ + bound.eq -1 + error + sprintf + "Couldn't bind win32 socket '%d' to '%s:%d', WSA error code: %d" + * sock.sd sock.address sock.port sock.last-error.code + if. + listened.eq -1 + error + sprintf + "Failed to listen for connections to '%s:%d' on socket '%d', WSA error code: %d" + * sock.address sock.port sock.sd sock.last-error.code + as-bytes. + dataized + scope + [] >> + ^.sock.scoped-socket ^.sock.sd > @ + + # Accept incoming connection on socket + # On success the client socket is returned, otherwise `error` is returned. + # Client socket decorates `scoped-socket` object, so all the attributes like + # `send`, `recv`, `as-input`, `as-output` are available. + [scope] > accept + ^.^.sock > sock + code. > client-sockfd + win32 + "accept" + * sock.sd sock.sockaddr sock.sockaddr.size + try > @ + if. + client-sockfd.eq -1 + error + sprintf + "Failed to accept a connection on win32 socket '%d', WSA error code: %d" + * sock.sd sock.last-error.code + as-bytes. + dataized + scope + sock.scoped-socket + client-sockfd + error ex > [ex] + sock.closed-socket client-sockfd diff --git "a/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/EOwin32$EO\317\206.java" "b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/EOwin32$EO\317\206.java" index a91f370126..95c49be5fc 100644 --- "a/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/EOwin32$EO\317\206.java" +++ "b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/EOwin32$EO\317\206.java" @@ -27,13 +27,18 @@ */ package EOorg.EOeolang.EOsys; // NOPMD +import EOorg.EOeolang.EOsys.Win32.AcceptFuncCall; +import EOorg.EOeolang.EOsys.Win32.BindFuncCall; import EOorg.EOeolang.EOsys.Win32.ClosesocketFuncCall; import EOorg.EOeolang.EOsys.Win32.ConnectFuncCall; import EOorg.EOeolang.EOsys.Win32.GetCurrentProcessIdFuncCall; import EOorg.EOeolang.EOsys.Win32.GetEnvironmentVariableFuncCall; import EOorg.EOeolang.EOsys.Win32.GetSystemTimeFuncCall; import EOorg.EOeolang.EOsys.Win32.InetAddrFuncCall; +import EOorg.EOeolang.EOsys.Win32.ListenFuncCall; import EOorg.EOeolang.EOsys.Win32.ReadFileFuncCall; +import EOorg.EOeolang.EOsys.Win32.RecvFuncCall; +import EOorg.EOeolang.EOsys.Win32.SendFuncCall; import EOorg.EOeolang.EOsys.Win32.SocketFuncCall; import EOorg.EOeolang.EOsys.Win32.WSACleanupFuncCall; import EOorg.EOeolang.EOsys.Win32.WSAGetLastErrorFuncCall; @@ -75,6 +80,11 @@ public final class EOwin32$EOφ extends PhDefault implements Atom { EOwin32$EOφ.FUNCTIONS.put("WSAGetLastError", WSAGetLastErrorFuncCall::new); EOwin32$EOφ.FUNCTIONS.put("socket", SocketFuncCall::new); EOwin32$EOφ.FUNCTIONS.put("connect", ConnectFuncCall::new); + EOwin32$EOφ.FUNCTIONS.put("accept", AcceptFuncCall::new); + EOwin32$EOφ.FUNCTIONS.put("bind", BindFuncCall::new); + EOwin32$EOφ.FUNCTIONS.put("listen", ListenFuncCall::new); + EOwin32$EOφ.FUNCTIONS.put("send", SendFuncCall::new); + EOwin32$EOφ.FUNCTIONS.put("recv", RecvFuncCall::new); EOwin32$EOφ.FUNCTIONS.put("closesocket", ClosesocketFuncCall::new); EOwin32$EOφ.FUNCTIONS.put("inet_addr", InetAddrFuncCall::new); } diff --git a/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/AcceptFuncCall.java b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/AcceptFuncCall.java new file mode 100644 index 0000000000..ff2f187e3b --- /dev/null +++ b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/AcceptFuncCall.java @@ -0,0 +1,79 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016-2024 Objectionary.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * @checkstyle PackageNameCheck (4 lines) + * @checkstyle TrailingCommentCheck (3 lines) + */ +package EOorg.EOeolang.EOsys.Win32; // NOPMD + +import EOorg.EOeolang.EOsys.SockaddrIn; +import EOorg.EOeolang.EOsys.Syscall; +import com.sun.jna.ptr.IntByReference; +import org.eolang.Data; +import org.eolang.Dataized; +import org.eolang.PhDefault; +import org.eolang.Phi; + +/** + * The socket WS2_32 function call. + * @see here for details + * @since 0.40.0 + */ +public final class AcceptFuncCall implements Syscall { + /** + * Win32 object. + */ + private final Phi win; + + /** + * Ctor. + * @param win Win32 object + */ + public AcceptFuncCall(final Phi win) { + this.win = win; + } + + @Override + public Phi make(final Phi... params) { + final Phi result = this.win.take("return").copy(); + result.put( + 0, + new Data.ToPhi( + Winsock.INSTANCE.accept( + new Dataized(params[0]).asNumber().intValue(), + new SockaddrIn( + new Dataized(params[1].take("sin-family")).take(Short.class), + new Dataized(params[1].take("sin-port")).take(Short.class), + new Dataized(params[1].take("sin-addr")).take(Integer.class), + new Dataized(params[1].take("sin-zero")).take() + ), + new IntByReference(new Dataized(params[2]).asNumber().intValue()) + ) + ) + ); + result.put(1, new PhDefault()); + return result; + } +} diff --git a/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/BindFuncCall.java b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/BindFuncCall.java new file mode 100644 index 0000000000..258409100d --- /dev/null +++ b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/BindFuncCall.java @@ -0,0 +1,78 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016-2024 Objectionary.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * @checkstyle PackageNameCheck (4 lines) + * @checkstyle TrailingCommentCheck (3 lines) + */ +package EOorg.EOeolang.EOsys.Win32; // NOPMD + +import EOorg.EOeolang.EOsys.SockaddrIn; +import EOorg.EOeolang.EOsys.Syscall; +import org.eolang.Data; +import org.eolang.Dataized; +import org.eolang.PhDefault; +import org.eolang.Phi; + +/** + * The socket WS2_32 function call. + * @see here for details + * @since 0.40.0 + */ +public final class BindFuncCall implements Syscall { + /** + * Win32 object. + */ + private final Phi win; + + /** + * Ctor. + * @param win Win32 object + */ + public BindFuncCall(final Phi win) { + this.win = win; + } + + @Override + public Phi make(final Phi... params) { + final Phi result = this.win.take("return").copy(); + result.put( + 0, + new Data.ToPhi( + Winsock.INSTANCE.bind( + new Dataized(params[0]).asNumber().intValue(), + new SockaddrIn( + new Dataized(params[1].take("sin-family")).take(Short.class), + new Dataized(params[1].take("sin-port")).take(Short.class), + new Dataized(params[1].take("sin-addr")).take(Integer.class), + new Dataized(params[1].take("sin-zero")).take() + ), + new Dataized(params[2]).asNumber().intValue() + ) + ) + ); + result.put(1, new PhDefault()); + return result; + } +} diff --git a/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/InetAddrFuncCall.java b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/InetAddrFuncCall.java index 3775b60495..8e5996f692 100644 --- a/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/InetAddrFuncCall.java +++ b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/InetAddrFuncCall.java @@ -66,7 +66,7 @@ public Phi make(final Phi... params) { ).getAddress(); final ByteBuffer buffer = ByteBuffer.allocate(4); buffer.put(bytes); - result.put(0, new Data.ToPhi(buffer.getInt(0))); + result.put(0, new Data.ToPhi(Integer.reverseBytes(buffer.getInt(0)))); } catch (final UnknownHostException exception) { Kernel32.INSTANCE.SetLastError(Winsock.WSAEINVAL); result.put(0, new Data.ToPhi(-1)); diff --git a/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/ListenFuncCall.java b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/ListenFuncCall.java new file mode 100644 index 0000000000..60a95e80d6 --- /dev/null +++ b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/ListenFuncCall.java @@ -0,0 +1,71 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016-2024 Objectionary.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * @checkstyle PackageNameCheck (4 lines) + * @checkstyle TrailingCommentCheck (3 lines) + */ +package EOorg.EOeolang.EOsys.Win32; // NOPMD + +import EOorg.EOeolang.EOsys.Syscall; +import org.eolang.Data; +import org.eolang.Dataized; +import org.eolang.PhDefault; +import org.eolang.Phi; + +/** + * WriteFile kernel32 function call. + * @see here for details + * @since 0.40.0 + */ +public final class ListenFuncCall implements Syscall { + /** + * Win32 object. + */ + private final Phi win; + + /** + * Ctor. + * @param win Win32 object + */ + public ListenFuncCall(final Phi win) { + this.win = win; + } + + @Override + public Phi make(final Phi... params) { + final Phi result = this.win.take("return").copy(); + result.put( + 0, + new Data.ToPhi( + Winsock.INSTANCE.listen( + new Dataized(params[0]).asNumber().intValue(), + new Dataized(params[1]).asNumber().intValue() + ) + ) + ); + result.put(1, new PhDefault()); + return result; + } +} diff --git a/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/RecvFuncCall.java b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/RecvFuncCall.java new file mode 100644 index 0000000000..1291435759 --- /dev/null +++ b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/RecvFuncCall.java @@ -0,0 +1,71 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016-2024 Objectionary.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * @checkstyle PackageNameCheck (4 lines) + * @checkstyle TrailingCommentCheck (3 lines) + */ +package EOorg.EOeolang.EOsys.Win32; // NOPMD + +import EOorg.EOeolang.EOsys.Syscall; +import java.util.Arrays; +import org.eolang.Data; +import org.eolang.Dataized; +import org.eolang.Phi; + +/** + * ReadFile kernel32 function call. + * @see here for details + * @since 0.40.0 + */ +public final class RecvFuncCall implements Syscall { + /** + * Win32 object. + */ + private final Phi win; + + /** + * Ctor. + * @param win Win32 object + */ + public RecvFuncCall(final Phi win) { + this.win = win; + } + + @Override + public Phi make(final Phi... params) { + final Phi result = this.win.take("return").copy(); + final int size = new Dataized(params[1]).asNumber().intValue(); + final byte[] buf = new byte[(int) size]; + final int received = Winsock.INSTANCE.recv( + new Dataized(params[0]).asNumber().intValue(), + buf, + size, + new Dataized(params[2]).asNumber().intValue() + ); + result.put(0, new Data.ToPhi(received)); + result.put(1, new Data.ToPhi(Arrays.copyOf(buf, received))); + return result; + } +} diff --git a/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/SendFuncCall.java b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/SendFuncCall.java new file mode 100644 index 0000000000..d0c8fe60ab --- /dev/null +++ b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/SendFuncCall.java @@ -0,0 +1,73 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016-2024 Objectionary.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * @checkstyle PackageNameCheck (4 lines) + * @checkstyle TrailingCommentCheck (3 lines) + */ +package EOorg.EOeolang.EOsys.Win32; // NOPMD + +import EOorg.EOeolang.EOsys.Syscall; +import org.eolang.Data; +import org.eolang.Dataized; +import org.eolang.PhDefault; +import org.eolang.Phi; + +/** + * WriteFile kernel32 function call. + * @see here for details + * @since 0.40.0 + */ +public final class SendFuncCall implements Syscall { + /** + * Win32 object. + */ + private final Phi win; + + /** + * Ctor. + * @param win Win32 object + */ + public SendFuncCall(final Phi win) { + this.win = win; + } + + @Override + public Phi make(final Phi... params) { + final Phi result = this.win.take("return").copy(); + result.put( + 0, + new Data.ToPhi( + Winsock.INSTANCE.send( + new Dataized(params[0]).asNumber().intValue(), + new Dataized(params[1]).take(), + new Dataized(params[2]).asNumber().intValue(), + new Dataized(params[3]).asNumber().intValue() + ) + ) + ); + result.put(1, new PhDefault()); + return result; + } +} diff --git a/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/Winsock.java b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/Winsock.java index bea6616050..b4fef22e9f 100644 --- a/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/Winsock.java +++ b/eo-runtime/src/main/java/EOorg/EOeolang/EOsys/Win32/Winsock.java @@ -30,6 +30,7 @@ import EOorg.EOeolang.EOsys.SockaddrIn; import com.sun.jna.Native; +import com.sun.jna.ptr.IntByReference; import com.sun.jna.win32.StdCallLibrary; import com.sun.jna.win32.W32APIOptions; @@ -40,7 +41,7 @@ * @checkstyle ParameterNumberCheck (1000 lines) * @checkstyle AbbreviationAsWordInNameCheck (1000 lines) */ -@SuppressWarnings("PMD.MethodNamingConventions") +@SuppressWarnings({"PMD.MethodNamingConventions", "PMD.TooManyMethods"}) public interface Winsock extends StdCallLibrary { /** * Instance. @@ -125,6 +126,57 @@ public interface Winsock extends StdCallLibrary { */ int connect(int sockfd, SockaddrIn addr, int addrlen); + /** + * Assigns the address specified by {@code addr} to the socket referred to + * by the file descriptor {@code sockfd}. + * @param sockfd Socket descriptor + * @param addr Address structure + * @param addrlen The size of the address structure + * @return Zero on success, -1 on error + */ + int bind(int sockfd, SockaddrIn addr, int addrlen); + + /** + * Listen for incoming connections on socket. + * @param sockfd Socket descriptor + * @param backlog Specifies the queue length for completely established sockets + * waiting to be accepted + * @return Zero on success, -1 on error + */ + int listen(int sockfd, int backlog); + + /** + * Accept connection on socket. + * @param sockfd Socket descriptor + * @param addr Address structure + * @param addrlen The size of the address structure + * @return On success, file descriptor for the accepted socket (a nonnegative integer) + * is returned. On error, -1 is returned. + */ + int accept(int sockfd, SockaddrIn addr, IntByReference addrlen); + + /** + * Send a message to a socket. + * @param sockfd Socket descriptor + * @param buf Byte buffer to store sent bytes + * @param len Size of sent data + * @param flags Flags + * @return The number of sent bytes on success, -1 on error + * @checkstyle ParameterNumberCheck (5 lines) + */ + int send(int sockfd, byte[] buf, int len, int flags); + + /** + * Receive a message from a socket. + * @param sockfd Socket descriptor + * @param buf Byte buffer to store received bytes + * @param len Size of received data + * @param flags Flags + * @return The number of received bytes on success, -1 on error + * @checkstyle ParameterNumberCheck (5 lines) + */ + int recv(int sockfd, byte[] buf, int len, int flags); + /** * Retrieve the last error from winsock. * @return The code of the last winsock error. diff --git a/eo-runtime/src/test/java/EOorg/EOeolang/EOnet/EOsocketTest.java b/eo-runtime/src/test/java/EOorg/EOeolang/EOnet/EOsocketTest.java index 168b0472b7..6886ca895a 100644 --- a/eo-runtime/src/test/java/EOorg/EOeolang/EOnet/EOsocketTest.java +++ b/eo-runtime/src/test/java/EOorg/EOeolang/EOnet/EOsocketTest.java @@ -53,7 +53,6 @@ import org.eolang.Phi; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; @@ -64,15 +63,13 @@ /** * Test case for {@link EOsocket}. * @since 0.40 - * @todo #3251:30min Enable the tests for windows. For some reason socket tests for windows - * are flaky. We need to deal with them somehow and enable. * @checkstyle TypeNameCheck (100 lines) */ @SuppressWarnings({ "PMD.TooManyMethods", - "JTCOP.RuleAllTestsHaveProductionClass", "PMD.AvoidUsingHardCodedIP", - "PMD.CloseResource" + "PMD.CloseResource", + "JTCOP.RuleAllTestsHaveProductionClass" }) @Execution(ExecutionMode.SAME_THREAD) final class EOsocketTest { @@ -87,29 +84,30 @@ final class EOsocketTest { private static final Random RANDOM = new Random(); @Test - @DisabledOnOs(OS.WINDOWS) void connectsToLocalServerViaSocketObject() throws IOException { final RandomServer server = new RandomServer().started(); - final Phi socket = Phi.Φ.take("org.eolang.net.socket").copy(); - socket.put(0, new Data.ToPhi(EOsocketTest.LOCALHOST)); - socket.put(1, new Data.ToPhi(server.port)); - final Phi connected = socket.take("connect").copy(); - connected.put(0, new Simple()); - final byte[] expected = {1}; - final byte[] actual = new Dataized(connected).take(); - MatcherAssert.assertThat( - String.format( - "The 'socket.connect' should have been successfully connected to local server, but it didn't, reason: %s", - new String(actual, StandardCharsets.UTF_8) - ), - actual, - Matchers.equalTo(expected) - ); - server.stop(); + try { + final Phi socket = Phi.Φ.take("org.eolang.net.socket").copy(); + socket.put(0, new Data.ToPhi(EOsocketTest.LOCALHOST)); + socket.put(1, new Data.ToPhi(server.port)); + final Phi connected = socket.take("connect").copy(); + connected.put(0, new Simple()); + final byte[] expected = {1}; + final byte[] actual = new Dataized(connected).take(); + MatcherAssert.assertThat( + String.format( + "The 'socket.connect' should have been successfully connected to local server, but it didn't, reason: %s", + new String(actual, StandardCharsets.UTF_8) + ), + actual, + Matchers.equalTo(expected) + ); + } finally { + server.stop(); + } } @Test - @DisabledOnOs(OS.WINDOWS) void sendsAndReceivesMessageViaSocketObject() throws InterruptedException, IOException { final String msg = "Hello, Socket!"; final AtomicReference bytes = new AtomicReference<>(); @@ -172,19 +170,17 @@ private static int randomPort() { */ @Nested @DisabledOnOs({OS.MAC, OS.LINUX}) - @Disabled @Execution(ExecutionMode.SAME_THREAD) final class WindowsSocketTest { @Test - @DisabledOnOs({OS.LINUX, OS.MAC}) - void connectsToLocalServerViaWindowsSyscall() throws IOException { + void connectsToLocalServerViaSyscall() throws IOException, InterruptedException { final RandomServer server = new RandomServer().started(); final int started = this.startup(); try { this.ensure(started == 0); final int socket = this.openSocket(); try { - this.ensure(socket >= 0); + this.ensure(socket > 0); final SockaddrIn addr = this.sockaddr(server.port); MatcherAssert.assertThat( String.format( @@ -204,13 +200,13 @@ void connectsToLocalServerViaWindowsSyscall() throws IOException { } @Test - void refusesConnectionViaWindowsSyscall() throws UnknownHostException { + void refusesConnectionViaSyscall() throws UnknownHostException { final int started = this.startup(); try { this.ensure(started == 0); final int socket = this.openSocket(); try { - this.ensure(socket >= 0); + this.ensure(socket > 0); final SockaddrIn addr = new SockaddrIn( (short) Winsock.AF_INET, EOsocketTest.htons(8080), @@ -225,7 +221,200 @@ void refusesConnectionViaWindowsSyscall() throws UnknownHostException { this.closeSocket(socket); } } finally { - Winsock.INSTANCE.WSACleanup(); + this.cleanup(); + } + } + + @Test + void bindsSocketSuccessfullyViaSyscall() throws UnknownHostException { + final int started = this.startup(); + try { + this.ensure(started == 0); + final int socket = this.openSocket(); + try { + this.ensure(socket > 0); + MatcherAssert.assertThat( + String.format( + "Win socket should have been bound to localhost via syscall, but it didn't, error code is: %d", + this.getError() + ), + this.bindSocket(socket, EOsocketTest.randomPort()), + Matchers.equalTo(0) + ); + } finally { + this.closeSocket(socket); + } + } finally { + this.cleanup(); + } + } + + @Test + void startsListenOnPosixSocket() throws UnknownHostException { + final int started = this.startup(); + try { + this.ensure(started == 0); + final int socket = this.openSocket(); + try { + this.ensure(socket > 0); + this.ensure(this.bindSocket(socket, EOsocketTest.randomPort()) == 0); + MatcherAssert.assertThat( + String.format( + "Posix socket should have been bound to localhost via syscall, but it didn't, reason: %s", + this.getError() + ), + Winsock.INSTANCE.listen(socket, 2), + Matchers.equalTo(0) + ); + } finally { + this.closeSocket(socket); + } + } finally { + this.cleanup(); + } + } + + @Test + @SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes") + void acceptsConnectionOnSocket() throws InterruptedException, UnknownHostException { + final int started = this.startup(); + try { + this.ensure(started == 0); + final AtomicInteger accept = new AtomicInteger(0); + final AtomicInteger error = new AtomicInteger(); + final AtomicInteger port = new AtomicInteger(EOsocketTest.randomPort()); + final Thread server = new Thread( + () -> { + final int socket = this.openSocket(); + try { + this.ensure(socket > 0); + while (this.bindSocket(socket, port.get()) != 0) { + port.set(EOsocketTest.randomPort()); + } + this.ensure(Winsock.INSTANCE.listen(socket, 5) == 0); + final SockaddrIn addr = new SockaddrIn(); + final int accepted = Winsock.INSTANCE.accept( + socket, addr, new IntByReference(addr.size()) + ); + Logger.info(this, "Accepted socket: %d", accepted); + accept.set(accepted); + if (accepted < 0) { + error.set(this.getError()); + } + } catch (final UnknownHostException exception) { + throw new RuntimeException(exception); + } finally { + if (accept.get() > 0) { + this.closeSocket(accept.get()); + } + this.closeSocket(socket); + } + } + ); + server.start(); + Thread.sleep(2000); + final int client = this.openSocket(); + try { + this.ensure(client >= 0); + final SockaddrIn sockaddr = this.sockaddr(port.get()); + MatcherAssert.assertThat( + String.format( + "Socket should have been connected to local server on sockets, but it didn't, reason: %s", + this.getError() + ), + Winsock.INSTANCE.connect(client, sockaddr, sockaddr.size()), + Matchers.equalTo(0) + ); + server.join(); + MatcherAssert.assertThat( + String.format( + "Accepted client socket must be positive, but it isn't, reason: %s", + error.get() + ), + accept.get(), + Matchers.greaterThan(0) + ); + } finally { + this.closeSocket(client); + } + } finally { + this.cleanup(); + } + } + + @Test + @SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes") + void sendsAndReceivesMessagesViaSyscalls() + throws InterruptedException, UnknownHostException { + final int started = this.startup(); + try { + this.ensure(started == 0); + final AtomicInteger received = new AtomicInteger(-1); + final AtomicReference bytes = new AtomicReference<>(); + final AtomicInteger port = new AtomicInteger(EOsocketTest.randomPort()); + final Thread server = new Thread( + () -> { + final int socket = this.openSocket(); + int accepted = 0; + try { + this.ensure(socket > 0); + while (this.bindSocket(socket, port.get()) != 0) { + port.set(EOsocketTest.randomPort()); + } + this.ensure(Winsock.INSTANCE.listen(socket, 5) == 0); + final SockaddrIn addr = new SockaddrIn(); + accepted = Winsock.INSTANCE.accept( + socket, addr, new IntByReference(addr.size()) + ); + Logger.info(this, "Accepted socket: %d", accepted); + this.ensure(accepted > 0); + final byte[] buf = new byte[1024]; + received.set(Winsock.INSTANCE.recv(accepted, buf, buf.length, 0)); + bytes.set(Arrays.copyOf(buf, received.get())); + } catch (final UnknownHostException exception) { + throw new RuntimeException(exception); + } finally { + this.closeSocket(accepted); + this.closeSocket(socket); + } + } + ); + server.start(); + Thread.sleep(2000); + final int client = this.openSocket(); + try { + this.ensure(client >= 0); + final SockaddrIn sockaddr = this.sockaddr(port.get()); + this.ensure(Winsock.INSTANCE.connect(client, sockaddr, sockaddr.size()) == 0); + final byte[] buf = "Hello, Socket!".getBytes(StandardCharsets.UTF_8); + final int sent = Winsock.INSTANCE.send(client, buf, buf.length, 0); + MatcherAssert.assertThat( + String.format( + "Client had to sent message to the server, but it didn't, reason: %s", + this.getError() + ), + sent, + Matchers.equalTo(buf.length) + ); + server.join(); + MatcherAssert.assertThat( + String.format( + "Server hat to receive message from the client, but it didn't, reason: %s", + this.getError() + ), + received.get(), + Matchers.equalTo(buf.length) + ); + MatcherAssert.assertThat( + "Received bytes must be equal to sent, but they didn't", + new String(bytes.get(), StandardCharsets.UTF_8), + Matchers.equalTo(new String(buf, StandardCharsets.UTF_8)) + ); + } finally { + this.closeSocket(client); + } + } finally { + this.cleanup(); } } @@ -295,6 +484,20 @@ private int getError() { return Winsock.INSTANCE.WSAGetLastError(); } + /** + * Bind socket. + * @param socket Socket + * @param port Port + * @return Zero on success, -1 on error + */ + private int bindSocket(final int socket, final int port) throws UnknownHostException { + return Winsock.INSTANCE.bind( + socket, + this.sockaddr(port), + 16 + ); + } + /** * Call posix inet addr. * @param address IP address @@ -304,7 +507,7 @@ private int inetAddr(final String address) throws UnknownHostException { final byte[] bytes = InetAddress.getByName(address).getAddress(); final ByteBuffer buffer = ByteBuffer.allocate(4); buffer.put(bytes); - return buffer.getInt(0); + return Integer.reverseBytes(buffer.getInt(0)); } /** @@ -334,7 +537,7 @@ void connectsToLocalServerViaSyscall() throws IOException { final RandomServer server = new RandomServer().started(); final int socket = this.openSocket(); try { - this.ensure(socket >= 0); + this.ensure(socket > 0); final SockaddrIn addr = this.sockaddr(server.port); MatcherAssert.assertThat( String.format( @@ -354,7 +557,7 @@ void connectsToLocalServerViaSyscall() throws IOException { void refusesConnectionViaSyscall() { final int socket = this.openSocket(); try { - this.ensure(socket >= 0); + this.ensure(socket > 0); final SockaddrIn addr = this.sockaddr(1234); final int connected = CStdLib.INSTANCE.connect(socket, addr, addr.size()); MatcherAssert.assertThat( @@ -371,7 +574,7 @@ void refusesConnectionViaSyscall() { void bindsSocketSuccessfullyViaSyscall() { final int socket = this.openSocket(); try { - this.ensure(socket >= 0); + this.ensure(socket > 0); MatcherAssert.assertThat( String.format( "Posix socket should have been bound to localhost via syscall, but it didn't, reason: %s", @@ -386,11 +589,10 @@ void bindsSocketSuccessfullyViaSyscall() { } @Test - @DisabledOnOs(OS.WINDOWS) void startsListenOnPosixSocket() { final int socket = this.openSocket(); try { - this.ensure(socket >= 0); + this.ensure(socket > 0); this.ensure(this.bindSocket(socket, EOsocketTest.randomPort()) == 0); MatcherAssert.assertThat( String.format( @@ -406,7 +608,6 @@ void startsListenOnPosixSocket() { } @Test - @DisabledOnOs(OS.WINDOWS) void acceptsConnectionOnSocket() throws InterruptedException { final AtomicInteger accept = new AtomicInteger(0); final AtomicReference error = new AtomicReference<>(); @@ -415,7 +616,7 @@ void acceptsConnectionOnSocket() throws InterruptedException { () -> { final int socket = this.openSocket(); try { - this.ensure(socket >= 0); + this.ensure(socket > 0); while (this.bindSocket(socket, port.get()) != 0) { port.set(EOsocketTest.randomPort()); } @@ -644,6 +845,7 @@ RandomServer started() { this.socket.setReuseAddress(true); this.socket.bind(new InetSocketAddress(EOsocketTest.LOCALHOST, this.port)); bound = true; + Logger.info(this, "Server started on port %d", this.port); } catch (final IOException exception) { Logger.info(this, "Port %d is unavailable, trying another port...", this.port); }