Skip to content

Implemented basic Windows support (not WSL) - #194

Merged
janpfeifer merged 3 commits into
janpfeifer:mainfrom
StarWindv:windows-support
Jul 27, 2026
Merged

Implemented basic Windows support (not WSL)#194
janpfeifer merged 3 commits into
janpfeifer:mainfrom
StarWindv:windows-support

Conversation

@StarWindv

Copy link
Copy Markdown
Contributor

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.go file 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 client gonbui does not need to be modified.

ProcessManagement: Remove the Unix-specific syscall.SIGKILL and replace it with the cross-platform cmd.Process.Kill().

CompilationResult: Automatically adds the .exe extension 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 /c instead.
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:
image

@janpfeifer janpfeifer left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread internal/goexec/goplsclient/exec.go Outdated
// Not a :0 address, use as-is with tcp; prefix.
return "tcp;" + addr
}
// Find a free port by listening on :0 and closing.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! I was a bit sloppy there, to be honest.

"os/exec"
)

func setNewProcessGroup(cmd *exec.Cmd) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, after some investigation, don't you want to do the following in Windows:

cmd.SysProcAttr = &syscall.SysProcAttr{
    CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}

?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow

@janpfeifer janpfeifer Jul 26, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never dived into the Windows native APIs, so I gave the file to Gemini to review, and it had the following comments:

"

  1. 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.

  1. 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).

  1. 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(&params)),
		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
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@StarWindv
StarWindv requested a review from janpfeifer July 26, 2026 13:20

@janpfeifer janpfeifer left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@janpfeifer
janpfeifer merged commit f56378e into janpfeifer:main Jul 27, 2026
1 check passed
@StarWindv
StarWindv deleted the windows-support branch July 27, 2026 07:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants