Implemented basic Windows support (not WSL) - #194
Conversation
Added a comment explaining that setNewProcessGroup is a no-op for Windows.
janpfeifer
left a comment
There was a problem hiding this comment.
Thank you Changfeng (@StarWindv ). It is awesome to have GoNB working in Windows.
I added a few minor requests for change, I hope you don't mind.
cheers
| // Not a :0 address, use as-is with tcp; prefix. | ||
| return "tcp;" + addr | ||
| } | ||
| // Find a free port by listening on :0 and closing. |
There was a problem hiding this comment.
What about refactoring this into its own function:
// GetFreePort asks the OS for an available port on localhost.
// It works on dual-stack IPv4/IPv6 on the loopback interface.
func GetFreePort() (int, error) {
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
return 0, err
}
defer l.Close()
return l.Addr().(*net.TCPAddr).Port, nil
}There was a problem hiding this comment.
Good idea! I was a bit sloppy there, to be honest.
| "os/exec" | ||
| ) | ||
|
|
||
| func setNewProcessGroup(cmd *exec.Cmd) { |
There was a problem hiding this comment.
Actually, after some investigation, don't you want to do the following in Windows:
cmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}?
There was a problem hiding this comment.
I never dived into the Windows native APIs, so I gave the file to Gemini to review, and it had the following comments:
"
- Broken RTL_USER_PROCESS_PARAMETERS struct layout
The struct definition is missing field offsets. CurrentDirectoryPath is actually an RTL_DRIVE_LETTER_CURDIR or UNICODE_STRING wrapped inside CURDIR, located at offset 0x38 on 64-bit Windows. Your layout maps to entirely wrong offsets.
CurrentDirectory string in the struct is an invalid C-struct mapping—Go strings contain headers (uintptr + int) which breaks layout spacing and memory layout alignment.
- 32-bit / 64-bit Architecture Discrepancies
PEB offsets differ between 32-bit (x86) and 64-bit (x64) architectures. uintptr does not safely abstract the PEB structure across architecture boundaries or WOW64 (32-bit process reading 64-bit process or vice versa).
- Error Handling on System Calls
procNtQueryInformationProcess.Call() returns an NTSTATUS as its first return value (r1). You are ignoring the status code and assuming success if PebBaseAddress != 0.
"
And it suggests the following code, with the caveats:
- I didn't test it, I don't have easy access to Windows here.
- It is for 64-bits only, so I added the tag
!386
//go:build windows && !386
package main
import "golang.org/x/sys/windows"
type unicodeString struct {
Length uint16
MaximumLength uint16
_ uint32 // Padding on x64
Buffer uintptr
}
type processBasicInformation struct {
ExitStatus uintptr
PebBaseAddress uintptr
AffinityMask uintptr
BasePriority uintptr
UniqueProcessID uintptr
InheritedFromUniqueProcessID uintptr
}
// Partial RTL_USER_PROCESS_PARAMETERS layout for 64-bit Windows.
type rtlUserProcessParameters struct {
_ [32]byte
_ [3]windows.Handle
CurrentDirectoryPath unicodeString // Offset 0x38 on x64
}
func CurrentWorkingDirectoryForPid(pid int) (string, error) {
handle, err := windows.OpenProcess(
windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ,
false,
uint32(pid),
)
if err != nil {
return "", err
}
defer windows.CloseHandle(handle)
// 1. Fetch PEB Base Address via NtQueryInformationProcess
var pbi processBasicInformation
var retLen uint32
status := windows.NtQueryInformationProcess(
handle,
0, // ProcessBasicInformation
uintptr(windows.Pointer(&pbi)),
uint32(windows.Sizeof(pbi)),
&retLen,
)
if status != 0 {
return "", windows.NTStatus(status)
}
// 2. Read PEB -> ProcessParameters pointer (offset 0x20 on x64)
var procParamsPtr uintptr
err = windows.ReadProcessMemory(
handle,
pbi.PebBaseAddress+0x20,
(*byte)(windows.Pointer(&procParamsPtr)),
windows.Sizeof(procParamsPtr),
nil,
)
if err != nil {
return "", err
}
// 3. Read RTL_USER_PROCESS_PARAMETERS
var params rtlUserProcessParameters
err = windows.ReadProcessMemory(
handle,
procParamsPtr,
(*byte)(windows.Pointer(¶ms)),
windows.Sizeof(params),
nil,
)
if err != nil {
return "", err
}
if params.CurrentDirectoryPath.Buffer == 0 || params.CurrentDirectoryPath.Length == 0 {
return "", windows.ERROR_NOT_FOUND
}
// 4. Read UTF-16 CWD Buffer
buf := make([]uint16, params.CurrentDirectoryPath.Length/2)
err = windows.ReadProcessMemory(
handle,
params.CurrentDirectoryPath.Buffer,
(*byte)(windows.Pointer(&buf[0])),
uintptr(params.CurrentDirectoryPath.Length),
nil,
)
if err != nil {
return "", err
}
return windows.UTF16ToString(buf), nil
}There was a problem hiding this comment.
Ah, my bad — this was an oversight on my part. I'd only been running the program as a standard user and forgot to test it with admin privileges, sorry about that
Extract the port search as an independent function. Implement the process group.
janpfeifer
left a comment
There was a problem hiding this comment.
Thanks again!
I'm not familar with the WindowNT APIs, so to tell you truth I'm just copy&pasting what gemini says 😄 , but reading it, it looks right to me. I hope it's working in windows. I'll add a note to the REAME.md file.
Add native Windows support (not WSL) for gonb, allowing Windows users to use it directly without the need for Docker or WSL.
NamedPipes: The
namedpipes.gofile has been split into a platform-independent dispatch layer, as well as two sets of implementations for Unix FIFO and Windows named pipes (\\.\pipe\gonb_xxx). The clientgonbuidoes not need to be modified.ProcessManagement: Remove the Unix-specific
syscall.SIGKILLand replace it with the cross-platformcmd.Process.Kill().CompilationResult: Automatically adds the
.exeextension on Windows.Process CWD query: On Windows, the actual working directory is obtained using
NtQueryInformationProcess, and if the query fails, the directory where the executable file is located is used as a fallback.ShellCommand: On Windows, use
cmd.exe /cinstead.GoplsCompatibility: On Windows, use TCP addresses instead of Unix sockets.
It should be noted that, due to the limited number of machines I have at my disposal, I am unable to determine whether certain actions have had any impact on the behavior of other platforms.
Here is an example picture:
