Fix recursive lock
This commit is contained in:
parent
8bf9f42416
commit
3dc33cd441
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
30
main.go
@ -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
5
web/alpine.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
@ -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())
|
||||||
|
Loading…
x
Reference in New Issue
Block a user