Before you start#
Review Wine handles and server objects, waits and events, and file paths and sharing. This chapter uses Wine tag wine-10.0, commit b073859675060c9211fcbccfd90e4e87520dc2c2. It does not assume that Wine or a Windows compiler is installed.
Learning goals#
Follow a service start through manager state and a failed control connection. Explain the lifetime of an asynchronous socket request and a named-pipe disconnect. Distinguish a Wine service from a Linux system service and a Windows socket value from an independently owned Unix descriptor. Design evidence that could disprove an assumed cause.
Source walkthrough#
- Service
- Manager entry retains process state and waits for control.
- Socket
- Startup count and pending I/O govern separate resources.
- Named pipe
- Endpoint state and queued operations change on disconnect.
The following excerpts are actual Wine 10.0 source, shortened at the boundaries indicated. Wine programs/services/services.c, service_start_process, is licensed LGPL-2.1-or-later. Within the branch that has located a shared service process, the code sets:
service_entry->status.dwCurrentState = SERVICE_START_PENDING;
service_entry->status.dwControlsAccepted = 0;
ResetEvent(service_entry->status_changed_event);
The first assignment records a transition, not proof that initialization completed. The second says no controls are currently accepted in this state; an early stop request must not be assumed deliverable as though the service were running. The third resets the notification event so a later status change can be observed. Immediately afterwards, the code obtains a process reference, increases its use count and releases the service lock. It then waits for a control mutex, with a 30-second timeout in this branch. Failure to acquire it releases the local process reference and returns ERROR_SERVICE_REQUEST_TIMEOUT. An earlier missing process also unlocks and returns the timeout. Reading only the state assignment would miss both reference ownership and failure. Source.
The wait itself is the WaitForSingleObject call in the same shared-process branch of programs/services/services.c, Wine 10.0, LGPL-2.1-or-later:
err = WaitForSingleObject(process->control_mutex, 30000);
if (err != WAIT_OBJECT_0)
{
release_process(process);
return ERROR_SERVICE_REQUEST_TIMEOUT;
}
The timeout path releases the reference this request holds. The service entry keeps a separate reference in service_entry->process, so the failed request does not release the entry's association with the process. A timeout here says nothing about whether the service later reached running; later status queries are the evidence for that.
- Shared process found
Entry records start-pending, accepts no controls and resets its status event.
- Hold references
The entry keeps one process reference; the request holds another while the lock is dropped.
- Wait for control mutex
WaitForSingleObject with 30000 ms in this shared-process branch.
- Timeout
Release the request reference and return ERROR_SERVICE_REQUEST_TIMEOUT.
- Acquired
Return success; later status queries still decide whether the service runs.
The next excerpt is from Wine dlls/ws2_32/socket.c, WSAStartup, licensed LGPL-2.1-or-later; earlier version selection and descriptive fields are omitted:
if (!LOBYTE(version))
return WSAVERNOTSUPPORTED;
if (!data) return WSAEFAULT;
num_startup++;
A zero low-order version byte is rejected first. A null output pointer fails next, so it does not increase the startup count. Successful calls increment that process-side count; callers must balance successful starts with cleanup. The preceding code selects a supported version for the returned structure and describes the implementation. A startup result is about Winsock initialization, not a network connection, permission to reach an address, or a guarantee that every requested protocol exists. In the same file, WSACleanup decrements the count and, on its transition to zero, closes tracked socket handles and clears the tracking array. Do not treat an intermediate cleanup as the last one when another successful startup remains outstanding.
A manager, a service, and the host#
The Windows service control manager provides a database and a control interface for service entries. An entry represents configuration and mutable status; it is not the process itself. A service process can host one or more service entries, so an entry must retain the process object while it needs that association. Wine's services.exe implements this Windows-facing machinery within Wine. A distribution might happen to start Wine from a systemd unit, but a Wine service entry is not thereby a systemd unit, and the manager does not gain root simply because the Windows API calls it a service. Host UID, groups, namespaces, Linux security policy and filesystem permissions still constrain its process.
A stopped service may be requested to start, enter start-pending, report progress and eventually report running. A request to stop a running service may move through stop-pending before stopped. These names describe reported state; a process can exit prematurely, fail to send a timely control response, or report a misleading state. Status fields such as accepted controls, checkpoint, wait hint and error code carry different information. In particular, a checkpoint and wait hint are evidence for progress and timeout handling, not proof that useful work occurred. A program waiting for readiness must handle the service dying between its status query and the next operation.
The manager coordinates configured dependencies before an applicable service starts. A missing dependency, failure of a required service, circular or malformed configuration, or a process creation error prevents a simple success story. Do not replace the returned error with a generic network diagnosis. The service manager implementation locks an entry while manipulating its state; the quoted shared-process branch takes a process reference before dropping the lock. Holding the entry lock across an unbounded wait would obstruct other state updates, so the reference and lock solve distinct problems: lifetime versus serialization. Other branches have different setup and cleanup and should be read individually.
Control messages use Wine's Windows-compatible service machinery, not a universal direct systemd call. A control request can fail before reaching the service, be rejected because its current state does not accept it, time out while waiting, or be delivered while the service exits. The caller owns any returned handle according to the API, and the manager's entry and process references must survive concurrent operations. A closed caller handle does not itself prove the service has stopped; conversely, a process exit does not turn an old numeric handle into a reusable reference to its replacement.
Winsock is an API and an ownership contract#
Winsock presents Windows-facing initialization, socket creation, address operations, errors and asynchronous interfaces. Its SOCKET is an opaque Windows-facing value, not an integer the application may safely pass to Linux read or close. Wine may use a host descriptor for a socket's underlying I/O, while server objects and handle tables retain additional state. Looking at an fd number in a diagnostic does not establish a one-to-one permanent identity between the fd and the SOCKET; descriptors can be duplicated, closed and reused. See Wine socket server objects and Wine Unix socket implementation.
A typical stream connection initializes Winsock, creates a socket, supplies an address and connects. A listening peer binds, listens and accepts a new connection; the accepted socket is a separate resource from the listener. Sending bytes may complete partially. Receiving zero bytes is an orderly end of the stream, not an empty message that can be interpreted as a request. A failure result is followed by the Windows-facing error retrieval mechanism where the API specifies it; Linux errno is an implementation-side result, not a value the Windows application should directly assume. The mapping can depend on the failed operation and Wine's implementation. Address-in-use, refused connection and timeout suggest different causes and should remain different observations.
Overlapped I/O permits submission and later completion. The request carries state such as an overlapped structure and buffers; keep them valid until the operation's terminal outcome makes reuse safe. Completion can be observed through an event, a completion routine delivered as an APC in the appropriate alertable context, or an I/O completion port where configured. A pending return is not a completed read. Closing a socket while work is outstanding does not by itself make its application buffers safe to reuse: account for the API's cancellation/completion behavior. In particular, final WSACleanup cancels pending overlapped operations with WSA_OPERATION_ABORTED and Microsoft documents that it does so without signaling the event or invoking the completion routine. Do not wait only on that event or callback as proof of termination when cleanup is involved; reconcile the API's documented outcome and any completion-port behavior used by the program. Completion port packets identify completed work; they do not themselves own the application's arbitrary buffers forever. A robust application counts outstanding requests and drains their terminal results before releasing the enclosing connection object. Microsoft: WSACleanup
Event selection is a different interface: an application associates selected network events with an event object and later enumerates network events and per-event errors. Readiness is not a promise that a subsequent read will return a full application message. Another thread might consume data, an edge can correspond to a state change, and a peer can disconnect. Synchronize application ownership and obey the selected API's rearming rules rather than treating one event as a permanent lock on the bytes. Wine compatibility and host networking features vary by version and configuration; a successful loopback demonstration does not establish identical timing or every Windows extension.
Named pipes and pending operations#
A named pipe has a named server-side endpoint and a client that connects to an available instance. Multiple instances can share the name while maintaining separate conversations. Byte mode is a stream: application writes need framing if message boundaries matter. Message mode retains message boundaries; read mode and buffer size affect how a message is received. A Windows named pipe is not automatically equivalent to a Unix filesystem FIFO, even when Wine uses Unix mechanisms internally. Wine server/named_pipe.c defines server objects, endpoints, queues and asynchronous work that implement selected semantics.
A connected endpoint can have a queued read with no bytes yet. Wine's pipe_end_read checks endpoint state, queues an asynchronous request in read_q, reselects the queue and returns pending. If the endpoint is disconnected, listening, or closing, the source selects different error paths rather than treating all states as no data. A write creates a message and retains an asynchronous object before queueing; if allocation fails it cannot simply promise a successful completion. The server must retain objects as long as queues or completions reference them, and terminal paths must release those references exactly once.
In server/named_pipe.c, Wine 10.0, LGPL-2.1-or-later, pipe_end_read is the complete read entry point. Its state switch decides whether a read fails immediately or joins the queue:
static void pipe_end_read( struct fd *fd, struct async *async, file_pos_t pos )
{
struct pipe_end *pipe_end = get_fd_user( fd );
switch (pipe_end->state)
{
case FILE_PIPE_CONNECTED_STATE:
if ((pipe_end->flags & NAMED_PIPE_NONBLOCKING_MODE) && list_empty( &pipe_end->message_queue ))
{
set_error( STATUS_PIPE_EMPTY );
return;
}
break;
case FILE_PIPE_DISCONNECTED_STATE:
set_error( STATUS_PIPE_DISCONNECTED );
return;
case FILE_PIPE_LISTENING_STATE:
set_error( STATUS_PIPE_LISTENING );
return;
case FILE_PIPE_CLOSING_STATE:
if (!list_empty( &pipe_end->message_queue )) break;
set_error( STATUS_PIPE_BROKEN );
return;
}
queue_async( &pipe_end->read_q, async );
reselect_read_queue( pipe_end, 0 );
set_error( STATUS_PENDING );
}
A disconnected or listening endpoint fails at once. A connected endpoint in nonblocking mode with an empty queue returns STATUS_PIPE_EMPTY. A blocking connected read is queued and reports STATUS_PENDING. A closing endpoint still accepts a read while messages remain, and otherwise reports STATUS_PIPE_BROKEN. The function does not show what happens when the queued read is later completed, which is covered by the disconnect path below.
The disconnect path sets the endpoint's connection pointer to null, assigns a disconnected or closing state, wakes waiters and readers, and visits queued messages. For pending asynchronous messages it terminates work with the chosen status and releases the asynchronous object; it also breaks the reverse connection and processes the peer. The source makes a distinction between disconnected and closing when deciding which messages to free. That distinction matters when reasoning about bytes already queued and must not be simplified to “all queued data always disappears.” A client must use the operation's returned status and byte count, not infer that a close either delivered everything or delivered nothing. No sample here represents a captured Wine run.
- Reader clientNamed-pipe endpoint
Read request on a connected pipe with an empty queue
- Named-pipe endpointReader client
Return. Queued on read_q; STATUS_PENDING
- Peer endpointNamed-pipe endpoint
Peer disconnects
- Named-pipe endpointReader client
Return. Pending read completes with the disconnect status
Time runs downward. Dashed arrows show returns.
Read the text version
- Reader client → Named-pipe endpoint: Read request on a connected pipe with an empty queue
- Named-pipe endpoint → Reader client (return): Queued on read_q; STATUS_PENDING
- Peer endpoint → Named-pipe endpoint: Peer disconnects
- Named-pipe endpoint → Reader client (return): Pending read completes with the disconnect status
Worked timeline: three independent failures#
The following states are synthetic teaching evidence, constructed to exercise the source branches, not measured output. Suppose service A is requested to start and its required service B has already succeeded. The manager finds the shared process for A, records start-pending, clears accepted controls and resets the status event. It holds a process reference while it drops the entry lock. If the control mutex is unavailable for the entire branch's 30-second wait, the local process reference is released and the request reports ERROR_SERVICE_REQUEST_TIMEOUT. This result supports an investigation of manager/process coordination; it does not tell us that A reached running, that DNS failed, or that the process reference retained by the service entry has already been destroyed. Inspect later status and cleanup before asserting the final state.
Separately, imagine a client successfully initializes Winsock twice, opens one socket and calls cleanup once. The count moves from two to one; the quoted final-cleanup branch has not run. If a later connect fails with connection refused, investigate the addressed listener and returned Windows-facing error. It is not evidence that the first cleanup necessarily closed the socket. After the final matching cleanup, the source's tracked handles are closed; any concurrent application I/O still needs the appropriate completion and lifetime reasoning. This is an illustrative sequence, not a promise about application races outside the API contract.
Finally, suppose a named-pipe read is pending, then the peer disconnects before the expected response arrives. The disconnect path changes endpoint state and wakes or terminates relevant queued work. The read's terminal status and count determine what the application can consume. If a cancellation races with completion, the program needs a single ownership decision for the request, not a second free from both the cancellation path and the completion callback. It must also avoid reopening a new connection and treating an old completion as belonging to the new connection merely because a numerical handle was reused.
A useful investigation table has columns for object, owner, last confirmed state, pending operation, terminal observation and cleanup. For these three examples the objects are a service entry/process, a socket/request, and a pipe endpoint/request. Their apparent similarity does not mean one shared lock protects them or that all operations travel through wineserver. Some Wine work is handled in client-side code, some uses Unix operations, and some requires server coordination. Determine which actual branch is involved before proposing a fix.
Practice#
A bounded native loopback experiment
Run this only on a Linux machine you are authorized to use. It opens an ephemeral TCP listener on loopback, connects from the same Python process, exchanges a short authored message and closes both ends. It starts no service, does not use Wine, and sends no traffic to external hosts. A local policy can prohibit even loopback; in that case record the error and use the source-only evidence below. This is an observation of native sockets, not a Wine test.
python3 - <<'PY'
import socket
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener:
listener.settimeout(2)
listener.bind(('127.0.0.1', 0))
listener.listen(1)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
client.settimeout(2)
client.connect(listener.getsockname())
with listener.accept()[0] as peer:
peer.settimeout(2)
client.sendall(b'hello')
data = peer.recv(5)
print('received:', data, 'bytes:', len(data))
except OSError as exc:
print('loopback unavailable:', type(exc).__name__, str(exc))
PY
A common result is five bytes containing hello. TCP is a byte stream, so a single receive is permitted to return fewer; an application that requires exactly five must loop and handle EOF. That partial-read possibility is the counterexample to interpreting one read as one message. Bind or connection denial, timeout, an unavailable Python interpreter or a short read should be recorded rather than fixed with privilege or host policy changes. Context managers close all successfully created sockets even when an operation fails. Do not expose the listener on a wildcard address.
For a source-only alternative, use the supplied service and pipe timeline above: draw references before the mutex wait, identify the timeout release, then identify the pipe endpoint whose state changes and the queued request receiving a terminal result. Explain why service start-pending and pipe pending refer to different operations, and why neither establishes a running service. This exercise requires no network or Wine installation.
Knowledge check#
- Why does the shared-service branch obtain a process reference before releasing the entry lock, and what does its mutex timeout prove?
- Two successful startup calls are followed by one cleanup. Must Wine's tracked socket array already be cleared?
- A pending pipe read races with disconnection. Which state and ownership observations are required before freeing its request buffer?
- Why is receiving five bytes from a native loopback socket insufficient to prove Wine Winsock compatibility?
- A Windows service reports running. What can and cannot be inferred about Linux privilege and external connectivity?
Answers#
- The reference keeps the process object alive while the entry lock is no longer held; the lock serialized entry access but cannot by itself protect later use after unlock. Timeout proves that this branch did not acquire the control mutex within its wait. It does not establish that a network call failed or fully describe later manager cleanup.
- No. In the inspected source the count drops from two to one, so the branch that closes tracked handles at zero has not executed. The application must still balance the other successful startup and obey socket and pending-operation lifetimes.
- The application needs the terminal completion status and byte count, including the outcome of cancellation if requested. The server changes endpoint state, wakes pending work and releases queued references on its relevant disconnect paths. Application buffer ownership must survive until the API permits release; a stale numeric handle cannot identify a newly opened connection.
- It demonstrates one local Linux byte-stream exchange under current permissions. It does not run Wine, exercise Winsock startup, error translation, overlapped completion, event selection, or named-pipe semantics. A short read is valid stream behavior and should be handled rather than called corruption.
- The manager has received or recorded a service status; that is not Linux root authority and does not prove a connection succeeds. The host credentials and policy still apply, while DNS, routing, listener state and network restrictions are separate evidence.
Checkpoint#
Submit the three-object state and ownership table plus the native observation or source-only explanation. Pass when the timeout path includes the dropped process reference, the socket count reflects balanced initialization, and the pipe explanation follows pending work through a terminal status. If any step conflates a service with a systemd unit or a socket with a Unix fd, revisit the cited source and repeat the table. Completion is a readiness check, not certification.