Add per-room stop/leave/restart, with a unified status display
- "share stop <code>" pauses just that room (kept in rooms.json and in "share status", so its code can still be looked up and resumed) without touching any other room or the daemon. - "share leave <code>" forgets a room entirely — no bare "stop" collapses into "leave" anymore, since those need to be distinct: pausing must survive to be resumable, forgetting must not. - "share restart <code>" now works whether the room is still active or already paused (falls back to the persisted list), rejoining with its original settings. - "share status" shows the same layout for running and stopped rooms — an explicit running/stopped label, relay address, and file details in both cases. A stopped room's file details come from stat()ing its seed files (always locally complete); a stopped receiving room has no file list since that's only knowable once it reconnects.
This commit is contained in:
+74
-11
@@ -35,22 +35,34 @@ USAGE EXAMPLES:
|
||||
Join a room to receive whatever's shared there (and help re-share it):
|
||||
share secret-code
|
||||
|
||||
See what rooms are currently active:
|
||||
See what rooms are currently active (or paused):
|
||||
share status
|
||||
|
||||
Stop the background daemon (leaves every room it's in):
|
||||
share stop
|
||||
|
||||
Pause just one room, without affecting any other room or the daemon —
|
||||
it stays in "share status" and can be resumed later:
|
||||
share stop secret-code
|
||||
|
||||
Forget one room entirely (it stops showing up in "share status" and
|
||||
won't come back on a restart):
|
||||
share leave secret-code
|
||||
|
||||
Restart the background daemon (e.g. after upgrading the binary) —
|
||||
rooms it was in resume automatically:
|
||||
share restart
|
||||
|
||||
Restart (or resume a paused) room on its own:
|
||||
share restart secret-code
|
||||
|
||||
COMMANDS:
|
||||
send seed file(s) into a room (see options with: share send -h)
|
||||
relay start your own relay (optional; see options with: share relay -h)
|
||||
status show the rooms the background daemon is currently in
|
||||
stop stop the background daemon and leave every room
|
||||
restart stop and relaunch the background daemon
|
||||
status show the rooms the background daemon knows about
|
||||
stop stop the daemon, or pause just one room if given a room code
|
||||
leave forget one specific room for good
|
||||
restart restart the daemon, or just one room if given a room code
|
||||
daemon run the background daemon in the foreground (mostly for debugging)
|
||||
|
||||
Run "share [command] -h" for the options of a given command.
|
||||
@@ -87,6 +99,8 @@ func Run() error {
|
||||
return runRelay(splice(args, i))
|
||||
case "stop":
|
||||
return runStop(splice(args, i))
|
||||
case "leave":
|
||||
return runLeave(splice(args, i))
|
||||
case "restart":
|
||||
return runRestart(splice(args, i))
|
||||
case "status":
|
||||
@@ -244,12 +258,21 @@ func runRelay(args []string) error {
|
||||
func runStop(args []string) error {
|
||||
fs := flag.NewFlagSet("stop", flag.ExitOnError)
|
||||
fs.Usage = func() {
|
||||
fmt.Fprintln(os.Stderr, "share stop")
|
||||
fmt.Fprintln(os.Stderr, "share stop [room-code]")
|
||||
}
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if code := fs.Arg(0); code != "" {
|
||||
if err := daemon.StopRoom(code); err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("Stopped room %s; it's still remembered — \"share status\" will show it, and \"share restart %s\" resumes it.\n", code, code)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := daemon.Stop(); err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
@@ -258,10 +281,33 @@ func runStop(args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runLeave(args []string) error {
|
||||
fs := flag.NewFlagSet("leave", flag.ExitOnError)
|
||||
fs.Usage = func() {
|
||||
fmt.Fprintln(os.Stderr, "share leave room-code")
|
||||
}
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code := fs.Arg(0)
|
||||
if code == "" {
|
||||
fs.Usage()
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := daemon.Leave(code); err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("Left room %s.\n", code)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runRestart(args []string) error {
|
||||
fs := flag.NewFlagSet("restart", flag.ExitOnError)
|
||||
fs.Usage = func() {
|
||||
fmt.Fprintln(os.Stderr, "share restart [options]")
|
||||
fmt.Fprintln(os.Stderr, "share restart [options] [room-code]")
|
||||
fs.PrintDefaults()
|
||||
}
|
||||
debug := fs.Bool("debug", false, "toggle debug mode for the relaunched daemon")
|
||||
@@ -269,6 +315,14 @@ func runRestart(args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if code := fs.Arg(0); code != "" {
|
||||
if err := daemon.RestartRoom(code); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Restarting room %s.\n", code)
|
||||
return nil
|
||||
}
|
||||
|
||||
if daemon.IsRunning() {
|
||||
if err := daemon.Stop(); err != nil {
|
||||
return fmt.Errorf("could not stop the running daemon: %w", err)
|
||||
@@ -300,16 +354,25 @@ func runStatus(args []string) error {
|
||||
return nil
|
||||
}
|
||||
if len(rooms) == 0 {
|
||||
fmt.Println("No active rooms.")
|
||||
fmt.Println("No rooms.")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, r := range rooms {
|
||||
peerWord := "peers"
|
||||
if r.Peers == 1 {
|
||||
peerWord = "peer"
|
||||
state := "running"
|
||||
detail := fmt.Sprintf(", %d peer", r.Peers)
|
||||
if r.Peers != 1 {
|
||||
detail += "s"
|
||||
}
|
||||
if r.Stopped {
|
||||
state = "stopped"
|
||||
detail = ""
|
||||
}
|
||||
fmt.Printf("Room %s (%s%s)\n", r.Code, state, detail)
|
||||
fmt.Printf(" relay: %s\n", r.RelayAddress)
|
||||
if r.Stopped {
|
||||
fmt.Printf(" run \"share restart %s\" to resume\n", r.Code)
|
||||
}
|
||||
fmt.Printf("Room %s (%d %s)\n", r.Code, r.Peers, peerWord)
|
||||
for _, f := range r.Files {
|
||||
state := "in progress"
|
||||
if f.Complete {
|
||||
|
||||
@@ -95,6 +95,40 @@ func Stop() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// StopRoom pauses one specific room without affecting any other room or
|
||||
// the daemon itself. Unlike Leave, the room stays remembered — it still
|
||||
// shows up in Status and resumes on the next daemon restart, and can be
|
||||
// restarted on its own with RestartRoom.
|
||||
func StopRoom(code string) error {
|
||||
if !IsRunning() {
|
||||
return fmt.Errorf("daemon is not running")
|
||||
}
|
||||
_, err := call(request{Action: "stop-room", Code: code})
|
||||
return err
|
||||
}
|
||||
|
||||
// Leave tells the running daemon to stop participating in one specific
|
||||
// room, without affecting any other room or the daemon itself. Unlike
|
||||
// StopRoom, the room is forgotten — it will not resume on the next
|
||||
// restart and stops showing up in Status.
|
||||
func Leave(code string) error {
|
||||
if !IsRunning() {
|
||||
return fmt.Errorf("daemon is not running")
|
||||
}
|
||||
_, err := call(request{Action: "leave", Code: code})
|
||||
return err
|
||||
}
|
||||
|
||||
// RestartRoom tells the running daemon to stop and rejoin one specific
|
||||
// room with its original settings, without affecting any other room.
|
||||
func RestartRoom(code string) error {
|
||||
if !IsRunning() {
|
||||
return fmt.Errorf("daemon is not running")
|
||||
}
|
||||
_, err := call(request{Action: "restart-room", Code: code})
|
||||
return err
|
||||
}
|
||||
|
||||
// WaitUntilStopped polls until the daemon is no longer reachable, or
|
||||
// returns an error once timeout elapses. Useful right after Stop, since
|
||||
// its response is sent before the daemon has necessarily finished tearing
|
||||
|
||||
@@ -5,8 +5,9 @@ import "github.com/tzgyn/share/src/swarm"
|
||||
// request/response are the only vocabulary spoken over the control
|
||||
// socket between a CLI invocation and the background daemon.
|
||||
type request struct {
|
||||
Action string `json:"action"` // "join", "stop", or "status"
|
||||
Action string `json:"action"` // "join", "stop-room", "leave", "restart-room", "stop", or "status"
|
||||
Join *joinRequest `json:"join,omitempty"`
|
||||
Code string `json:"code,omitempty"` // "stop-room"/"leave"/"restart-room" only
|
||||
}
|
||||
|
||||
// joinRequest asks the daemon to start participating in a room. Paths
|
||||
|
||||
+202
-14
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -12,9 +13,21 @@ import (
|
||||
"github.com/tzgyn/share/src/swarm"
|
||||
)
|
||||
|
||||
// activeRoom pairs a running room with the request that started it, so a
|
||||
// per-room restart can rejoin with identical settings.
|
||||
type activeRoom struct {
|
||||
client *swarm.Client
|
||||
req joinRequest
|
||||
}
|
||||
|
||||
type server struct {
|
||||
mu sync.Mutex
|
||||
rooms map[string]*swarm.Client
|
||||
rooms map[string]*activeRoom
|
||||
// leaving/restarting record intent for the *next* time a room's Wait
|
||||
// resolves, set right before calling Stop so the room's own
|
||||
// completion goroutine (started in join) knows what to do next.
|
||||
leaving map[string]bool
|
||||
restarting map[string]bool
|
||||
}
|
||||
|
||||
// Serve runs the daemon: it listens on the control socket and services
|
||||
@@ -41,7 +54,11 @@ func Serve() error {
|
||||
}
|
||||
defer os.Remove(path)
|
||||
|
||||
s := &server{rooms: make(map[string]*swarm.Client)}
|
||||
s := &server{
|
||||
rooms: make(map[string]*activeRoom),
|
||||
leaving: make(map[string]bool),
|
||||
restarting: make(map[string]bool),
|
||||
}
|
||||
log.Infof("daemon listening on %s", path)
|
||||
|
||||
// Resuming persisted rooms happens in the background, not before the
|
||||
@@ -108,6 +125,39 @@ func (s *server) handleConn(conn net.Conn) (shutdown bool) {
|
||||
}
|
||||
respond(conn, response{OK: true})
|
||||
return false
|
||||
case "stop-room":
|
||||
if req.Code == "" {
|
||||
respond(conn, response{OK: false, Error: "missing room code"})
|
||||
return false
|
||||
}
|
||||
if err := s.stopRoom(req.Code); err != nil {
|
||||
respond(conn, response{OK: false, Error: err.Error()})
|
||||
return false
|
||||
}
|
||||
respond(conn, response{OK: true})
|
||||
return false
|
||||
case "leave":
|
||||
if req.Code == "" {
|
||||
respond(conn, response{OK: false, Error: "missing room code"})
|
||||
return false
|
||||
}
|
||||
if err := s.leave(req.Code); err != nil {
|
||||
respond(conn, response{OK: false, Error: err.Error()})
|
||||
return false
|
||||
}
|
||||
respond(conn, response{OK: true})
|
||||
return false
|
||||
case "restart-room":
|
||||
if req.Code == "" {
|
||||
respond(conn, response{OK: false, Error: "missing room code"})
|
||||
return false
|
||||
}
|
||||
if err := s.restartRoom(req.Code); err != nil {
|
||||
respond(conn, response{OK: false, Error: err.Error()})
|
||||
return false
|
||||
}
|
||||
respond(conn, response{OK: true})
|
||||
return false
|
||||
case "stop":
|
||||
respond(conn, response{OK: true})
|
||||
return true
|
||||
@@ -146,7 +196,7 @@ func (s *server) join(req joinRequest) error {
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.rooms[req.Code] = cl
|
||||
s.rooms[req.Code] = &activeRoom{client: cl, req: req}
|
||||
s.mu.Unlock()
|
||||
log.Infof("joined room %s", req.Code)
|
||||
|
||||
@@ -156,22 +206,39 @@ func (s *server) join(req joinRequest) error {
|
||||
|
||||
go func() {
|
||||
err := cl.Wait()
|
||||
|
||||
s.mu.Lock()
|
||||
delete(s.rooms, req.Code)
|
||||
leftDeliberately := s.leaving[req.Code]
|
||||
restart := s.restarting[req.Code]
|
||||
delete(s.leaving, req.Code)
|
||||
delete(s.restarting, req.Code)
|
||||
s.mu.Unlock()
|
||||
if err != nil {
|
||||
|
||||
switch {
|
||||
case restart:
|
||||
log.Infof("restarting room %s", req.Code)
|
||||
if err := s.join(req); err != nil {
|
||||
log.Errorf("could not restart room %s: %v", req.Code, err)
|
||||
}
|
||||
case err != nil:
|
||||
log.Errorf("room %s ended: %v", req.Code, err)
|
||||
} else if req.OneTime {
|
||||
case leftDeliberately:
|
||||
log.Infof("left room %s", req.Code)
|
||||
if err := removePersistedRoom(req.Code); err != nil {
|
||||
log.Warnf("could not remove persisted room %s: %v", req.Code, err)
|
||||
}
|
||||
case req.OneTime:
|
||||
// finished on its own, not stopped — a one-shot fetch, don't
|
||||
// resume it on the next restart
|
||||
log.Infof("room %s finished", req.Code)
|
||||
if err := removePersistedRoom(req.Code); err != nil {
|
||||
log.Warnf("could not remove persisted room %s: %v", req.Code, err)
|
||||
}
|
||||
} else {
|
||||
// a non-one-time room only stops via an explicit Stop (i.e.
|
||||
// "share stop", shutting the whole daemon down) — leave it
|
||||
// persisted so it resumes next time the daemon starts.
|
||||
default:
|
||||
// only stops via an explicit Stop/leave/restart — if none of
|
||||
// those set a flag above, this was "share stop" shutting the
|
||||
// whole daemon down, so leave it persisted to resume next time
|
||||
log.Infof("room %s stopped", req.Code)
|
||||
}
|
||||
}()
|
||||
@@ -179,21 +246,142 @@ func (s *server) join(req joinRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// stopRoom pauses one room without forgetting it: it stays in rooms.json
|
||||
// and shows up (as stopped) in Status, so its code can be looked up and
|
||||
// handed to restartRoom later. This is the single-room equivalent of
|
||||
// "share stop" with no code, just scoped to one room.
|
||||
func (s *server) stopRoom(code string) error {
|
||||
s.mu.Lock()
|
||||
room, ok := s.rooms[code]
|
||||
s.mu.Unlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("not in room %s", code)
|
||||
}
|
||||
room.client.Stop()
|
||||
return nil
|
||||
}
|
||||
|
||||
// leave ends this room's participation and forgets it: it will not
|
||||
// resume on the next daemon start, and won't be listed by Status. Works
|
||||
// whether the room is currently active or already paused via stopRoom.
|
||||
func (s *server) leave(code string) error {
|
||||
s.mu.Lock()
|
||||
room, active := s.rooms[code]
|
||||
if active {
|
||||
s.leaving[code] = true
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
if active {
|
||||
room.client.Stop() // its completion goroutine removes the persisted entry
|
||||
return nil
|
||||
}
|
||||
return forgetIfPersisted(code)
|
||||
}
|
||||
|
||||
func forgetIfPersisted(code string) error {
|
||||
persisted, err := loadPersistedRooms()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, req := range persisted {
|
||||
if req.Code == code {
|
||||
return removePersistedRoom(code)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("not in room %s", code)
|
||||
}
|
||||
|
||||
// restartRoom stops (if currently active) or picks back up (if already
|
||||
// paused) one room with the exact settings it was originally joined
|
||||
// with, without touching any other room.
|
||||
func (s *server) restartRoom(code string) error {
|
||||
s.mu.Lock()
|
||||
room, active := s.rooms[code]
|
||||
if active {
|
||||
s.restarting[code] = true
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
if active {
|
||||
room.client.Stop() // its completion goroutine rejoins with the same request
|
||||
return nil
|
||||
}
|
||||
|
||||
persisted, err := loadPersistedRooms()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, req := range persisted {
|
||||
if req.Code == code {
|
||||
return s.join(req)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("not in room %s", code)
|
||||
}
|
||||
|
||||
func (s *server) stopAll() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for code, cl := range s.rooms {
|
||||
for code, room := range s.rooms {
|
||||
log.Infof("leaving room %s", code)
|
||||
cl.Stop()
|
||||
room.client.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// status reports every active room plus every persisted-but-stopped
|
||||
// room (paused via stopRoom, or resumed-but-not-yet-active), so a stopped
|
||||
// room's code stays visible in "share status" for restarting later.
|
||||
func (s *server) status() []swarm.RoomStatus {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
statuses := make([]swarm.RoomStatus, 0, len(s.rooms))
|
||||
for _, cl := range s.rooms {
|
||||
statuses = append(statuses, cl.Status())
|
||||
active := make(map[string]bool, len(s.rooms))
|
||||
for code, room := range s.rooms {
|
||||
statuses = append(statuses, room.client.Status())
|
||||
active[code] = true
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
persisted, err := loadPersistedRooms()
|
||||
if err != nil {
|
||||
log.Warnf("could not load persisted rooms for status: %v", err)
|
||||
return statuses
|
||||
}
|
||||
for _, req := range persisted {
|
||||
if !active[req.Code] {
|
||||
statuses = append(statuses, stoppedRoomStatus(req))
|
||||
}
|
||||
}
|
||||
return statuses
|
||||
}
|
||||
|
||||
// stoppedRoomStatus reports what's on record for a paused room, so its
|
||||
// display looks the same as a running one: relay address always, and
|
||||
// file details for a seeding room (its files are always fully present
|
||||
// locally, so their progress is trivially "complete"). A receiving
|
||||
// room's file details aren't knowable until it reconnects and gets a
|
||||
// manifest, so those are simply omitted rather than guessed at.
|
||||
func stoppedRoomStatus(req joinRequest) swarm.RoomStatus {
|
||||
files := make([]swarm.FileStatus, 0, len(req.Files))
|
||||
for _, path := range req.Files {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
log.Debugf("could not stat %s for status: %v", path, err)
|
||||
continue
|
||||
}
|
||||
chunks := swarm.NumChunks(info.Size())
|
||||
files = append(files, swarm.FileStatus{
|
||||
Name: filepath.Base(path),
|
||||
Size: info.Size(),
|
||||
HaveChunks: chunks,
|
||||
TotalChunks: chunks,
|
||||
Complete: true,
|
||||
})
|
||||
}
|
||||
return swarm.RoomStatus{
|
||||
Code: req.Code,
|
||||
RelayAddress: req.RelayAddress,
|
||||
Stopped: true,
|
||||
Files: files,
|
||||
}
|
||||
}
|
||||
|
||||
+13
-7
@@ -150,11 +150,16 @@ type FileStatus struct {
|
||||
Complete bool
|
||||
}
|
||||
|
||||
// RoomStatus summarizes one room this client is participating in.
|
||||
// RoomStatus summarizes one room this client is participating in. A
|
||||
// stopped room (Stopped true) has no live Client to report Peers from,
|
||||
// but RelayAddress/Files are still filled in from what's on record so
|
||||
// the display looks the same whether the room is running or paused.
|
||||
type RoomStatus struct {
|
||||
Code string
|
||||
Peers int
|
||||
Files []FileStatus
|
||||
Code string
|
||||
RelayAddress string
|
||||
Stopped bool
|
||||
Peers int
|
||||
Files []FileStatus
|
||||
}
|
||||
|
||||
// Status reports this room's current code, peer count, and per-file
|
||||
@@ -179,9 +184,10 @@ func (cl *Client) Status() RoomStatus {
|
||||
}
|
||||
}
|
||||
return RoomStatus{
|
||||
Code: cl.opts.Code,
|
||||
Peers: len(distinctPeers),
|
||||
Files: files,
|
||||
Code: cl.opts.Code,
|
||||
RelayAddress: cl.opts.RelayAddress,
|
||||
Peers: len(distinctPeers),
|
||||
Files: files,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user