devkitPro / wut

Let's try to make a Wii U Toolchain / SDK for creating rpx/rpl.
zlib License
244 stars 52 forks source link

BSD sockes don't respect non-blocking settings #171

Closed csunday95 closed 2 years ago

csunday95 commented 3 years ago

It appears that the current socket implementation doesn't respect non-blocking socket operations. The below program should immediately exit recv with a value of -1 for bytesReceived, but it seems to block.

#include <coreinit/thread.h>
#include <coreinit/time.h>
#include <coreinit/systeminfo.h>
#include <nn/ac.h>

#include <whb/proc.h>
#include <whb/log.h>
#include <whb/log_console.h>

#include <thread>
#include <chrono>

#include <netinet/in.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/types.h> 
#include <sys/socket.h>
#include <poll.h>
#include <unistd.h>
#include <errno.h>
#include <cstring>

static bool accepting = false;
static int port = 1234;

int test_thread()
{
    int acceptSocket = -1, clientSocket = -1;
    int haveEvent;
    socklen_t clientLen;
    sockaddr_in serverAddr{};
    sockaddr_in clientAddr{};
    char clientIP[64];
    pollfd pfds[1];
    int bytesReceived = 0;
    char data[256];

    clientLen = sizeof(clientAddr);
    acceptSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (acceptSocket == -1)
    {
        WHBLogPrintf("Unable to create accept socket: %d", errno);
        WHBLogConsoleDraw();
        return 1;
    }
    // set socket to non-blocking
    int flags = fcntl(acceptSocket, F_GETFL, 0);
    if (flags == -1)
    {
        WHBLogPrintf("unable to get flags from fnctl: %d", errno);
        WHBLogConsoleDraw();
        return 1;
    }
    flags |= O_NONBLOCK;
    if (fcntl(acceptSocket, F_SETFL, flags) == -1)
    {
        WHBLogPrintf("unable to set flags with fnctl: %d", errno);
        WHBLogConsoleDraw();
        return 1;
    }

    bzero(&serverAddr, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = INADDR_ANY;
    serverAddr.sin_port = htons(port);
    if(bind(acceptSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0)
    {
        WHBLogPrintf("Unable to bind accept socket: %d", errno);
        WHBLogConsoleDraw();
        return 1;
    }
    listen(acceptSocket, 5);

    pfds[0].fd = acceptSocket;
    pfds[0].events = POLLIN;

    while(accepting)
    {
        haveEvent = poll(pfds, 1, 1000);
        if (haveEvent == 0) {
            WHBLogPrintf("accept poll timed out");
            WHBLogConsoleDraw();
            continue;
        }
        if (haveEvent < 0)
        {
            WHBLogPrintf("Poll encountered error: %d", errno);
            WHBLogConsoleDraw();
            std::this_thread::sleep_for(std::chrono::milliseconds(500));
            continue;
        }
        if(pfds[0].revents & POLLIN)
        {
            // client socket should inherit non-blocking status from accepting socket
            clientSocket = accept(acceptSocket, (struct sockaddr*)&clientAddr, &clientLen);
            if (clientSocket != -1)
            {
                inet_ntop(AF_INET, &clientAddr.sin_addr, clientIP, sizeof(clientIP));
                WHBLogPrintf("client FD %d connected from addr %s", clientSocket, clientIP);
                WHBLogConsoleDraw();
                break;
            }
        }
    }

    while(accepting)
    {
        // add don't wait flag, even though socket should be non-blocking already
        bytesReceived = recv(clientSocket, data, sizeof(data) - 1, MSG_DONTWAIT);
        if (bytesReceived == 0)
        {
            WHBLogPrintf("client FD %d disconnected", clientSocket);
            WHBLogConsoleDraw();
            break;
        }
        else if (bytesReceived < 0)
        {
            switch (errno)
            {
            case EWOULDBLOCK:
                std::this_thread::sleep_for(std::chrono::milliseconds(500));
                continue;
            default:
                WHBLogPrintf("Error on recv: %d", errno);
                WHBLogConsoleDraw();
                std::this_thread::sleep_for(std::chrono::milliseconds(500));
                return 1;
            }
        }
        else 
        {
            data[bytesReceived] = '\0';
            WHBLogPrintf("Got data: %s", data);
            WHBLogConsoleDraw();
        }
    }

    WHBLogPrintf("test thread exiting");
    WHBLogConsoleDraw();

    return 0;
}

int
main(int argc, char **argv)
{
    nn::ac::ConfigIdNum configId;

    nn::ac::Initialize();
    nn::ac::GetStartupId(&configId);
    nn::ac::Connect(configId);

    WHBProcInit();
    WHBLogConsoleInit();

    accepting = true;

    std::thread t(test_thread);

    while(WHBProcIsRunning()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    accepting = false;

    t.join();

    WHBLogConsoleFree();
    WHBProcShutdown();

    nn::ac::Finalize();
    return 0;
}

An example python script to test

import socket
import time

WIIU_IP = '192.168.1.163'

sock = socket.create_connection((WIIU_IP, 1234), timeout=1.0)
time.sleep(5.0)
print(sock.send(b"Here's some example data"))
time.sleep(5.0)

Expected Output:

accept poll timed out
...
accept poll timed out
client FD 4 connected from addr X.X.X.X <-- point where test python script begins running
waiting to recv: 11
waiting to recv: 11
...
waiting to recv: 11
waiting to recv: 11
Got data: Here's some example data <-- point where test script calls send()
waiting to recv: 11
waiting to recv: 11
...
waiting to recv: 11
waiting to recv: 11
client FD 4 disconnected <-- point where test script exits

Actual Output

accept poll timed out
...
accept poll timed out
client FD 4 connected from addr X.X.X.X 
Got data: Here's some example data 
client FD 4 disconnected 

This demonstrates that the recv() call is blocking despite the socket having O_NONBLOCK set and MSG_DONTWAIT being passed. Its possible that this simply isn't implemented yet, but I was unable to find that documented anywhere. It's also possible I'm just missing some step of initialization to enable non-blocking operations.

GaryOderNichts commented 3 years ago

I'm unable to reproduce this. When testing your example, calling recv does not block and will set errno to EWOULDBLOCK, like expected. Your example seems to be missing the "waiting to recv" log message from your expected output. After adding that at line 126, the output matches the expected output.