hmssql
Safe HaskellNone
LanguageHaskell2010

Hmssql.Connection.Backend

Description

What a connection's bytes travel on: a Backend is a connected byte stream, as a record of operations. hmssql has three of its own (TCP, and on Windows Shared Memory and Named Pipes), chosen by connectProtocol. TLS, when the connection is encrypted, runs on top of whichever it is.

On Linux and macOS TCP uses network's calls. On Windows all three use hmssql's own overlapped IO, except under GHC's native IO manager with a network and a GHC that support it, where TCP uses network's calls and the pipes the manager (networkTcp).

The record is public so that a transport can be replaced or wrapped without changing hmssql: ProtocolCustom connects over a Backend the caller makes, which may be one of these with an operation changed, a tunnel, or an in-memory stream for a test.

The pipe functions work on Windows only, since SQL Server listens on pipes only there. On other platforms they throw an IOException (pipesSupported says which it is).

Synopsis

The record

data Backend Source #

A connected byte stream.

One thread receives at a time and one sends at a time, but a receive and a send may be in progress together, and backendStopReceiving, backendShutdown and backendClose are called from other threads while they are.

An asynchronous exception should end a blocked backendRecv, backendSend or backendSendMany promptly. hmssql doesn't use the backend's stream afterwards: an interrupted send or receive ends the connection. (Canceling a query never interrupts one of these: the thread that is canceled waits for the connection's reader thread, and the Attention is an ordinary send.) A backend that can't be interrupted in some operation should say so; hmssql's own can be in all of them, and network's except where socketBackend says.

The operations report failure by throwing IOException. They must not refer to the Connection they serve, which would keep a connection dropped without close from ever being collected.

Constructors

Backend 

Fields

  • backendRecv :: !(Ptr Word8 -> Int -> IO Int)

    Receive up to that many bytes into the buffer, waiting until at least one arrives: how many did, or 0 at the end of the stream.

  • backendSend :: !(ByteString -> IO ())

    Send all of the bytes. hmssql always passes whole units: one or more whole TDS packets, on a MARS connection whole SMP packets, or over TLS exactly one TLS record. A transport that delivers writes one at a time must keep it one write: SQL Server reads a pipe a write at a time, and drops a connection whose write holds a part of a packet, or two TLS records.

  • backendSendMany :: !([[ByteString]] -> IO ())

    Send all of the writes, in order, with no other send's bytes between them. Each write is given as its pieces, and together the pieces of one write are a unit as for backendSend, which one piece alone need not be: a plaintext SMP packet comes as its header and its TDS packet. Where the write boundaries don't matter (a socket) all of it can go in one system call; where they do (a pipe) each write must reach the other end as one.

  • backendStopReceiving :: !(IO ())

    Wake a blocked receive, and make it and every later one fail. Sends still work. It must not block, and is called from any thread, more than once, and after backendClose. A backend whose receive an asynchronous exception interrupts reliably may do nothing here, as the socket backend does with network's calls: hmssql kills its reader thread right after calling it.

  • backendShutdown :: !(IO ())

    Make blocked and later sends and receives fail, and let the other end know, so that a server holding part of a message stops waiting for the rest. Called from any thread, more than once, and after backendClose, but never from inside one of the backend's own operations. It should return promptly. A socket is shut down and stays open; a pipe, which has no shutdown, is closed here.

  • backendClose :: !(IO ())

    Release the stream. hmssql calls it once its reader thread has stopped and no send is in progress, and may call it again. It should not throw: there is nothing a caller could do.

  • backendSocket :: !(Maybe Socket)

    The socket underneath, if there is one: to set an option on it (keep-alive, a buffer size) or ask for the peer's address. Reading, writing or closing it is the backend's business.

TCP

tcpBackend :: String -> String -> IO Backend Source #

Connect over TCP to the host and port (a number or a service name). The host's addresses are tried in the resolver's order until one accepts; if none does, the last failure is thrown, an IOException. An address that neither accepts nor refuses within addressConnectTimeout is given up and the next one tried; the last address alone waits as long as the operating system does. Nagle's algorithm is turned off (TCP_NODELAY).

Interruptible. An exception that arrives just as it returns can lose the backend, so call it masked (mask_ will do: the waits inside are interruptible) and install the cleanup before unmasking, as bracket does.

socketBackend :: Socket -> IO Backend Source #

A backend over a connected socket, which it owns from then on: backendClose closes it.

It uses network's own calls on Linux and macOS, and on Windows under GHC's native I/O manager when networkTcp says network supports that manager. There a send of many pieces and a connect are blocking foreign calls in network, which an asynchronous exception doesn't interrupt, and which without -threaded stop every Haskell thread while they block. Otherwise, on Windows, the socket is read and written as a handle, with hmssql's own overlapped I/O, because network's calls block in a foreign call there, where nothing can interrupt them; a program linked without -threaded must then run with GHC's default I/O manager.

Shared Memory and Named Pipes

pipeBackend :: FilePath -> IO Backend Source #

Connect to a named pipe by its path, such as \\.\pipe\SQLLocal\MSSQLSERVER (sharedMemoryPipe, namedPipe). Throws an IOException if the pipe can't be opened; on a platform without pipes, always.

The pipe is opened so that its server can identify the client but not act as the client (SECURITY_IDENTIFICATION), the most restrictive level SQL Server was seen to accept. A pipe on another machine is reached over SMB, so Windows authenticates the user to that machine first. Interruptible, except that without -threaded the open itself stops the program while it waits; call it masked, as tcpBackend.

The pipes go through GHC's native I/O manager exactly when TCP goes through network's calls (networkTcp), with or without -threaded, and through hmssql's own overlapped I/O otherwise, where a program linked without -threaded must run with GHC's default I/O manager.

pipesSupported :: Bool Source #

Whether this platform has Shared Memory and Named Pipes: Windows only.

sharedMemoryPipe :: String -> FilePath Source #

The pipe of the Shared Memory protocol for an instance on this machine, by the instance's name: empty, or MSSQLSERVER, for the default instance. SQL Server's Shared Memory protocol is TDS over this local pipe.

namedPipe :: Bool -> String -> String -> FilePath Source #

The pipe of the Named Pipes protocol for an instance on a host, at the path SQL Server uses unless it was configured with another. A host that is this machine (isLocalHost, or a loopback address) gives the local form, \\.\pipe\..., which doesn't go through the network.

isLocalHost :: String -> IO Bool Source #

Whether a host name means this machine: localhost, ., (local), or the machine's own name, in any case. An IP address is not, even a loopback one: Microsoft's clients connect to those over TCP.

isDefaultInstance :: String -> Bool Source #

Whether an instance name means the default instance: empty, or MSSQLSERVER in any case.

Serving a pipe

data PipeListener Source #

The server side of a named pipe: for a mock server in tests, or a proxy.

pipeListen :: FilePath -> IO PipeListener Source #

Create a named pipe that clients on this machine can connect to, such as \\.\pipe\my-test-pipe. No pipe of that name may exist yet. Creating one needs no privilege. Its connections go through the same I/O as pipeBackend's.

pipeAccept :: PipeListener -> IO (Backend, IO ()) Source #

Wait for a client: the connection to it, and an action that disconnects the client abruptly, discarding what it hasn't read yet (so that its next read or write fails, as after a TCP reset). The backend must still be closed. Interruptible; one thread accepts at a time.

pipeListenerClose :: PipeListener -> IO () Source #

Stop listening. Connections already accepted are not affected. It waits for an accept in progress, so stop the accepting thread first.

Internal

networkTcp :: Bool Source #

Whether TCP goes through network's own calls: on every platform but Windows, and on Windows under GHC's native I/O manager, with or without -threaded, when hmssql was built against network 3.3 or later with a GHC whose native manager network supports. The pipes then go through that manager too. Otherwise, on Windows, TCP and the pipes go through hmssql's own overlapped I/O.

tcpBackendWith :: (forall a. IO a -> IO a) -> String -> String -> IO Backend Source #

tcpBackend, called masked, with the caller's restore for the parts that wait: the lookup and each connect. Returns still masked.

connectAddresses :: (forall a. IO a -> IO a) -> NonEmpty AddrInfo -> IO Socket Source #

Connect to the first of the addresses that accepts, trying them in the order given (the resolver's: a name such as localhost often resolves to ::1 before 127.0.0.1, and the server may listen on only one of them). Every address but the last is given addressConnectTimeout; when it refuses or the time runs out, the next is tried. The last address waits as long as the operating system does, so a host with one address behaves as a plain connect, and when it fails too, its failure is the one thrown. Called masked, with the caller's restore for the wait of each connect; a caller's timeout around the whole still bounds it, since the limit here catches only its own expiry.

addressConnectTimeout :: Int Source #

How long an address that is not the host's last is given to accept or refuse before the next address is tried: 4 seconds, in microseconds. An address that refuses fails at once; one that is silent (a firewall that drops the SYN, a host that is down, a stale DNS record) fails only when the operating system gives up, after about two minutes on Linux and 21 seconds on Windows, so without this a silent first address cost that whole wait. Four seconds outlasts one retransmission of the SYN on either platform, so a single lost SYN doesn't skip a working address, and leaves a caller's 15-second timeout room for a third address behind two silent ones.

closeSocket :: Socket -> IO () Source #

Close a socket, where a failure to close must not get in the way: in the cleanup after a failed connect (where it would replace the exception that made it fail), in close, and in the finalizer of a connection dropped without it. It throws no synchronous exception, and the descriptor is closed even when close fails, which it can:

close marks the socket closed, then closes the descriptor with GHC's closeFdWith, which first removes it from the IO manager. With kqueue (macOS, BSD), that throws kevent: does not exist when the IO manager's table has a registration for the descriptor number that the kernel doesn't. Before GHC 9.14, a thread that starts to wait on a descriptor just after another thread closed it leaves one (checked on 9.6 to 9.12), and the next socket to get the number then fails to close. closeFdWith gives up before closing the descriptor (in 9.14 too), which would stay open (and the connection with it) for the life of the process, since the socket is already marked closed. So on failure the descriptor is closed here directly: not through closeFdWith again, which would block for good (after that failure it leaves the IO manager's lock for the descriptor taken, and anything waiting on a descriptor under the same lock hangs; nothing here can undo that). Both are bugs in GHC's kqueue IO manager.

Runs masked, so that an asynchronous exception can't come between close marking the socket closed and closing the descriptor.

closeSocketWith :: (Socket -> IO ()) -> Socket -> IO () Source #

closeSocket with the given way to close the socket, standing in for close. If it fails after marking the socket closed (as close does when closeFdWith fails), the descriptor is closed directly; if it fails before, the socket is left as it is (network's finalizer closes it). A synchronous exception is not rethrown.

socketCloser :: IORef (Socket -> IO ()) Source #

How closeSocket closes a socket: close. Tests replace it to make closing fail.