Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 124 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,20 @@ on:
- "main"
pull_request:

concurrency:
# For PRs, later CI runs preempt previous ones. e.g. a force push on a PR
# cancels running CI jobs and starts all new ones.
#
# For non-PR pushes, concurrency.group needs to be unique for every distinct
# CI run we want to have happen. Use run_id, which in practice means all
# non-PR CI runs will be allowed to run without preempting each other.
group: ${{ github.workflow }}-$${{ github.pull_request.number || github.run_id }}
cancel-in-progress: true

jobs:
test:
strategy:
fail-fast: false # don't abort the entire matrix if one element fails
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
Expand All @@ -19,14 +30,11 @@ jobs:
with:
go-version-file: go.mod

- name: "[windows] install handle.exe"
if: runner.os == 'Windows'
run: choco install handle

- run: go test ./...

nfs-test:
strategy:
fail-fast: false # don't abort the entire matrix if one element fails
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -67,3 +75,115 @@ jobs:
- name: "mount gomodfs NFS"
run: |
go run ./testing/nfsmount

- name: "[unix] list GOMODCACHE directory"
if: runner.os == 'Linux' || runner.os == 'macOS'
run: |
ls -l $GOMODCACHE

- name: "[windows] list GOMODCACHE directory"
if: runner.os == 'Windows'
shell: powershell
run: |
dir $env:GOMODCACHE

- name: "show gomodfs status (1)"
shell: bash
run: |
cat $GOMODCACHE/.gomodfs-status

# so go.mod go version differences in testdata go.mod files don't
# cause Go to download different Go toolchains and fail to write
# to GOMODCACHE.
- name: disable GOTOOLCHAIN downloads
run: echo "GOTOOLCHAIN=local" >> $GITHUB_ENV

- name: "run go env"
working-directory: testdata/exoticmod
run: |
go env

# NFS can't handle UTF-8 filenames on Windows, so skip this step there.
- name: "[unix] run go mod verify"
if: runner.os != 'Windows'
working-directory: testdata/exoticmod
run: |
go mod verify

# NFS can't handle UTF-8 filenames on Windows, so skip this step there.
- name: "[windows] run go mod verify"
if: runner.os == 'Windows'
working-directory: testdata/go4mod
run: |
go mod verify

- name: "show gomodfs status (2)"
shell: bash
run: |
cat $GOMODCACHE/.gomodfs-status

- name: "verify used"
run: |
go test -v ./testing/ci '-verify-used'

winfsp-test:
runs-on: windows-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5
with:
go-version-file: go.mod

- name: build gomodfs
run: |
go build -o gomodfs.exe ./cmd/gomodfs

- name: "[windows] install winfsp"
if: runner.os == 'Windows'
shell: powershell
run: |
winget install --id=WinFsp.WinFsp --source=winget

- name: "start gomodfs winfsp server"
run: |
go run ./testing/startgomodfs --winfsp

- name: "set GOMODCACHE"
shell: powershell
run: |
echo "GOMODCACHE=M:\" >> $env:GITHUB_ENV

# so go.mod go version differences in testdata go.mod files don't
# cause Go to download different Go toolchains and fail to write
# to GOMODCACHE.
- name: disable GOTOOLCHAIN downloads
shell: powershell
run: echo "GOTOOLCHAIN=local" >> $env:GITHUB_ENV

- name: list drives
shell: powershell
run: Get-PSDrive -PSProvider FileSystem

- name: "list GOMODCACHE directory"
shell: powershell
run: |
dir $env:GOMODCACHE

- name: "show gomodfs status (1)"
shell: powershell
run: |
Get-Content -Path $env:GOMODCACHE\.gomodfs-status

- name: "run go mod verify"
working-directory: testdata/exoticmod
run: go mod verify

- name: "show gomodfs status (2)"
shell: powershell
run: |
Get-Content -Path $env:GOMODCACHE\.gomodfs-status

- name: "verify used"
run: |
go test -v ./testing/ci '-verify-used'
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ benchcorp:
cd "$${HOME}/src/tailscale.io"; \
echo "corp test build..."; \
time go test -exec=true tailscale.io/...; \

pushbradwin:
GOOS=windows GOARCH=amd64 go build -o /Volumes/home/gomodfs.exe ./cmd/gomodfs
10 changes: 5 additions & 5 deletions cmd/gomodfs/gomodfs-main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func main() {
cmd.Run() // best effort

mntDir := *flagMountPoint
if mntDir != "" {
if mntDir != "" && runtime.GOOS != "windows" {
exec.Command("umount", mntDir).Run() // best effort
if os.Getenv("GOOS") == "darwin" {
exec.Command("diskutil", "unmount", "force", mntDir).Run() // best effort
Expand Down Expand Up @@ -108,6 +108,10 @@ func main() {

debugMux := http.NewServeMux()
debugMux.Handle("/metrics", metricsHandler)
debugMux.HandleFunc("/status.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write(mfs.StatusJSON())
})
debugMux.Handle("/", mfs)
debugMux.HandleFunc("/debug/pprof/", pprof.Index)
debugMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
Expand Down Expand Up @@ -147,10 +151,6 @@ func main() {
mntDir = "M:"
}

if runtime.GOOS == "windows" && *flagWinFSP && mntDir == "" {
mntDir = "M:"
}

if mntDir == "" {
log.Printf("Not mounting filesystem, use --mount flag to specify mount point")
select {}
Expand Down
2 changes: 1 addition & 1 deletion fuse.go
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ type statusFileNode struct {
}

func (n *statusFileNode) Open(_ context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) {
return &statusFH{json: n.fs.statusJSON()}, fuse.FOPEN_DIRECT_IO, 0
return &statusFH{json: n.fs.StatusJSON()}, fuse.FOPEN_DIRECT_IO, 0
}

type statusFH struct {
Expand Down
6 changes: 2 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
module github.com/tailscale/gomodfs

go 1.24.4

toolchain go1.24.6
go 1.25.1

require (
github.com/aegistudio/go-winfsp v1.0.1
Expand All @@ -19,6 +17,7 @@ require (
golang.org/x/mod v0.26.0
golang.org/x/net v0.42.0
golang.org/x/sync v0.16.0
golang.org/x/sys v0.34.0
tailscale.com v1.86.4
)

Expand All @@ -41,7 +40,6 @@ require (
github.com/sergi/go-diff v1.4.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/sys v0.34.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
13 changes: 8 additions & 5 deletions gomodfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,9 @@ func (fs *FS) addPathHashTargetLocked(path string) {

var procStart = time.Now()

type procStat struct {
// StatusJSON is the JSON type of the <root>/.gomodfs-status file and the debug
// HTTP handler's /status.json endpoint.
type StatusJSON struct {
Filesystem string `json:"filesystem"`
Uptime float64 `json:"uptime"` // seconds since process start
Ops map[string]*stats.OpStat `json:"ops,omitzero"`
Expand Down Expand Up @@ -650,10 +652,11 @@ func (s *FS) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.Stats.ServeHTTP(w, r)
}

// statusJSON returns the JSON-encoded status
// of the <root>/.gomodfs-status file.
func (f *FS) statusJSON() []byte {
stj, _ := json.MarshalIndent(procStat{
// StatusJSON returns the JSON-encoded status
// of the <root>/.gomodfs-status file and the debug
// HTTP handler's /status.json endpoint.
func (f *FS) StatusJSON() []byte {
stj, _ := json.MarshalIndent(StatusJSON{
Filesystem: "gomodfs",
Uptime: time.Since(procStart).Seconds(),
Ops: f.Stats.Clone(),
Expand Down
72 changes: 72 additions & 0 deletions gomodfs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package gomodfs

import (
"fmt"
"net/http"
"os"
"strings"
"testing"
"time"

"github.com/tailscale/gomodfs/store"
"github.com/tailscale/gomodfs/store/gitstore"
)

// test for https://github.com/tailscale/gomodfs/issues/15a
func TestExoticZip(t *testing.T) {
gitCacheDir := testGitDir(t)
defer func() {
if t.Failed() && os.Getenv("CI") != "true" {
t.Logf("test failed; preserving git cache dir %q and pausing for inspection...", gitCacheDir)
time.Sleep(5 * time.Minute)
}
}()
st := &gitstore.Storage{GitRepo: gitCacheDir}
addStopGitStoreCleanup(t, st)
fs := &FS{
Store: st,
Client: &http.Client{
Transport: testDataTransport{},
},
Logf: t.Logf,
}

ctx := t.Context()
mv := store.ModuleVersion{
Module: "github.com/bramvdbogaerde/go-scp",
Version: "v1.4.0",
}
mh, err := fs.downloadZip(ctx, mv)
if err != nil {
t.Fatalf("downloadZip: %v", err)
}

zipHash, err := st.GetZipHash(ctx, mh)
if err != nil {
t.Fatalf("GetZipHash: %v", err)
}
if g, w := string(zipHash), "h1:jKMwpwCbcX1KyvDbm/PDJuXcMuNVlLGi0Q0reuzjyKY="; g != w {
t.Fatalf("zip hash = %q; want %q", g, w)
}

ents, err := st.Readdir(ctx, mh, "tests/data")
if err != nil {
t.Fatalf("Readdir: %v", err)
}
var gotBuf strings.Builder
for i, ent := range ents {
fmt.Fprintf(&gotBuf, "entry[%d]: %s, %v, size=%v\n", i, ent.Name, ent.Mode, ent.Size)
}
got := gotBuf.String()

want := `entry[0]: Exöt1ç download file.txt.txt, -rw-r--r--, size=23
entry[1]: another_file.txt, -rw-r--r--, size=50
entry[2]: upload_file.txt, -rw-r--r--, size=9
`
if got != want {
t.Fatalf("bad directory entries; got:\n%s\nwant:\n%s", got, want)
}
}
2 changes: 1 addition & 1 deletion nfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (h *NFSHandler) statusFile() *statusMeta {
if fs.statusCache != nil && time.Since(fs.statusCache.fi.ModTime()) < regenEvery {
return fs.statusCache
}
j := h.fs.statusJSON()
j := h.fs.StatusJSON()
fs.statusCache = &statusMeta{
fi: regFileInfo{
name: statusFile,
Expand Down
Loading