github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/ipc/ipc_windows.go (about)

     1  package ipc
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"os"
     8  	"os/user"
     9  
    10  	"github.com/google/uuid"
    11  
    12  	"github.com/Microsoft/go-winio"
    13  )
    14  
    15  // DialContext attempts to establish an IPC connection, timing out if the
    16  // provided context expires.
    17  func DialContext(ctx context.Context, path string) (net.Conn, error) {
    18  	// Read the pipe name.
    19  	pipeNameBytes, err := os.ReadFile(path)
    20  	if err != nil {
    21  		return nil, fmt.Errorf("unable to read pipe name: %w", err)
    22  	}
    23  	pipeName := string(pipeNameBytes)
    24  
    25  	// Attempt to connect.
    26  	return winio.DialPipeContext(ctx, pipeName)
    27  }
    28  
    29  // listener implements net.Listener but provides additional cleanup facilities
    30  // on top of those provided by the underlying named pipe listener.
    31  type listener struct {
    32  	// Listener is the underlying named pipe listener.
    33  	net.Listener
    34  	// path is the path to the file where the named pipe name is stored.
    35  	path string
    36  }
    37  
    38  // Close closes the listener and removes the pipe name record.
    39  func (l *listener) Close() error {
    40  	// Remove the pipe name record.
    41  	if err := os.Remove(l.path); err != nil {
    42  		l.Listener.Close()
    43  		return fmt.Errorf("unable to remove pipe name record: %w", err)
    44  	}
    45  
    46  	// Close the underlying listener.
    47  	return l.Listener.Close()
    48  }
    49  
    50  // NewListener creates a new IPC listener.
    51  func NewListener(path string) (net.Listener, error) {
    52  	// Create a unique pipe name.
    53  	randomUUID, err := uuid.NewRandom()
    54  	if err != nil {
    55  		return nil, fmt.Errorf("unable to generate UUID for named pipe: %w", err)
    56  	}
    57  	pipeName := fmt.Sprintf(`\\.\pipe\mutagen-ipc-%s`, randomUUID.String())
    58  
    59  	// Compute the SID of the user.
    60  	user, err := user.Current()
    61  	if err != nil {
    62  		return nil, fmt.Errorf("unable to look up current user: %w", err)
    63  	}
    64  	sid := user.Uid
    65  
    66  	// Create the security descriptor for the pipe. This is constructed using
    67  	// the Security Descriptor Definition Language (SDDL) (the Discretionary
    68  	// Access Control List (DACL) format), where the value in parentheses is an
    69  	// Access Control Entry (ACE) string. The P flag in the DACL prevents
    70  	// inherited permissions. The ACE string in this case grants "Generic All"
    71  	// (GA) permissions to its associated SID. More information can be found
    72  	// here:
    73  	//  SDDL: https://msdn.microsoft.com/en-us/library/windows/desktop/aa379570(v=vs.85).aspx
    74  	//  ACEs: https://msdn.microsoft.com/en-us/library/windows/desktop/aa374928(v=vs.85).aspx
    75  	//  SIDs: https://msdn.microsoft.com/en-us/library/windows/desktop/aa379602(v=vs.85).aspx
    76  	securityDescriptor := fmt.Sprintf("D:P(A;;GA;;;%s)", sid)
    77  
    78  	// Create the pipe configuration.
    79  	configuration := &winio.PipeConfig{
    80  		SecurityDescriptor: securityDescriptor,
    81  	}
    82  
    83  	// Attempt to create (and open) the endpoint path where we will record the
    84  	// underlying named pipe name. In order to match the semantics of UNIX
    85  	// domain sockets, we enforce that the file doesn't exist. We do this before
    86  	// attempt to create the named pipe to avoid unnecessary overhead.
    87  	file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
    88  	if err != nil {
    89  		if os.IsExist(err) {
    90  			return nil, err
    91  		}
    92  		return nil, fmt.Errorf("unable to open endpoint: %w", err)
    93  	}
    94  
    95  	// Defer closure of the endpoint file when we're done, along with removal in
    96  	// the event of failure.
    97  	var successful bool
    98  	defer func() {
    99  		file.Close()
   100  		if !successful {
   101  			os.Remove(path)
   102  		}
   103  	}()
   104  
   105  	// Create the named pipe listener.
   106  	rawListener, err := winio.ListenPipe(pipeName, configuration)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  
   111  	// Write the pipe name. This isn't 100% atomic since the name could be
   112  	// partially written, but MoveFileEx isn't guaranteed to be atomic either,
   113  	// so renaming a file into place here doesn't help much.
   114  	if _, err := file.Write([]byte(pipeName)); err != nil {
   115  		return nil, fmt.Errorf("unable to write pipe name: %w", err)
   116  	}
   117  
   118  	// Mark ourselves as successful.
   119  	successful = true
   120  
   121  	// Success.
   122  	return &listener{rawListener, path}, nil
   123  }