Skip to content

Bf 696 socket#697

Merged
lidaobing merged 20 commits into
mainfrom
bf_696_socket
Jan 25, 2026
Merged

Bf 696 socket#697
lidaobing merged 20 commits into
mainfrom
bf_696_socket

Conversation

@lidaobing

@lidaobing lidaobing commented Jan 19, 2026

Copy link
Copy Markdown
Member

Summary by Sourcery

Migrate UDP handling in CoreThread to use GLib/GIO UDP sockets and socket addresses instead of raw file descriptors and sockaddr structures.

New Features:

  • Add UdpDataService processing entrypoint that accepts a GSocketAddress peer object for incoming UDP messages.

Bug Fixes:

  • Ensure UDP receive errors are logged and handled without tearing down the event source when using GIO sockets.

Enhancements:

  • Refactor UdpThread and CoreThread to use GSocket and GIO-based sources for UDP I/O instead of low-level POSIX sockets and GIOChannel.
  • Route all CoreThread UDP send paths through a helper that retrieves the underlying UDP file descriptor from the GSocket to keep the legacy API working.
  • Improve CoreThread test helper connection loop logging and increase detection retry interval for more stable tests.

Build:

  • Extend iptux-core Meson build configuration to depend on gio-2.0 wherever UDP core or tests are linked.

@sourcery-ai

sourcery-ai Bot commented Jan 19, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refactors UDP handling to use GLib's GSocket/GIO infrastructure instead of raw UDP file descriptors, updates UdpDataService to accept GSocketAddress peers, propagates the new socket usage through CoreThread APIs and tests, and wires in gio-2.0 as a build dependency.

Sequence diagram for UDP message reception with GSocket

sequenceDiagram
    participant Sender as SenderHost
    participant GSocket as GSocket
    participant GSource as GLibSource
    participant UdpThread as UdpThread
    participant UdpCb as udpThreadCb
    participant Ops as UdpThreadOps
    participant Service as UdpDataService

    activate GSocket
    Sender->>GSocket: UDP datagram

    GSocket-->>GSource: socket ready (G_IO_IN)
    GSource->>UdpCb: callback udpThreadCb(UdpThread)
    activate UdpCb

    UdpCb->>GSocket: g_socket_receive_from(socket, peer, buf, size, NULL, error)
    alt receive error
        GSocket-->>UdpCb: n < 0 and error
        UdpCb->>UdpCb: log g_socket_receive_from failed
        UdpCb-->>GSource: TRUE
    else zero bytes
        GSocket-->>UdpCb: n == 0
        UdpCb->>GSocket: g_object_unref(peer)
        UdpCb-->>GSource: TRUE
    else valid data
        GSocket-->>UdpCb: n > 0, peer, buf
        UdpCb->>UdpCb: terminate buf with '\0'
        UdpCb->>Ops: on_new_msg(udpThread, peer, buf, n)
        activate Ops
        Ops->>Service: process(peer, buf, n)
        activate Service
        Service->>Service: extract port and IP from peer
        Service->>Service: process(in_addr, port, buf, n, true)
        Service-->>Ops: std::unique_ptr~UdpData~
        deactivate Service
        Ops-->>UdpCb: bool
        deactivate Ops
        UdpCb->>GSocket: g_object_unref(peer)
        UdpCb-->>GSource: TRUE
    end
    deactivate UdpCb
Loading

Class diagram for updated UDP socket handling

classDiagram
    class CoreThread {
        +int tcpSock
        +int getUdpSock()
        +bool start()
        +bool bind_iptux_port()
        +void SendNotifyToAll(CoreThread pcthrd)
        +void sendFeatureData(PPalInfo pal)
        +void SendMyIcon(PPalInfo pal, istream iss)
        +void SendDetectPacket(const string ipv4)
        +void SendDetectPacket(in_addr ipv4)
        +bool SendAskSharedWithPassword(const PalKey palKey, const string password)
        +void SendUnitMessage(const PalKey palKey, uint32_t opttype, const string message)
        +void SendGroupMessage(const PalKey palKey, const string message)
        -std::shared_ptr<ProgramData> programData
        -std::shared_ptr<IptuxConfig> config
        -mutable std::mutex mutex
        -std::unique_ptr<Impl> pImpl
    }

    class CoreThread_Impl {
        +UdpThread* udpThread
        +CoreThreadErr lastErr
        +GSocket* udpSocket
        +std::unique_ptr<UdpDataService> udp_data_service
        +bool debugDontBroadcast
        +std::list<PPalInfo> pallist
        +~Impl()
    }

    class UdpThreadOps {
        +bool (*on_new_msg)(UdpThread udpThread, GSocketAddress peer, const char msg, size_t size)
        +void (*on_init_failed)(UdpThread udpThread)
        +void (*on_loop_stopped)(UdpThread udpThread)
    }

    class UdpThread {
        +GSocket* socket
        +void* data
        +const UdpThreadOps* ops
        +atomic~UdpThreadState~ state
        +GMainLoop* loop
        +GThread* thread
        +GMainContext* ctx
        +guint id
        +UdpThread()
        +~UdpThread()
    }

    class UdpDataService {
        +UdpDataService(CoreThread coreThread)
        +std::unique_ptr<UdpData> process(GSocketAddress peer, const char buf, size_t size)
        +std::unique_ptr<UdpData> process(in_addr ipv4, int port, const char buf, size_t size)
    }

    class GSocket {
        +gboolean g_socket_set_broadcast(gboolean broadcast)
        +gboolean g_socket_bind(GSocketAddress address, gboolean allow_reuse, GError error)
        +gssize g_socket_receive_from(GSocketAddress peer, void buffer, gsize size, GCancellable cancellable, GError error)
        +int g_socket_get_fd()
    }

    class GSocketAddress
    class GInetSocketAddress
    class GInetAddress

    CoreThread --> CoreThread_Impl : has
    CoreThread_Impl --> UdpThread : owns
    CoreThread_Impl --> UdpDataService : uses
    CoreThread_Impl --> GSocket : owns udpSocket

    UdpThread --> UdpThreadOps : uses
    UdpThread --> GSocket : uses socket

    UdpThreadOps --> UdpDataService : on_new_msg calls process

    UdpDataService --> CoreThread : holds reference
    UdpDataService --> GSocketAddress : process peer
    UdpDataService --> GInetSocketAddress : casts peer
    UdpDataService --> GInetAddress : extracts IP

    GInetSocketAddress --> GSocketAddress : inherits
Loading

File-Level Changes

Change Details Files
Switch UDP thread from raw file descriptor + GIOChannel to GLib GSocket and GSource APIs.
  • Replace UdpThread fd field with a GSocket pointer and remove GIOChannel usage and cleanup.
  • Update udpThreadCb to receive datagrams via g_socket_receive_from, passing GSocketAddress to callbacks and handling GError-based failures.
  • Use g_socket_create_source to watch the UDP socket in udpThreadAttachUdp and adjust error logging and lifecycle.
src/iptux-core/CoreThread.cpp
Create and bind the UDP socket via GSocket in CoreThread and adapt all UDP usages to the new abstraction.
  • Add GSocket* udpSocket to CoreThread::Impl and construct it with g_socket_new, enabling broadcast and GLib-style error handling.
  • Bind UDP using g_socket_bind with a GInetSocketAddress constructed from the configured bind IP and port, with updated error paths and cleanup.
  • Make CoreThread::getUdpSock() return the underlying file descriptor from the GSocket and route all Command send helpers through getUdpSock() instead of a raw udpSock member.
  • Stop calling shutdown on the UDP socket directly and adjust ClearSublayer/teardown responsibilities.
src/iptux-core/CoreThread.cpp
src/api/iptux-core/CoreThread.h
Extend UdpDataService to accept GLib socket addresses while preserving existing IPv4-based processing.
  • Add an overload of UdpDataService::process that takes GSocketAddress*, extracts IP and port via GInetSocketAddress helpers, and forwards to the existing IPv4-based process implementation.
  • Include gio/gio.h in the implementation and header to support the new types.
  • Wire CoreThread UDP receive path to call the new overload with the peer address.
src/iptux-core/internal/UdpDataService.cpp
src/iptux-core/internal/UdpDataService.h
src/iptux-core/CoreThread.cpp
Adjust test helper handshake timing and logging to be more observable and slightly less tight-looped.
  • Introduce a local variable to cache GetOnlineCount() in loops, improving logging consistency.
  • Increase sleep between detection attempts from 10ms to 1000ms in initAndConnnectThreadsFromConfig to avoid overly aggressive polling.
src/iptux-core/TestHelper.cpp
Add gio-2.0 as a core, test helper, and test binary dependency in the Meson build configuration.
  • Declare gio_dep in iptux-core meson.build and add it to the core library dependency list for both static and shared builds.
  • Propagate gio_dep to the generated pkg-config file requirements and to the core test helper and core test executables.
src/iptux-core/meson.build

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@lidaobing lidaobing linked an issue Jan 19, 2026 that may be closed by this pull request

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • The new GIO-based code leaks several GObjects/strings: any and bind_addr in bind_iptux_port and the ip string from g_inet_address_to_string in UdpDataService::process(GSocketAddress*,...) are never freed/unref’d; consider adding corresponding g_object_unref/g_free calls on both success and error paths.
  • In udpThreadCb, g_socket_receive_from is called with &error but the code unconditionally dereferences error when n < 0; it would be safer to check for error != nullptr (and handle the NULL case) before logging to avoid a potential null dereference.
  • Now that the UDP socket is managed as a GSocket (udpSocket in Impl), please double-check the lifetime and shutdown path (e.g., in ClearSublayer and Impl’s destructor) so that the UDP socket is explicitly shut down/unref’d at the appropriate time rather than relying only on destruction.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new GIO-based code leaks several GObjects/strings: `any` and `bind_addr` in `bind_iptux_port` and the `ip` string from `g_inet_address_to_string` in `UdpDataService::process(GSocketAddress*,...)` are never freed/unref’d; consider adding corresponding `g_object_unref`/`g_free` calls on both success and error paths.
- In `udpThreadCb`, `g_socket_receive_from` is called with `&error` but the code unconditionally dereferences `error` when `n < 0`; it would be safer to check for `error != nullptr` (and handle the `NULL` case) before logging to avoid a potential null dereference.
- Now that the UDP socket is managed as a `GSocket` (`udpSocket` in `Impl`), please double-check the lifetime and shutdown path (e.g., in `ClearSublayer` and `Impl`’s destructor) so that the UDP socket is explicitly shut down/unref’d at the appropriate time rather than relying only on destruction.

## Individual Comments

### Comment 1
<location> `src/iptux-core/internal/UdpDataService.cpp:26-29` </location>
<code_context>
+  GInetSocketAddress* isa = G_INET_SOCKET_ADDRESS(peer);
+  guint16 port = g_inet_socket_address_get_port(isa);
+  GInetAddress* ia = g_inet_socket_address_get_address(isa);
+  char* ip = g_inet_address_to_string(ia);
+
+  // TODO: too many conversions, optimize it
+  in_addr ipv4 = inAddrFromString(ip);
+  return process(ipv4, port, buf, size, true);
+}
</code_context>

<issue_to_address>
**issue (bug_risk):** The string from g_inet_address_to_string is leaked and should be freed.

`g_inet_address_to_string()` returns a heap-allocated string that must be freed with `g_free()`. Here, `ip` is leaked on every call. Free it after `inAddrFromString(ip)`:

```c++
char* ip = g_inet_address_to_string(ia);
in_addr ipv4 = inAddrFromString(ip);
g_free(ip);
return process(ipv4, port, buf, size, true);
```
</issue_to_address>

### Comment 2
<location> `src/iptux-core/CoreThread.cpp:468-471` </location>
<code_context>

-  if (::bind(udpSock, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
-    int ec = errno;
+  GInetAddress* any = g_inet_address_new_from_string(bind_ip.c_str());
+  GSocketAddress* bind_addr = g_inet_socket_address_new(any, port);
+
+  if (!g_socket_bind(pImpl->udpSocket, bind_addr, TRUE, &error)) {
     close(tcpSock);
-    close(udpSock);
</code_context>

<issue_to_address>
**issue (bug_risk):** GInetAddress and GSocketAddress instances are leaked and error paths don’t clean up the UDP socket.

The GInetAddress from g_inet_address_new_from_string() and the GSocketAddress from g_inet_socket_address_new() are never unreffed, and on the UDP bind failure path the GSocket is also not unreffed, leaking both memory and the socket handle.

You can structure this so all objects are released on both success and error paths, for example:

```c++
GInetAddress* any = g_inet_address_new_from_string(bind_ip.c_str());
if (!any) {
  LOG_ERROR("Invalid bind IP: %s", bind_ip.c_str());
  close(tcpSock);
  g_object_unref(pImpl->udpSocket);
  pImpl->lastErr = CORE_THREAD_ERR_UDP_BIND_FAILED;
  return false;
}

GSocketAddress* bind_addr = g_inet_socket_address_new(any, port);
g_object_unref(any);

if (!g_socket_bind(pImpl->udpSocket, bind_addr, TRUE, &error)) {
  g_object_unref(bind_addr);
  close(tcpSock);
  g_object_unref(pImpl->udpSocket);
  auto errmsg = stringFormat(
      _("Fatal Error!! Failed to bind the UDP port(%s:%d)!\n%s"),
      bind_ip.c_str(), port, error->message);
  g_error_free(error);
  LOG_ERROR("%s", errmsg.c_str());
  pImpl->lastErr = CORE_THREAD_ERR_UDP_BIND_FAILED;
  return false;
}

g_object_unref(bind_addr);
```

Any equivalent pattern that reliably unrefs `any`, `bind_addr`, and `udpSocket` on all paths works.
</issue_to_address>

### Comment 3
<location> `src/iptux-core/CoreThread.cpp:541-544` </location>
<code_context>
-  shutdown(udpSock, SHUT_RDWR);
 }

 int CoreThread::getUdpSock() const {
-  return udpSock;
+  // TODO: should no longer use it
+  return g_socket_get_fd(pImpl->udpSocket);
 }

</code_context>

<issue_to_address>
**suggestion (bug_risk):** getUdpSock() now relies on a GSocket but doesn’t handle a null or closed socket case.

Because `getUdpSock()` now calls `g_socket_get_fd(pImpl->udpSocket)`, it assumes `udpSocket` is always valid. If this is called during teardown or before `bind_iptux_port()` succeeds, `udpSocket` could be null or closed and `g_socket_get_fd` may misbehave. Consider guarding and returning an invalid fd instead:

```c++
if (!pImpl->udpSocket)
  return -1;
return g_socket_get_fd(pImpl->udpSocket);
```

Callers can then check for `-1` and handle the failure case explicitly.

```suggestion
int CoreThread::getUdpSock() const {
  // TODO: should no longer use it
  if (!pImpl->udpSocket) {
    return -1;
  }
  return g_socket_get_fd(pImpl->udpSocket);
}
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/iptux-core/internal/UdpDataService.cpp
@github-actions

github-actions Bot commented Jan 19, 2026

Copy link
Copy Markdown

Test Results

69 tests  +3   69 ✅ +3   3s ⏱️ -1s
32 suites ±0    0 💤 ±0 
 1 files   ±0    0 ❌ ±0 

Results for commit c837618. ± Comparison against base commit d888023.

♻️ This comment has been updated with latest results.

@codecov

codecov Bot commented Jan 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 52.71318% with 244 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.00%. Comparing base (d888023) to head (c837618).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/iptux-core/CoreThread.cpp 65.31% 103 Missing ⚠️
src/iptux-core/internal/Command.cpp 29.03% 66 Missing ⚠️
src/iptux-core/internal/RecvFileData.cpp 0.00% 46 Missing ⚠️
src/iptux/Application.cpp 33.33% 10 Missing ⚠️
src/iptux-core/internal/TcpData.cpp 75.67% 9 Missing ⚠️
src/iptux-utils/utils.cpp 0.00% 7 Missing ⚠️
src/iptux/MainWindow.cpp 25.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #697      +/-   ##
==========================================
- Coverage   52.37%   52.00%   -0.37%     
==========================================
  Files          64       64              
  Lines        8281     8599     +318     
==========================================
+ Hits         4337     4472     +135     
- Misses       3944     4127     +183     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

lidaobing and others added 18 commits January 18, 2026 23:49
- Replace listen/poll/accept in RecvTcpData with GIO equivalents
  (g_socket_listen, g_socket_condition_timed_wait, g_socket_accept)
- Replace shutdown calls in ClearSublayer with g_socket_close
- Update TcpData to accept GSocket* instead of raw fd
- Replace getpeername calls with g_socket_get_remote_address
- TcpData now owns and manages the GSocket lifecycle

This improves portability for Windows by using GIO's cross-platform
socket abstraction instead of Unix-specific APIs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add TcpThread struct similar to UdpThread with GMainLoop/GMainContext
- Use g_socket_create_source for event-driven TCP accept
- Use g_main_context_invoke for clean thread shutdown instead of
  polling a flag in a loop
- Remove RecvTcpData polling function, replaced by tcpThreadCb callback
- Add CORE_THREAD_ERR_TCP_THREAD_START_FAILED error code
- Simplify ClearSublayer (socket cleanup handled by destructors)

This makes TCP thread handling consistent with UDP thread and removes
the 10ms polling loop for better efficiency and cleaner shutdown.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Change TCP handler from detached std::thread to managed GThread
- Track handler threads in std::list for O(1) removal
- Clean up finished threads via callback on TCP thread's main context
- Join all handler threads on CoreThread stop
- Add exception handling in TCP handler thread
- Add unit tests for TCP handler thread lifecycle

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@lidaobing lidaobing merged commit 89e5243 into main Jan 25, 2026
15 of 19 checks passed
@lidaobing lidaobing deleted the bf_696_socket branch January 25, 2026 07:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

replace socket with g_socket_new

1 participant