Skip to content

Commit 8c4a288

Browse files
committed
before tui changes
Signed-off-by: abzcoding <abzcoding@gmail.com>
1 parent 2b862d9 commit 8c4a288

8 files changed

Lines changed: 611 additions & 174 deletions

File tree

cmd/hget/main.go

Lines changed: 83 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package main
22

33
import (
4+
"context"
5+
"errors"
46
"flag"
57
"os"
8+
"os/signal"
69
"runtime"
10+
"syscall"
711
"time"
812

913
"github.com/abzcoding/hget/internal/batch"
@@ -40,37 +44,22 @@ func main() {
4044
return
4145
}
4246

47+
// Top-level cancellation context. External SIGINT/SIGTERM/SIGHUP/SIGQUIT
48+
// cancel this context with cause = downloader.ErrAbortBatch, which both
49+
// the TUI and the downloader/batch loop observe.
50+
rootCtx, rootCancel := context.WithCancelCause(context.Background())
51+
sigCh := make(chan os.Signal, 1)
52+
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
53+
defer signal.Stop(sigCh)
54+
go func() {
55+
<-sigCh
56+
rootCancel(downloader.ErrAbortBatch)
57+
}()
58+
defer rootCancel(nil)
59+
4360
// Resume mode.
4461
if resumeTask != "" {
45-
st, err := state.Resume(resumeTask)
46-
if err != nil {
47-
if !os.IsNotExist(err) {
48-
ui.Errorf("Resume failed: %v\n", err)
49-
os.Exit(1)
50-
}
51-
// No state.json — try to reconstruct from existing part files.
52-
st, err = downloader.ReconstructStateFromParts(resumeTask, *skiptls, proxy, *timeout)
53-
if err == nil {
54-
ui.Printf("Reconstructed state from %d part files — resuming.\n", len(st.Parts))
55-
ui.RunWithTUI(func() {
56-
downloader.Execute(st.URL, st, *conn, *skiptls, proxy, bwLimit, *timeout)
57-
}, *conn, false, 0, 0)
58-
return
59-
}
60-
// No part files either — start fresh if it looks like a URL.
61-
ui.Warnf("No saved state found for %q — starting fresh download.\n", resumeTask)
62-
if !util.IsURL(resumeTask) {
63-
ui.Errorf("No saved state found for task %q and it is not a URL.\n", resumeTask)
64-
os.Exit(1)
65-
}
66-
ui.RunWithTUI(func() {
67-
downloader.Execute(resumeTask, nil, *conn, *skiptls, proxy, bwLimit, *timeout)
68-
}, *conn, false, 0, 0)
69-
return
70-
}
71-
ui.RunWithTUI(func() {
72-
downloader.Execute(st.URL, st, *conn, *skiptls, proxy, bwLimit, *timeout)
73-
}, *conn, false, 0, 0)
62+
runResume(rootCtx, resumeTask, *conn, *skiptls, proxy, bwLimit, *timeout)
7463
return
7564
}
7665

@@ -81,7 +70,7 @@ func main() {
8170
ui.PrintHelp()
8271
os.Exit(1)
8372
}
84-
batch.RunBatchDownloads(filePath, *conn, *skiptls, proxy, bwLimit, *timeout, *verify)
73+
batch.RunBatchDownloads(rootCtx, filePath, *conn, *skiptls, proxy, bwLimit, *timeout, *verify)
8574
return
8675
}
8776

@@ -106,19 +95,80 @@ func main() {
10695
util.FatalCheck(err)
10796
}
10897

98+
itemCtx, cancelItem := context.WithCancelCause(rootCtx)
99+
defer cancelItem(nil)
100+
109101
var verifyOK bool
110102
var verifyDetail string
111103
var didVerify bool
112104

113-
ui.RunWithTUI(func() {
114-
downloader.Execute(downloadURL, nil, *conn, *skiptls, proxy, bwLimit, *timeout)
105+
runErr := ui.RunWithTUI(ui.RunOptions{
106+
Ctx: itemCtx,
107+
OnQuit: func() { cancelItem(downloader.ErrUserQuit) },
108+
NumConns: *conn,
109+
WillVerify: *verify,
110+
BatchCurrent: 0,
111+
BatchTotal: 0,
112+
}, func() error {
113+
if err := downloader.Execute(itemCtx, downloadURL, nil, *conn, *skiptls, proxy, bwLimit, *timeout); err != nil {
114+
return err
115+
}
115116
if *verify {
116117
verifyOK, verifyDetail = downloader.RunVerify(downloadURL, *skiptls, proxy, *timeout)
117118
didVerify = true
118119
}
119-
}, *conn, *verify, 0, 0)
120+
return nil
121+
})
120122

121123
if didVerify {
122124
ui.PrintVerifySummary(verifyOK, verifyDetail)
123125
}
126+
127+
if runErr != nil &&
128+
!errors.Is(runErr, downloader.ErrUserQuit) &&
129+
!errors.Is(runErr, downloader.ErrAbortBatch) &&
130+
!errors.Is(runErr, context.Canceled) {
131+
os.Exit(1)
132+
}
133+
}
134+
135+
// runResume handles --resume in both forms (task-name and URL).
136+
func runResume(rootCtx context.Context, resumeTask string, conn int, skiptls bool, proxy, bwLimit string, timeout time.Duration) {
137+
st, err := state.Resume(resumeTask)
138+
if err != nil {
139+
if !os.IsNotExist(err) {
140+
ui.Errorf("Resume failed: %v\n", err)
141+
os.Exit(1)
142+
}
143+
// No state.json — try to reconstruct from existing part files.
144+
st, err = downloader.ReconstructStateFromParts(resumeTask, skiptls, proxy, timeout)
145+
if err == nil {
146+
ui.Printf("Reconstructed state from %d part files — resuming.\n", len(st.Parts))
147+
runOne(rootCtx, st.URL, st, conn, skiptls, proxy, bwLimit, timeout)
148+
return
149+
}
150+
// No part files either — start fresh if it looks like a URL.
151+
ui.Warnf("No saved state found for %q — starting fresh download.\n", resumeTask)
152+
if !util.IsURL(resumeTask) {
153+
ui.Errorf("No saved state found for task %q and it is not a URL.\n", resumeTask)
154+
os.Exit(1)
155+
}
156+
runOne(rootCtx, resumeTask, nil, conn, skiptls, proxy, bwLimit, timeout)
157+
return
158+
}
159+
runOne(rootCtx, st.URL, st, conn, skiptls, proxy, bwLimit, timeout)
160+
}
161+
162+
func runOne(rootCtx context.Context, url string, st *state.State, conn int, skiptls bool, proxy, bwLimit string, timeout time.Duration) {
163+
itemCtx, cancelItem := context.WithCancelCause(rootCtx)
164+
defer cancelItem(nil)
165+
166+
_ = ui.RunWithTUI(ui.RunOptions{
167+
Ctx: itemCtx,
168+
OnQuit: func() { cancelItem(downloader.ErrUserQuit) },
169+
NumConns: conn,
170+
WillVerify: false,
171+
}, func() error {
172+
return downloader.Execute(itemCtx, url, st, conn, skiptls, proxy, bwLimit, timeout)
173+
})
124174
}

cmd/hget/main_test.go

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package main
22

33
import (
44
"bytes"
5+
"context"
6+
"errors"
57
"fmt"
68
"net/http"
79
"net/http/httptest"
@@ -11,7 +13,6 @@ import (
1113
"strconv"
1214
"strings"
1315
"sync/atomic"
14-
"syscall"
1516
"testing"
1617
"time"
1718

@@ -21,8 +22,11 @@ import (
2122
"github.com/abzcoding/hget/internal/util"
2223
)
2324

24-
// Execute is re-exported here for backward compatibility with tests.
25-
var Execute = downloader.Execute
25+
// Execute is a backward-compat wrapper around downloader.Execute that uses a
26+
// background context, mirroring the pre-refactor signature used by tests.
27+
func Execute(url string, st *state.State, conn int, skiptls bool, proxy, bwLimit string, timeout time.Duration) {
28+
_ = downloader.Execute(context.Background(), url, st, conn, skiptls, proxy, bwLimit, timeout)
29+
}
2630

2731
func makeContent(size int) []byte {
2832
data := make([]byte, size)
@@ -291,21 +295,22 @@ func TestE2EInterruptCancelsAndSavesState(t *testing.T) {
291295
ts, _ := startTestServer(t, content, true, true, true, true, path)
292296
url := ts.URL + path
293297

294-
doneSig := make(chan struct{})
298+
ctx, cancel := context.WithCancelCause(context.Background())
295299
go func() {
296300
time.Sleep(200 * time.Millisecond)
297-
_ = syscall.Kill(os.Getpid(), syscall.SIGINT)
298-
close(doneSig)
301+
cancel(downloader.ErrUserQuit)
299302
}()
300303

301304
start := time.Now()
302-
Execute(url, nil, 4, false, "", "50KB", 15*time.Second)
303-
<-doneSig
305+
err := downloader.Execute(ctx, url, nil, 4, false, "", "50KB", 15*time.Second)
304306
dur := time.Since(start)
305307

306308
if dur > 10*time.Second {
307309
t.Fatalf("interrupt handling too slow: %v", dur)
308310
}
311+
if !errors.Is(err, downloader.ErrUserQuit) && !errors.Is(err, context.Canceled) {
312+
t.Fatalf("expected ErrUserQuit/Canceled, got: %v", err)
313+
}
309314

310315
usr, _ := user.Current()
311316
folder := filepath.Join(usr.HomeDir, state.DataFolder, util.TaskFromURL(url))
@@ -336,3 +341,35 @@ func TestE2EInterruptCancelsAndSavesState(t *testing.T) {
336341
t.Fatalf("expected part files in %s after interrupt", folder)
337342
}
338343
}
344+
345+
// TestE2ESkipDiscardsState verifies that cancelling Execute with
346+
// ErrSkipCurrent removes the partial download folder instead of saving state.
347+
func TestE2ESkipDiscardsState(t *testing.T) {
348+
ui.DisplayProgress = false
349+
restoreCwd := withTempCwd(t)
350+
defer restoreCwd()
351+
restoreDF := withTestDataFolder(t)
352+
defer restoreDF()
353+
354+
content := makeContent(2 * 1024 * 1024)
355+
path := "/skip.bin"
356+
ts, _ := startTestServer(t, content, true, true, true, true, path)
357+
url := ts.URL + path
358+
359+
ctx, cancel := context.WithCancelCause(context.Background())
360+
go func() {
361+
time.Sleep(150 * time.Millisecond)
362+
cancel(downloader.ErrSkipCurrent)
363+
}()
364+
365+
err := downloader.Execute(ctx, url, nil, 4, false, "", "50KB", 15*time.Second)
366+
if !errors.Is(err, downloader.ErrSkipCurrent) {
367+
t.Fatalf("expected ErrSkipCurrent, got: %v", err)
368+
}
369+
370+
usr, _ := user.Current()
371+
folder := filepath.Join(usr.HomeDir, state.DataFolder, util.TaskFromURL(url))
372+
if _, err := os.Stat(folder); !os.IsNotExist(err) {
373+
t.Fatalf("expected folder %s removed after skip, stat err=%v", folder, err)
374+
}
375+
}

0 commit comments

Comments
 (0)