Closes #44. Chrome now starts on first CDP connection, tracks concurrent helpers, reaps after 300 seconds idle, preserves the named profile, and classifies reap interruptions. Shutdown stops Chrome's process group so cookie batches flush. ADR-0005 records the measured constraints and decisions. Verification: docker build, live CDP wake, graceful stop cleanup, sh -n, and go test ./... (7 packages, 3 no tests). Reviewed-on: #50 Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com> Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
This commit was merged in pull request #50.
This commit is contained in:
@@ -178,6 +178,36 @@ func (f *BrowserFetcher) Image(ctx context.Context, imageID string) ([]byte, str
|
||||
// the poller answers with a 403 and its ordinary cooldown.
|
||||
var errChallengeHeld = errors.New("challenge held")
|
||||
|
||||
// errBrowserInterrupted distinguishes a remote Chrome restart from the
|
||||
// caller's own deadline. chromedp reports both as context.Canceled.
|
||||
var errBrowserInterrupted = errors.New("browser interrupted")
|
||||
|
||||
func classifyBrowserError(ctx context.Context, browserLost bool, err error) error {
|
||||
if err == nil || ctx.Err() != nil {
|
||||
return err
|
||||
}
|
||||
if !browserLost {
|
||||
return err
|
||||
}
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("%w: %w", errBrowserInterrupted, err)
|
||||
}
|
||||
|
||||
func browserConnectionLost(ctx context.Context) bool {
|
||||
c := chromedp.FromContext(ctx)
|
||||
if c == nil || c.Browser == nil {
|
||||
return true
|
||||
}
|
||||
select {
|
||||
case <-c.Browser.LostConnection:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// challengePollInterval paces re-reads while a challenge solves itself.
|
||||
const challengePollInterval = 2 * time.Second
|
||||
|
||||
@@ -203,6 +233,7 @@ func (f *BrowserFetcher) run(ctx context.Context, target string, read chromedp.A
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
callerCtx := ctx
|
||||
ctx, cancel := context.WithTimeout(ctx, challengeTimeout)
|
||||
defer cancel()
|
||||
tabCtx, cancelTab := chromedp.NewContext(f.allocCtx)
|
||||
@@ -219,20 +250,26 @@ func (f *BrowserFetcher) run(ctx context.Context, target string, read chromedp.A
|
||||
chromedp.Navigate(target),
|
||||
chromedp.WaitReady("body", chromedp.ByQuery),
|
||||
); err != nil {
|
||||
return err
|
||||
return classifyBrowserError(callerCtx, browserConnectionLost(tabCtx), err)
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for {
|
||||
// The challenge reloads the page when it passes, which tears down the
|
||||
// execution context mid-read. That is a retry, not a failure.
|
||||
if err := chromedp.Run(tabCtx, read); err != nil {
|
||||
err = classifyBrowserError(callerCtx, browserConnectionLost(tabCtx), err)
|
||||
if errors.Is(err, errBrowserInterrupted) {
|
||||
return err
|
||||
}
|
||||
lastErr = err
|
||||
} else if done() {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if err := callerCtx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if lastErr != nil {
|
||||
return fmt.Errorf("%w (last read: %v)", errChallengeHeld, lastErr)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package latest
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKaganeAPIURL(t *testing.T) {
|
||||
const uuid = "019f84bc-9ba0-7ed9-86f5-8b905ec7c28b"
|
||||
@@ -57,3 +61,17 @@ func TestNovelfullSeriesURL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestClassifyBrowserInterruption(t *testing.T) {
|
||||
if err := classifyBrowserError(context.Background(), true, context.Canceled); !errors.Is(err, errBrowserInterrupted) {
|
||||
t.Fatalf("classifyBrowserError(context.Canceled) = %v, want browser interruption", err)
|
||||
}
|
||||
if err := classifyBrowserError(context.Background(), false, context.Canceled); errors.Is(err, errBrowserInterrupted) {
|
||||
t.Fatalf("ordinary cancellation misclassified as browser interruption: %v", err)
|
||||
}
|
||||
|
||||
caller, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if err := classifyBrowserError(caller, true, context.Canceled); errors.Is(err, errBrowserInterrupted) {
|
||||
t.Fatalf("caller cancellation misclassified as browser interruption: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user