| 1 | //go:build windows |
| 2 | |
| 3 | package instanceidentity |
| 4 | |
| 5 | import ( |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "io" |
| 9 | "log/slog" |
| 10 | "sync" |
| 11 | |
| 12 | "golang.org/x/sys/windows" |
| 13 | ) |
| 14 | |
| 15 | func endpointName(id string) (*uint16, error) { |
| 16 | if !Valid(id) { |
| 17 | return nil, fmt.Errorf("invalid desktop instance identity") |
| 18 | } |
| 19 | return windows.UTF16PtrFromString(`\\.\pipe\reasonix-desktop-` + id) |
| 20 | } |
| 21 | |
| 22 | // ListenEndpoint exposes a kernel-owned service PID to the updater. No PID file |
| 23 | // can outlive a generation or be confused with a recycled process. It carries |
| 24 | // no business RPC and rejects remote clients. |
| 25 | func ListenEndpoint(id string) (func(), error) { |
| 26 | name, err := endpointName(id) |
| 27 | if err != nil { |
| 28 | return nil, err |
| 29 | } |
| 30 | pipe, err := windows.CreateNamedPipe(name, windows.PIPE_ACCESS_DUPLEX|windows.FILE_FLAG_OVERLAPPED|windows.FILE_FLAG_FIRST_PIPE_INSTANCE, windows.PIPE_TYPE_BYTE|windows.PIPE_READMODE_BYTE|windows.PIPE_WAIT|windows.PIPE_REJECT_REMOTE_CLIENTS, 1, 1, 1, 0, nil) |
| 31 | if err != nil { |
| 32 | return nil, err |
| 33 | } |
| 34 | event, err := windows.CreateEvent(nil, 1, 0, nil) |
| 35 | if err != nil { |
| 36 | windows.CloseHandle(pipe) |
| 37 | return nil, err |
| 38 | } |
| 39 | done := make(chan struct{}) |
| 40 | var mu sync.Mutex |
| 41 | stopped := false |
| 42 | go func() { |
| 43 | defer close(done) |
| 44 | wait := func(err error, ov *windows.Overlapped) bool { |
| 45 | if !errors.Is(err, windows.ERROR_IO_PENDING) { |
| 46 | return err == nil || errors.Is(err, windows.ERROR_PIPE_CONNECTED) |
| 47 | } |
| 48 | _, err = windows.WaitForSingleObject(event, windows.INFINITE) |
| 49 | if err != nil { |
| 50 | return false |
| 51 | } |
| 52 | var transferred uint32 |
| 53 | return windows.GetOverlappedResult(pipe, ov, &transferred, false) == nil |
| 54 | } |
| 55 | submit := func(call func(*windows.Overlapped) error) bool { |
| 56 | mu.Lock() |
| 57 | if stopped { |
| 58 | mu.Unlock() |
| 59 | return false |
| 60 | } |
| 61 | if err := windows.ResetEvent(event); err != nil { |
| 62 | mu.Unlock() |
| 63 | slog.Warn("desktop identity endpoint: reset completion event", "err", err) |
| 64 | return false |
| 65 | } |
| 66 | ov := windows.Overlapped{HEvent: event} |
| 67 | err := call(&ov) |
| 68 | mu.Unlock() |
| 69 | return wait(err, &ov) |
| 70 | } |
| 71 | for { |
| 72 | if !submit(func(ov *windows.Overlapped) error { return windows.ConnectNamedPipe(pipe, ov) }) { |
| 73 | return |
| 74 | } |
| 75 | var buf [1]byte |
| 76 | var read uint32 |
| 77 | // The client acknowledges only after querying its server PID, so the |
| 78 | // connection remains bound during identity inspection. |
| 79 | submit(func(ov *windows.Overlapped) error { return windows.ReadFile(pipe, buf[:], &read, ov) }) |
| 80 | if err := windows.DisconnectNamedPipe(pipe); err != nil && !errors.Is(err, windows.ERROR_PIPE_NOT_CONNECTED) { |
| 81 | slog.Warn("desktop identity endpoint: disconnect client", "err", err) |
| 82 | return |
| 83 | } |
| 84 | } |
| 85 | }() |
| 86 | var once sync.Once |
| 87 | return func() { |
| 88 | once.Do(func() { |
| 89 | mu.Lock() |
| 90 | stopped = true |
| 91 | // No pending I/O is normal between submissions. On any other |
| 92 | // failure, retain the handles and OVERLAPPED until the worker |
| 93 | // finishes; cancellation failure cannot prove I/O completion. |
| 94 | if err := windows.CancelIoEx(pipe, nil); err != nil && !errors.Is(err, windows.ERROR_NOT_FOUND) { |
| 95 | slog.Warn("desktop identity endpoint: cancel pending I/O", "err", err) |
| 96 | } |
| 97 | mu.Unlock() |
| 98 | <-done |
| 99 | windows.CloseHandle(pipe) |
| 100 | windows.CloseHandle(event) |
| 101 | }) |
| 102 | }, nil |
| 103 | } |
| 104 | |
| 105 | // EndpointImage queries the server process through an actual local pipe handle. |
| 106 | // An absent endpoint means no Electron service owns this data home. |
| 107 | func EndpointImage(id string) (string, error) { |
| 108 | name, err := endpointName(id) |
| 109 | if err != nil { |
| 110 | return "", err |
| 111 | } |
| 112 | pipe, err := windows.CreateFile(name, windows.GENERIC_READ|windows.GENERIC_WRITE, 0, nil, windows.OPEN_EXISTING, 0, 0) |
| 113 | if errors.Is(err, windows.ERROR_FILE_NOT_FOUND) { |
| 114 | return "", nil |
| 115 | } |
| 116 | if err != nil { |
| 117 | return "", err |
| 118 | } |
| 119 | defer windows.CloseHandle(pipe) |
| 120 | var pid uint32 |
| 121 | if err := windows.GetNamedPipeServerProcessId(pipe, &pid); err != nil { |
| 122 | return "", err |
| 123 | } |
| 124 | process, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION|windows.SYNCHRONIZE, false, pid) |
| 125 | if err != nil { |
| 126 | return "", err |
| 127 | } |
| 128 | defer windows.CloseHandle(process) |
| 129 | size := uint32(32768) |
| 130 | buffer := make([]uint16, size) |
| 131 | if err := windows.QueryFullProcessImageName(process, 0, &buffer[0], &size); err != nil { |
| 132 | return "", err |
| 133 | } |
| 134 | state, err := windows.WaitForSingleObject(process, 0) |
| 135 | if err != nil || state != uint32(windows.WAIT_TIMEOUT) { |
| 136 | return "", fmt.Errorf("desktop endpoint process exited") |
| 137 | } |
| 138 | var written uint32 |
| 139 | if err := windows.WriteFile(pipe, []byte{1}, &written, nil); err != nil { |
| 140 | return "", fmt.Errorf("desktop endpoint closed during inspection: %w", err) |
| 141 | } |
| 142 | if written != 1 { |
| 143 | return "", fmt.Errorf("desktop endpoint acknowledgement: %w", io.ErrShortWrite) |
| 144 | } |
| 145 | return windows.UTF16ToString(buffer[:size]), nil |
| 146 | } |
| 147 |