mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-04 11:51:17 +02:00
tstest/integration: use linkat to hardlink test binaries on Linux
Use linkat via /proc/self/fd with AT_SYMLINK_FOLLOW to create a hardlink of the test binary instead of copying it. This avoids copying ~50MB+ binaries into each test's temp directory, making test setup faster and reducing disk I/O. The simpler os.Link(b.Path, ret.Path) can't be used here because the source binary lives in the first test's TempDir, which may be cleaned up before later tests call CopyTo. The open FD keeps the inode alive after the path is deleted, but os.Link needs a valid path. (See also b9f468240f which tried os.Link but is racy for this reason.) The /proc/self/fd approach works without elevated privileges, unlike AT_EMPTY_PATH which requires CAP_DAC_READ_SEARCH. If the linkat fails for any reason (e.g. cross-filesystem temp dirs), it falls back to the existing full-copy path. Fixes #19397 Change-Id: I4b1f97f7e63a9ae9e09dce36dfbdd1f6cff92320 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
621dc9cf1b
commit
a0a8fae856
@ -73,7 +73,11 @@ type Binaries struct {
|
||||
|
||||
// BinaryInfo describes a tailscale or tailscaled binary.
|
||||
type BinaryInfo struct {
|
||||
Path string // abs path to tailscale or tailscaled binary
|
||||
// Path is the absolute path to the tailscale or tailscaled binary.
|
||||
// This path may become invalid after the owning test's TempDir is
|
||||
// cleaned up; use FD (or Contents on Windows) to access the binary
|
||||
// contents.
|
||||
Path string
|
||||
Size int64
|
||||
|
||||
// FD and FDmu are set on Unix to efficiently copy the binary to a new
|
||||
@ -88,16 +92,24 @@ type BinaryInfo struct {
|
||||
Contents []byte
|
||||
}
|
||||
|
||||
// CopyTo copies or hardlinks the binary into dir, returning a new BinaryInfo
|
||||
// with an updated Path. The source bytes come from FD (or Contents on Windows),
|
||||
// not from b.Path, which may have been deleted when its owning test's TempDir
|
||||
// was cleaned up.
|
||||
func (b BinaryInfo) CopyTo(dir string) (BinaryInfo, error) {
|
||||
ret := b
|
||||
ret.Path = filepath.Join(dir, path.Base(b.Path))
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
// TODO(bradfitz): be fancy and use linkat with AT_EMPTY_PATH to avoid
|
||||
// copying? I couldn't get it to work, though.
|
||||
// For now, just do the same thing as every other Unix and copy
|
||||
// the binary.
|
||||
// Try to hardlink from the open FD via /proc/self/fd, avoiding a
|
||||
// full copy of the binary. We can't use os.Link(b.Path, ret.Path)
|
||||
// because b.Path is in the first test's TempDir, which may be
|
||||
// cleaned up before later tests call CopyTo. The open FD keeps the
|
||||
// inode alive after the path is deleted.
|
||||
if err := tryLinkat(b.FD, ret.Path); err == nil {
|
||||
return ret, nil
|
||||
}
|
||||
fallthrough
|
||||
case "darwin", "freebsd", "openbsd", "netbsd":
|
||||
f, err := os.OpenFile(ret.Path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o755)
|
||||
|
||||
24
tstest/integration/integration_linkat_linux.go
Normal file
24
tstest/integration/integration_linkat_linux.go
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// tryLinkat attempts to hardlink the file referenced by fd to newpath,
|
||||
// avoiding a full copy of the binary. It uses /proc/self/fd/<N> with
|
||||
// AT_SYMLINK_FOLLOW, which works without elevated privileges (unlike
|
||||
// AT_EMPTY_PATH which requires CAP_DAC_READ_SEARCH).
|
||||
func tryLinkat(fd *os.File, newpath string) error {
|
||||
procPath := fmt.Sprintf("/proc/self/fd/%d", fd.Fd())
|
||||
err := unix.Linkat(unix.AT_FDCWD, procPath, unix.AT_FDCWD, newpath, unix.AT_SYMLINK_FOLLOW)
|
||||
if err != nil {
|
||||
return fmt.Errorf("linkat via /proc/self/fd: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
48
tstest/integration/integration_linkat_linux_test.go
Normal file
48
tstest/integration/integration_linkat_linux_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestTryLinkat(t *testing.T) {
|
||||
src := filepath.Join(t.TempDir(), "src")
|
||||
if err := os.WriteFile(src, []byte("hello world"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fd, err := os.Open(src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
dst := filepath.Join(t.TempDir(), "dst")
|
||||
if err := tryLinkat(fd, dst); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(dst)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "hello world" {
|
||||
t.Fatalf("got %q, want %q", got, "hello world")
|
||||
}
|
||||
|
||||
var stSrc, stDst unix.Stat_t
|
||||
if err := unix.Stat(src, &stSrc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := unix.Stat(dst, &stDst); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stSrc.Ino != stDst.Ino {
|
||||
t.Fatalf("inodes differ: src=%d, dst=%d", stSrc.Ino, stDst.Ino)
|
||||
}
|
||||
}
|
||||
15
tstest/integration/integration_linkat_other.go
Normal file
15
tstest/integration/integration_linkat_other.go
Normal file
@ -0,0 +1,15 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
func tryLinkat(_ *os.File, _ string) error {
|
||||
return errors.New("linkat with AT_EMPTY_PATH not supported on this OS")
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user