Fix recursive lock

This commit is contained in:
Justin Hawkins 2022-01-05 23:56:12 +10:30
parent 8bf9f42416
commit 3dc33cd441
8 changed files with 135 additions and 18 deletions

View File

@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file.
## [Unreleased] ## [Unreleased]
- Check the chosen command exists when configuring a profile - Check the chosen command exists when configuring a profile
- Add a stop button in the popup to abort a download
- Move included JS to local app instead of accessing from a CDN
- Make the simultaneous download limit apply to each unique domain
## [v0.5.3] - 2021-11-21 ## [v0.5.3] - 2021-11-21

View File

@ -3,11 +3,14 @@ package download
import ( import (
"fmt" "fmt"
"io" "io"
"log"
"net/url"
"os/exec" "os/exec"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/tardisx/gropple/config" "github.com/tardisx/gropple/config"
@ -28,6 +31,7 @@ type Download struct {
Percent float32 `json:"percent"` Percent float32 `json:"percent"`
Log []string `json:"log"` Log []string `json:"log"`
Config *config.Config Config *config.Config
mutex sync.Mutex
} }
type Downloads []*Download type Downloads []*Download
@ -35,26 +39,31 @@ type Downloads []*Download
// StartQueued starts any downloads that have been queued, we would not exceed // StartQueued starts any downloads that have been queued, we would not exceed
// maxRunning. If maxRunning is 0, there is no limit. // maxRunning. If maxRunning is 0, there is no limit.
func (dls Downloads) StartQueued(maxRunning int) { func (dls Downloads) StartQueued(maxRunning int) {
active := 0 active := make(map[string]int)
queued := 0
for _, dl := range dls { for _, dl := range dls {
dl.mutex.Lock()
if dl.State == "downloading" { if dl.State == "downloading" {
active++ active[dl.domain()]++
}
if dl.State == "queued" {
queued++
} }
dl.mutex.Unlock()
} }
// there is room, so start one for _, dl := range dls {
if queued > 0 && (active < maxRunning || maxRunning == 0) {
for _, dl := range dls { dl.mutex.Lock()
if dl.State == "queued" {
dl.State = "downloading" if dl.State == "queued" && (maxRunning == 0 || active[dl.domain()] < maxRunning) {
go func() { dl.Begin() }() dl.State = "downloading"
return active[dl.domain()]++
} log.Printf("Starting download for %#v", dl)
dl.mutex.Unlock()
go func() { dl.Begin() }()
} else {
dl.mutex.Unlock()
} }
} }
@ -65,23 +74,57 @@ func (dls Downloads) StartQueued(maxRunning int) {
func (dls Downloads) Cleanup() Downloads { func (dls Downloads) Cleanup() Downloads {
newDLs := Downloads{} newDLs := Downloads{}
for _, dl := range dls { for _, dl := range dls {
dl.mutex.Lock()
if dl.Finished && time.Since(dl.FinishedTS) > time.Duration(time.Hour) { if dl.Finished && time.Since(dl.FinishedTS) > time.Duration(time.Hour) {
// do nothing // do nothing
} else { } else {
newDLs = append(newDLs, dl) newDLs = append(newDLs, dl)
} }
dl.mutex.Unlock()
} }
return newDLs return newDLs
} }
// Queue queues a download // Queue queues a download
func (dl *Download) Queue() { func (dl *Download) Queue() {
dl.mutex.Lock()
defer dl.mutex.Unlock()
dl.State = "queued" dl.State = "queued"
}
func (dl *Download) Stop() {
log.Printf("stopping the download")
dl.mutex.Lock()
defer dl.mutex.Unlock()
syscall.Kill(dl.Pid, syscall.SIGTERM)
}
func (dl *Download) domain() string {
// note that we expect to already have the mutex locked by the caller
url, err := url.Parse(dl.Url)
if err != nil {
log.Printf("Unknown domain for url: %s", dl.Url)
return "unknown"
}
return url.Hostname()
} }
// Begin starts a download, by starting the command specified in the DownloadProfile. // Begin starts a download, by starting the command specified in the DownloadProfile.
// It blocks until the download is complete. // It blocks until the download is complete.
func (dl *Download) Begin() { func (dl *Download) Begin() {
dl.mutex.Lock()
dl.State = "downloading" dl.State = "downloading"
cmdSlice := []string{} cmdSlice := []string{}
cmdSlice = append(cmdSlice, dl.DownloadProfile.Args...) cmdSlice = append(cmdSlice, dl.DownloadProfile.Args...)
@ -112,6 +155,7 @@ func (dl *Download) Begin() {
return return
} }
log.Printf("Starting %v", cmd)
err = cmd.Start() err = cmd.Start()
if err != nil { if err != nil {
dl.State = "failed" dl.State = "failed"
@ -124,6 +168,8 @@ func (dl *Download) Begin() {
var wg sync.WaitGroup var wg sync.WaitGroup
dl.mutex.Unlock()
wg.Add(2) wg.Add(2)
go func() { go func() {
defer wg.Done() defer wg.Done()
@ -138,6 +184,8 @@ func (dl *Download) Begin() {
wg.Wait() wg.Wait()
cmd.Wait() cmd.Wait()
dl.mutex.Lock()
dl.State = "complete" dl.State = "complete"
dl.Finished = true dl.Finished = true
dl.FinishedTS = time.Now() dl.FinishedTS = time.Now()
@ -146,6 +194,7 @@ func (dl *Download) Begin() {
if dl.ExitCode != 0 { if dl.ExitCode != 0 {
dl.State = "failed" dl.State = "failed"
} }
dl.mutex.Unlock()
} }
@ -164,9 +213,13 @@ func (dl *Download) updateDownload(r io.Reader) {
continue continue
} }
dl.mutex.Lock()
// append the raw log // append the raw log
dl.Log = append(dl.Log, l) dl.Log = append(dl.Log, l)
dl.mutex.Unlock()
// look for the percent and eta and other metadata // look for the percent and eta and other metadata
dl.updateMetadata(l) dl.updateMetadata(l)
} }
@ -179,6 +232,10 @@ func (dl *Download) updateDownload(r io.Reader) {
func (dl *Download) updateMetadata(s string) { func (dl *Download) updateMetadata(s string) {
dl.mutex.Lock()
defer dl.mutex.Unlock()
// [download] 49.7% of ~15.72MiB at 5.83MiB/s ETA 00:07 // [download] 49.7% of ~15.72MiB at 5.83MiB/s ETA 00:07
etaRE := regexp.MustCompile(`download.+ETA +(\d\d:\d\d(?::\d\d)?)$`) etaRE := regexp.MustCompile(`download.+ETA +(\d\d:\d\d(?::\d\d)?)$`)
matches := etaRE.FindStringSubmatch(s) matches := etaRE.FindStringSubmatch(s)

View File

@ -71,7 +71,7 @@ func TestQueue(t *testing.T) {
new1 := Download{Id: 1, State: "queued", DownloadProfile: conf.DownloadProfiles[0], Config: conf} new1 := Download{Id: 1, State: "queued", DownloadProfile: conf.DownloadProfiles[0], Config: conf}
new2 := Download{Id: 2, State: "queued", DownloadProfile: conf.DownloadProfiles[0], Config: conf} new2 := Download{Id: 2, State: "queued", DownloadProfile: conf.DownloadProfiles[0], Config: conf}
new3 := Download{Id: 3, State: "queued", DownloadProfile: conf.DownloadProfiles[0], Config: conf} new3 := Download{Id: 3, State: "queued", DownloadProfile: conf.DownloadProfiles[0], Config: conf}
new4 := Download{Id: 4, State: "queued", DownloadProfile: conf.DownloadProfiles[0], Config: conf} new4 := Download{Id: 4, Url: "http://company.org/", State: "queued", DownloadProfile: conf.DownloadProfiles[0], Config: conf}
dls := Downloads{&new1, &new2, &new3, &new4} dls := Downloads{&new1, &new2, &new3, &new4}
dls.StartQueued(1) dls.StartQueued(1)
@ -81,6 +81,9 @@ func TestQueue(t *testing.T) {
if dls[1].State != "queued" { if dls[1].State != "queued" {
t.Error("#2 is not queued") t.Error("#2 is not queued")
} }
if dls[3].State == "queued" {
t.Error("#4 is not started")
}
// this should start no more, as one is still going // this should start no more, as one is still going
dls.StartQueued(1) dls.StartQueued(1)
@ -93,4 +96,9 @@ func TestQueue(t *testing.T) {
t.Error("#2 was not started but it should be") t.Error("#2 was not started but it should be")
} }
dls.StartQueued(2)
if dls[3].State == "queued" {
t.Error("#4 was not started but it should be")
}
} }

30
main.go
View File

@ -53,6 +53,7 @@ func main() {
r := mux.NewRouter() r := mux.NewRouter()
r.HandleFunc("/", homeHandler) r.HandleFunc("/", homeHandler)
r.HandleFunc("/static/{filename}", staticHandler)
r.HandleFunc("/config", configHandler) r.HandleFunc("/config", configHandler)
r.HandleFunc("/fetch", fetchHandler) r.HandleFunc("/fetch", fetchHandler)
r.HandleFunc("/fetch/{id}", fetchHandler) r.HandleFunc("/fetch/{id}", fetchHandler)
@ -136,6 +137,26 @@ func homeHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
// staticHandler handles requests for static files
func staticHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
filename := vars["filename"]
log.Printf("WOw :%s", filename)
if strings.Index(filename, ".js") == len(filename)-3 {
f, err := webFS.Open("web/" + filename)
if err != nil {
log.Printf("error accessing %s - %v", filename, err)
w.WriteHeader(http.StatusNotFound)
return
}
w.WriteHeader(http.StatusOK)
io.Copy(w, f)
return
}
w.WriteHeader(http.StatusNotFound)
}
// configHandler returns the configuration page // configHandler returns the configuration page
func configHandler(w http.ResponseWriter, r *http.Request) { func configHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@ -236,6 +257,15 @@ func fetchInfoOneRESTHandler(w http.ResponseWriter, r *http.Request) {
w.Write(succResB) w.Write(succResB)
return return
} }
if thisReq.Action == "stop" {
thisDownload.Stop()
succRes := successResponse{Success: true, Message: "download stopped"}
succResB, _ := json.Marshal(succRes)
w.Write(succResB)
return
}
} }
// just a get, return the object // just a get, return the object

5
web/alpine.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -33,9 +33,9 @@
<input type="text" id="config-server-downloadpath" placeholder="path" class="input-long" x-model="config.server.download_path" /> <input type="text" id="config-server-downloadpath" placeholder="path" class="input-long" x-model="config.server.download_path" />
<span class="pure-form-message">The path on the server to download files to.</span> <span class="pure-form-message">The path on the server to download files to.</span>
<label for="config-server-max-downloads">Maximum active downloads</label> <label for="config-server-max-downloads">Maximum active downloads per domain</label>
<input type="text" id="config-server-max-downloads" placeholder="2" class="input-long" x-model.number="config.server.maximum_active_downloads_per_domain" /> <input type="text" id="config-server-max-downloads" placeholder="2" class="input-long" x-model.number="config.server.maximum_active_downloads_per_domain" />
<span class="pure-form-message">How many downloads can be simultaneously active. Use '0' for no limit.</span> <span class="pure-form-message">How many downloads can be simultaneously active. Use '0' for no limit. This limit is applied per domain that you download from.</span>
<legend>UI</legend> <legend>UI</legend>

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>gropple</title> <title>gropple</title>
<script src="//unpkg.com/alpinejs" defer></script> <script src="/static/alpine.min.js" defer></script>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/purecss@2.0.6/build/pure-min.css" integrity="sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5" crossorigin="anonymous"> <link rel="stylesheet" href="https://unpkg.com/purecss@2.0.6/build/pure-min.css" integrity="sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/purecss@2.0.6/build/grids-responsive-min.css"> <link rel="stylesheet" href="https://unpkg.com/purecss@2.0.6/build/grids-responsive-min.css">

View File

@ -18,8 +18,10 @@
<tr><th>state</th><td x-text="state"></td></tr> <tr><th>state</th><td x-text="state"></td></tr>
<tr><th>progress</th><td x-text="percent"></td></tr> <tr><th>progress</th><td x-text="percent"></td></tr>
<tr><th>ETA</th><td x-text="eta"></td></tr> <tr><th>ETA</th><td x-text="eta"></td></tr>
</table> </table>
<p>You can close this window and your download will continue. Check the <a href="/" target="_gropple_status">Status page</a> to see all downloads in progress.</p> <p>You can close this window and your download will continue. Check the <a href="/" target="_gropple_status">Status page</a> to see all downloads in progress.</p>
<button x-show="state=='downloading'" class="pure-button" @click="stop()">stop</button>
<div> <div>
<h4>Logs</h4> <h4>Logs</h4>
<pre x-text="log"> <pre x-text="log">
@ -50,6 +52,18 @@
console.log(info) console.log(info)
}) })
}, },
stop() {
let op = {
method: 'POST',
body: JSON.stringify({action: 'stop'}),
headers: { 'Content-Type': 'application/json' }
};
fetch('/rest/fetch/{{ .dl.Id }}', op)
.then(response => response.json())
.then(info => {
console.log(info)
})
},
fetch_data() { fetch_data() {
fetch('/rest/fetch/{{ .dl.Id }}') fetch('/rest/fetch/{{ .dl.Id }}')
.then(response => response.json()) .then(response => response.json())