Skip to content

Commit 113c328

Browse files
authored
Port to and test on Windows (#3)
1 parent c720349 commit 113c328

11 files changed

Lines changed: 113 additions & 24 deletions

File tree

.github/workflows/go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
fail-fast: false
2121
matrix:
2222
go: ["1.25.x"]
23-
os: [ubuntu-24.04, macos-26]
23+
os: [ubuntu-24.04, macos-26, windows-2025]
2424

2525
steps:
2626
- name: Fix git settings on Windows

internal/caches/local/disk_other.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build !unix
1+
//go:build !unix && !windows
22

33
package local
44

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package local
2+
3+
import (
4+
"github.com/AlekSi/lazyerrors"
5+
"golang.org/x/sys/windows"
6+
)
7+
8+
// DiskInfo returns the total and free disk space in bytes.
9+
func DiskInfo(dir string) (total, free int64, err error) {
10+
directoryName, err := windows.UTF16PtrFromString(dir)
11+
if err != nil {
12+
return -1, -1, lazyerrors.Error(err)
13+
}
14+
15+
var freeBytesAvailableToCaller uint64
16+
var totalNumberOfBytes uint64
17+
var totalNumberOfFreeBytes uint64
18+
err = windows.GetDiskFreeSpaceEx(directoryName, &freeBytesAvailableToCaller, &totalNumberOfBytes, &totalNumberOfFreeBytes)
19+
if err != nil {
20+
return -1, -1, lazyerrors.Error(err)
21+
}
22+
23+
total = int64(totalNumberOfBytes)
24+
free = int64(totalNumberOfFreeBytes)
25+
return
26+
}

internal/go/lockedfile/internal/filelock/filelock.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package filelock
99

1010
import (
11+
"errors"
1112
"io/fs"
1213
)
1314

@@ -73,3 +74,10 @@ func (lt lockType) String() string {
7374
return "Unlock"
7475
}
7576
}
77+
78+
// IsNotSupported returns a boolean indicating whether the error is known to
79+
// report that a function is not supported (possibly for a specific input).
80+
// It is satisfied by errors.ErrUnsupported as well as some syscall errors.
81+
func IsNotSupported(err error) bool {
82+
return errors.Is(err, errors.ErrUnsupported)
83+
}

internal/go/lockedfile/internal/filelock/filelock_windows.go

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,51 @@
77
package filelock
88

99
import (
10-
"errors"
1110
"io/fs"
11+
12+
"golang.org/x/sys/windows"
1213
)
1314

1415
type lockType uint32
1516

1617
const (
1718
readLock lockType = 0
18-
writeLock lockType = 0x00000002
19+
writeLock lockType = windows.LOCKFILE_EXCLUSIVE_LOCK
20+
)
21+
22+
const (
23+
reserved = 0
24+
allBytes = ^uint32(0)
1925
)
2026

2127
func lock(f File, lt lockType) error {
22-
return &fs.PathError{
23-
Op: lt.String(),
24-
Path: f.Name(),
25-
Err: errors.ErrUnsupported,
28+
// Per https://golang.org/issue/19098, “Programs currently expect the Fd
29+
// method to return a handle that uses ordinary synchronous I/O.”
30+
// However, LockFileEx still requires an OVERLAPPED structure,
31+
// which contains the file offset of the beginning of the lock range.
32+
// We want to lock the entire file, so we leave the offset as zero.
33+
ol := new(windows.Overlapped)
34+
35+
err := windows.LockFileEx(windows.Handle(f.Fd()), uint32(lt), reserved, allBytes, allBytes, ol)
36+
if err != nil {
37+
return &fs.PathError{
38+
Op: lt.String(),
39+
Path: f.Name(),
40+
Err: err,
41+
}
2642
}
43+
return nil
2744
}
2845

2946
func unlock(f File) error {
30-
return &fs.PathError{
31-
Op: "Unlock",
32-
Path: f.Name(),
33-
Err: errors.ErrUnsupported,
47+
ol := new(windows.Overlapped)
48+
err := windows.UnlockFileEx(windows.Handle(f.Fd()), reserved, allBytes, allBytes, ol)
49+
if err != nil {
50+
return &fs.PathError{
51+
Op: "Unlock",
52+
Path: f.Name(),
53+
Err: err,
54+
}
3455
}
56+
return nil
3557
}

internal/go/lockedfile/lockedfile.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,9 @@ func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) {
5252
// especially don't want to leave a file locked after we're done with it. Our
5353
// Close method is what releases the locks, so use a cleanup to report
5454
// missing Close calls on a best-effort basis.
55-
f.cleanup = runtime.AddCleanup(
56-
f,
57-
func(fileName string) {
58-
panic(fmt.Sprintf("lockedfile.File %s became unreachable without a call to Close", fileName))
59-
},
60-
f.Name(),
61-
)
55+
f.cleanup = runtime.AddCleanup(f, func(fileName string) {
56+
panic(fmt.Sprintf("lockedfile.File %s became unreachable without a call to Close", fileName))
57+
}, f.Name())
6258

6359
return f, nil
6460
}

internal/go/lockedfile/lockedfile_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5+
// js and wasip1 do not support inter-process file locking.
6+
//
57
//go:build !js && !wasip1
68

79
package lockedfile_test
@@ -112,9 +114,9 @@ func TestReadWaitsForLock(t *testing.T) {
112114
t.Logf("WriteString(%q) = <nil>", part1)
113115

114116
wait := mustBlock(t, "Read", func() {
115-
b, err2 := lockedfile.Read(path)
116-
if err2 != nil {
117-
t.Errorf("Read: %v", err2)
117+
b, err := lockedfile.Read(path)
118+
if err != nil {
119+
t.Errorf("Read: %v", err)
118120
return
119121
}
120122

internal/go/lockedfile/transform_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5+
// js and wasip1 do not support inter-process file locking.
6+
//
57
//go:build !js && !wasip1
68

79
package lockedfile_test

internal/go/mmap/mmap_windows.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,42 @@
55
package mmap
66

77
import (
8+
"fmt"
89
"os"
10+
"unsafe"
11+
12+
"golang.org/x/sys/windows"
913
)
1014

1115
func mmapFile(f *os.File) (Data, error) {
12-
panic("not implemented yet")
16+
st, err := f.Stat()
17+
if err != nil {
18+
return Data{}, err
19+
}
20+
size := st.Size()
21+
if size == 0 {
22+
return Data{f, nil}, nil
23+
}
24+
h, err := windows.CreateFileMapping(windows.Handle(f.Fd()), nil, windows.PAGE_READONLY, 0, 0, nil)
25+
if err != nil {
26+
return Data{}, fmt.Errorf("CreateFileMapping %s: %w", f.Name(), err)
27+
}
28+
29+
addr, err := windows.MapViewOfFile(h, windows.FILE_MAP_READ, 0, 0, 0)
30+
if err != nil {
31+
return Data{}, fmt.Errorf("MapViewOfFile %s: %w", f.Name(), err)
32+
}
33+
var info windows.MemoryBasicInformation
34+
err = windows.VirtualQuery(addr, &info, unsafe.Sizeof(info))
35+
if err != nil {
36+
return Data{}, fmt.Errorf("VirtualQuery %s: %w", f.Name(), err)
37+
}
38+
data := unsafe.Slice((*byte)(unsafe.Pointer(addr)), int(info.RegionSize))
39+
if len(data) < int(size) {
40+
// In some cases, especially on 386, we may not receive a in incomplete mapping:
41+
// one that is shorter than the file itself. Return an error in those cases because
42+
// incomplete mappings are not useful.
43+
return Data{}, fmt.Errorf("mmapFile: received incomplete mapping of file")
44+
}
45+
return Data{f, data[:int(size)]}, nil
1346
}

0 commit comments

Comments
 (0)