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
40 changes: 40 additions & 0 deletions internal/desktopruntime/quick_tunnel_log.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package desktopruntime

import (
"errors"
"io"
"os"
"regexp"
"strings"
)
Expand Down Expand Up @@ -34,3 +37,40 @@ func findQuickTunnelURL(log []byte) string {
}
return ""
}

type quickTunnelLogCursor struct {
size int64
}

func captureQuickTunnelLogCursor(path string) (quickTunnelLogCursor, error) {
info, err := os.Stat(path)
if errors.Is(err, os.ErrNotExist) {
return quickTunnelLogCursor{}, nil
}
if err != nil {
return quickTunnelLogCursor{}, err
}
return quickTunnelLogCursor{size: info.Size()}, nil
}

func readQuickTunnelLogSince(path string, cursor quickTunnelLogCursor) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()

info, err := file.Stat()
if err != nil {
return nil, err
}
offset := cursor.size
// 轮转或截断后 active log 会比启动前更短,此时新一代日志从文件开头读取。
if info.Size() < offset {
offset = 0
}
if _, err := file.Seek(offset, io.SeekStart); err != nil {
return nil, err
}
return io.ReadAll(file)
}
67 changes: 66 additions & 1 deletion internal/desktopruntime/quick_tunnel_log_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package desktopruntime

import "testing"
import (
"os"
"path/filepath"
"strings"
"testing"
)

func TestFindQuickTunnelURL(t *testing.T) {
tests := []struct {
Expand Down Expand Up @@ -43,3 +48,63 @@ func TestFindQuickTunnelURL(t *testing.T) {
})
}
}

func TestReadQuickTunnelLogSinceSkipsPreviousGeneration(t *testing.T) {
path := filepath.Join(t.TempDir(), "cloudflared.err.log")
oldLog := "INF Your quick Tunnel has been created! Visit it at:\nhttps://old.trycloudflare.com\n"
if err := os.WriteFile(path, []byte(oldLog), 0o600); err != nil {
t.Fatal(err)
}
cursor, err := captureQuickTunnelLogCursor(path)
if err != nil {
t.Fatal(err)
}

newLog := "INF Your quick Tunnel has been created! Visit it at:\nhttps://new.trycloudflare.com\n"
file, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o600)
if err != nil {
t.Fatal(err)
}
if _, err := file.WriteString(newLog); err != nil {
_ = file.Close()
t.Fatal(err)
}
if err := file.Close(); err != nil {
t.Fatal(err)
}

data, err := readQuickTunnelLogSince(path, cursor)
if err != nil {
t.Fatal(err)
}
if got := findQuickTunnelURL(data); got != "https://new.trycloudflare.com" {
t.Fatalf("findQuickTunnelURL(new generation) = %q", got)
}
}

func TestReadQuickTunnelLogSinceReadsResetLogFromStart(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "cloudflared.err.log")
if err := os.WriteFile(path, []byte(strings.Repeat("x", 1024)), 0o600); err != nil {
t.Fatal(err)
}
cursor, err := captureQuickTunnelLogCursor(path)
if err != nil {
t.Fatal(err)
}
if err := os.Rename(path, path+".1"); err != nil {
t.Fatal(err)
}
newLog := "INF Your quick Tunnel has been created! Visit it at:\nhttps://rotated.trycloudflare.com\n"
if err := os.WriteFile(path, []byte(newLog), 0o600); err != nil {
t.Fatal(err)
}

data, err := readQuickTunnelLogSince(path, cursor)
if err != nil {
t.Fatal(err)
}
if got := findQuickTunnelURL(data); got != "https://rotated.trycloudflare.com" {
t.Fatalf("findQuickTunnelURL(rotated generation) = %q", got)
}
}
46 changes: 39 additions & 7 deletions internal/desktopruntime/tunnel_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,23 @@ func platformTunnelAction(ctx context.Context, runtimeRoot, action string) error
}
}

type quickTunnelLogCursors struct {
stdout quickTunnelLogCursor
stderr quickTunnelLogCursor
}

func captureQuickTunnelLogCursors(files tunnelFiles) (quickTunnelLogCursors, error) {
stdout, err := captureQuickTunnelLogCursor(files.stdoutLog)
if err != nil {
return quickTunnelLogCursors{}, fmt.Errorf("记录 cloudflared stdout 日志位置失败: %w", err)
}
stderr, err := captureQuickTunnelLogCursor(files.stderrLog)
if err != nil {
return quickTunnelLogCursors{}, fmt.Errorf("记录 cloudflared stderr 日志位置失败: %w", err)
}
return quickTunnelLogCursors{stdout: stdout, stderr: stderr}, nil
}

func startTunnel(ctx context.Context, runtime tunnelRuntime) error {
if runtime.mode == "none" {
return nil
Expand All @@ -122,12 +139,14 @@ func startTunnel(ctx context.Context, runtime tunnelRuntime) error {
return readyErr
}
if readyURL == "" {
return finalizeQuickTunnel(ctx, runtime)
// 已有进程可能早于当前控制命令启动,此时需要从完整日志恢复 ready URL。
return finalizeQuickTunnel(ctx, runtime, quickTunnelLogCursors{})
}
}
return nil
}

logCursors := quickTunnelLogCursors{}
if runtime.mode == "quick" {
// 旧临时地址在新进程真正拿到 URL 前不能继续暴露为 ready。
if err := clearActivePublicURL(runtime.files); err != nil {
Expand All @@ -136,6 +155,12 @@ func startTunnel(ctx context.Context, runtime tunnelRuntime) error {
if err := runtime.updateManifest("none", ""); err != nil {
return err
}
// cloudflared 日志按设计持续追加;记录本轮启动前的位置,避免 regenerate 把历史 URL 当成新地址。
var err error
logCursors, err = captureQuickTunnelLogCursors(runtime.files)
if err != nil {
return err
}
}
if err := launchCloudflared(runtime); err != nil {
return err
Expand All @@ -144,7 +169,7 @@ func startTunnel(ctx context.Context, runtime tunnelRuntime) error {
return err
}
if runtime.mode == "quick" {
return finalizeQuickTunnel(ctx, runtime)
return finalizeQuickTunnel(ctx, runtime, logCursors)
}
return nil
}
Expand Down Expand Up @@ -214,8 +239,8 @@ func cloudflaredCommand(ctx context.Context, runtime tunnelRuntime) (*exec.Cmd,
return command, nil
}

func finalizeQuickTunnel(ctx context.Context, runtime tunnelRuntime) error {
publicURL, err := waitQuickTunnelURL(ctx, runtime, 35*time.Second)
func finalizeQuickTunnel(ctx context.Context, runtime tunnelRuntime, cursors quickTunnelLogCursors) error {
publicURL, err := waitQuickTunnelURL(ctx, runtime, cursors, 35*time.Second)
if err != nil {
_ = StopBinaryProcesses(context.Background(), runtime.manifest.CloudflaredBinary, 5*time.Second)
return err
Expand All @@ -233,11 +258,18 @@ func finalizeQuickTunnel(ctx context.Context, runtime tunnelRuntime) error {
return writeRuntimeText(runtime.files.quickURL, publicURL)
}

func waitQuickTunnelURL(ctx context.Context, runtime tunnelRuntime, timeout time.Duration) (string, error) {
func waitQuickTunnelURL(ctx context.Context, runtime tunnelRuntime, cursors quickTunnelLogCursors, timeout time.Duration) (string, error) {
deadline := time.Now().Add(timeout)
logs := []struct {
path string
cursor quickTunnelLogCursor
}{
{path: runtime.files.stdoutLog, cursor: cursors.stdout},
{path: runtime.files.stderrLog, cursor: cursors.stderr},
}
for time.Now().Before(deadline) {
for _, path := range []string{runtime.files.stdoutLog, runtime.files.stderrLog} {
data, err := os.ReadFile(path)
for _, log := range logs {
data, err := readQuickTunnelLogSince(log.path, log.cursor)
if err == nil {
if publicURL := findQuickTunnelURL(data); publicURL != "" {
return publicURL, nil
Expand Down
Loading