This commit is contained in:
itexpert228 2026-03-06 14:47:54 +03:00
parent 90583f66c7
commit 61709a44f5
No known key found for this signature in database
8 changed files with 214 additions and 23 deletions

View file

@ -14,8 +14,8 @@ func NewController() *Controller {
} }
} }
func (c *Controller) StartTorrent(path string) error { func (c *Controller) StartTorrent(path, outputRoot string) error {
return c.engine.LoadTorrent(path) return c.engine.LoadTorrent(path, outputRoot)
} }
func (c *Controller) StopTorrent() { func (c *Controller) StopTorrent() {

17
internal/torrent/debug.go Normal file
View file

@ -0,0 +1,17 @@
package torrent
import "log"
// debug enables verbose protocol and scheduler logs.
var debug = true
func debugf(format string, args ...any) {
if !debug {
return
}
log.Printf(format, args...)
}
func SetDebug(enabled bool) {
debug = enabled
}

View file

@ -8,6 +8,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
@ -39,6 +40,10 @@ type Engine struct {
completedPieces int completedPieces int
totalPieces int totalPieces int
outputPath string outputPath string
outputRoot string
downloadSpeed float64
speedAt time.Time
speedBytes int64
} }
type Status struct { type Status struct {
@ -50,6 +55,7 @@ type Status struct {
CompletedPieces int CompletedPieces int
DownloadedBytes int64 DownloadedBytes int64
DownloadSpeed float64
TotalBytes int64 TotalBytes int64
OutputPath string OutputPath string
@ -98,13 +104,17 @@ func NewEngine() *Engine {
} }
} }
func (e *Engine) LoadTorrent(path string) error { func (e *Engine) LoadTorrent(path, outputRoot string) error {
e.Stop() e.Stop()
e.resetStateForNewLoad() e.resetStateForNewLoad()
e.setPhase("loading_metadata") e.setPhase("loading_metadata")
outputRoot = normalizeOutputRoot(outputRoot)
log.Printf("loading torrent metadata from %s", path)
debugf("download root selected: %s", outputRoot)
tf, err := torrentfile.Open(path) tf, err := torrentfile.Open(path)
if err != nil { if err != nil {
log.Printf("failed to parse torrent %s: %v", path, err)
e.setError(err) e.setError(err)
e.setPhase("failed") e.setPhase("failed")
return err return err
@ -131,7 +141,11 @@ func (e *Engine) LoadTorrent(path string) error {
e.totalPieces = len(tf.PieceHashes) e.totalPieces = len(tf.PieceHashes)
e.downloadedBytes = 0 e.downloadedBytes = 0
e.completedPieces = 0 e.completedPieces = 0
e.outputPath = buildOutputPath(tf, "downloads") e.outputRoot = outputRoot
e.outputPath = buildOutputPath(tf, outputRoot)
e.downloadSpeed = 0
e.speedAt = time.Now()
e.speedBytes = 0
if len(peers) == 0 { if len(peers) == 0 {
warnErr := fmt.Errorf("no peers yet via %d trackers", len(trackerStatuses)) warnErr := fmt.Errorf("no peers yet via %d trackers", len(trackerStatuses))
if trErr := firstTrackerError(trackerStatuses); trErr != nil { if trErr := firstTrackerError(trackerStatuses); trErr != nil {
@ -143,6 +157,7 @@ func (e *Engine) LoadTorrent(path string) error {
} }
e.phase = "ready" e.phase = "ready"
e.mu.Unlock() e.mu.Unlock()
log.Printf("torrent loaded: name=%s size=%d peers=%d output=%s", tf.Name, tf.Length, len(peers), e.outputPath)
go e.runDownload(downloadCtx, tf) go e.runDownload(downloadCtx, tf)
return nil return nil
@ -182,6 +197,7 @@ func (e *Engine) Status() Status {
Phase: e.phase, Phase: e.phase,
CompletedPieces: e.completedPieces, CompletedPieces: e.completedPieces,
DownloadedBytes: e.downloadedBytes, DownloadedBytes: e.downloadedBytes,
DownloadSpeed: e.downloadSpeed,
OutputPath: e.outputPath, OutputPath: e.outputPath,
} }
@ -207,6 +223,7 @@ func (e *Engine) queryTrackers(ctx context.Context, tf *torrentfile.TorrentFile,
statuses := make([]TrackerStatus, 0, len(trackersToTry)) statuses := make([]TrackerStatus, 0, len(trackersToTry))
for _, announce := range trackersToTry { for _, announce := range trackersToTry {
log.Printf("tracker announce: %s", announce)
perTrackerCtx, cancel := context.WithTimeout(ctx, opts.Timeout) perTrackerCtx, cancel := context.WithTimeout(ctx, opts.Timeout)
peers, err := tracker.GetPeersFromURL(perTrackerCtx, announce, tf, opts) peers, err := tracker.GetPeersFromURL(perTrackerCtx, announce, tf, opts)
cancel() cancel()
@ -215,6 +232,7 @@ func (e *Engine) queryTrackers(ctx context.Context, tf *torrentfile.TorrentFile,
if err != nil { if err != nil {
st.State = "error" st.State = "error"
st.Error = err.Error() st.Error = err.Error()
log.Printf("tracker announce failed %s: %v", announce, err)
statuses = append(statuses, st) statuses = append(statuses, st)
continue continue
} }
@ -226,6 +244,7 @@ func (e *Engine) queryTrackers(ctx context.Context, tf *torrentfile.TorrentFile,
st.PeerCount = len(peers) st.PeerCount = len(peers)
} }
statuses = append(statuses, st) statuses = append(statuses, st)
log.Printf("tracker announce result %s: peers=%d state=%s", announce, st.PeerCount, st.State)
for _, peer := range peers { for _, peer := range peers {
addPeer(peerMap, peer, announce) addPeer(peerMap, peer, announce)
@ -250,9 +269,11 @@ func (e *Engine) runDownload(ctx context.Context, tf *torrentfile.TorrentFile) {
defer e.clearCancel() defer e.clearCancel()
e.setPhase("preparing_download") e.setPhase("preparing_download")
log.Printf("preparing download to %s", e.outputRoot)
partPath, partFile, err := createPartFile(tf) partPath, partFile, err := createPartFile(tf, e.outputRoot)
if err != nil { if err != nil {
log.Printf("failed to create part file: %v", err)
e.setTerminalError("failed", fmt.Errorf("create temp file: %w", err)) e.setTerminalError("failed", fmt.Errorf("create temp file: %w", err))
return return
} }
@ -294,6 +315,7 @@ func (e *Engine) runDownload(ctx context.Context, tf *torrentfile.TorrentFile) {
defer cleanupWorkers() defer cleanupWorkers()
e.setPhase("downloading") e.setPhase("downloading")
log.Printf("download started: pieces=%d peers=%d", len(tf.PieceHashes), len(e.snapshotPeers()))
lastProgressAt := time.Now() lastProgressAt := time.Now()
lastReannounceAt := time.Now() lastReannounceAt := time.Now()
startWorkers(e.snapshotPeers()) startWorkers(e.snapshotPeers())
@ -313,22 +335,27 @@ func (e *Engine) runDownload(ctx context.Context, tf *torrentfile.TorrentFile) {
continue continue
} }
lastProgressAt = time.Now() lastProgressAt = time.Now()
debugf("piece completed: %d/%d", e.currentCompletedPieces(), len(tf.PieceHashes))
case <-scheduler.Done(): case <-scheduler.Done():
downloadCompleted = true downloadCompleted = true
case workerKey := <-workerDoneCh: case workerKey := <-workerDoneCh:
delete(startedWorkers, workerKey) delete(startedWorkers, workerKey)
debugf("worker finished for peer %s", workerKey)
case <-heartbeatTicker.C: case <-heartbeatTicker.C:
e.updateSpeed(time.Now())
if time.Since(lastReannounceAt) >= reannounceInterval { if time.Since(lastReannounceAt) >= reannounceInterval {
e.setPhase("querying_trackers") e.setPhase("querying_trackers")
refreshCtx, cancel := context.WithTimeout(ctx, 20*time.Second) refreshCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
_ = e.refreshPeersFromTrackers(refreshCtx, tf) added := e.refreshPeersFromTrackers(refreshCtx, tf)
cancel() cancel()
lastReannounceAt = time.Now() lastReannounceAt = time.Now()
e.setPhase("downloading") e.setPhase("downloading")
debugf("reannounce complete, added peers=%d total=%d", added, len(e.snapshotPeers()))
} }
startWorkers(e.snapshotPeers()) startWorkers(e.snapshotPeers())
if time.Since(lastProgressAt) >= downloadStallTimeout { if time.Since(lastProgressAt) >= downloadStallTimeout {
log.Printf("download stalled: no progress for %s", downloadStallTimeout.Round(time.Second))
e.setTerminalError("stalled", fmt.Errorf("download stalled: no piece progress for %s", downloadStallTimeout.Round(time.Second))) e.setTerminalError("stalled", fmt.Errorf("download stalled: no piece progress for %s", downloadStallTimeout.Round(time.Second)))
return return
} }
@ -338,19 +365,24 @@ func (e *Engine) runDownload(ctx context.Context, tf *torrentfile.TorrentFile) {
cleanupWorkers() cleanupWorkers()
e.setPhase("writing_files") e.setPhase("writing_files")
if err := materializeDownloadedFiles(tf, partPath, "downloads"); err != nil { if err := materializeDownloadedFiles(tf, partPath, e.outputRoot); err != nil {
log.Printf("failed to materialize files: %v", err)
e.setTerminalError("failed", fmt.Errorf("write output files: %w", err)) e.setTerminalError("failed", fmt.Errorf("write output files: %w", err))
return return
} }
_ = os.Remove(partPath) if err := os.Remove(partPath); err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("failed to remove part file %s: %v", partPath, err)
}
e.mu.Lock() e.mu.Lock()
e.downloadedBytes = int64(tf.Length) e.downloadedBytes = int64(tf.Length)
e.completedPieces = len(tf.PieceHashes) e.completedPieces = len(tf.PieceHashes)
e.phase = "completed" e.phase = "completed"
e.lastErr = nil e.lastErr = nil
e.downloadSpeed = 0
e.mu.Unlock() e.mu.Unlock()
log.Printf("download completed: %s", e.outputPath)
} }
func (e *Engine) runPeerWorker( func (e *Engine) runPeerWorker(
@ -363,11 +395,13 @@ func (e *Engine) runPeerWorker(
e.setPeerState(peer.Address, peer.Port, "connecting", "") e.setPeerState(peer.Address, peer.Port, "connecting", "")
peerAddr := net.JoinHostPort(peer.Address, strconv.Itoa(int(peer.Port))) peerAddr := net.JoinHostPort(peer.Address, strconv.Itoa(int(peer.Port)))
debugf("starting peer worker for %s", peerAddr)
client, err := newPeerClient(ctx, peerAddr, tf.InfoHash, e.peerID, len(tf.PieceHashes)) client, err := newPeerClient(ctx, peerAddr, tf.InfoHash, e.peerID, len(tf.PieceHashes))
if err != nil { if err != nil {
if ctx.Err() != nil { if ctx.Err() != nil {
e.setPeerState(peer.Address, peer.Port, "stopped", "") e.setPeerState(peer.Address, peer.Port, "stopped", "")
} else { } else {
log.Printf("peer %s connection failed: %v", peerAddr, err)
e.setPeerState(peer.Address, peer.Port, "error", err.Error()) e.setPeerState(peer.Address, peer.Port, "error", err.Error())
} }
return return
@ -389,6 +423,7 @@ func (e *Engine) runPeerWorker(
if ctx.Err() != nil { if ctx.Err() != nil {
e.setPeerState(peer.Address, peer.Port, "stopped", "") e.setPeerState(peer.Address, peer.Port, "stopped", "")
} else { } else {
log.Printf("peer %s scheduler acquire failed: %v", peerAddr, err)
e.setPeerState(peer.Address, peer.Port, "error", err.Error()) e.setPeerState(peer.Address, peer.Port, "error", err.Error())
} }
return return
@ -411,13 +446,16 @@ func (e *Engine) runPeerWorker(
pieceSize := pieceSizeForIndex(tf, task.Index) pieceSize := pieceSizeForIndex(tf, task.Index)
pieceData, err := client.DownloadPiece(ctx, task.Index, pieceSize) pieceData, err := client.DownloadPiece(ctx, task.Index, pieceSize)
if err != nil { if err != nil {
_, _ = scheduler.Report(ctx, task.Index, false) if _, reportErr := scheduler.Report(ctx, task.Index, false); reportErr != nil && ctx.Err() == nil {
log.Printf("scheduler report failure for piece %d after peer error: %v", task.Index, reportErr)
}
if ctx.Err() != nil { if ctx.Err() != nil {
e.setPeerState(peer.Address, peer.Port, "stopped", "") e.setPeerState(peer.Address, peer.Port, "stopped", "")
return return
} }
consecutiveFailures++ consecutiveFailures++
log.Printf("peer %s disconnected: %v", peerAddr, err)
e.setPeerState(peer.Address, peer.Port, "error", err.Error()) e.setPeerState(peer.Address, peer.Port, "error", err.Error())
if shouldDropPeer(err) || consecutiveFailures >= 3 { if shouldDropPeer(err) || consecutiveFailures >= 3 {
return return
@ -427,8 +465,11 @@ func (e *Engine) runPeerWorker(
hash := sha1.Sum(pieceData) hash := sha1.Sum(pieceData)
if hash != tf.PieceHashes[task.Index] { if hash != tf.PieceHashes[task.Index] {
_, _ = scheduler.Report(ctx, task.Index, false) if _, reportErr := scheduler.Report(ctx, task.Index, false); reportErr != nil && ctx.Err() == nil {
log.Printf("scheduler report hash mismatch for piece %d failed: %v", task.Index, reportErr)
}
consecutiveFailures++ consecutiveFailures++
log.Printf("peer %s piece %d hash mismatch", peerAddr, task.Index)
e.setPeerState(peer.Address, peer.Port, "error", fmt.Sprintf("piece %d hash mismatch", task.Index)) e.setPeerState(peer.Address, peer.Port, "error", fmt.Sprintf("piece %d hash mismatch", task.Index))
if consecutiveFailures >= 3 { if consecutiveFailures >= 3 {
return return
@ -438,7 +479,10 @@ func (e *Engine) runPeerWorker(
offset := int64(task.Index * tf.PieceLength) offset := int64(task.Index * tf.PieceLength)
if _, err := partFile.WriteAt(pieceData, offset); err != nil { if _, err := partFile.WriteAt(pieceData, offset); err != nil {
_, _ = scheduler.Report(ctx, task.Index, false) if _, reportErr := scheduler.Report(ctx, task.Index, false); reportErr != nil && ctx.Err() == nil {
log.Printf("scheduler report write failure for piece %d failed: %v", task.Index, reportErr)
}
log.Printf("io error writing piece %d from peer %s: %v", task.Index, peerAddr, err)
e.setPeerState(peer.Address, peer.Port, "error", fmt.Sprintf("write piece %d: %v", task.Index, err)) e.setPeerState(peer.Address, peer.Port, "error", fmt.Sprintf("write piece %d: %v", task.Index, err))
return return
} }
@ -454,6 +498,9 @@ func (e *Engine) runPeerWorker(
} }
if accepted { if accepted {
e.recordPieceComplete(peer.Address, peer.Port, len(pieceData)) e.recordPieceComplete(peer.Address, peer.Port, len(pieceData))
debugf("peer %s committed piece %d", peerAddr, task.Index)
} else {
debugf("scheduler rejected piece %d report from %s", task.Index, peerAddr)
} }
consecutiveFailures = 0 consecutiveFailures = 0
@ -512,6 +559,7 @@ func (e *Engine) refreshPeersFromTrackers(ctx context.Context, tf *torrentfile.T
waitErr = fmt.Errorf("waiting for peers: %w", trErr) waitErr = fmt.Errorf("waiting for peers: %w", trErr)
} }
e.lastErr = waitErr e.lastErr = waitErr
debugf("reannounce returned no peers")
return 0 return 0
} }
e.lastErr = nil e.lastErr = nil
@ -540,6 +588,9 @@ func (e *Engine) refreshPeersFromTrackers(ctx context.Context, tf *torrentfile.T
return e.peers[i].Address < e.peers[j].Address return e.peers[i].Address < e.peers[j].Address
}) })
} }
if added > 0 {
log.Printf("discovered %d new peers (total=%d)", added, len(e.peers))
}
return added return added
} }
@ -550,11 +601,18 @@ func (e *Engine) currentDownloadedBytes() int64 {
return e.downloadedBytes return e.downloadedBytes
} }
func (e *Engine) currentCompletedPieces() int {
e.mu.RLock()
defer e.mu.RUnlock()
return e.completedPieces
}
func (e *Engine) recordPieceComplete(address string, port uint16, pieceSize int) { func (e *Engine) recordPieceComplete(address string, port uint16, pieceSize int) {
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock() defer e.mu.Unlock()
e.downloadedBytes += int64(pieceSize) e.downloadedBytes += int64(pieceSize)
e.completedPieces++ e.completedPieces++
e.updateSpeedLocked(time.Now())
for i := range e.peers { for i := range e.peers {
if e.peers[i].Address == address && e.peers[i].Port == port { if e.peers[i].Address == address && e.peers[i].Port == port {
e.peers[i].DownloadedPieces++ e.peers[i].DownloadedPieces++
@ -580,6 +638,7 @@ func (e *Engine) setTerminalError(phase string, err error) {
defer e.mu.Unlock() defer e.mu.Unlock()
e.phase = phase e.phase = phase
e.lastErr = err e.lastErr = err
e.downloadSpeed = 0
} }
func (e *Engine) progressUnsafe() float64 { func (e *Engine) progressUnsafe() float64 {
@ -628,6 +687,36 @@ func (e *Engine) resetStateForNewLoad() {
e.completedPieces = 0 e.completedPieces = 0
e.totalPieces = 0 e.totalPieces = 0
e.outputPath = "" e.outputPath = ""
e.outputRoot = ""
e.downloadSpeed = 0
e.speedAt = time.Time{}
e.speedBytes = 0
}
func (e *Engine) updateSpeed(now time.Time) {
e.mu.Lock()
defer e.mu.Unlock()
e.updateSpeedLocked(now)
}
func (e *Engine) updateSpeedLocked(now time.Time) {
if e.speedAt.IsZero() {
e.speedAt = now
e.speedBytes = e.downloadedBytes
e.downloadSpeed = 0
return
}
elapsed := now.Sub(e.speedAt).Seconds()
if elapsed < 0.8 {
return
}
delta := e.downloadedBytes - e.speedBytes
if delta < 0 {
delta = 0
}
e.downloadSpeed = float64(delta) / elapsed
e.speedAt = now
e.speedBytes = e.downloadedBytes
} }
func (e *Engine) setError(err error) { func (e *Engine) setError(err error) {
@ -755,8 +844,8 @@ func firstTrackerError(statuses []TrackerStatus) error {
return nil return nil
} }
func createPartFile(tf *torrentfile.TorrentFile) (string, *os.File, error) { func createPartFile(tf *torrentfile.TorrentFile, outputRoot string) (string, *os.File, error) {
partsDir := filepath.Join("downloads", ".parts") partsDir := filepath.Join(outputRoot, ".ztorrent-parts")
if err := os.MkdirAll(partsDir, 0o755); err != nil { if err := os.MkdirAll(partsDir, 0o755); err != nil {
return "", nil, err return "", nil, err
} }
@ -834,12 +923,7 @@ func safeOutputPath(root, rel string) (string, error) {
} }
func buildOutputPath(tf *torrentfile.TorrentFile, outputRoot string) string { func buildOutputPath(tf *torrentfile.TorrentFile, outputRoot string) string {
if len(tf.Files) == 1 { return filepath.Join(outputRoot, tf.Name)
if path, err := safeOutputPath(outputRoot, tf.Files[0].Path); err == nil {
return path
}
}
return filepath.Join(outputRoot, sanitizePathPart(tf.Name))
} }
func sanitizePathPart(s string) string { func sanitizePathPart(s string) string {
@ -864,3 +948,16 @@ func sanitizePathPart(s string) string {
} }
return out return out
} }
func normalizeOutputRoot(root string) string {
root = strings.TrimSpace(root)
if root == "" {
cwd, err := os.Getwd()
if err == nil && cwd != "" {
return cwd
}
return "."
}
clean := filepath.Clean(root)
return clean
}

View file

@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"net" "net"
"time" "time"
@ -50,8 +51,10 @@ func newPeerClient(ctx context.Context, addr string, infoHash [20]byte, peerID [
dialer := net.Dialer{Timeout: peerConnectTimeout} dialer := net.Dialer{Timeout: peerConnectTimeout}
conn, err := dialer.DialContext(ctx, "tcp", addr) conn, err := dialer.DialContext(ctx, "tcp", addr)
if err != nil { if err != nil {
log.Printf("peer dial failed %s: %v", addr, err)
return nil, err return nil, err
} }
log.Printf("connected to peer %s", addr)
pc := &peerClient{ pc := &peerClient{
conn: conn, conn: conn,
@ -72,8 +75,10 @@ func newPeerClient(ctx context.Context, addr string, infoHash [20]byte, peerID [
conn.Close() conn.Close()
return nil, err return nil, err
} }
debugf("sent interested to %s", addr)
if err := pc.readInitialMessages(ctx); err != nil { if err := pc.readInitialMessages(ctx); err != nil {
log.Printf("peer %s initial message read failed: %v", addr, err)
conn.Close() conn.Close()
return nil, err return nil, err
} }
@ -132,6 +137,7 @@ func (pc *peerClient) readInitialMessages(ctx context.Context) error {
msg, err := readWireMessage(pc.conn) msg, err := readWireMessage(pc.conn)
if err != nil { if err != nil {
if isTimeout(err) { if isTimeout(err) {
debugf("peer initial message window finished")
return nil return nil
} }
return err return err
@ -166,12 +172,14 @@ func (pc *peerClient) waitForUnchoke(ctx context.Context) error {
} }
pc.consumeMessage(msg) pc.consumeMessage(msg)
if msg.ID == msgUnchoke { if msg.ID == msgUnchoke {
debugf("peer unchoked")
return nil return nil
} }
} }
} }
func (pc *peerClient) sendRequest(ctx context.Context, pieceIndex, begin, length int) error { func (pc *peerClient) sendRequest(ctx context.Context, pieceIndex, begin, length int) error {
debugf("request piece %d offset %d length %d", pieceIndex, begin, length)
payload := make([]byte, 12) payload := make([]byte, 12)
binary.BigEndian.PutUint32(payload[0:4], uint32(pieceIndex)) binary.BigEndian.PutUint32(payload[0:4], uint32(pieceIndex))
binary.BigEndian.PutUint32(payload[4:8], uint32(begin)) binary.BigEndian.PutUint32(payload[4:8], uint32(begin))
@ -214,6 +222,7 @@ func (pc *peerClient) readPieceBlock(ctx context.Context, pieceIndex, begin, exp
if len(block) > expectedLen { if len(block) > expectedLen {
block = block[:expectedLen] block = block[:expectedLen]
} }
debugf("received piece %d offset %d block=%d", pieceIndex, begin, len(block))
return block, nil return block, nil
case msgChoke: case msgChoke:
pc.consumeMessage(msg) pc.consumeMessage(msg)
@ -228,8 +237,10 @@ func (pc *peerClient) consumeMessage(msg wireMessage) {
switch msg.ID { switch msg.ID {
case msgChoke: case msgChoke:
pc.peerIsChoked = true pc.peerIsChoked = true
debugf("peer sent choke")
case msgUnchoke: case msgUnchoke:
pc.peerIsChoked = false pc.peerIsChoked = false
debugf("peer sent unchoke")
case msgHave: case msgHave:
if len(msg.Payload) < 4 { if len(msg.Payload) < 4 {
return return
@ -238,9 +249,11 @@ func (pc *peerClient) consumeMessage(msg wireMessage) {
if idx >= 0 && idx < len(pc.have) { if idx >= 0 && idx < len(pc.have) {
pc.have[idx] = true pc.have[idx] = true
pc.hasPieceInfo = true pc.hasPieceInfo = true
debugf("received have piece %d", idx)
} }
case msgBitfield: case msgBitfield:
pc.hasPieceInfo = true pc.hasPieceInfo = true
debugf("received bitfield (%d bytes)", len(msg.Payload))
for i := range pc.have { for i := range pc.have {
pc.have[i] = bitfieldHasPiece(msg.Payload, i) pc.have[i] = bitfieldHasPiece(msg.Payload, i)
} }
@ -248,6 +261,7 @@ func (pc *peerClient) consumeMessage(msg wireMessage) {
} }
func (pc *peerClient) sendHandshake(ctx context.Context, infoHash [20]byte, peerID [20]byte) error { func (pc *peerClient) sendHandshake(ctx context.Context, infoHash [20]byte, peerID [20]byte) error {
debugf("sending handshake")
payload := make([]byte, 49+len(wireProtocolString)) payload := make([]byte, 49+len(wireProtocolString))
payload[0] = byte(len(wireProtocolString)) payload[0] = byte(len(wireProtocolString))
copy(payload[1:1+len(wireProtocolString)], wireProtocolString) copy(payload[1:1+len(wireProtocolString)], wireProtocolString)
@ -288,6 +302,7 @@ func (pc *peerClient) readHandshake(ctx context.Context, expectedInfoHash [20]by
if !bytes.Equal(rest[infoHashOffset:infoHashOffset+20], expectedInfoHash[:]) { if !bytes.Equal(rest[infoHashOffset:infoHashOffset+20], expectedInfoHash[:]) {
return errors.New("peer info_hash mismatch") return errors.New("peer info_hash mismatch")
} }
debugf("handshake complete")
return nil return nil
} }

View file

@ -149,6 +149,7 @@ func (ps *pieceScheduler) run(pieceCount int) {
pieceIndex := selectPendingPiece(states, req.have, req.hasInfo) pieceIndex := selectPendingPiece(states, req.have, req.hasInfo)
if pieceIndex >= 0 { if pieceIndex >= 0 {
states[pieceIndex] = pieceInProgress states[pieceIndex] = pieceInProgress
debugf("scheduler assigned piece %d", pieceIndex)
req.responseCh <- assignPieceResponse{ req.responseCh <- assignPieceResponse{
task: pieceTask{Index: pieceIndex}, task: pieceTask{Index: pieceIndex},
ok: true, ok: true,
@ -164,11 +165,13 @@ func (ps *pieceScheduler) run(pieceCount int) {
if req.success { if req.success {
if states[req.pieceIndex] == pieceDone { if states[req.pieceIndex] == pieceDone {
debugf("scheduler report piece %d ignored (already done)", req.pieceIndex)
req.responseCh <- false req.responseCh <- false
continue continue
} }
states[req.pieceIndex] = pieceDone states[req.pieceIndex] = pieceDone
completed++ completed++
debugf("scheduler report piece %d success (%d/%d)", req.pieceIndex, completed, pieceCount)
select { select {
case ps.progressCh <- req.pieceIndex: case ps.progressCh <- req.pieceIndex:
default: default:
@ -179,6 +182,7 @@ func (ps *pieceScheduler) run(pieceCount int) {
if states[req.pieceIndex] == pieceInProgress { if states[req.pieceIndex] == pieceInProgress {
states[req.pieceIndex] = piecePending states[req.pieceIndex] = piecePending
debugf("scheduler report piece %d failed, re-queued", req.pieceIndex)
} }
req.responseCh <- false req.responseCh <- false
} }

BIN
torrent-client Executable file

Binary file not shown.

View file

@ -27,6 +27,7 @@ type desktopUI struct {
window fyne.Window window fyne.Window
pathEntry *widget.Entry pathEntry *widget.Entry
downloadDirEntry *widget.Entry
progress *widget.ProgressBar progress *widget.ProgressBar
phaseLabel *widget.Label phaseLabel *widget.Label
phaseMetricLabel *widget.Label phaseMetricLabel *widget.Label
@ -38,6 +39,7 @@ type desktopUI struct {
pieceCountLabel *widget.Label pieceCountLabel *widget.Label
downloadedLabel *widget.Label downloadedLabel *widget.Label
outputLabel *widget.Label outputLabel *widget.Label
speedLabel *widget.Label
peerCountLabel *widget.Label peerCountLabel *widget.Label
mu sync.RWMutex mu sync.RWMutex
@ -68,6 +70,7 @@ func newDesktopUI(controller *appcore.Controller) *desktopUI {
window: w, window: w,
stopRefresh: make(chan struct{}), stopRefresh: make(chan struct{}),
pathEntry: widget.NewEntry(), pathEntry: widget.NewEntry(),
downloadDirEntry: widget.NewEntry(),
progress: widget.NewProgressBar(), progress: widget.NewProgressBar(),
phaseLabel: widget.NewLabel("Phase: idle"), phaseLabel: widget.NewLabel("Phase: idle"),
phaseMetricLabel: widget.NewLabel("idle"), phaseMetricLabel: widget.NewLabel("idle"),
@ -79,14 +82,22 @@ func newDesktopUI(controller *appcore.Controller) *desktopUI {
pieceCountLabel: widget.NewLabel("-"), pieceCountLabel: widget.NewLabel("-"),
downloadedLabel: widget.NewLabel("0 / 0"), downloadedLabel: widget.NewLabel("0 / 0"),
outputLabel: widget.NewLabel("-"), outputLabel: widget.NewLabel("-"),
speedLabel: widget.NewLabel("0 B/s"),
peerCountLabel: widget.NewLabel("0"), peerCountLabel: widget.NewLabel("0"),
} }
ui.pathEntry.SetPlaceHolder("/path/to/file.torrent") ui.pathEntry.SetPlaceHolder("/path/to/file.torrent")
ui.downloadDirEntry.SetPlaceHolder("/path/to/download/folder")
if home, err := os.UserHomeDir(); err == nil {
ui.downloadDirEntry.SetText(home)
}
browseButton := widget.NewButton("Browse", ui.onBrowse) browseButton := widget.NewButton("Browse Torrent", ui.onBrowse)
browseOutputButton := widget.NewButton("Browse Folder", ui.onBrowseOutputFolder)
loadButton := widget.NewButton("Load Torrent", ui.onLoad) loadButton := widget.NewButton("Load Torrent", ui.onLoad)
topRow := container.NewBorder(nil, nil, nil, container.NewHBox(browseButton, loadButton), ui.pathEntry) torrentRow := container.NewBorder(nil, nil, nil, browseButton, ui.pathEntry)
outputRow := container.NewBorder(nil, nil, nil, container.NewHBox(browseOutputButton, loadButton), ui.downloadDirEntry)
topRows := container.NewVBox(torrentRow, outputRow)
metaGrid := container.NewGridWithColumns(4, metaGrid := container.NewGridWithColumns(4,
wrapMetric("Name", ui.nameLabel), wrapMetric("Name", ui.nameLabel),
@ -94,6 +105,7 @@ func newDesktopUI(controller *appcore.Controller) *desktopUI {
wrapMetric("Piece length", ui.pieceLabel), wrapMetric("Piece length", ui.pieceLabel),
wrapMetric("Completed pieces", ui.pieceCountLabel), wrapMetric("Completed pieces", ui.pieceCountLabel),
wrapMetric("Downloaded", ui.downloadedLabel), wrapMetric("Downloaded", ui.downloadedLabel),
wrapMetric("Speed", ui.speedLabel),
wrapMetric("Output", ui.outputLabel), wrapMetric("Output", ui.outputLabel),
wrapMetric("Discovered peers", ui.peerCountLabel), wrapMetric("Discovered peers", ui.peerCountLabel),
wrapMetric("Phase", ui.phaseMetricLabel), wrapMetric("Phase", ui.phaseMetricLabel),
@ -117,7 +129,7 @@ func newDesktopUI(controller *appcore.Controller) *desktopUI {
) )
content := container.NewBorder( content := container.NewBorder(
container.NewVBox(topRow), topRows,
nil, nil,
nil, nil,
nil, nil,
@ -172,12 +184,26 @@ func (ui *desktopUI) onLoad() {
ui.setError("select a .torrent file first") ui.setError("select a .torrent file first")
return return
} }
downloadRoot := strings.TrimSpace(ui.downloadDirEntry.Text)
if downloadRoot == "" {
ui.setError("select a download folder first")
return
}
info, err := os.Stat(downloadRoot)
if err != nil {
ui.setError("download folder is not accessible: " + err.Error())
return
}
if !info.IsDir() {
ui.setError("download path must be a folder")
return
}
ui.setMessage("Loading torrent...") ui.setMessage("Loading torrent...")
ui.setError("") ui.setError("")
go func() { go func() {
err := ui.controller.StartTorrent(path) err := ui.controller.StartTorrent(path, downloadRoot)
status := ui.controller.Status() status := ui.controller.Status()
ui.updateFromStatus(status) ui.updateFromStatus(status)
if err != nil { if err != nil {
@ -188,6 +214,20 @@ func (ui *desktopUI) onLoad() {
}() }()
} }
func (ui *desktopUI) onBrowseOutputFolder() {
fd := dialog.NewFolderOpen(func(uri fyne.ListableURI, err error) {
if err != nil {
ui.setError(err.Error())
return
}
if uri == nil {
return
}
ui.downloadDirEntry.SetText(uri.Path())
}, ui.window)
fd.Show()
}
func (ui *desktopUI) refreshLoop() { func (ui *desktopUI) refreshLoop() {
ticker := time.NewTicker(1200 * time.Millisecond) ticker := time.NewTicker(1200 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
@ -216,6 +256,7 @@ func (ui *desktopUI) updateFromStatus(status torrent.Status) {
ui.pieceCountLabel.SetText("-") ui.pieceCountLabel.SetText("-")
} }
ui.downloadedLabel.SetText(fmt.Sprintf("%d / %d", status.DownloadedBytes, status.TotalBytes)) ui.downloadedLabel.SetText(fmt.Sprintf("%d / %d", status.DownloadedBytes, status.TotalBytes))
ui.speedLabel.SetText(formatBytesRate(status.DownloadSpeed))
ui.outputLabel.SetText(status.OutputPath) ui.outputLabel.SetText(status.OutputPath)
ui.peerCountLabel.SetText(strconv.Itoa(status.PeerCount)) ui.peerCountLabel.SetText(strconv.Itoa(status.PeerCount))
@ -237,6 +278,23 @@ func (ui *desktopUI) updateFromStatus(status torrent.Status) {
}) })
} }
func formatBytesRate(bytesPerSec float64) string {
if bytesPerSec < 0 {
bytesPerSec = 0
}
units := []string{"B/s", "KB/s", "MB/s", "GB/s", "TB/s"}
value := bytesPerSec
unit := 0
for value >= 1024 && unit < len(units)-1 {
value /= 1024
unit++
}
if unit == 0 {
return fmt.Sprintf("%.0f %s", value, units[unit])
}
return fmt.Sprintf("%.2f %s", value, units[unit])
}
func (ui *desktopUI) setMessage(msg string) { func (ui *desktopUI) setMessage(msg string) {
fyne.Do(func() { fyne.Do(func() {
ui.messageLabel.SetText(msg) ui.messageLabel.SetText(msg)