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
109 changes: 89 additions & 20 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,49 +1,118 @@
version: "2"
output:
sort-order:
- file
linters:
default: none
enable:
- bidichk
- bodyclose
- dogsled
- dupl
- depguard
- errcheck
- exhaustive
- goconst
- forbidigo
- gocheckcompilerdirectives
- gocritic
- gocyclo
- goprintffuncname
- gosec
- govet
- ineffassign
- misspell
- mirror
- modernize
- nakedret
- noctx
- nilnil
- nolintlint
- rowserrcheck
- perfsprint
- revive
- staticcheck
- testifylint
- unconvert
- unparam
- unused
- whitespace
- usestdlibvars
- usetesting
- wastedassign
settings:
depguard:
rules:
main:
deny:
- pkg: io/ioutil
desc: use os or io instead
- pkg: golang.org/x/exp
desc: it's experimental and unreliable
- pkg: github.com/pkg/errors
desc: use builtin errors package instead
nolintlint:
allow-unused: false
require-explanation: true
require-specific: true
gocritic:
enabled-checks:
- equalFold
disabled-checks: []
revive:
severity: error
rules:
- name: blank-imports
- name: constant-logical-expr
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: empty-lines
- name: error-return
- name: error-strings
- name: exported
- name: identical-branches
- name: if-return
- name: increment-decrement
- name: modifies-value-receiver
- name: package-comments
- name: redefines-builtin-id
- name: superfluous-else
- name: time-naming
- name: unexported-return
- name: var-declaration
- name: var-naming
disabled: true
staticcheck:
checks:
- all
testifylint: {}
usetesting:
os-temp-dir: true
forbidigo:
forbid:
- pattern: "^(print|println)$"
msg: "use fmt.Print* instead of built-in print/println"
perfsprint:
concat-loop: false
govet:
enable:
- nilness
- unusedwrite
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
rules:
- linters:
- errcheck
- staticcheck
- unparam
path: _test\.go
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofmt
- gofumpt
- goimports
- golines
settings:
gofumpt:
extra-rules: true
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
run:
timeout: 10m
16 changes: 10 additions & 6 deletions filelock.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import (
"time"
)

const (
lockMaxRetries = 50
lockRetryDelay = 100 * time.Millisecond
staleLockTimeout = 30 * time.Second
)

// fileLock represents a file lock.
type fileLock struct {
lockFile *os.File
Expand All @@ -16,10 +22,8 @@ type fileLock struct {
// Uses a separate lock file to coordinate access across processes.
func acquireFileLock(filePath string) (*fileLock, error) {
lockPath := filePath + ".lock"
maxRetries := 50
retryDelay := 100 * time.Millisecond

for i := 0; i < maxRetries; i++ {
for range lockMaxRetries {
lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err == nil {
fmt.Fprintf(lockFile, "%d", os.Getpid())
Expand All @@ -31,7 +35,7 @@ func acquireFileLock(filePath string) (*fileLock, error) {

if os.IsExist(err) {
if info, statErr := os.Stat(lockPath); statErr == nil {
if time.Since(info.ModTime()) > 30*time.Second {
if time.Since(info.ModTime()) > staleLockTimeout {
if remErr := os.Remove(lockPath); remErr != nil && !os.IsNotExist(remErr) {
return nil, fmt.Errorf(
"failed to remove stale lock file %s: %w",
Expand All @@ -42,7 +46,7 @@ func acquireFileLock(filePath string) (*fileLock, error) {
continue
}
}
time.Sleep(retryDelay)
time.Sleep(lockRetryDelay)
continue
}

Expand All @@ -51,7 +55,7 @@ func acquireFileLock(filePath string) (*fileLock, error) {

return nil, fmt.Errorf(
"timeout waiting for file lock after %v",
time.Duration(maxRetries)*retryDelay,
time.Duration(lockMaxRetries)*lockRetryDelay,
)
}

Expand Down
2 changes: 1 addition & 1 deletion filelock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestConcurrentLocks(t *testing.T) {
var mu sync.Mutex
concurrent := 0

for i := 0; i < goroutines; i++ {
for i := range goroutines {
wg.Add(1)
go func(idx int) {
defer wg.Done()
Expand Down
41 changes: 23 additions & 18 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func getEnv(key, defaultValue string) string {
// validateServerURL validates that the server URL is properly formatted
func validateServerURL(rawURL string) error {
if rawURL == "" {
return fmt.Errorf("server URL cannot be empty")
return errors.New("server URL cannot be empty")
}

u, err := url.Parse(rawURL)
Expand All @@ -170,7 +170,7 @@ func validateServerURL(rawURL string) error {
}

if u.Host == "" {
return fmt.Errorf("URL must include a host")
return errors.New("URL must include a host")
}

return nil
Expand All @@ -182,12 +182,12 @@ type ErrorResponse struct {
}

// ErrRefreshTokenExpired indicates that the refresh token has expired or is invalid
var ErrRefreshTokenExpired = fmt.Errorf("refresh token expired or invalid")
var ErrRefreshTokenExpired = errors.New("refresh token expired or invalid")

// validateTokenResponse validates the OAuth token response
func validateTokenResponse(accessToken, tokenType string, expiresIn int) error {
if accessToken == "" {
return fmt.Errorf("access_token is empty")
return errors.New("access_token is empty")
}

if len(accessToken) < 10 {
Expand Down Expand Up @@ -330,7 +330,7 @@ func requestDeviceCode(ctx context.Context) (*oauth2.DeviceAuthResponse, error)

req, err := http.NewRequestWithContext(
reqCtx,
"POST",
http.MethodPost,
serverURL+"/oauth/device/code",
strings.NewReader(data.Encode()),
)
Expand Down Expand Up @@ -491,21 +491,20 @@ func pollForTokenWithProgress(
case "slow_down":
// Server requests slower polling - increase interval
backoffMultiplier *= 1.5
newInterval := time.Duration(float64(pollInterval) * backoffMultiplier)
if newInterval > 60*time.Second {
newInterval = 60 * time.Second // Cap at 60 seconds
}
pollInterval = newInterval
pollInterval = min(
time.Duration(float64(pollInterval)*backoffMultiplier),
60*time.Second,
)
pollTicker.Reset(pollInterval)
continue

case "expired_token":
fmt.Println()
return nil, fmt.Errorf("device code expired, please restart the flow")
return nil, errors.New("device code expired, please restart the flow")

case "access_denied":
fmt.Println()
return nil, fmt.Errorf("user denied authorization")
return nil, errors.New("user denied authorization")

default:
fmt.Println()
Expand Down Expand Up @@ -558,7 +557,7 @@ func exchangeDeviceCode(

req, err := http.NewRequestWithContext(
reqCtx,
"POST",
http.MethodPost,
tokenURL,
strings.NewReader(data.Encode()),
)
Expand Down Expand Up @@ -623,7 +622,9 @@ func verifyToken(ctx context.Context, accessToken string) error {
reqCtx, cancel := context.WithTimeout(ctx, tokenVerificationTimeout)
defer cancel()

req, err := http.NewRequestWithContext(reqCtx, "GET", serverURL+"/oauth/tokeninfo", nil)
req, err := http.NewRequestWithContext(
reqCtx, http.MethodGet, serverURL+"/oauth/tokeninfo", nil,
)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
Expand Down Expand Up @@ -666,7 +667,7 @@ func loadTokens() (*TokenStorage, error) {
}

if storageMap.Tokens == nil {
return nil, fmt.Errorf("no tokens found in token file")
return nil, errors.New("no tokens found in token file")
}

// Look up token for current client_id
Expand Down Expand Up @@ -755,7 +756,7 @@ func refreshAccessToken(ctx context.Context, refreshToken string) (*TokenStorage

req, err := http.NewRequestWithContext(
reqCtx,
"POST",
http.MethodPost,
serverURL+"/oauth/token",
strings.NewReader(data.Encode()),
)
Expand Down Expand Up @@ -840,7 +841,9 @@ func makeAPICallWithAutoRefresh(ctx context.Context, storage *TokenStorage) erro
reqCtx, cancel := context.WithTimeout(ctx, tokenVerificationTimeout)
defer cancel()

req, err := http.NewRequestWithContext(reqCtx, "GET", serverURL+"/oauth/tokeninfo", nil)
req, err := http.NewRequestWithContext(
reqCtx, http.MethodGet, serverURL+"/oauth/tokeninfo", nil,
)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
Expand Down Expand Up @@ -877,7 +880,9 @@ func makeAPICallWithAutoRefresh(ctx context.Context, storage *TokenStorage) erro
retryCtx, retryCancel := context.WithTimeout(ctx, tokenVerificationTimeout)
defer retryCancel()

req, err = http.NewRequestWithContext(retryCtx, "GET", serverURL+"/oauth/tokeninfo", nil)
req, err = http.NewRequestWithContext(
retryCtx, http.MethodGet, serverURL+"/oauth/tokeninfo", nil,
)
if err != nil {
return fmt.Errorf("failed to create retry request: %w", err)
}
Expand Down
Loading
Loading