A Session with listen = true (the default) now listens on 127.0.0.1 with the ALPN crafter-debug/1 and writes its discovery record (port, token, protocol, source id, client path), which goes first when it ends. One request per stream the client opens: the request and a FIN in, zero or more stream lines and then the reply and a FIN out, one JSON object per line. It holds no thread while nothing arrives. Crafter.Network delivers a new stream on the pool, its bytes on msquic's worker through OnRecieve, and replies leave through SendAsync, which never waits. The handler, which takes the session's mutex, runs on a pool task rather than inside the stream's callback, so no msquic worker ever waits on the session, and the session's threads only send, which Network allows under any lock. Stream wrappers are let go outside every lock their callbacks take. The rules head the implementation. Limits: a request over 1 MiB is answered with an error, not buffered, and a log follower more than 8 MiB behind is reset. quit --force exits 200 ms after its reply was taken, from a thread of its own, since "sent" means msquic has the bytes, not that they left. A client killed without closing is let go at the 10 s idle timeout. The Session refuses a pool without a worker: Network hands it new streams there, and with Start(1) nothing would run them. The partition exports nothing, so an app's view of the module is unchanged and it does not see Crafter.Network's types. Crafter.Network comes from forgejo, unpinned, with the args unchanged; --local takes ../Crafter.Network, and $CRAFTER_DEBUG_NETWORK any project.cpp, for the branch the callback API is on until it lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
49 lines
2 KiB
C++
49 lines
2 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// The app's end of the wire: QUIC on 127.0.0.1 with the ALPN
|
|
// crafter-debug/1, one request per stream the client opens.
|
|
//
|
|
// client {"token", "method", "params", "timeoutMs"}, then FIN
|
|
// server zero or more stream lines ({"entry": ...}, `log --follow`),
|
|
// then the reply ({"result": ...} or {"error": ...}), then FIN
|
|
//
|
|
// Every object is one line of JSON. Nothing here is exported: the session
|
|
// is what an app sees, and this partition keeps Crafter.Network's types out
|
|
// of the interface an app imports.
|
|
export module Crafter.Debug:Transport;
|
|
import std;
|
|
import Crafter.Debug.Core;
|
|
import :Session;
|
|
|
|
namespace Crafter::Debug::Detail {
|
|
// Listens, and hands each complete request to the handler on a pool
|
|
// task, with an Exchange that answers on the request's own stream.
|
|
//
|
|
// Holds no thread while nothing arrives: Crafter.Network delivers bytes
|
|
// on msquic's workers and new streams on the host's pool, and replies go
|
|
// out without waiting. The one thread it has is ListenerQUIC's accept
|
|
// loop, which only registers each new connection's callbacks.
|
|
class Server {
|
|
public:
|
|
using Handler = std::function<void(const Value& request, std::shared_ptr<Exchange> exchange)>;
|
|
|
|
// On 127.0.0.1:port, 0 for any free port. Throws when it can't
|
|
// listen. Needs Crafter.Thread's pool running, with a worker.
|
|
Server(std::uint16_t port, Handler handler);
|
|
// Stops listening and closes every connection. Replies already
|
|
// given still reach their clients: each connection waits, bounded,
|
|
// for its streams to finish before it closes.
|
|
~Server();
|
|
Server(const Server&) = delete;
|
|
Server& operator=(const Server&) = delete;
|
|
|
|
std::uint16_t Port() const;
|
|
|
|
struct State;
|
|
|
|
private:
|
|
std::shared_ptr<State> state;
|
|
std::uint16_t port = 0;
|
|
};
|
|
}
|