From 5a71463360c1135d16dc54d8275a16d9a82189e6 Mon Sep 17 00:00:00 2001 From: LYJW131 Date: Mon, 13 Jul 2026 10:52:34 +0800 Subject: [PATCH] feat: support multiple wrappers per account Resolve the runtime image update against the current amd64 publishing and automatic wrapper installation flow. Co-authored-by: Codex Signed-off-by: LYJW131 --- Dockerfile | 2 +- README.md | 3 ++- handler.go | 13 +++++++++++ instance.go | 10 +++++++++ instance_test.go | 22 ++++++++++++++++++ main.go | 58 +++++++++++++++++++++++++----------------------- wrapper.go | 3 +-- 7 files changed, 79 insertions(+), 32 deletions(-) create mode 100644 instance_test.go diff --git a/Dockerfile b/Dockerfile index 869867d..d890000 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ RUN test "$TARGETARCH" = "amd64" && \ CGO_ENABLED=0 GOOS="$TARGETOS" GOARCH="$TARGETARCH" \ go build -trimpath -ldflags="-s -w" -o /out/wrapper-manager . -FROM ubuntu:24.04 +FROM debian:13.2 WORKDIR /root diff --git a/README.md b/README.md index eb1335d..b6484fc 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Support linux x86_64 and arm64 arch ## Features - Multi-instance management - Add accounts at runtime (support 2FA) +- Start multiple Wrapper instances for the same account by logging in again after each instance is ready - Multi-connection decryption - gRPC API - Get lyrics without an account @@ -54,4 +55,4 @@ This repository is forked to implement several performance optimizations, thread - Adaptations for recent Apple token structure updates. - Enhanced error handling when `checkAvailableOnRegion` fails due to network issues (returns clean errors instead of crashing). - Fixed standard panics caused by nil interface conversions. - - Restored missing `SelectInstance` validations for music videos. \ No newline at end of file + - Restored missing `SelectInstance` validations for music videos. diff --git a/handler.go b/handler.go index ea45783..73f3d80 100644 --- a/handler.go +++ b/handler.go @@ -8,6 +8,7 @@ import ( ) var LoginConnMap = sync.Map{} +var PendingLoginByAccount = sync.Map{} func Login2FAHandler(id string) { conn, _ := LoginConnMap.Load(id) @@ -25,6 +26,7 @@ func Login2FAHandler(id string) { func LoginDoneHandler(id string) { SaveInstances() + clearPendingLogin(id) conn, _ := LoginConnMap.LoadAndDelete(id) if conn == nil { return @@ -43,6 +45,7 @@ func LoginDoneHandler(id string) { func LoginFailedHandler(id string) { RemoveWrapperData(id) + clearPendingLogin(id) conn, _ := LoginConnMap.LoadAndDelete(id) if conn == nil { return @@ -58,3 +61,13 @@ func LoginFailedHandler(id string) { log.Println(err) } } + +func clearPendingLogin(id string) { + PendingLoginByAccount.Range(func(account, pendingID any) bool { + if pendingID == id { + PendingLoginByAccount.Delete(account) + return false + } + return true + }) +} diff --git a/instance.go b/instance.go index 0f329e8..b93335f 100644 --- a/instance.go +++ b/instance.go @@ -71,3 +71,13 @@ func GetInstance(id string) *WrapperInstance { } return &WrapperInstance{} } + +func GetInstancesByAccount(account string) []*WrapperInstance { + var matches []*WrapperInstance + for _, instance := range Instances { + if instance.Account == account { + matches = append(matches, instance) + } + } + return matches +} diff --git a/instance_test.go b/instance_test.go new file mode 100644 index 0000000..c0601c2 --- /dev/null +++ b/instance_test.go @@ -0,0 +1,22 @@ +package main + +import "testing" + +func TestGetInstancesByAccountReturnsAllMatchingWrappers(t *testing.T) { + original := Instances + t.Cleanup(func() { Instances = original }) + + Instances = []*WrapperInstance{ + {Id: "first", Account: "same@example.com"}, + {Id: "other", Account: "other@example.com"}, + {Id: "second", Account: "same@example.com"}, + } + + matches := GetInstancesByAccount("same@example.com") + if len(matches) != 2 { + t.Fatalf("expected 2 wrappers, got %d", len(matches)) + } + if matches[0].Id != "first" || matches[1].Id != "second" { + t.Fatalf("unexpected wrappers: %#v", matches) + } +} diff --git a/main.go b/main.go index e8bb66d..1131cb7 100644 --- a/main.go +++ b/main.go @@ -78,25 +78,30 @@ func (s *server) Login(stream grpc.BidiStreamingServer[pb.LoginRequest, pb.Login if err != nil { return err } - id := uuid.NewV5(uuid.FromStringOrNil("77777777-7777-7777-7777-77777777"), req.Data.Username).String() - for _, instance := range Instances { - if instance.Id == id { - err = stream.Send(&pb.LoginReply{ - Header: &pb.ReplyHeader{ - Code: -1, - Msg: "already login", - }, - }) - if err != nil { + if req.Data.TwoStepCode != "" { + pendingID, ok := PendingLoginByAccount.Load(req.Data.Username) + if !ok { + if err = stream.Send(&pb.LoginReply{Header: &pb.ReplyHeader{Code: -1, Msg: "no pending login"}}); err != nil { return err } + continue } - } - if req.Data.TwoStepCode != "" { + id := pendingID.(string) provide2FACode(id, req.Data.TwoStepCode) } else { - LoginConnMap.Store(id, stream) - go WrapperInitial(req.Data.Username, req.Data.Password) + id, idErr := uuid.NewV4() + if idErr != nil { + return idErr + } + idString := id.String() + if _, loaded := PendingLoginByAccount.LoadOrStore(req.Data.Username, idString); loaded { + if err = stream.Send(&pb.LoginReply{Header: &pb.ReplyHeader{Code: -1, Msg: "login already pending"}}); err != nil { + return err + } + continue + } + LoginConnMap.Store(idString, stream) + go WrapperInitial(id, req.Data.Username, req.Data.Password) } } } @@ -108,9 +113,8 @@ func (s *server) Logout(c context.Context, req *pb.LogoutRequest) (*pb.LogoutRep } else { log.Infof("logout request from unknown peer") } - id := uuid.NewV5(uuid.FromStringOrNil("77777777-7777-7777-7777-77777777"), req.Data.Username).String() - instance := GetInstance(id) - if instance.Id == "" { + instances := GetInstancesByAccount(req.Data.Username) + if len(instances) == 0 { return &pb.LogoutReply{ Header: &pb.ReplyHeader{ Code: -1, @@ -119,18 +123,16 @@ func (s *server) Logout(c context.Context, req *pb.LogoutRequest) (*pb.LogoutRep Data: &pb.LogoutData{Username: req.Data.Username}, }, nil } - instance.NoRestart = true - err := KillWrapper(instance.Id) - if err != nil { - return &pb.LogoutReply{ - Header: &pb.ReplyHeader{ - Code: -1, - Msg: "failed to kill wrapper", - }, - Data: &pb.LogoutData{Username: req.Data.Username}, - }, nil + for _, instance := range instances { + instance.NoRestart = true + if err := KillWrapper(instance.Id); err != nil { + return &pb.LogoutReply{ + Header: &pb.ReplyHeader{Code: -1, Msg: "failed to kill wrapper"}, + Data: &pb.LogoutData{Username: req.Data.Username}, + }, nil + } + RemoveWrapperData(instance.Id) } - RemoveWrapperData(instance.Id) return &pb.LogoutReply{ Header: &pb.ReplyHeader{ Code: 0, diff --git a/wrapper.go b/wrapper.go index 3d55127..fda3a48 100644 --- a/wrapper.go +++ b/wrapper.go @@ -77,8 +77,7 @@ func prepareWrapper(mirror bool, arch string) error { return nil } -func WrapperInitial(account string, password string) { - id := uuid.NewV5(uuid.FromStringOrNil("77777777-7777-7777-7777-77777777"), account) +func WrapperInitial(id uuid.UUID, account string, password string) { err := os.MkdirAll("data/wrapper/rootfs/data/instances/"+id.String(), 0777) if err != nil { panic(err)