11package main
22
33import (
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}
0 commit comments