initial commit

This commit is contained in:
2026-08-27 15:38:55 +02:00
commit 0426b6eeec
23 changed files with 1635 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# clipsync Desktop Daemon (Odin)
This component runs in background and syncs clipboard with paired Android devices.
Initial milestone:
1. Start TCP server on default port `45821`.
2. Broadcast LAN discovery messages.
3. Handle v1 handshake and pairing state.
4. Send/receive text clipboard updates.
5. Persist paired peers and reconnect automatically.
+881
View File
@@ -0,0 +1,881 @@
package main
import "core:fmt"
import cjson "core:encoding/json"
import cnet "core:net"
import "core:os"
import "core:strings"
import "core:thread"
import "core:time"
DEFAULT_PORT :: 45821
MAX_FRAME_BYTES :: 1024 * 1024 // 1 MiB
HELLO_JSON :: string(`{"version":"1","type":"HELLO","device_id":"desktop-dev","device_name":"clipsync-desktop","platform":"linux","tcp_port":45821,"timestamp_ms":0,"nonce":"todo-nonce"}`)
DISCOVERY_PERIOD :: 2 * time.Second
PAIRED_PEERS_FILE :: "desktop-daemon/paired-peers.json"
SOCKET_RECV_TIMEOUT :: 1 * time.Second
CLIP_MIN_INTERVAL :: 250 * time.Millisecond
KEEPALIVE_PING_INTERVAL :: 15 * time.Second
KEEPALIVE_IDLE_TIMEOUT :: 45 * time.Second
server_clip_sequence: i64
server_ping_sequence: i64
Peer_Runtime_State :: struct {
device_id: string,
last_clip_update_at: time.Time,
has_last_clip_update_at: bool,
last_content_hash: string,
has_last_content_hash: bool,
}
Paired_Peer :: struct {
peer_device_id: string `json:"peer_device_id"`,
peer_device_name:string `json:"peer_device_name"`,
peer_platform: string `json:"peer_platform"`,
peer_public_key: string `json:"peer_public_key"`,
paired_at: i64 `json:"paired_at"`,
}
Paired_Peers_Doc :: struct {
peers: []Paired_Peer `json:"peers"`,
}
Pair_Request :: struct {
version: string `json:"version"`,
message_type: string `json:"type"`,
request_id: string `json:"request_id"`,
device_id: string `json:"device_id"`,
device_name: string `json:"device_name"`,
platform: string `json:"platform"`,
public_key: string `json:"public_key"`,
pairing_method: string `json:"pairing_method"`,
timestamp_ms: i64 `json:"timestamp_ms"`,
}
Pair_Accept :: struct {
version: string `json:"version"`,
message_type: string `json:"type"`,
request_id: string `json:"request_id"`,
device_id: string `json:"device_id"`,
timestamp_ms: i64 `json:"timestamp_ms"`,
status: string `json:"status"`,
}
Message_Header :: struct {
version: string `json:"version"`,
message_type: string `json:"type"`,
request_id: string `json:"request_id"`,
device_id: string `json:"device_id"`,
}
Clip_Update :: struct {
version: string `json:"version"`,
message_type: string `json:"type"`,
request_id: string `json:"request_id"`,
device_id: string `json:"device_id"`,
timestamp_ms: i64 `json:"timestamp_ms"`,
origin_device_id: string `json:"origin_device_id"`,
content_type: string `json:"content_type"`,
encoding: string `json:"encoding"`,
content_hash: string `json:"content_hash"`,
content: string `json:"content"`,
sequence: i64 `json:"sequence"`,
}
Ping_Message :: struct {
version: string `json:"version"`,
message_type: string `json:"type"`,
request_id: string `json:"request_id"`,
device_id: string `json:"device_id"`,
timestamp_ms: i64 `json:"timestamp_ms"`,
}
Pong_Message :: struct {
version: string `json:"version"`,
message_type: string `json:"type"`,
request_id: string `json:"request_id"`,
device_id: string `json:"device_id"`,
timestamp_ms: i64 `json:"timestamp_ms"`,
}
paired_peers: [dynamic]Paired_Peer
peer_runtime_states: [dynamic]Peer_Runtime_State
find_peer_index :: proc(device_id: string) -> int {
for peer, idx in paired_peers {
if peer.peer_device_id == device_id {
return idx
}
}
return -1
}
is_paired_peer :: proc(device_id: string) -> bool {
return find_peer_index(device_id) >= 0
}
find_peer_runtime_state :: proc(device_id: string) -> ^Peer_Runtime_State {
for i in 0..<len(peer_runtime_states) {
if peer_runtime_states[i].device_id == device_id {
return &peer_runtime_states[i]
}
}
append(&peer_runtime_states, Peer_Runtime_State{device_id = fmt.aprintf("%s", device_id)})
return &peer_runtime_states[len(peer_runtime_states)-1]
}
save_paired_peers :: proc() -> bool {
doc := Paired_Peers_Doc{peers = paired_peers[:]}
data, err := cjson.marshal(doc)
if err != nil {
fmt.printf("failed to encode paired peers: %v\n", err)
return false
}
defer delete(data)
write_err := os.write_entire_file(PAIRED_PEERS_FILE, data)
if write_err != nil {
fmt.printf("failed to write paired peers file %q: %v\n", PAIRED_PEERS_FILE, write_err)
return false
}
return true
}
load_paired_peers :: proc() {
data, err := os.read_entire_file(PAIRED_PEERS_FILE, context.allocator)
if err != nil {
fmt.printf("paired peers file not loaded (%q): %v\n", PAIRED_PEERS_FILE, err)
return
}
defer delete(data)
doc: Paired_Peers_Doc
if uerr := cjson.unmarshal(data, &doc); uerr != nil {
fmt.printf("paired peers file parse failed: %v\n", uerr)
return
}
append_elems(&paired_peers, ..doc.peers)
fmt.printf("loaded %d paired peer(s)\n", len(paired_peers))
}
upsert_paired_peer :: proc(peer: Paired_Peer) {
idx := find_peer_index(peer.peer_device_id)
if idx >= 0 {
paired_peers[idx] = peer
} else {
append(&paired_peers, peer)
}
_ = save_paired_peers()
}
platform_name :: proc() -> string {
when ODIN_OS == .Windows {
return "windows"
} else {
return "linux"
}
}
discovery_announce_payload :: proc(port: int) -> string {
return fmt.tprintf(
`{"version":"1","type":"DISCOVERY_ANNOUNCE","device_id":"desktop-dev","device_name":"clipsync-desktop","platform":"%s","tcp_port":%d,"capabilities":["clipboard:text"],"timestamp_ms":0}`,
platform_name(),
port,
)
}
run_discovery_broadcaster :: proc() {
socket, err := cnet.make_unbound_udp_socket(.IP4)
if err != nil {
fmt.printf("discovery broadcaster socket create failed: %v\n", err)
return
}
defer cnet.close(socket)
if opt_err := cnet.set_option(socket, .Broadcast, true); opt_err != nil {
fmt.printf("discovery broadcaster failed to enable broadcast: %v\n", opt_err)
return
}
broadcast := cnet.Endpoint{
address = cnet.IP4_Address{255, 255, 255, 255},
port = DEFAULT_PORT,
}
fmt.printf("discovery broadcaster enabled on udp://255.255.255.255:%d\n", DEFAULT_PORT)
for {
payload := discovery_announce_payload(DEFAULT_PORT)
written, send_err := cnet.send_udp(socket, transmute([]byte)payload, broadcast)
if send_err != nil {
fmt.printf("discovery announce send failed: %v\n", send_err)
} else {
fmt.printf("discovery announce sent (%d bytes)\n", written)
}
time.sleep(DISCOVERY_PERIOD)
}
}
run_discovery_listener :: proc() {
socket, err := cnet.make_bound_udp_socket(cnet.IP4_Any, DEFAULT_PORT)
if err != nil {
fmt.printf("discovery listener bind failed on udp://0.0.0.0:%d: %v\n", DEFAULT_PORT, err)
return
}
defer cnet.close(socket)
fmt.printf("discovery listener active on udp://0.0.0.0:%d\n", DEFAULT_PORT)
buf: [2048]byte
for {
read, source, recv_err := cnet.recv_udp(socket, buf[:])
if recv_err != nil {
fmt.printf("discovery recv failed: %v\n", recv_err)
continue
}
if read <= 0 {
continue
}
msg := string(buf[:read])
if strings.contains(msg, `"type":"DISCOVERY_ANNOUNCE"`) {
fmt.printf("discovery packet from %v (%d bytes)\n", source, read)
}
}
}
Framing_Error :: enum {
None,
Frame_Too_Large,
Connection_Closed,
Socket_Recv_Timeout,
Socket_Recv_Failed,
Socket_Send_Failed,
}
Inbound_Hello :: struct {
version: string `json:"version"`,
message_type: string `json:"type"`,
request_id: string `json:"request_id"`,
device_id: string `json:"device_id"`,
device_name: string `json:"device_name"`,
platform: string `json:"platform"`,
tcp_port: int `json:"tcp_port"`,
timestamp_ms: i64 `json:"timestamp_ms"`,
nonce: string `json:"nonce"`,
}
Ack_Message :: struct {
version: string `json:"version"`,
message_type: string `json:"type"`,
request_id: string `json:"request_id"`,
device_id: string `json:"device_id"`,
timestamp_ms: i64 `json:"timestamp_ms"`,
status: string `json:"status"`,
}
Error_Message :: struct {
version: string `json:"version"`,
message_type: string `json:"type"`,
request_id: string `json:"request_id"`,
device_id: string `json:"device_id"`,
timestamp_ms: i64 `json:"timestamp_ms"`,
error_code: string `json:"error_code"`,
error_message: string `json:"error_message"`,
}
encode_json_message :: proc(v: any, allocator := context.allocator) -> ([]byte, bool) {
data, err := cjson.marshal(v, allocator=allocator)
if err != nil {
fmt.printf("json encode failed: %v\n", err)
return nil, false
}
return data, true
}
send_protocol_error :: proc(client: cnet.TCP_Socket, request_id, error_code, error_message: string) {
msg := Error_Message{
version = "1",
message_type = "ERROR",
request_id = request_id,
device_id = "desktop-dev",
timestamp_ms = 0,
error_code = error_code,
error_message = error_message,
}
data, ok := encode_json_message(msg)
if !ok {
return
}
defer delete(data)
if ferr := send_frame(client, data); ferr != .None {
fmt.printf("failed to send ERROR frame: %v\n", ferr)
}
}
send_protocol_ack :: proc(client: cnet.TCP_Socket, request_id: string) -> bool {
msg := Ack_Message{
version = "1",
message_type = "ACK",
request_id = request_id,
device_id = "desktop-dev",
timestamp_ms = 0,
status = "ok",
}
data, ok := encode_json_message(msg)
if !ok {
return false
}
defer delete(data)
if ferr := send_frame(client, data); ferr != .None {
fmt.printf("failed to send ACK frame: %v\n", ferr)
return false
}
return true
}
send_ping :: proc(client: cnet.TCP_Socket) -> bool {
server_ping_sequence += 1
msg := Ping_Message{
version = "1",
message_type = "PING",
request_id = fmt.tprintf("srv-ping-%d", server_ping_sequence),
device_id = "desktop-dev",
timestamp_ms = 0,
}
data, ok := encode_json_message(msg)
if !ok {
return false
}
defer delete(data)
if ferr := send_frame(client, data); ferr != .None {
fmt.printf("failed to send PING frame: %v\n", ferr)
return false
}
return true
}
send_pong :: proc(client: cnet.TCP_Socket, request_id: string) -> bool {
msg := Pong_Message{
version = "1",
message_type = "PONG",
request_id = request_id,
device_id = "desktop-dev",
timestamp_ms = 0,
}
data, ok := encode_json_message(msg)
if !ok {
return false
}
defer delete(data)
if ferr := send_frame(client, data); ferr != .None {
fmt.printf("failed to send PONG frame: %v\n", ferr)
return false
}
return true
}
send_pair_accept :: proc(client: cnet.TCP_Socket, request_id: string) -> bool {
msg := Pair_Accept{
version = "1",
message_type = "PAIR_ACCEPT",
request_id = request_id,
device_id = "desktop-dev",
timestamp_ms = 0,
status = "paired",
}
data, ok := encode_json_message(msg)
if !ok {
return false
}
defer delete(data)
if ferr := send_frame(client, data); ferr != .None {
fmt.printf("failed to send PAIR_ACCEPT frame: %v\n", ferr)
return false
}
return true
}
run_capture_command :: proc(command: []string) -> (string, bool) {
desc := os.Process_Desc{command = command}
state, stdout, stderr, exec_err := os.process_exec(desc, context.allocator)
defer delete(stdout)
defer delete(stderr)
if exec_err != nil || !state.exited || state.exit_code != 0 {
return "", false
}
trimmed := strings.trim_right(string(stdout), "\r\n")
return fmt.aprintf("%s", trimmed), true
}
run_stdin_command :: proc(command: []string, stdin_text: string) -> bool {
stdin_r, stdin_w, pipe_err := os.pipe()
if pipe_err != nil {
return false
}
defer os.close(stdin_r)
desc := os.Process_Desc{
command = command,
stdin = stdin_r,
}
process, start_err := os.process_start(desc)
if start_err != nil {
return false
}
_, write_err := os.write(stdin_w, transmute([]byte)stdin_text)
if write_err != nil {
_, _ = os.process_wait(process)
return false
}
_ = os.close(stdin_w)
state, wait_err := os.process_wait(process)
if wait_err != nil || !state.exited || state.exit_code != 0 {
return false
}
return true
}
read_local_clipboard_text :: proc() -> (string, bool) {
when ODIN_OS == .Windows {
return run_capture_command([]string{"powershell", "-NoProfile", "-Command", "Get-Clipboard"})
} else {
if text, ok := run_capture_command([]string{"wl-paste", "--no-newline"}); ok {
return text, true
}
if text, ok := run_capture_command([]string{"xclip", "-selection", "clipboard", "-o"}); ok {
return text, true
}
if text, ok := run_capture_command([]string{"xsel", "--clipboard", "--output"}); ok {
return text, true
}
return "", false
}
}
set_local_clipboard_text :: proc(text: string) -> bool {
when ODIN_OS == .Windows {
return run_stdin_command([]string{"powershell", "-NoProfile", "-Command", "$input | Set-Clipboard"}, text)
} else {
if run_stdin_command([]string{"wl-copy"}, text) {
return true
}
if run_stdin_command([]string{"xclip", "-selection", "clipboard"}, text) {
return true
}
if run_stdin_command([]string{"xsel", "--clipboard", "--input"}, text) {
return true
}
return false
}
}
send_clip_update :: proc(client: cnet.TCP_Socket, request_device_id: string, clipboard_text: string) -> bool {
server_clip_sequence += 1
msg := Clip_Update{
version = "1",
message_type = "CLIP_UPDATE",
request_id = fmt.tprintf("srv-clip-%d", server_clip_sequence),
device_id = "desktop-dev",
timestamp_ms = 0,
origin_device_id = "desktop-dev",
content_type = "text/plain",
encoding = "utf-8",
content_hash = fmt.tprintf("len:%d", len(clipboard_text)),
content = clipboard_text,
sequence = server_clip_sequence,
}
data, ok := encode_json_message(msg)
if !ok {
return false
}
defer delete(data)
if ferr := send_frame(client, data); ferr != .None {
fmt.printf("failed to send outgoing CLIP_UPDATE to %q: %v\n", request_device_id, ferr)
return false
}
fmt.printf("sent outgoing CLIP_UPDATE to %q (%d bytes text)\n", request_device_id, len(clipboard_text))
return true
}
maybe_emit_local_clipboard_update :: proc(client: cnet.TCP_Socket, request_device_id: string, last_sent_clipboard: ^string) {
if !is_paired_peer(request_device_id) {
return
}
clipboard_text, ok := read_local_clipboard_text()
if !ok {
return
}
defer delete(clipboard_text)
if clipboard_text == last_sent_clipboard^ {
return
}
if send_clip_update(client, request_device_id, clipboard_text) {
last_sent_clipboard^ = fmt.aprintf("%s", clipboard_text)
}
}
handle_pair_request :: proc(client: cnet.TCP_Socket, payload: []byte) -> bool {
pair_request: Pair_Request
if uerr := cjson.unmarshal(payload, &pair_request); uerr != nil {
fmt.printf("invalid PAIR_REQUEST json payload: %v\n", uerr)
send_protocol_error(client, "", "INVALID_MESSAGE", "unable to parse PAIR_REQUEST JSON")
return false
}
if pair_request.version != "1" || pair_request.message_type != "PAIR_REQUEST" {
send_protocol_error(client, pair_request.request_id, "INVALID_MESSAGE", "expected PAIR_REQUEST")
return false
}
if len(pair_request.device_id) == 0 || len(pair_request.public_key) == 0 {
send_protocol_error(client, pair_request.request_id, "INVALID_MESSAGE", "PAIR_REQUEST missing required fields")
return false
}
upsert_paired_peer(Paired_Peer{
peer_device_id = pair_request.device_id,
peer_device_name = pair_request.device_name,
peer_platform = pair_request.platform,
peer_public_key = pair_request.public_key,
paired_at = 0,
})
fmt.printf("paired peer stored: id=%q method=%q\n", pair_request.device_id, pair_request.pairing_method)
return send_pair_accept(client, pair_request.request_id)
}
handle_clip_update :: proc(client: cnet.TCP_Socket, payload: []byte, request_device_id: string) -> (applied_text: string, applied: bool) {
clip_update: Clip_Update
if uerr := cjson.unmarshal(payload, &clip_update); uerr != nil {
fmt.printf("invalid CLIP_UPDATE json payload: %v\n", uerr)
send_protocol_error(client, "", "INVALID_MESSAGE", "unable to parse CLIP_UPDATE JSON")
return "", false
}
if clip_update.version != "1" || clip_update.message_type != "CLIP_UPDATE" {
send_protocol_error(client, clip_update.request_id, "INVALID_MESSAGE", "expected CLIP_UPDATE")
return "", false
}
if !is_paired_peer(request_device_id) {
send_protocol_error(client, clip_update.request_id, "UNPAIRED", "device is not paired")
fmt.printf("rejected CLIP_UPDATE from unpaired device_id=%q\n", request_device_id)
return "", false
}
peer_state := find_peer_runtime_state(request_device_id)
if peer_state.has_last_clip_update_at {
elapsed := time.since(peer_state.last_clip_update_at)
if elapsed < CLIP_MIN_INTERVAL {
send_protocol_error(client, clip_update.request_id, "RATE_LIMITED", "clip updates are arriving too quickly")
fmt.printf("rate-limited CLIP_UPDATE from device_id=%q (elapsed=%v)\n", request_device_id, elapsed)
return "", false
}
}
incoming_hash := clip_update.content_hash
if len(incoming_hash) == 0 {
incoming_hash = fmt.tprintf("fallback-len:%d", len(clip_update.content))
}
if peer_state.has_last_content_hash && peer_state.last_content_hash == incoming_hash {
fmt.printf("deduped CLIP_UPDATE from device_id=%q hash=%q\n", request_device_id, incoming_hash)
_ = send_protocol_ack(client, clip_update.request_id)
return "", false
}
if clip_update.content_type != "text/plain" {
send_protocol_error(client, clip_update.request_id, "INVALID_MESSAGE", "unsupported content_type")
return "", false
}
if !set_local_clipboard_text(clip_update.content) {
send_protocol_error(client, clip_update.request_id, "INVALID_MESSAGE", "failed to write local clipboard")
fmt.printf("failed to apply CLIP_UPDATE to local clipboard for device_id=%q\n", request_device_id)
return "", false
}
fmt.printf("applied CLIP_UPDATE from paired device_id=%q content_type=%q bytes=%d\n", request_device_id, clip_update.content_type, len(clip_update.content))
peer_state.last_clip_update_at = time.now()
peer_state.has_last_clip_update_at = true
peer_state.last_content_hash = fmt.aprintf("%s", incoming_hash)
peer_state.has_last_content_hash = true
_ = send_protocol_ack(client, clip_update.request_id)
return clip_update.content, true
}
handle_post_hello_message :: proc(client: cnet.TCP_Socket, payload: []byte, hello_device_id: string, last_sent_clipboard: ^string, awaiting_pong: ^bool) {
header: Message_Header
if uerr := cjson.unmarshal(payload, &header); uerr != nil {
send_protocol_error(client, "", "INVALID_MESSAGE", "unable to parse message header")
return
}
if header.version != "1" {
send_protocol_error(client, header.request_id, "UNSUPPORTED_VERSION", "protocol version is not supported")
return
}
if len(header.device_id) > 0 && header.device_id != hello_device_id {
send_protocol_error(client, header.request_id, "INVALID_MESSAGE", "device_id mismatch with HELLO")
return
}
switch header.message_type {
case "PAIR_REQUEST":
_ = handle_pair_request(client, payload)
case "CLIP_UPDATE":
content, ok := handle_clip_update(client, payload, hello_device_id)
if ok {
last_sent_clipboard^ = fmt.aprintf("%s", content)
}
case "PING":
_ = send_pong(client, header.request_id)
case "PONG":
awaiting_pong^ = false
case:
send_protocol_error(client, header.request_id, "INVALID_MESSAGE", "unsupported message type")
}
}
validate_hello :: proc(hello: Inbound_Hello) -> (valid: bool, code: string, message: string) {
if hello.version != "1" {
return false, "UNSUPPORTED_VERSION", "protocol version is not supported"
}
if hello.message_type != "HELLO" {
return false, "INVALID_MESSAGE", "expected HELLO as first message"
}
if len(hello.device_id) == 0 {
return false, "INVALID_MESSAGE", "missing device_id"
}
if len(hello.nonce) == 0 {
return false, "INVALID_MESSAGE", "missing nonce"
}
return true, "", ""
}
handle_first_client_message :: proc(client: cnet.TCP_Socket, payload: []byte) {
hello: Inbound_Hello
if uerr := cjson.unmarshal(payload, &hello); uerr != nil {
fmt.printf("invalid HELLO json payload: %v\n", uerr)
send_protocol_error(client, "", "INVALID_MESSAGE", "unable to parse HELLO JSON")
return
}
valid, code, message := validate_hello(hello)
if !valid {
fmt.printf("HELLO validation failed: %s (%s)\n", code, message)
send_protocol_error(client, hello.request_id, code, message)
return
}
fmt.printf("HELLO accepted from device_id=%q name=%q platform=%q\n", hello.device_id, hello.device_name, hello.platform)
_ = send_protocol_ack(client, hello.request_id)
last_sent_clipboard := ""
last_peer_activity_at := time.now()
last_ping_sent_at := time.now()
awaiting_pong := false
for {
next_payload, ferr := recv_frame(client)
if ferr != .None {
if ferr == .Socket_Recv_Timeout {
if time.since(last_peer_activity_at) >= KEEPALIVE_IDLE_TIMEOUT {
fmt.printf("disconnecting idle peer device_id=%q after %v\n", hello.device_id, KEEPALIVE_IDLE_TIMEOUT)
return
}
if !awaiting_pong && time.since(last_ping_sent_at) >= KEEPALIVE_PING_INTERVAL {
if send_ping(client) {
awaiting_pong = true
last_ping_sent_at = time.now()
}
}
maybe_emit_local_clipboard_update(client, hello.device_id, &last_sent_clipboard)
continue
}
fmt.printf("client message loop ended: %v\n", ferr)
return
}
last_peer_activity_at = time.now()
awaiting_pong = false
handle_post_hello_message(client, next_payload, hello.device_id, &last_sent_clipboard, &awaiting_pong)
delete(next_payload)
}
}
u32_to_be_bytes :: proc(v: u32) -> [4]byte {
return [4]byte{
byte((v >> 24) & 0xff),
byte((v >> 16) & 0xff),
byte((v >> 8) & 0xff),
byte(v & 0xff),
}
}
be_bytes_to_u32 :: proc(prefix: [4]byte) -> u32 {
return (u32(prefix[0]) << 24) |
(u32(prefix[1]) << 16) |
(u32(prefix[2]) << 8) |
u32(prefix[3])
}
recv_exact :: proc(socket: cnet.TCP_Socket, out: []byte) -> Framing_Error {
offset := 0
for offset < len(out) {
read, err := cnet.recv_tcp(socket, out[offset:])
if err != nil {
if err == .Timeout {
if offset == 0 {
return .Socket_Recv_Timeout
}
continue
}
return .Socket_Recv_Failed
}
if read == 0 {
return .Connection_Closed
}
offset += read
}
return .None
}
send_all :: proc(socket: cnet.TCP_Socket, data: []byte) -> Framing_Error {
offset := 0
for offset < len(data) {
written, err := cnet.send_tcp(socket, data[offset:])
if err != nil {
return .Socket_Send_Failed
}
if written == 0 {
return .Connection_Closed
}
offset += written
}
return .None
}
send_frame :: proc(socket: cnet.TCP_Socket, payload: []byte) -> Framing_Error {
if len(payload) > MAX_FRAME_BYTES {
return .Frame_Too_Large
}
prefix := u32_to_be_bytes(u32(len(payload)))
if err := send_all(socket, prefix[:]); err != .None {
return err
}
return send_all(socket, payload)
}
recv_frame :: proc(socket: cnet.TCP_Socket, allocator := context.allocator) -> ([]byte, Framing_Error) {
len_prefix: [4]byte
if err := recv_exact(socket, len_prefix[:]); err != .None {
return nil, err
}
size := int(be_bytes_to_u32(len_prefix))
if size > MAX_FRAME_BYTES {
return nil, .Frame_Too_Large
}
buf := make([]byte, size, allocator)
if size == 0 {
return buf, .None
}
if err := recv_exact(socket, buf); err != .None {
delete(buf)
return nil, err
}
return buf, .None
}
handle_client :: proc(client: cnet.TCP_Socket, source: cnet.Endpoint) {
defer cnet.close(client)
fmt.printf("client connected from %v\n", source)
_ = cnet.set_option(client, .Receive_Timeout, SOCKET_RECV_TIMEOUT)
if err := send_frame(client, transmute([]byte)HELLO_JSON); err != .None {
fmt.printf("failed to send HELLO: %v\n", err)
return
}
payload, err := recv_frame(client)
if err != .None {
fmt.printf("failed to receive frame: %v\n", err)
return
}
defer delete(payload)
fmt.printf("received first frame (%d bytes)\n", len(payload))
handle_first_client_message(client, payload)
}
run_tcp_listener :: proc(port: int) {
endpoint := cnet.Endpoint{address = cnet.IP4_Any, port = port}
listener, lerr := cnet.listen_tcp(endpoint)
if lerr != nil {
fmt.printf("failed to listen on port %d: %v\n", port, lerr)
return
}
defer cnet.close(listener)
fmt.printf("clipsync daemon listening on tcp://0.0.0.0:%d\n", port)
for {
client, source, aerr := cnet.accept_tcp(listener)
if aerr != nil {
fmt.printf("accept failed: %v\n", aerr)
continue
}
handle_client(client, source)
}
}
main :: proc() {
load_paired_peers()
thread.run(run_discovery_broadcaster)
thread.run(run_discovery_listener)
run_tcp_listener(DEFAULT_PORT)
}
+98
View File
@@ -0,0 +1,98 @@
package clipsync_net
import cnet "core:net"
// TCP framing for protocol v1:
// [4-byte big-endian length][UTF-8 JSON payload]
MAX_FRAME_BYTES :: 1024 * 1024 // 1 MiB
Framing_Error :: enum {
None,
Frame_Too_Large,
Connection_Closed,
Socket_Recv_Failed,
Socket_Send_Failed,
}
u32_to_be_bytes :: proc(v: u32) -> [4]byte {
return [4]byte{
byte((v >> 24) & 0xff),
byte((v >> 16) & 0xff),
byte((v >> 8) & 0xff),
byte(v & 0xff),
}
}
be_bytes_to_u32 :: proc(prefix: [4]byte) -> u32 {
return (u32(prefix[0]) << 24) |
(u32(prefix[1]) << 16) |
(u32(prefix[2]) << 8) |
u32(prefix[3])
}
recv_exact :: proc(socket: cnet.TCP_Socket, out: []byte) -> Framing_Error {
offset := 0
for offset < len(out) {
read, err := cnet.recv_tcp(socket, out[offset:])
if err != nil {
return .Socket_Recv_Failed
}
if read == 0 {
return .Connection_Closed
}
offset += read
}
return .None
}
send_all :: proc(socket: cnet.TCP_Socket, data: []byte) -> Framing_Error {
offset := 0
for offset < len(data) {
written, err := cnet.send_tcp(socket, data[offset:])
if err != nil {
return .Socket_Send_Failed
}
if written == 0 {
return .Connection_Closed
}
offset += written
}
return .None
}
send_frame :: proc(socket: cnet.TCP_Socket, payload: []byte) -> Framing_Error {
if len(payload) > MAX_FRAME_BYTES {
return .Frame_Too_Large
}
prefix := u32_to_be_bytes(u32(len(payload)))
if err := send_all(socket, prefix[:]); err != .None {
return err
}
return send_all(socket, payload)
}
recv_frame :: proc(socket: cnet.TCP_Socket, allocator := context.allocator) -> ([]byte, Framing_Error) {
len_prefix: [4]byte
if err := recv_exact(socket, len_prefix[:]); err != .None {
return nil, err
}
size := int(be_bytes_to_u32(len_prefix))
if size > MAX_FRAME_BYTES {
return nil, .Frame_Too_Large
}
buf := make([]byte, size, allocator)
if size == 0 {
return buf, .None
}
if err := recv_exact(socket, buf); err != .None {
delete(buf)
return nil, err
}
return buf, .None
}
+39
View File
@@ -0,0 +1,39 @@
package protocol
// Protocol message type names for clipsync v1.
Message_Type :: enum {
HELLO,
PAIR_REQUEST,
PAIR_CHALLENGE,
PAIR_CONFIRM,
PAIR_ACCEPT,
CLIP_UPDATE,
ACK,
ERROR,
PING,
PONG,
}
HELLO_Message :: struct {
version: string,
message_type: string,
device_id: string,
device_name: string,
platform: string,
tcp_port: int,
timestamp_ms: i64,
nonce: string,
}
make_hello_message :: proc(device_id, device_name, platform, nonce: string, tcp_port: int) -> HELLO_Message {
return HELLO_Message{
version = "1",
message_type = "HELLO",
device_id = device_id,
device_name = device_name,
platform = platform,
tcp_port = tcp_port,
timestamp_ms = 0,
nonce = nonce,
}
}