From ed4f65adee8045b76718f6f9f23a396ec5168e81 Mon Sep 17 00:00:00 2001 From: sewn Date: Sun, 15 Oct 2023 10:01:17 +0300 Subject: [PATCH 01/55] cmd/vinegar: fix prefix initialization dir thingy Signed-off-by: sewn --- cmd/vinegar/vinegar.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/vinegar/vinegar.go b/cmd/vinegar/vinegar.go index f0229b78..f9a9b54d 100644 --- a/cmd/vinegar/vinegar.go +++ b/cmd/vinegar/vinegar.go @@ -57,6 +57,11 @@ func main() { } pfx := wine.New(dirs.Prefix) + // Always ensure its created, wine will complain if the root + // directory doesnt exist + if err := os.MkdirAll(dirs.Prefix, 0o755); err != nil { + log.Fatal(err) + } c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) From 7670b5fab3ff0566f183df2b45f8beec7fb6d741 Mon Sep 17 00:00:00 2001 From: sewn Date: Sun, 15 Oct 2023 16:58:27 +0300 Subject: [PATCH 02/55] cmd/vinegar: remove broken font for studio --- cmd/vinegar/binary.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index 0738df8c..a7b360ed 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -3,6 +3,7 @@ package main import ( "fmt" "log" + "os" "os/exec" "path/filepath" "strings" @@ -185,6 +186,15 @@ func (b *Binary) Install() error { return err } + if b.Type == roblox.Studio { + brokenFont := filepath.Join(b.Dir, "StudioFonts", "SourceSansPro-Black.ttf") + + log.Printf("Removing broken font %s", brokenFont) + if err := os.RemoveAll(brokenFont); err != nil { + log.Println("Failed to remove font: %s", err) + } + } + if err := bootstrapper.WriteAppSettings(b.Dir); err != nil { return err } From 3369404bc7ff37cbc90d646d27e65736b1022bef Mon Sep 17 00:00:00 2001 From: sewn Date: Sun, 15 Oct 2023 20:50:55 +0300 Subject: [PATCH 03/55] initial discord rich presence --- README.md | 2 + bloxstraprpc/discord.go | 153 +++++++++++++++++++++++++++++++++++ bloxstraprpc/logs.go | 165 ++++++++++++++++++++++++++++++++++++++ bloxstraprpc/message.go | 59 ++++++++++++++ cmd/vinegar/binary.go | 57 ++++++++++++- go.mod | 5 ++ go.sum | 11 +++ internal/config/config.go | 3 +- roblox/api/api.go | 18 +++++ roblox/api/games.go | 51 ++++++++++++ roblox/api/thumbnails.go | 29 +++++++ roblox/api/universe.go | 20 +++++ util/paths.go | 24 ++++++ 13 files changed, 595 insertions(+), 2 deletions(-) create mode 100644 bloxstraprpc/discord.go create mode 100644 bloxstraprpc/logs.go create mode 100644 bloxstraprpc/message.go create mode 100644 roblox/api/api.go create mode 100644 roblox/api/games.go create mode 100644 roblox/api/thumbnails.go create mode 100644 roblox/api/universe.go diff --git a/README.md b/README.md index c45b2cda..9557ddef 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ An open-source, minimal, configurable, fast bootstrapper for running Roblox on L + Automatic DXVK Installer and uninstaller + Automatic Wineprefix killer when Roblox has quit + Automatic removal of outdated cached packages and versions of Roblox ++ Discord Rich Presence support ++ Roblox's logs appear within Vinegar + FPS Unlocking for Player by default, without rbxfpsunlocker + Browser launch via MIME + Custom execution of wine program within wineprefix diff --git a/bloxstraprpc/discord.go b/bloxstraprpc/discord.go new file mode 100644 index 00000000..3e0f9552 --- /dev/null +++ b/bloxstraprpc/discord.go @@ -0,0 +1,153 @@ +package bloxstraprpc + +import ( + "log" + "strconv" + "time" + + "github.com/hugolgst/rich-go/client" + "github.com/vinegarhq/vinegar/roblox/api" +) + +const RPCAppID = "1159891020956323923" + +func Login() error { + log.Println("Authenticating Discord RPC") + return client.Login(RPCAppID) +} + +func Logout() { + log.Println("Deauthenticating Discord RPC") + client.Logout() +} + +func (a *Activity) SetCurrentGame() error { + if !a.InGame { + log.Println("Not in game, clearing presence") + a.presence = client.Activity{} + } else { + if err := a.SetPresence(); err != nil { + return err + } + } + + return a.UpdatePresence() +} + +func (a *Activity) SetPresence() error { + var status string + log.Printf("Setting presence for Place ID %s", a.PlaceID) + + uid, err := api.GetUniverseID(a.PlaceID) + if err != nil { + return err + } + log.Printf("Got Universe ID as %s", uid) + + if !a.IsTeleport || uid != a.currentUniverseID { + a.timeStartedUniverse = time.Now() + } + + a.currentUniverseID = uid + + gd, err := api.GetGameDetails(uid) + if err != nil { + return err + } + log.Println("Got Game details") + + tn, err := api.GetGameIcon(uid, "PlaceHolder", "512x512", "Png", false) + if err != nil { + return err + } + log.Printf("Got Universe thumbnail as %s", tn.ImageURL) + + switch a.ServerType { + case Public: + status = "by " + gd.Creator.Name + case Private: + status = "In a private server" + case Reserved: + status = "In a reserved server" + } + + a.presence = client.Activity{ + State: status, + Details: "Playing " + gd.Name, + LargeImage: tn.ImageURL, + LargeText: gd.Name, + SmallImage: "roblox", + SmallText: "Roblox", + Timestamps: &client.Timestamps{ + Start: &a.timeStartedUniverse, + }, + Buttons: []*client.Button{ + { + Label: "See game page", + Url: "https://www.roblox.com/games/meh", + }, + }, + } + + return nil +} + +func (a *Activity) ProcessMessage(m *Message) { + if m.Command != "SetRichPresence" { + return + } + + if m.Data.Details != "" { + a.presence.Details = m.Data.Details + } + + if m.Data.State != "" { + a.presence.State = m.Data.State + } + + if a.presence.Timestamps != nil { + if m.TimestampStart == 0 { + a.presence.Timestamps.Start = nil + } else { + ts := time.UnixMilli(m.TimestampStart) + a.presence.Timestamps.Start = &ts + } + } + + if a.presence.Timestamps != nil { + if m.TimestampEnd == 0 { + a.presence.Timestamps.End = nil + } else { + te := time.UnixMilli(m.TimestampEnd) + a.presence.Timestamps.End = &te + } + } + + if m.SmallImage.Clear { + a.presence.SmallImage = "" + } + + if m.SmallImage.AssetID != 0 { + a.presence.SmallImage = "https://assetdelivery.roblox.com/v1/asset/?id" + + strconv.FormatInt(m.SmallImage.AssetID, 10) + } + + if m.LargeImage.Clear { + a.presence.LargeImage = "" + } + + if m.LargeImage.AssetID != 0 { + a.presence.LargeImage = "https://assetdelivery.roblox.com/v1/asset/?id" + + strconv.FormatInt(m.LargeImage.AssetID, 10) + } +} + +func (a *Activity) UpdatePresence() error { + // if a.presence == client.Activity{} { + // log.Println("Presence is empty, clearing") + // return ClearPresence() + // } + + log.Printf("Updating presence: %+v", a.presence) + return client.SetActivity(a.presence) +} diff --git a/bloxstraprpc/logs.go b/bloxstraprpc/logs.go new file mode 100644 index 00000000..c940d50e --- /dev/null +++ b/bloxstraprpc/logs.go @@ -0,0 +1,165 @@ +package bloxstraprpc + +import ( + "log" + "regexp" + "strings" + "time" + + "github.com/hugolgst/rich-go/client" +) + +const ( + GameJoiningEntry = "[FLog::Output] ! Joining game" + GameJoiningPrivateServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::joinGamePostPrivateServer" + GameJoiningReservedServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToReservedServer" + GameJoiningUDMUXEntry = "[FLog::Network] UDMUX Address = " + GameJoinedEntry = "[FLog::Network] serverId:" + GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:" + GameTeleportingEntry = "[FLog::SingleSurfaceApp] initiateTeleport" + GameMessageEntry = "[FLog::Output] [BloxstrapRPC]" +) + +var ( + GameJoiningEntryPattern = regexp.MustCompile(`! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)`) + GameJoiningUDMUXPattern = regexp.MustCompile(`UDMUX Address = ([0-9\.]+), Port = [0-9]+ \| RCC Server Address = ([0-9\.]+), Port = [0-9]+`) + GameJoinedEntryPattern = regexp.MustCompile(`serverId: ([0-9\.]+)\|[0-9]+`) +) + +type ServerType int + +const ( + Public ServerType = iota + Private + Reserved +) + +type Activity struct { + presence client.Activity + timeStartedUniverse time.Time + currentUniverseID string + + InGame bool + IsTeleport bool + ServerType + PlaceID string + JobID string + MAC string + + teleport bool + reservedteleport bool +} + +func (a *Activity) HandleLog(line string) error { + if !a.InGame && a.PlaceID == "" { + if strings.Contains(line, GameJoiningPrivateServerEntry) { + a.ServerType = Private + return nil + } + + if strings.Contains(line, GameJoiningEntry) { + a.handleGameJoining(line) + return nil + } + } + + if !a.InGame && a.PlaceID != "" { + if strings.Contains(line, GameJoiningUDMUXEntry) { + a.handleUDMUX(line) + return nil + } + + if strings.Contains(line, GameJoinedEntry) { + a.handleGameJoined(line) + + return a.SetCurrentGame() + } + } + + if a.InGame && a.PlaceID != "" { + if strings.Contains(line, GameDisconnectedEntry) { + log.Printf("Disconnected From Game (%s/%s/%s)", a.PlaceID, a.JobID, a.MAC) + a.Clear() + return a.SetCurrentGame() + } + + if strings.Contains(line, GameTeleportingEntry) { + log.Printf("Teleporting to server (%s/%s/%s)", a.PlaceID, a.JobID, a.MAC) + a.teleport = true + return nil + } + + if a.teleport && strings.Contains(line, GameJoiningReservedServerEntry) { + log.Printf("Teleporting to reserved server") + a.reservedteleport = true + return nil + } + + if strings.Contains(line, GameMessageEntry) { + m, err := ParseMessage(line) + if err != nil { + return err + } + + a.ProcessMessage(&m) + return a.UpdatePresence() + } + } + + return nil +} + +func (a *Activity) handleUDMUX(line string) { + m := GameJoiningUDMUXPattern.FindStringSubmatch(line) + if len(m) != 3 || m[2] != a.MAC { + return + } + + a.MAC = m[1] + log.Printf("Got game join UDMUX: %s", a.MAC) +} + +func (a *Activity) handleGameJoining(line string) { + m := GameJoiningEntryPattern.FindStringSubmatch(line) + if len(m) != 4 { + return + } + + a.InGame = false + a.JobID = m[1] + a.PlaceID = m[2] + a.MAC = m[3] + + if a.teleport { + a.IsTeleport = true + a.teleport = false + } + + if a.reservedteleport { + a.ServerType = Reserved + a.reservedteleport = false + } + + log.Printf("Joining Game (%s/%s/%s)", a.JobID, a.PlaceID, a.MAC) +} + +func (a *Activity) handleGameJoined(line string) { + m := GameJoinedEntryPattern.FindStringSubmatch(line) + if len(m) != 2 || m[1] != a.MAC { + return + } + + a.InGame = true + log.Printf("Joined Game (%s/%s/%s)", a.PlaceID, a.JobID, a.MAC) + // handle rpc +} + +func (a *Activity) Clear() { + a.InGame = false + a.PlaceID = "" + a.JobID = "" + a.MAC = "" + a.ServerType = Public + a.IsTeleport = false + a.presence = client.Activity{} +} diff --git a/bloxstraprpc/message.go b/bloxstraprpc/message.go new file mode 100644 index 00000000..fd0f3e38 --- /dev/null +++ b/bloxstraprpc/message.go @@ -0,0 +1,59 @@ +package bloxstraprpc + +import ( + "encoding/json" + "errors" + "log" + "strings" +) + +type RichPresenceImage struct { + AssetID int64 `json:"assetId"` + HoverText int64 `json:"hoverText"` + Clear bool `json:"clear"` + Reset bool `json:"reset"` +} + +type Data struct { + Details string `json:"details"` + State string `json:"state"` + TimestampStart int64 `json:"timeStart"` + TimestampEnd int64 `json:"timeEnd"` + SmallImage RichPresenceImage `json:"smallImage"` + LargeImage RichPresenceImage `json:"largeImage"` +} + +type Message struct { + Command string `json:"command"` + Data `json:"data"` +} + +func ParseMessage(line string) (Message, error) { + var m Message + + if !strings.Contains(line, GameMessageEntry) { + return m, nil + } + + msg := line[strings.Index(line, GameMessageEntry)+len(GameMessageEntry)+1:] + + if err := json.Unmarshal([]byte(msg), &m); err != nil { + return m, err + } + + if m.Command == "" { + return m, errors.New("command is empty") + } + + if len(m.Data.Details) > 128 { + return m, errors.New("details cannot be longer than 128 characters") + } + + if len(m.Data.State) > 128 { + return m, errors.New("details cannot be longer than 128 characters") + } + + log.Printf("Received message: %+v", m) + + return m, nil +} diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index a7b360ed..49b4cd84 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -9,6 +9,8 @@ import ( "strings" "time" + "github.com/nxadm/tail" + bsrpc "github.com/vinegarhq/vinegar/bloxstraprpc" "github.com/vinegarhq/vinegar/internal/config" "github.com/vinegarhq/vinegar/internal/config/state" "github.com/vinegarhq/vinegar/internal/dirs" @@ -58,7 +60,9 @@ func NewBinary(bt roblox.BinaryType, cfg *config.Config, pfx *wine.Prefix) Binar } func (b *Binary) Run(args ...string) error { + then := time.Now() exe := b.Type.Executable() + cmd, err := b.Command(args...) if err != nil { return err @@ -76,6 +80,8 @@ func (b *Binary) Run(args ...string) error { kill = false } + log.Println(kill, exe) + // Launches into foreground if err := cmd.Start(); err != nil { return err @@ -84,6 +90,20 @@ func (b *Binary) Run(args ...string) error { time.Sleep(2500 * time.Millisecond) b.Splash.Close() + if b.Config.DiscordRPC { + err := bsrpc.Login() + if err != nil { + return err + } + + go func() { + time.Sleep(6 * time.Second) + if err := b.LogFile(&then); err != nil { + log.Printf("epic fail: %s", err) + } + }() + } + if kill && b.Config.AutoKillPrefix { log.Println("Waiting for Roblox's process to die :)") @@ -98,6 +118,41 @@ func (b *Binary) Run(args ...string) error { b.Prefix.Kill() } + if b.Config.DiscordRPC { + bsrpc.Logout() + } + + return nil +} + +func (b *Binary) LogFile(comparison *time.Time) error { + appData, err := b.Prefix.AppDataDir() + if err != nil { + return err + } + + fi, err := util.FindTimeFile(filepath.Join(appData, "Local", "Roblox", "logs"), comparison) + if err != nil { + return err + } + + log.Printf("Found Roblox log file: %s", fi) + t, err := tail.TailFile(fi, tail.Config{Follow: true}) + if err != nil { + return err + } + + var a bsrpc.Activity + + for line := range t.Lines { + fmt.Println(line.Text) + + err = a.HandleLog(line.Text) + if err != nil { + log.Printf("epic fail: %s", err) + } + } + return nil } @@ -191,7 +246,7 @@ func (b *Binary) Install() error { log.Printf("Removing broken font %s", brokenFont) if err := os.RemoveAll(brokenFont); err != nil { - log.Println("Failed to remove font: %s", err) + log.Printf("Failed to remove font: %s", err) } } diff --git a/go.mod b/go.mod index d2b8d78b..a761b85d 100644 --- a/go.mod +++ b/go.mod @@ -12,17 +12,22 @@ require ( require ( dario.cat/mergo v1.0.0 gioui.org v0.3.0 + github.com/hugolgst/rich-go v0.0.0-20230917173849-4a4fb1d3c362 + github.com/nxadm/tail v1.4.11 golang.org/x/sys v0.11.0 ) require ( gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect gioui.org/shader v1.0.6 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 // indirect golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 // indirect golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 // indirect golang.org/x/image v0.5.0 // indirect golang.org/x/text v0.7.0 // indirect + gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect ) retract ( diff --git a/go.sum b/go.sum index 395b7f31..828fcb81 100644 --- a/go.sum +++ b/go.sum @@ -14,9 +14,15 @@ github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo= github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI= +github.com/hugolgst/rich-go v0.0.0-20230917173849-4a4fb1d3c362 h1:Q8D2HP1l2mOoeRVLhHjDhK8MRb7LkjESWRtd2gbauws= +github.com/hugolgst/rich-go v0.0.0-20230917173849-4a4fb1d3c362/go.mod h1:nGaW7CGfNZnhtiFxMpc4OZdqIexGXjUlBnlmpZmjEKA= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/otiai10/copy v1.12.0 h1:cLMgSQnXBs1eehF0Wy/FAGsgDTDmAqFR7rQylBb1nDY= github.com/otiai10/copy v1.12.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= @@ -48,6 +54,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -62,6 +69,10 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go index 20d1a951..1b15fe6f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -27,6 +27,7 @@ type Binary struct { Channel string `toml:"channel"` Launcher string `toml:"launcher"` Renderer string `toml:"renderer"` + DiscordRPC bool `toml:"discord_rpc"` ForcedVersion string `toml:"forced_version"` AutoKillPrefix bool `toml:"auto_kill_prefix"` Dxvk bool `toml:"dxvk"` @@ -95,7 +96,7 @@ func Default() Config { }, }, Studio: Binary{ - Dxvk: true, + Dxvk: true, AutoKillPrefix: true, }, diff --git a/roblox/api/api.go b/roblox/api/api.go new file mode 100644 index 00000000..682e0389 --- /dev/null +++ b/roblox/api/api.go @@ -0,0 +1,18 @@ +package api + +import ( + "encoding/json" + "log" + + "github.com/vinegarhq/vinegar/util" +) + +func UnmarshalBody(url string, v any) error { + log.Printf("Sending API Request for %s", url) + body, err := util.Body(url) + if err != nil { + return err + } + + return json.Unmarshal([]byte(body), &v) +} diff --git a/roblox/api/games.go b/roblox/api/games.go new file mode 100644 index 00000000..b3d30bb9 --- /dev/null +++ b/roblox/api/games.go @@ -0,0 +1,51 @@ +package api + +type Creator struct { + ID int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsRNVAccount bool `json:"isRNVAccount"` + HasVerifiedBadge bool `json:"hasVerifiedBadge"` +} + +type GameDetail struct { + ID int64 `json:"id"` + RootPlaceID int64 `json:"rootPlaceId"` + Name string `json:"name"` + Description string `json:"description"` + SourceName string `json:"sourceName"` + SourceDescription string `json:"sourceDescription"` + Creator Creator `json:"creator"` + Price int64 `json:"price"` + AllowedGearGenres []string `json:"allowedGearGenres"` + AllowedGearCategories []string `json:"allowedGearCategories"` + IsGenreEnforced bool `json:"isGenreEnforced"` + CopyingAllowed bool `json:"copyingAllowed"` + Playing int64 `json:"playing"` + Visits int64 `json:"visits"` + MaxPlayers int32 `json:"maxPlayers"` + Created string `json:"created"` + Updated string `json:"updated"` + StudioAccessToApisAllowed bool `json:"studioAccessToApisAllowed"` + CreateVipServersAllowed bool `json:"createVipServersAllowed"` + UniverseAvatarType string `json:"universeAvatarType"` + Genre string `json:"genre"` + IsAllGenre bool `json:"isAllGenre"` + IsFavoritedByUser bool `json:"isFavoritedByUser"` + FavoritedCount int64 `json:"favoritedCount"` +} + +type GameDetailResponse struct { + Data []GameDetail `json:"data"` +} + +func GetGameDetails(universeID string) (GameDetail, error) { + var gdr GameDetailResponse + + err := UnmarshalBody("https://games.roblox.com/v1/games?universeIds="+universeID, &gdr) + if err != nil { + return GameDetail{}, err + } + + return gdr.Data[0], nil +} diff --git a/roblox/api/thumbnails.go b/roblox/api/thumbnails.go new file mode 100644 index 00000000..0db65ecc --- /dev/null +++ b/roblox/api/thumbnails.go @@ -0,0 +1,29 @@ +package api + +import ( + "fmt" +) + +type Thumbnail struct { + TargetID int64 `json:"targetId"` + State string `json:"state"` + ImageURL string `json:"imageUrl"` + Version string `json:"version"` +} + +type ThumbnailResponse struct { + Data []Thumbnail `json:"data"` +} + +func GetGameIcon(universeID, returnPolicy, size, format string, isCircular bool) (Thumbnail, error) { + var tnr ThumbnailResponse + err := UnmarshalBody( + fmt.Sprintf("https://thumbnails.roblox.com/v1/games/icons?universeIds=%s&returnPolicy=%s&size=%s&format=%s&isCircular=%t", + universeID, returnPolicy, size, format, isCircular), &tnr, + ) + if err != nil { + return Thumbnail{}, err + } + + return tnr.Data[0], nil +} diff --git a/roblox/api/universe.go b/roblox/api/universe.go new file mode 100644 index 00000000..44b3d518 --- /dev/null +++ b/roblox/api/universe.go @@ -0,0 +1,20 @@ +package api + +import ( + "strconv" +) + +type UniverseIdResponse struct { + UniverseID int64 `json:"universeId"` +} + +func GetUniverseID(placeID string) (string, error) { + var uidr UniverseIdResponse + + err := UnmarshalBody("https://apis.roblox.com/universes/v1/places/"+placeID+"/universe", &uidr) + if err != nil { + return "", err + } + + return strconv.FormatInt(uidr.UniverseID, 10), nil +} diff --git a/util/paths.go b/util/paths.go index c79094e6..8c87cd48 100644 --- a/util/paths.go +++ b/util/paths.go @@ -1,7 +1,10 @@ package util import ( + "io/fs" "os" + "path/filepath" + "time" ) func WalkDirExcluded(dir string, included []string, onExcluded func(string) error) error { @@ -25,3 +28,24 @@ find: return nil } + +func FindTimeFile(dir string, comparison *time.Time) (string, error) { + var name string + + err := filepath.Walk(dir, func(p string, i fs.FileInfo, err error) error { + if i.ModTime().After(*comparison) { + name = p + } + return nil + }) + + if err != nil { + return "", err + } + + if name == "" { + return "", os.ErrNotExist + } + + return name, nil +} From 7f364c69ac45c318aef3b42d71e86cc1c1d8ea5f Mon Sep 17 00:00:00 2001 From: sewn Date: Sun, 15 Oct 2023 20:53:51 +0300 Subject: [PATCH 04/55] cmd/vinegar: rpc erorr message --- cmd/vinegar/binary.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index 49b4cd84..717a6188 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -97,9 +97,9 @@ func (b *Binary) Run(args ...string) error { } go func() { - time.Sleep(6 * time.Second) - if err := b.LogFile(&then); err != nil { - log.Printf("epic fail: %s", err) + time.Sleep(8 * time.Second) + if err := b.HandleRobloxLog(&then); err != nil { + log.Printf("Failed to handle Discord RPC: %s", err) } }() } @@ -125,7 +125,7 @@ func (b *Binary) Run(args ...string) error { return nil } -func (b *Binary) LogFile(comparison *time.Time) error { +func (b *Binary) HandleRobloxLog(comparison *time.Time) error { appData, err := b.Prefix.AppDataDir() if err != nil { return err From 10ce9ce43d2dd9fd923051615aee37ad23d9c138 Mon Sep 17 00:00:00 2001 From: sewn Date: Sun, 15 Oct 2023 21:22:07 +0300 Subject: [PATCH 05/55] enable rpc by default, poll log without rpc --- cmd/vinegar/binary.go | 64 ++++++++++++++++++++++++--------------- internal/config/config.go | 1 + util/paths.go | 1 - 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index 717a6188..fcfdfc97 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -34,6 +34,7 @@ type Binary struct { Prefix *wine.Prefix Type roblox.BinaryType Version roblox.Version + Started time.Time } func NewBinary(bt roblox.BinaryType, cfg *config.Config, pfx *wine.Prefix) Binary { @@ -60,7 +61,6 @@ func NewBinary(bt roblox.BinaryType, cfg *config.Config, pfx *wine.Prefix) Binar } func (b *Binary) Run(args ...string) error { - then := time.Now() exe := b.Type.Executable() cmd, err := b.Command(args...) @@ -80,9 +80,8 @@ func (b *Binary) Run(args ...string) error { kill = false } - log.Println(kill, exe) - // Launches into foreground + b.Started = time.Now() if err := cmd.Start(); err != nil { return err } @@ -93,17 +92,15 @@ func (b *Binary) Run(args ...string) error { if b.Config.DiscordRPC { err := bsrpc.Login() if err != nil { - return err + log.Printf("Failed to authenticate Discord RPC: %s, disabling RPC", err) + b.Config.DiscordRPC = false } - - go func() { - time.Sleep(8 * time.Second) - if err := b.HandleRobloxLog(&then); err != nil { - log.Printf("Failed to handle Discord RPC: %s", err) - } - }() } + go func() { + b.TailLog() + }() + if kill && b.Config.AutoKillPrefix { log.Println("Waiting for Roblox's process to die :)") @@ -125,35 +122,52 @@ func (b *Binary) Run(args ...string) error { return nil } -func (b *Binary) HandleRobloxLog(comparison *time.Time) error { +func (b *Binary) FindLog() (string, error) { appData, err := b.Prefix.AppDataDir() if err != nil { - return err + return "", err } - fi, err := util.FindTimeFile(filepath.Join(appData, "Local", "Roblox", "logs"), comparison) - if err != nil { - return err + for i := 0; i < 10; i++ { + time.Sleep(1 * time.Second) + log.Println("Polling for Roblox log file") + + name, err := util.FindTimeFile(filepath.Join(appData, "Local", "Roblox", "logs"), &b.Started) + if err != nil { + return "", err + } + + log.Printf("Found Roblox log file: %s", name) + return name, nil } - log.Printf("Found Roblox log file: %s", fi) - t, err := tail.TailFile(fi, tail.Config{Follow: true}) + return "", os.ErrNotExist +} + +func (b *Binary) TailLog() { + var a bsrpc.Activity + + p, err := b.FindLog() if err != nil { - return err + log.Printf("Failed to find Roblox log file: %s", err) + return } - var a bsrpc.Activity + t, err := tail.TailFile(p, tail.Config{Follow: true}) + if err != nil { + log.Printf("Failed to tail Roblox log file: %s", err) + return + } for line := range t.Lines { fmt.Println(line.Text) - err = a.HandleLog(line.Text) - if err != nil { - log.Printf("epic fail: %s", err) + if b.Config.DiscordRPC { + if err := a.HandleLog(line.Text); err != nil { + log.Printf("Failed to handle Discord RPC: %s", err) + } } } - - return nil } func (b *Binary) FetchVersion() (roblox.Version, error) { diff --git a/internal/config/config.go b/internal/config/config.go index 1b15fe6f..69ea5311 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -89,6 +89,7 @@ func Default() Config { }, Player: Binary{ + DiscordRPC: true, Dxvk: true, AutoKillPrefix: true, FFlags: roblox.FFlags{ diff --git a/util/paths.go b/util/paths.go index 8c87cd48..74c7970a 100644 --- a/util/paths.go +++ b/util/paths.go @@ -38,7 +38,6 @@ func FindTimeFile(dir string, comparison *time.Time) (string, error) { } return nil }) - if err != nil { return "", err } From 3443179a5e7c5b7e166468fb9cb92d95cc031fdb Mon Sep 17 00:00:00 2001 From: sewn Date: Sun, 15 Oct 2023 22:34:05 +0300 Subject: [PATCH 06/55] cmd/vinegar: forward roblox log to log output too --- cmd/vinegar/binary.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index fcfdfc97..f837ce3d 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -160,7 +160,7 @@ func (b *Binary) TailLog() { } for line := range t.Lines { - fmt.Println(line.Text) + fmt.Fprintln(b.Prefix.Output, line.Text) if b.Config.DiscordRPC { if err := a.HandleLog(line.Text); err != nil { From 7eaacb14829f48718e9c2bec4cad0dcea47c65c6 Mon Sep 17 00:00:00 2001 From: sewn Date: Mon, 16 Oct 2023 16:06:13 +0300 Subject: [PATCH 07/55] cmd/vinegar: unsplash after roblox started, check for roblox launch fail --- cmd/vinegar/binary.go | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index f837ce3d..261d4ca9 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -85,9 +85,7 @@ func (b *Binary) Run(args ...string) error { if err := cmd.Start(); err != nil { return err } - - time.Sleep(2500 * time.Millisecond) - b.Splash.Close() + defer cmd.Process.Kill() if b.Config.DiscordRPC { err := bsrpc.Login() @@ -97,8 +95,16 @@ func (b *Binary) Run(args ...string) error { } } + p, err := b.FindLog() + if err != nil { + return fmt.Errorf("%w, has roblox successfully started?", err) + } + // after the log file has been found, we assume by then that + // roblox has successfully started, so we quit the splash screen + b.Splash.Close() + go func() { - b.TailLog() + b.TailLog(p) }() if kill && b.Config.AutoKillPrefix { @@ -133,27 +139,19 @@ func (b *Binary) FindLog() (string, error) { log.Println("Polling for Roblox log file") name, err := util.FindTimeFile(filepath.Join(appData, "Local", "Roblox", "logs"), &b.Started) - if err != nil { - return "", err + if err == nil { + log.Printf("Found Roblox log file: %s", name) + return name, nil } - - log.Printf("Found Roblox log file: %s", name) - return name, nil } - return "", os.ErrNotExist + return "", fmt.Errorf("could not roblox log file after time %s", b.Started) } -func (b *Binary) TailLog() { +func (b *Binary) TailLog(name string) { var a bsrpc.Activity - p, err := b.FindLog() - if err != nil { - log.Printf("Failed to find Roblox log file: %s", err) - return - } - - t, err := tail.TailFile(p, tail.Config{Follow: true}) + t, err := tail.TailFile(name, tail.Config{Follow: true}) if err != nil { log.Printf("Failed to tail Roblox log file: %s", err) return From 6036859811604fca3a2e94bd46411e644234a4a3 Mon Sep 17 00:00:00 2001 From: sewn Date: Mon, 16 Oct 2023 16:08:04 +0300 Subject: [PATCH 08/55] cmd/vinegar: print poll message only once --- cmd/vinegar/binary.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index 261d4ca9..e3e1b69c 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -134,9 +134,9 @@ func (b *Binary) FindLog() (string, error) { return "", err } + log.Println("Polling for Roblox log file, 10 retries") for i := 0; i < 10; i++ { time.Sleep(1 * time.Second) - log.Println("Polling for Roblox log file") name, err := util.FindTimeFile(filepath.Join(appData, "Local", "Roblox", "logs"), &b.Started) if err == nil { From 086b95141b0a4644ccf0b60d8f58b18e355364f5 Mon Sep 17 00:00:00 2001 From: AvoMC <142677012+AvoMC@users.noreply.github.com> Date: Mon, 16 Oct 2023 20:54:12 +0400 Subject: [PATCH 09/55] Fixed "URL" GameID (See game page) for BloxstrapRPC (#209) Signed-off-by: AvoMC <142677012+AvoMC@users.noreply.github.com> --- bloxstraprpc/discord.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bloxstraprpc/discord.go b/bloxstraprpc/discord.go index 3e0f9552..1207a71a 100644 --- a/bloxstraprpc/discord.go +++ b/bloxstraprpc/discord.go @@ -84,7 +84,7 @@ func (a *Activity) SetPresence() error { Buttons: []*client.Button{ { Label: "See game page", - Url: "https://www.roblox.com/games/meh", + Url: "https://www.roblox.com/games/" + a.PlaceID, }, }, } From a8c358f8567ec6d6ab45c62376ed1475c2d12c64 Mon Sep 17 00:00:00 2001 From: sewn Date: Mon, 16 Oct 2023 22:02:49 +0300 Subject: [PATCH 10/55] cmd/vinegar: always create log dir, may be first run --- cmd/vinegar/binary.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index e3e1b69c..677e5b27 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -134,11 +134,17 @@ func (b *Binary) FindLog() (string, error) { return "", err } + dir := filepath.Join(appData, "Local", "Roblox", "logs") + // May not exist if roblox has its first run + if err := os.MkdirAll(dir, 0o755); err != nil { + return "", err + } + log.Println("Polling for Roblox log file, 10 retries") for i := 0; i < 10; i++ { time.Sleep(1 * time.Second) - name, err := util.FindTimeFile(filepath.Join(appData, "Local", "Roblox", "logs"), &b.Started) + name, err := util.FindTimeFile(dir, &b.Started) if err == nil { log.Printf("Found Roblox log file: %s", name) return name, nil From 0f656cd16da7fdacfe74f70a05f1a18b9c2bacd8 Mon Sep 17 00:00:00 2001 From: sewn Date: Thu, 19 Oct 2023 20:32:53 +0300 Subject: [PATCH 11/55] bloxstraprpc: use bloxstrap's app id --- bloxstraprpc/discord.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bloxstraprpc/discord.go b/bloxstraprpc/discord.go index 1207a71a..3e76b8a4 100644 --- a/bloxstraprpc/discord.go +++ b/bloxstraprpc/discord.go @@ -9,7 +9,8 @@ import ( "github.com/vinegarhq/vinegar/roblox/api" ) -const RPCAppID = "1159891020956323923" +// This is Bloxstrap's Discord RPC application ID. +const RPCAppID = "1005469189907173486" func Login() error { log.Println("Authenticating Discord RPC") From d7c312d86532c67dfe0ded2ca6f129a8413b573d Mon Sep 17 00:00:00 2001 From: sewn Date: Fri, 20 Oct 2023 13:48:10 +0300 Subject: [PATCH 12/55] cmd/vinegar: drop waiting for roblox process, handle exit error --- cmd/vinegar/binary.go | 40 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index 677e5b27..dc3703ce 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -61,8 +61,6 @@ func NewBinary(bt roblox.BinaryType, cfg *config.Config, pfx *wine.Prefix) Binar } func (b *Binary) Run(args ...string) error { - exe := b.Type.Executable() - cmd, err := b.Command(args...) if err != nil { return err @@ -93,36 +91,32 @@ func (b *Binary) Run(args ...string) error { log.Printf("Failed to authenticate Discord RPC: %s, disabling RPC", err) b.Config.DiscordRPC = false } + // this will fucking panic if it fails smh + defer bsrpc.Logout() } p, err := b.FindLog() if err != nil { - return fmt.Errorf("%w, has roblox successfully started?", err) + log.Printf("%s, has roblox successfully started?", err) + } else { + go func() { + b.TailLog(p) + }() } - // after the log file has been found, we assume by then that - // roblox has successfully started, so we quit the splash screen - b.Splash.Close() - go func() { - b.TailLog(p) - }() - - if kill && b.Config.AutoKillPrefix { - log.Println("Waiting for Roblox's process to die :)") - - for { - time.Sleep(1 * time.Second) - - if !util.CommFound(exe[:15]) { - break - } - } + // after the FindLog function fails or found a log, we check if the + // command process pid exists, we know by then if roblox had started + // correctly, and to close the splash screen with the appropiate error. + if cmd.Process.Pid != 0 { + b.Splash.Close() + } - b.Prefix.Kill() + if err := cmd.Wait(); err != nil { + return err } - if b.Config.DiscordRPC { - bsrpc.Logout() + if kill && b.Config.AutoKillPrefix { + b.Prefix.Kill() } return nil From 4e5ec77a787027c1723256d16886e47db75b5602 Mon Sep 17 00:00:00 2001 From: sewn Date: Fri, 20 Oct 2023 13:50:02 +0300 Subject: [PATCH 13/55] cmd/vinegar: defer kill, allows to kill after roblox fails or exits --- cmd/vinegar/binary.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index dc3703ce..3392624f 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -111,14 +111,16 @@ func (b *Binary) Run(args ...string) error { b.Splash.Close() } + defer func() { + if kill && b.Config.AutoKillPrefix { + b.Prefix.Kill() + } + }() + if err := cmd.Wait(); err != nil { return err } - if kill && b.Config.AutoKillPrefix { - b.Prefix.Kill() - } - return nil } From 04121c28316e01092921a01c8326a16a98515716 Mon Sep 17 00:00:00 2001 From: sewn Date: Fri, 20 Oct 2023 16:54:11 +0300 Subject: [PATCH 14/55] feat: display dialog regarding broken webview --- cmd/vinegar/binary.go | 16 ++++++ internal/splash/splash.go | 110 +++++++++++++++++++++++++++++--------- 2 files changed, 101 insertions(+), 25 deletions(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index 3392624f..c7b42f9a 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -152,6 +152,8 @@ func (b *Binary) FindLog() (string, error) { func (b *Binary) TailLog(name string) { var a bsrpc.Activity + const title = "WebView/InternalBrowser is broken" + auth := false t, err := tail.TailFile(name, tail.Config{Follow: true}) if err != nil { @@ -162,6 +164,20 @@ func (b *Binary) TailLog(name string) { for line := range t.Lines { fmt.Fprintln(b.Prefix.Output, line.Text) + // Easy way to figure out we are authenticated, to make a more + // babysit message to tell the user to use quick login + if strings.Contains(line.Text, "DID_LOG_IN") { + auth = true + } + + if strings.Contains(line.Text, "the local did not install any WebView2 runtime") { + if auth { + b.Splash.Dialog(title, "use the browser for whatever you were doing just now.") + } else { + b.Splash.Dialog(title, "Use Quick Log In to authenticate ('Log In With Another Device' button)") + } + } + if b.Config.DiscordRPC { if err := a.HandleLog(line.Text); err != nil { log.Printf("Failed to handle Discord RPC: %s", err) diff --git a/internal/splash/splash.go b/internal/splash/splash.go index 5515656e..930f97ed 100644 --- a/internal/splash/splash.go +++ b/internal/splash/splash.go @@ -68,11 +68,18 @@ func (ui *Splash) Close() { ui.Perform(system.ActionClose) } -func New(cfg *config.Splash) *Splash { - width := unit.Dp(448) - height := unit.Dp(240) +func window(width, height unit.Dp) *app.Window { + return app.NewWindow( + app.Decorated(false), + app.Size(width, height), + app.MinSize(width, height), + app.MaxSize(width, height), + app.Title("Vinegar"), + ) +} - th := material.NewTheme() +func theme(cfg *config.Splash) (th *material.Theme) { + th = material.NewTheme() th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection())) th.Palette = material.Palette{ Bg: rgb(cfg.Bg), @@ -81,22 +88,69 @@ func New(cfg *config.Splash) *Splash { ContrastFg: rgb(cfg.Gray2), } + return +} + +func New(cfg *config.Splash) *Splash { + width := unit.Dp(448) + height := unit.Dp(240) + logo, _, _ := image.Decode(bytes.NewReader(vinegarlogo)) return &Splash{ logo: logo, - Theme: th, + Theme: theme(cfg), Config: cfg, - Window: app.NewWindow( - app.Decorated(false), - app.Size(width, height), - app.MinSize(width, height), - app.MaxSize(width, height), - app.Title("Vinegar"), - ), + Window: window(width, height), } } +// Make a new application window using vinegar's existing properties to +// simulate a dialog. +func (ui *Splash) Dialog(title, msg string) { + var ops op.Ops + var okButton widget.Clickable + width := unit.Dp(480) + height := unit.Dp(144) + w := window(width, height) + + if !ui.Config.Enabled || ui.Theme == nil { + return + } + + for e := range w.Events() { + switch e := e.(type) { + case system.DestroyEvent: + // no real care for errors, this is a dialog + return + case system.FrameEvent: + gtx := layout.NewContext(&ops, e) + paint.Fill(gtx.Ops, ui.Theme.Palette.Bg) + + if okButton.Clicked() { + w.Perform(system.ActionClose) + } + + layout.Center.Layout(gtx, func(gtx C) D { + return layout.Flex{ + Axis: layout.Vertical, + Alignment: layout.Middle, + }.Layout(gtx, + layout.Rigid(material.H6(ui.Theme, title).Layout), + layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout), + layout.Rigid(material.Body2(ui.Theme, msg).Layout), + layout.Rigid(layout.Spacer{Height: unit.Dp(16)}.Layout), + layout.Rigid(button(ui.Theme, &okButton, "Ok").Layout), + ) + }) + + e.Frame(gtx.Ops) + } + } + + return +} + func (ui *Splash) Run() error { var ops op.Ops var showLogButton widget.Clickable @@ -164,19 +218,12 @@ func (ui *Splash) Run() error { }), layout.Rigid(func(gtx C) D { - inset := layout.Inset{ - Top: unit.Dp(16), - Right: unit.Dp(6), - Left: unit.Dp(6), - } - + inset := buttonInset() return layout.Flex{}.Layout(gtx, layout.Rigid(func(gtx C) D { return inset.Layout(gtx, func(gtx C) D { - btn := material.Button(ui.Theme, &exitButton, "Cancel") + btn := button(ui.Theme, &exitButton, "Cancel") btn.Background = rgb(ui.Config.Red) - btn.Color = ui.Theme.Palette.Fg - btn.CornerRadius = 16 return btn.Layout(gtx) }) }), @@ -186,10 +233,7 @@ func (ui *Splash) Run() error { } return inset.Layout(gtx, func(gtx C) D { - btn := material.Button(ui.Theme, &showLogButton, "Show Log") - btn.Color = ui.Theme.Palette.Fg - btn.CornerRadius = 16 - return btn.Layout(gtx) + return button(ui.Theme, &showLogButton, "Show Log").Layout(gtx) }) }), ) @@ -203,3 +247,19 @@ func (ui *Splash) Run() error { return nil } + + +func buttonInset() layout.Inset { + return layout.Inset{ + Top: unit.Dp(16), + Right: unit.Dp(6), + Left: unit.Dp(6), + } +} + +func button(th *material.Theme, button *widget.Clickable, txt string) (bs material.ButtonStyle) { + bs = material.Button(th, button, txt) + bs.Color = th.Palette.Fg + bs.CornerRadius = 16 + return +} From bab058dc4a9bd9bbbbdeb6adc175c5cd517d13a1 Mon Sep 17 00:00:00 2001 From: sewn Date: Fri, 20 Oct 2023 16:57:05 +0300 Subject: [PATCH 15/55] internal/splash: implement dialog for nosplash --- internal/splash/nosplash.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/splash/nosplash.go b/internal/splash/nosplash.go index 2ae30124..ea424233 100644 --- a/internal/splash/nosplash.go +++ b/internal/splash/nosplash.go @@ -3,6 +3,7 @@ package splash import ( + "log" "errors" "github.com/vinegarhq/vinegar/internal/config" @@ -27,6 +28,10 @@ func (ui *Splash) Progress(progress float32) { func (ui *Splash) Close() { } +func (ui *Splash) Dialog(title, msg string) { + log.Printf("Dialog: %s %s", title, msg) +} + func New(cfg *config.Splash) *Splash { return &Splash{ Config: cfg, From 06c47b041f21656269ad726d0debf27283768a6a Mon Sep 17 00:00:00 2001 From: sewn Date: Fri, 20 Oct 2023 20:17:56 +0300 Subject: [PATCH 16/55] internal/config: disable wine's mono --- internal/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/config/config.go b/internal/config/config.go index 69ea5311..00c5248d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -79,7 +79,7 @@ func Default() Config { "WINEARCH": "win64", "WINEDEBUG": "err-kerberos,err-ntlm", "WINEESYNC": "1", - "WINEDLLOVERRIDES": "dxdiagn=d;winemenubuilder.exe=d", + "WINEDLLOVERRIDES": "dxdiagn,winemenubuilder.exe,mscoree,mshtml=", "DXVK_LOG_LEVEL": "warn", "DXVK_LOG_PATH": "none", From 8d1560623c9fa174a7d2c8ae81a78754bb5f331b Mon Sep 17 00:00:00 2001 From: sewn Date: Fri, 20 Oct 2023 22:29:13 +0300 Subject: [PATCH 17/55] harder roblox process error handling --- cmd/vinegar/binary.go | 52 ++++++++++++++++++++++++++------------- cmd/vinegar/vinegar.go | 16 ------------ internal/splash/splash.go | 2 -- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index c7b42f9a..44676683 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" "time" + _ "syscall" "github.com/nxadm/tail" bsrpc "github.com/vinegarhq/vinegar/bloxstraprpc" @@ -83,7 +84,6 @@ func (b *Binary) Run(args ...string) error { if err := cmd.Start(); err != nil { return err } - defer cmd.Process.Kill() if b.Config.DiscordRPC { err := bsrpc.Login() @@ -91,26 +91,36 @@ func (b *Binary) Run(args ...string) error { log.Printf("Failed to authenticate Discord RPC: %s, disabling RPC", err) b.Config.DiscordRPC = false } - // this will fucking panic if it fails smh + // NOTE: This will panic if logout fails defer bsrpc.Logout() } + // after FindLog() fails to find a log, assume Roblox hasn't started. + // early. if it did, assume failure and jump to cmd.Wait(), which will give the + // returned error to the splash screen and output if wine returns one. p, err := b.FindLog() if err != nil { - log.Printf("%s, has roblox successfully started?", err) + log.Printf("%s, assuming roblox failure", err) } else { + b.Splash.Close() + go func() { - b.TailLog(p) + rblxExited, err := b.TailLog(p) + if err != nil { + log.Printf("tail roblox log file: %s", err) + return + } + + if rblxExited { + log.Println("Got Roblox shutdown") + // give roblox two seconds to cleanup its garbage + time.Sleep(2 * time.Second) + // force kill the process, causing cmd.Wait() to immediately return. + cmd.Process.Kill() + } }() } - // after the FindLog function fails or found a log, we check if the - // command process pid exists, we know by then if roblox had started - // correctly, and to close the splash screen with the appropiate error. - if cmd.Process.Pid != 0 { - b.Splash.Close() - } - defer func() { if kill && b.Config.AutoKillPrefix { b.Prefix.Kill() @@ -118,7 +128,7 @@ func (b *Binary) Run(args ...string) error { }() if err := cmd.Wait(); err != nil { - return err + return fmt.Errorf("roblox process: %w", err) } return nil @@ -147,18 +157,18 @@ func (b *Binary) FindLog() (string, error) { } } - return "", fmt.Errorf("could not roblox log file after time %s", b.Started) + return "", fmt.Errorf("could not find roblox log file after time %s", b.Started) } -func (b *Binary) TailLog(name string) { +// Boolean returned is if Roblox had exited, detecting via logs +func (b *Binary) TailLog(name string) (bool, error) { var a bsrpc.Activity const title = "WebView/InternalBrowser is broken" auth := false - t, err := tail.TailFile(name, tail.Config{Follow: true}) + t, err := tail.TailFile(name, tail.Config{Follow: true, MustExist: true}) if err != nil { - log.Printf("Failed to tail Roblox log file: %s", err) - return + return false, err } for line := range t.Lines { @@ -178,12 +188,20 @@ func (b *Binary) TailLog(name string) { } } + // Best we've got to know if roblox had actually quit + if strings.Contains(line.Text, "[FLog::SingleSurfaceApp] shutDown:") { + return true, nil + } + if b.Config.DiscordRPC { if err := a.HandleLog(line.Text); err != nil { log.Printf("Failed to handle Discord RPC: %s", err) } } } + + // this is should be unreachable + return false, nil } func (b *Binary) FetchVersion() (roblox.Version, error) { diff --git a/cmd/vinegar/vinegar.go b/cmd/vinegar/vinegar.go index f9a9b54d..479c53da 100644 --- a/cmd/vinegar/vinegar.go +++ b/cmd/vinegar/vinegar.go @@ -6,9 +6,7 @@ import ( "io" "log" "os" - "os/signal" "path/filepath" - "syscall" "github.com/vinegarhq/vinegar/internal/config" "github.com/vinegarhq/vinegar/internal/config/editor" @@ -50,7 +48,6 @@ func main() { // These commands (except player & studio) don't require a configuration, // but they require a wineprefix, hence wineroot of configuration is required. case "player", "studio", "exec", "kill", "install-webview2", "winetricks": - pfxKilled := false cfg, err := config.Load(*configPath) if err != nil { log.Fatal(err) @@ -63,19 +60,6 @@ func main() { log.Fatal(err) } - c := make(chan os.Signal, 1) - signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) - - go func() { - <-c - pfxKilled = true - pfx.Kill() - - if pfxKilled { - os.Exit(0) - } - }() - switch cmd { case "exec": if len(args) < 2 { diff --git a/internal/splash/splash.go b/internal/splash/splash.go index 930f97ed..680c3999 100644 --- a/internal/splash/splash.go +++ b/internal/splash/splash.go @@ -147,8 +147,6 @@ func (ui *Splash) Dialog(title, msg string) { e.Frame(gtx.Ops) } } - - return } func (ui *Splash) Run() error { From 0080e29f97b39a68aab7e20afa0a9edd8ff5f05f Mon Sep 17 00:00:00 2001 From: sewn Date: Fri, 20 Oct 2023 22:32:37 +0300 Subject: [PATCH 18/55] cmd/vinegar fix updating roblox log --- cmd/vinegar/binary.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index 44676683..a213a9e7 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -232,7 +232,7 @@ func (b *Binary) Setup() error { } if stateVer != ver.GUID { - log.Printf("Installing %s (%s -> %s)", b.Name, stateVer, ver) + log.Printf("Installing %s (%s -> %s)", b.Name, stateVer, ver.GUID) if err := b.Install(); err != nil { return err From baa1d6f742db77d7565295dd5c7118159409b2a4 Mon Sep 17 00:00:00 2001 From: sewn Date: Fri, 20 Oct 2023 22:32:53 +0300 Subject: [PATCH 19/55] roblox: drop fflags used log --- roblox/fflags.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/roblox/fflags.go b/roblox/fflags.go index 85673a92..7a035da9 100644 --- a/roblox/fflags.go +++ b/roblox/fflags.go @@ -48,8 +48,6 @@ func (f *FFlags) Apply(versionDir string) error { return err } - log.Printf("FFlags used: %s", string(fflags)) - _, err = file.Write(fflags) if err != nil { return err From fbc87188d6cd1c650fdfe2f7fa2eed8eaf64624e Mon Sep 17 00:00:00 2001 From: sewn Date: Fri, 20 Oct 2023 22:44:16 +0300 Subject: [PATCH 20/55] cmd/vinegar: handle ctrl-c to kill roblox --- cmd/vinegar/binary.go | 17 ++++++++++++++++- cmd/vinegar/vinegar.go | 2 +- internal/splash/splash.go | 6 ++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index a213a9e7..42884e9f 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -8,7 +8,8 @@ import ( "path/filepath" "strings" "time" - _ "syscall" + "os/signal" + "syscall" "github.com/nxadm/tail" bsrpc "github.com/vinegarhq/vinegar/bloxstraprpc" @@ -85,6 +86,20 @@ func (b *Binary) Run(args ...string) error { return err } + // act as the signal holder, as roblox/wine will not do anything + // with the INT signal. + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) + + go func() { + <-c + // This way, cmd.Wait() will return and the wineprefix killer + // will be ran. + log.Println("Killing Roblox") + cmd.Process.Kill() + signal.Stop(c) + }() + if b.Config.DiscordRPC { err := bsrpc.Login() if err != nil { diff --git a/cmd/vinegar/vinegar.go b/cmd/vinegar/vinegar.go index 479c53da..951a9b52 100644 --- a/cmd/vinegar/vinegar.go +++ b/cmd/vinegar/vinegar.go @@ -107,7 +107,7 @@ func main() { b.Splash.Desc(b.Config.Channel) errHandler := func(err error) { - if !cfg.Splash.Enabled { + if !cfg.Splash.Enabled || b.Splash.IsClosed() { log.Fatal(err) } diff --git a/internal/splash/splash.go b/internal/splash/splash.go index 680c3999..74d01b5e 100644 --- a/internal/splash/splash.go +++ b/internal/splash/splash.go @@ -68,6 +68,10 @@ func (ui *Splash) Close() { ui.Perform(system.ActionClose) } +func (ui *Splash) IsClosed() bool { + return ui.closed +} + func window(width, height unit.Dp) *app.Window { return app.NewWindow( app.Decorated(false), @@ -155,6 +159,7 @@ func (ui *Splash) Run() error { var exitButton widget.Clickable if !ui.Config.Enabled { + ui.closed = true return nil } @@ -243,6 +248,7 @@ func (ui *Splash) Run() error { } } + ui.closed = true return nil } From 4c3fc8ee56292e67ef4e15f2acd0489d877825e0 Mon Sep 17 00:00:00 2001 From: sewn Date: Fri, 20 Oct 2023 22:45:57 +0300 Subject: [PATCH 21/55] internal/splash: implement IsClosed for nosplash --- internal/splash/nosplash.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/splash/nosplash.go b/internal/splash/nosplash.go index ea424233..4cfa3502 100644 --- a/internal/splash/nosplash.go +++ b/internal/splash/nosplash.go @@ -28,6 +28,10 @@ func (ui *Splash) Progress(progress float32) { func (ui *Splash) Close() { } +func (ui *Splash) IsClosed() { + return true +} + func (ui *Splash) Dialog(title, msg string) { log.Printf("Dialog: %s %s", title, msg) } From e19004be899a6e71ce2b690e44ab25f536815d49 Mon Sep 17 00:00:00 2001 From: sewn Date: Fri, 20 Oct 2023 22:51:55 +0300 Subject: [PATCH 22/55] internal/splash: actual nosplash --- internal/splash/nosplash.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/splash/nosplash.go b/internal/splash/nosplash.go index 4cfa3502..6b1a3d78 100644 --- a/internal/splash/nosplash.go +++ b/internal/splash/nosplash.go @@ -28,7 +28,7 @@ func (ui *Splash) Progress(progress float32) { func (ui *Splash) Close() { } -func (ui *Splash) IsClosed() { +func (ui *Splash) IsClosed() bool { return true } From 97f6f4aca011cf8a92a312d67d03f7f270efad2c Mon Sep 17 00:00:00 2001 From: sewn Date: Fri, 20 Oct 2023 22:53:20 +0300 Subject: [PATCH 23/55] README: include roblox in log feat --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9557ddef..1069ec28 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ An open-source, minimal, configurable, fast bootstrapper for running Roblox on L + Faster Multi-threaded installation and extraction of Roblox + Multiple instances of Roblox open simultaneously + Loading window during setup -+ Logging for both Vinegar and Wine ++ Logging for both Vinegar, Wine and Roblox # See Also + [Discord Server](https://discord.gg/dzdzZ6Pps2) From 3a60486f894be9be00f0f38732d1eb99abd570fc Mon Sep 17 00:00:00 2001 From: sewn Date: Sat, 21 Oct 2023 08:04:42 +0300 Subject: [PATCH 24/55] cmd/vinegar: remove disable crashdialogs --- cmd/vinegar/vinegar.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cmd/vinegar/vinegar.go b/cmd/vinegar/vinegar.go index 951a9b52..451fc1c1 100644 --- a/cmd/vinegar/vinegar.go +++ b/cmd/vinegar/vinegar.go @@ -146,10 +146,6 @@ func PrefixInit(pfx *wine.Prefix) error { return err } - if err := pfx.DisableCrashDialogs(); err != nil { - return err - } - return pfx.RegistryAdd("HKEY_CURRENT_USER\\Control Panel\\Desktop", "LogPixels", wine.REG_DWORD, "97") } From 717f147d94aceadaa7e0d52596f64d670327fb9c Mon Sep 17 00:00:00 2001 From: sewn Date: Sat, 21 Oct 2023 09:41:28 +0300 Subject: [PATCH 25/55] refactor api handling --- cmd/vinegar/binary.go | 15 ++--- internal/splash/nosplash.go | 2 +- internal/splash/splash.go | 1 - roblox/api/api.go | 47 ++++++++++++++-- roblox/api/clientsettings.go | 29 ++++++++++ roblox/api/error.go | 35 ++++++++++++ roblox/api/games.go | 7 ++- roblox/api/thumbnails.go | 9 +-- roblox/api/universe.go | 3 +- roblox/bootstrapper/manifest.go | 29 +++++++--- roblox/fflags.go | 25 ++++----- roblox/version.go | 97 --------------------------------- roblox/version/version.go | 64 ++++++++++++++++++++++ 13 files changed, 221 insertions(+), 142 deletions(-) create mode 100644 roblox/api/clientsettings.go create mode 100644 roblox/api/error.go delete mode 100644 roblox/version.go create mode 100644 roblox/version/version.go diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index 42884e9f..168d6792 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -5,11 +5,11 @@ import ( "log" "os" "os/exec" + "os/signal" "path/filepath" "strings" - "time" - "os/signal" "syscall" + "time" "github.com/nxadm/tail" bsrpc "github.com/vinegarhq/vinegar/bloxstraprpc" @@ -19,6 +19,7 @@ import ( "github.com/vinegarhq/vinegar/internal/splash" "github.com/vinegarhq/vinegar/roblox" "github.com/vinegarhq/vinegar/roblox/bootstrapper" + "github.com/vinegarhq/vinegar/roblox/version" "github.com/vinegarhq/vinegar/util" "github.com/vinegarhq/vinegar/wine" "github.com/vinegarhq/vinegar/wine/dxvk" @@ -35,7 +36,7 @@ type Binary struct { Dir string Prefix *wine.Prefix Type roblox.BinaryType - Version roblox.Version + Version version.Version Started time.Time } @@ -125,7 +126,7 @@ func (b *Binary) Run(args ...string) error { log.Printf("tail roblox log file: %s", err) return } - + if rblxExited { log.Println("Got Roblox shutdown") // give roblox two seconds to cleanup its garbage @@ -219,16 +220,16 @@ func (b *Binary) TailLog(name string) (bool, error) { return false, nil } -func (b *Binary) FetchVersion() (roblox.Version, error) { +func (b *Binary) FetchVersion() (version.Version, error) { b.Splash.Message("Fetching " + b.Alias) if b.Config.ForcedVersion != "" { log.Printf("WARNING: using forced version: %s", b.Config.ForcedVersion) - return roblox.NewVersion(b.Type, b.Config.Channel, b.Config.ForcedVersion) + return version.New(b.Type, b.Config.Channel, b.Config.ForcedVersion), nil } - return roblox.LatestVersion(b.Type, b.Config.Channel) + return version.Fetch(b.Type, b.Config.Channel) } func (b *Binary) Setup() error { diff --git a/internal/splash/nosplash.go b/internal/splash/nosplash.go index 6b1a3d78..622fdb17 100644 --- a/internal/splash/nosplash.go +++ b/internal/splash/nosplash.go @@ -3,8 +3,8 @@ package splash import ( - "log" "errors" + "log" "github.com/vinegarhq/vinegar/internal/config" ) diff --git a/internal/splash/splash.go b/internal/splash/splash.go index 74d01b5e..aa079f2e 100644 --- a/internal/splash/splash.go +++ b/internal/splash/splash.go @@ -252,7 +252,6 @@ func (ui *Splash) Run() error { return nil } - func buttonInset() layout.Inset { return layout.Inset{ Top: unit.Dp(16), diff --git a/roblox/api/api.go b/roblox/api/api.go index 682e0389..9355843a 100644 --- a/roblox/api/api.go +++ b/roblox/api/api.go @@ -2,17 +2,52 @@ package api import ( "encoding/json" + "errors" + "fmt" "log" - - "github.com/vinegarhq/vinegar/util" + "net/http" ) -func UnmarshalBody(url string, v any) error { - log.Printf("Sending API Request for %s", url) - body, err := util.Body(url) +const APIURL = "https://%s.roblox.com/%s" + +var httpClient = &http.Client{} + +var ErrBadStatus = errors.New("bad status") + +func SetClient(client *http.Client) { + httpClient = client +} + +func Request(method, service, endpoint string, v interface{}) error { + log.Printf("Performing %s request on %s/%s", method, service, endpoint) + + url := fmt.Sprintf(APIURL, service, endpoint) + + req, err := http.NewRequest(method, url, nil) if err != nil { return err } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + errsResp := new(errorsResponse) + if err := json.NewDecoder(resp.Body).Decode(errsResp); err != nil { + return err + } + + return errsResp + } + + if v != nil { + return json.NewDecoder(resp.Body).Decode(v) + } - return json.Unmarshal([]byte(body), &v) + return nil } diff --git a/roblox/api/clientsettings.go b/roblox/api/clientsettings.go new file mode 100644 index 00000000..e476b63f --- /dev/null +++ b/roblox/api/clientsettings.go @@ -0,0 +1,29 @@ +package api + +import ( + "github.com/vinegarhq/vinegar/roblox" +) + +type ClientVersion struct { + Version string `json:"version"` + ClientVersionUpload string `json:"clientVersionUpload"` + BootstrapperVersion string `json:"bootstrapperVersion"` + NextClientVersionUpload string `json:"nextClientVersionUpload,omitempty"` + NextClientVersion string `json:"nextClientVersion,omitempty"` +} + +func GetClientVersion(bt roblox.BinaryType, channel string) (ClientVersion, error) { + var cv ClientVersion + + ep := "v2/client-version/" + bt.BinaryName() + if channel != "" { + ep += "/channel/" + channel + } + + err := Request("GET", "clientsettings", ep, &cv) + if err != nil { + return ClientVersion{}, err + } + + return cv, nil +} diff --git a/roblox/api/error.go b/roblox/api/error.go new file mode 100644 index 00000000..47e8163e --- /dev/null +++ b/roblox/api/error.go @@ -0,0 +1,35 @@ +package api + +import ( + "fmt" + "strings" +) + +type ErrorResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Field string `json:"field,omitempty"` +} + +type errorsResponse struct { + Errors []ErrorResponse `json:"errors,omitempty"` +} + +func (err ErrorResponse) Error() string { + return fmt.Sprintf("response code %d: %s", err.Code, err.Message) +} + +func (errs errorsResponse) Error() string { + s := make([]string, len(errs.Errors)) + for i, e := range errs.Errors { + s[i] = e.Error() + } + return strings.Join(s, "; ") +} + +func (errs errorsResponse) Unwrap() error { + if len(errs.Errors) == 0 { + return nil + } + return errs.Errors[0] +} diff --git a/roblox/api/games.go b/roblox/api/games.go index b3d30bb9..57fcf614 100644 --- a/roblox/api/games.go +++ b/roblox/api/games.go @@ -35,14 +35,15 @@ type GameDetail struct { FavoritedCount int64 `json:"favoritedCount"` } -type GameDetailResponse struct { +type GameDetailsResponse struct { Data []GameDetail `json:"data"` } func GetGameDetails(universeID string) (GameDetail, error) { - var gdr GameDetailResponse + var gdr GameDetailsResponse - err := UnmarshalBody("https://games.roblox.com/v1/games?universeIds="+universeID, &gdr) + // uids := strings.Join(universeIDs, ",") + err := Request("GET", "games", "v1/games?universeIds="+universeID, &gdr) if err != nil { return GameDetail{}, err } diff --git a/roblox/api/thumbnails.go b/roblox/api/thumbnails.go index 0db65ecc..d84f4213 100644 --- a/roblox/api/thumbnails.go +++ b/roblox/api/thumbnails.go @@ -11,14 +11,15 @@ type Thumbnail struct { Version string `json:"version"` } -type ThumbnailResponse struct { +type thumbnailResponse struct { Data []Thumbnail `json:"data"` } func GetGameIcon(universeID, returnPolicy, size, format string, isCircular bool) (Thumbnail, error) { - var tnr ThumbnailResponse - err := UnmarshalBody( - fmt.Sprintf("https://thumbnails.roblox.com/v1/games/icons?universeIds=%s&returnPolicy=%s&size=%s&format=%s&isCircular=%t", + var tnr thumbnailResponse + + err := Request("GET", "thumbnails", + fmt.Sprintf("v1/games/icons?universeIds=%s&returnPolicy=%s&size=%s&format=%s&isCircular=%t", universeID, returnPolicy, size, format, isCircular), &tnr, ) if err != nil { diff --git a/roblox/api/universe.go b/roblox/api/universe.go index 44b3d518..24491c9b 100644 --- a/roblox/api/universe.go +++ b/roblox/api/universe.go @@ -11,7 +11,8 @@ type UniverseIdResponse struct { func GetUniverseID(placeID string) (string, error) { var uidr UniverseIdResponse - err := UnmarshalBody("https://apis.roblox.com/universes/v1/places/"+placeID+"/universe", &uidr) + // This API is undocumented. + err := Request("GET", "apis", "universes/v1/places/"+placeID+"/universe", &uidr) if err != nil { return "", err } diff --git a/roblox/bootstrapper/manifest.go b/roblox/bootstrapper/manifest.go index 254f4f14..083e7434 100644 --- a/roblox/bootstrapper/manifest.go +++ b/roblox/bootstrapper/manifest.go @@ -5,29 +5,40 @@ import ( "log" "strings" - "github.com/vinegarhq/vinegar/roblox" + "github.com/vinegarhq/vinegar/roblox/version" "github.com/vinegarhq/vinegar/util" ) type Manifest struct { - *roblox.Version + *version.Version DeployURL string Packages } -func FetchManifest(ver *roblox.Version) (Manifest, error) { +func channelPath(channel string) string { + // Ensure that the channel is lowercased, since internally in + // ClientSettings it will be lowercased, but not on the deploy mirror. + channel = strings.ToLower(channel) + + if channel == "" { + return "/" + } + + return "/channel/" + channel + "/" +} + +func FetchManifest(ver *version.Version) (Manifest, error) { cdn, err := CDN() if err != nil { return Manifest{}, err } + durl := cdn + channelPath(ver.Channel) + ver.GUID - deployURL := cdn + roblox.ChannelPath(ver.Channel) + ver.GUID - - log.Printf("Fetching manifest for %s (%s)", ver.GUID, deployURL) + log.Printf("Fetching manifest for %s (%s)", ver.GUID, durl) - manif, err := util.Body(deployURL + "-rbxPkgManifest.txt") + manif, err := util.Body(durl + "-rbxPkgManifest.txt") if err != nil { - return Manifest{}, fmt.Errorf("fetch %s manifest: %w, is your channel valid?", ver.GUID, err) + return Manifest{}, fmt.Errorf("fetch %s manifest: %w", ver.GUID, err) } pkgs, err := ParsePackages(strings.Split(manif, "\r\n")) @@ -37,7 +48,7 @@ func FetchManifest(ver *roblox.Version) (Manifest, error) { return Manifest{ Version: ver, - DeployURL: deployURL, + DeployURL: durl, Packages: pkgs, }, nil } diff --git a/roblox/fflags.go b/roblox/fflags.go index 7a035da9..3b8705a8 100644 --- a/roblox/fflags.go +++ b/roblox/fflags.go @@ -9,15 +9,12 @@ import ( "path/filepath" ) -var ( - DefaultRenderer = "D3D11" - Renderers = []string{ - "OpenGL", - "D3D11FL10", - DefaultRenderer, - "Vulkan", - } -) +var renderers = []string{ + "OpenGL", + "D3D11FL10", + "D3D11", + "Vulkan", +} type FFlags map[string]interface{} @@ -57,11 +54,12 @@ func (f *FFlags) Apply(versionDir string) error { } func ValidRenderer(renderer string) bool { + // Assume Roblox's internal default renderer if renderer == "" { - renderer = DefaultRenderer + return true } - for _, r := range Renderers { + for _, r := range renderers { if renderer == r { return true } @@ -71,8 +69,9 @@ func ValidRenderer(renderer string) bool { } func (f *FFlags) SetRenderer(renderer string) error { + // Assume Roblox's internal default renderer if renderer == "" { - renderer = DefaultRenderer + return nil } if !ValidRenderer(renderer) { @@ -86,7 +85,7 @@ func (f *FFlags) SetRenderer(renderer string) error { log.Printf("Using renderer: %s", renderer) // Disable all other renderers except the given one. - for _, r := range Renderers { + for _, r := range renderers { isRenderer := r == renderer (*f)["FFlagDebugGraphicsPrefer"+r] = isRenderer diff --git a/roblox/version.go b/roblox/version.go deleted file mode 100644 index 7c66d9a1..00000000 --- a/roblox/version.go +++ /dev/null @@ -1,97 +0,0 @@ -package roblox - -import ( - "encoding/json" - "errors" - "fmt" - "log" - "strings" - - "github.com/vinegarhq/vinegar/util" -) - -const ( - DefaultChannel = "live" - ClientSettingsURL = "https://clientsettingscdn.roblox.com/v2/client-version" -) - -var ErrNoVersion = errors.New("no version found") - -type ClientVersion struct { - Version string `json:"version"` - ClientVersionUpload string `json:"clientVersionUpload"` - BootstrapperVersion string `json:"bootstrapperVersion"` - NextClientVersionUpload string `json:"nextClientVersionUpload"` - NextClientVersion string `json:"nextClientVersion"` -} - -type Version struct { - Type BinaryType - Channel string - GUID string -} - -func ChannelPath(channel string) string { - // Ensure that the channel is lowercased, since internally in - // ClientSettings it will be lowercased, but not on the deploy mirror. - channel = strings.ToLower(channel) - - if channel == DefaultChannel { - return "/" - } - - return "/channel/" + channel + "/" -} - -func NewVersion(bt BinaryType, channel string, GUID string) (Version, error) { - if channel == "" { - channel = DefaultChannel - } - - if GUID == "" { - return Version{}, ErrNoVersion - } - - log.Printf("Found %s version %s", bt.BinaryName(), GUID) - - return Version{ - Type: bt, - Channel: channel, - GUID: GUID, - }, nil -} - -func LatestVersion(bt BinaryType, channel string) (Version, error) { - name := bt.BinaryName() - var cv ClientVersion - - if channel == "" { - channel = DefaultChannel - } - - url := ClientSettingsURL + "/" + name + ChannelPath(channel) - - log.Printf("Fetching latest version of %s for channel %s (%s)", name, channel, url) - - resp, err := util.Body(url) - if err != nil { - if errors.Is(err, util.ErrBadStatus) { - return Version{}, fmt.Errorf("invalid channel given for %s", name) - } else { - return Version{}, fmt.Errorf("fetch version for %s: %w", name, err) - } - } - - err = json.Unmarshal([]byte(resp), &cv) - if err != nil { - return Version{}, fmt.Errorf("version clientsettings unmarshal: %w", err) - } - - if cv.ClientVersionUpload == "" { - return Version{}, ErrNoVersion - } - - log.Printf("Fetched %s canonical version %s", bt.BinaryName(), cv.Version) - - return NewVersion(bt, channel, cv.ClientVersionUpload) -} diff --git a/roblox/version/version.go b/roblox/version/version.go new file mode 100644 index 00000000..e5c958c0 --- /dev/null +++ b/roblox/version/version.go @@ -0,0 +1,64 @@ +package version + +import ( + "log" + "strings" + + "github.com/vinegarhq/vinegar/roblox" + "github.com/vinegarhq/vinegar/roblox/api" +) + +const DefaultChannel = "live" + +type ClientVersion struct { + Version string `json:"version"` + ClientVersionUpload string `json:"clientVersionUpload"` + BootstrapperVersion string `json:"bootstrapperVersion"` + NextClientVersionUpload string `json:"nextClientVersionUpload"` + NextClientVersion string `json:"nextClientVersion"` +} + +type Version struct { + Type roblox.BinaryType + Channel string + GUID string +} + +func ChannelPath(channel string) string { + // Ensure that the channel is lowercased, since internally in + // ClientSettings it will be lowercased, but not on the deploy mirror. + channel = strings.ToLower(channel) + + if channel == DefaultChannel { + return "/" + } + + return "/channel/" + channel + "/" +} + +func New(bt roblox.BinaryType, channel string, GUID string) Version { + log.Printf("Got %s version %s", bt.BinaryName(), GUID) + + return Version{ + Type: bt, + Channel: channel, + GUID: GUID, + } +} + +func Fetch(bt roblox.BinaryType, channel string) (Version, error) { + c := "" + if c != "" { + c = "for channel " + channel + } + log.Printf("Fetching latest version of %s %s", bt.BinaryName(), c) + + cv, err := api.GetClientVersion(bt, channel) + if err != nil { + return Version{}, err + } + + log.Printf("Fetched %s canonical version %s", bt.BinaryName(), cv.Version) + + return New(bt, channel, cv.ClientVersionUpload), nil +} From ad834db3208e5979c133da2f6fd5ea7c465acefb Mon Sep 17 00:00:00 2001 From: sewn Date: Sat, 21 Oct 2023 09:47:43 +0300 Subject: [PATCH 26/55] cmd/vinegar: check if roblox is running before killing --- cmd/vinegar/binary.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index 168d6792..b9f66320 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -72,14 +72,6 @@ func (b *Binary) Run(args ...string) error { log.Printf("Launching %s", b.Name) b.Splash.Message("Launching " + b.Alias) - kill := true - - // If roblox is already running, don't kill wineprefix, even if - // auto kill prefix is enabled - if util.CommFound("Roblox") { - log.Println("Roblox is already running, not killing wineprefix after exit") - kill = false - } // Launches into foreground b.Started = time.Now() @@ -132,13 +124,27 @@ func (b *Binary) Run(args ...string) error { // give roblox two seconds to cleanup its garbage time.Sleep(2 * time.Second) // force kill the process, causing cmd.Wait() to immediately return. + log.Println("Killing Roblox") + cmd.Process.Kill() + cmd.Process.Kill() + cmd.Process.Kill() + cmd.Process.Kill() + cmd.Process.Kill() + cmd.Process.Kill() cmd.Process.Kill() } }() } defer func() { - if kill && b.Config.AutoKillPrefix { + // If roblox is already running, don't kill wineprefix, even if + // auto kill prefix is enabled + if util.CommFound("Roblox") { + log.Println("Roblox is already running, not killing wineprefix after exit") + return + } + + if b.Config.AutoKillPrefix { b.Prefix.Kill() } }() From a2cd6850275f431b314a09678b4da3063a096e77 Mon Sep 17 00:00:00 2001 From: sewn Date: Sat, 21 Oct 2023 15:44:41 +0300 Subject: [PATCH 27/55] refactor log handling --- cmd/vinegar/binary.go | 215 ++++++++++++++++++++--------------------- cmd/vinegar/vinegar.go | 11 +-- go.mod | 3 - go.sum | 7 -- wine/dxvk/dxvk.go | 6 +- wine/exec.go | 33 ++++++- wine/prefix.go | 12 +-- wine/user.go | 2 +- 8 files changed, 152 insertions(+), 137 deletions(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index b9f66320..f849e789 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -1,7 +1,9 @@ package main import ( + "bufio" "fmt" + "io" "log" "os" "os/exec" @@ -11,7 +13,6 @@ import ( "syscall" "time" - "github.com/nxadm/tail" bsrpc "github.com/vinegarhq/vinegar/bloxstraprpc" "github.com/vinegarhq/vinegar/internal/config" "github.com/vinegarhq/vinegar/internal/config/state" @@ -25,6 +26,14 @@ import ( "github.com/vinegarhq/vinegar/wine/dxvk" ) +const ( + DialogInternalBrowserBrokenTitle = "WebView/InternalBrowser is broken" + DialogUseBrowserMsg = "Use the browser for whatever you were doing just now." + DialogQuickLoginMsg = "Use Quick Log In to authenticate ('Log In With Another Device' button)" + RobloxLogShutdownEntry = "[FLog::SingleSurfaceApp] shutDown:" + RobloxLogAbsoluteExitEntry = "[FLog::SingleSurfaceApp] unregisterMemoryPrioritizationCallback" +) + type Binary struct { Splash *splash.Splash @@ -37,10 +46,15 @@ type Binary struct { Prefix *wine.Prefix Type roblox.BinaryType Version version.Version - Started time.Time + + // Logging + Auth bool + Exited chan bool + Activity bsrpc.Activity + Output io.Writer } -func NewBinary(bt roblox.BinaryType, cfg *config.Config, pfx *wine.Prefix) Binary { +func NewBinary(bt roblox.BinaryType, out io.Writer, cfg *config.Config, pfx *wine.Prefix) Binary { var bcfg config.Binary switch bt { @@ -60,170 +74,155 @@ func NewBinary(bt roblox.BinaryType, cfg *config.Config, pfx *wine.Prefix) Binar Name: bt.BinaryName(), Type: bt, Prefix: pfx, + + Output: out, + Exited: make(chan bool, 1), } } func (b *Binary) Run(args ...string) error { + // REQUIRED for HandleRobloxLog to function. + os.Setenv("WINEDEBUG", os.Getenv("WINEDEBUG")+",warn+debugstr") + cmd, err := b.Command(args...) if err != nil { return err } + o, err := cmd.OutputPipe() + if err != nil { + return err + } log.Printf("Launching %s", b.Name) b.Splash.Message("Launching " + b.Alias) - // Launches into foreground - b.Started = time.Now() if err := cmd.Start(); err != nil { return err } - // act as the signal holder, as roblox/wine will not do anything - // with the INT signal. + // Act as the signal holder, as roblox/wine will not do anything with the INT signal. + // Additionally, if Vinegar got TERM, it will also immediately exit, but roblox + // continues running. c := make(chan os.Signal, 1) - signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c - // This way, cmd.Wait() will return and the wineprefix killer - // will be ran. log.Println("Killing Roblox") + // This way, cmd.Wait() will return and the wineprefix killer will be ran. cmd.Process.Kill() + // Don't handle INT after it was recieved, this way if another signal was sent, + // Vinegar will immediately exit. signal.Stop(c) }() + // This is for cases where Roblox itself does not exit by itself, which + // happens sometimes (#173). + go func() { + // Gets sent by the log handler + <-b.Exited + log.Println("Got Roblox shutdown") + // Give roblox two seconds to cleanup its garbage + time.Sleep(2 * time.Second) + // Send a signal to the signal holder to kill the wine process + c <- syscall.SIGINT + }() + if b.Config.DiscordRPC { - err := bsrpc.Login() - if err != nil { + if err := bsrpc.Login(); err != nil { log.Printf("Failed to authenticate Discord RPC: %s, disabling RPC", err) b.Config.DiscordRPC = false } + // NOTE: This will panic if logout fails defer bsrpc.Logout() } - // after FindLog() fails to find a log, assume Roblox hasn't started. - // early. if it did, assume failure and jump to cmd.Wait(), which will give the - // returned error to the splash screen and output if wine returns one. - p, err := b.FindLog() - if err != nil { - log.Printf("%s, assuming roblox failure", err) - } else { - b.Splash.Close() - - go func() { - rblxExited, err := b.TailLog(p) - if err != nil { - log.Printf("tail roblox log file: %s", err) - return - } - - if rblxExited { - log.Println("Got Roblox shutdown") - // give roblox two seconds to cleanup its garbage - time.Sleep(2 * time.Second) - // force kill the process, causing cmd.Wait() to immediately return. - log.Println("Killing Roblox") - cmd.Process.Kill() - cmd.Process.Kill() - cmd.Process.Kill() - cmd.Process.Kill() - cmd.Process.Kill() - cmd.Process.Kill() - cmd.Process.Kill() - } - }() + // Log handler, this *should* return once the output reader of the + // Wine/Roblox process is closed, indicating that it has exited. + // r := io.TeeReader(o, b.Output) + // This ^ can alternatively be used to achieve both parsing of Wine+Roblox logs + // while also sending it to the log output, but debugstr from Wine + // should be ignored for a cleaner log output, which by itself should only + // be used for HandleRobloxLog(), which is also sent to the log output... + // Although, there may be some wine logs AFTER wine exits, which is why + // this function only returns once there is nothing to read. Only downside + // to that is this the command will take some time to flush and close the + // output pipes. + if err := b.HandleWineLog(o); err != nil { + return err } - defer func() { - // If roblox is already running, don't kill wineprefix, even if - // auto kill prefix is enabled - if util.CommFound("Roblox") { - log.Println("Roblox is already running, not killing wineprefix after exit") - return - } - - if b.Config.AutoKillPrefix { - b.Prefix.Kill() - } - }() - if err := cmd.Wait(); err != nil { return fmt.Errorf("roblox process: %w", err) } - return nil -} - -func (b *Binary) FindLog() (string, error) { - appData, err := b.Prefix.AppDataDir() - if err != nil { - return "", err + if util.CommFound("Roblox") { + log.Println("Another Roblox instance is already running, not killing wineprefix") + return nil } - dir := filepath.Join(appData, "Local", "Roblox", "logs") - // May not exist if roblox has its first run - if err := os.MkdirAll(dir, 0o755); err != nil { - return "", err + if b.Config.AutoKillPrefix { + b.Prefix.Kill() } - log.Println("Polling for Roblox log file, 10 retries") - for i := 0; i < 10; i++ { - time.Sleep(1 * time.Second) + return nil +} + +func (b *Binary) HandleWineLog(wr io.Reader) error { + s := bufio.NewScanner(wr) + for s.Scan() { + txt := s.Text() - name, err := util.FindTimeFile(dir, &b.Started) - if err == nil { - log.Printf("Found Roblox log file: %s", name) - return name, nil + // XXXX:channel:class OutputDebugStringA "[FLog::Foo] Message" + if len(txt) >= 39 && txt[19:37] == "OutputDebugStringA" { + // length of roblox Flog message + if len(txt) >= 90 { + b.HandleRobloxLog(txt[39 : len(txt)-1]) + } + continue } + + fmt.Fprintln(b.Output, txt) } - return "", fmt.Errorf("could not find roblox log file after time %s", b.Started) + return s.Err() } -// Boolean returned is if Roblox had exited, detecting via logs -func (b *Binary) TailLog(name string) (bool, error) { - var a bsrpc.Activity - const title = "WebView/InternalBrowser is broken" - auth := false - - t, err := tail.TailFile(name, tail.Config{Follow: true, MustExist: true}) - if err != nil { - return false, err +func (b *Binary) HandleRobloxLog(line string) { + // As soon as a singular Roblox log has been hit, close the splash window + if !b.Splash.IsClosed() { + b.Splash.Close() } - for line := range t.Lines { - fmt.Fprintln(b.Prefix.Output, line.Text) + fmt.Fprintln(b.Output, line) - // Easy way to figure out we are authenticated, to make a more - // babysit message to tell the user to use quick login - if strings.Contains(line.Text, "DID_LOG_IN") { - auth = true - } + if strings.Contains(line, "DID_LOG_IN") { + b.Auth = true + return + } - if strings.Contains(line.Text, "the local did not install any WebView2 runtime") { - if auth { - b.Splash.Dialog(title, "use the browser for whatever you were doing just now.") - } else { - b.Splash.Dialog(title, "Use Quick Log In to authenticate ('Log In With Another Device' button)") - } + if strings.Contains(line, "InternalBrowser") { + msg := DialogUseBrowserMsg + if !b.Auth { + msg = DialogQuickLoginMsg } - // Best we've got to know if roblox had actually quit - if strings.Contains(line.Text, "[FLog::SingleSurfaceApp] shutDown:") { - return true, nil - } + b.Splash.Dialog(DialogInternalBrowserBrokenTitle, msg) + return + } - if b.Config.DiscordRPC { - if err := a.HandleLog(line.Text); err != nil { - log.Printf("Failed to handle Discord RPC: %s", err) - } - } + if strings.Contains(line, RobloxLogShutdownEntry) { + b.Exited <- true + return } - // this is should be unreachable - return false, nil + if b.Config.DiscordRPC { + if err := b.Activity.HandleLog(line); err != nil { + log.Printf("Failed to handle Discord RPC: %s", err) + } + } } func (b *Binary) FetchVersion() (version.Version, error) { diff --git a/cmd/vinegar/vinegar.go b/cmd/vinegar/vinegar.go index 451fc1c1..2fcc9c39 100644 --- a/cmd/vinegar/vinegar.go +++ b/cmd/vinegar/vinegar.go @@ -84,17 +84,16 @@ func main() { logFile := logs.File(cmd) logOutput := io.MultiWriter(logFile, os.Stderr) - - pfx.Output = logOutput + b.Output = logOutput log.SetOutput(logOutput) defer logFile.Close() switch cmd { case "player": - b = NewBinary(roblox.Player, &cfg, &pfx) + b = NewBinary(roblox.Player, logOutput, &cfg, &pfx) case "studio": - b = NewBinary(roblox.Studio, &cfg, &pfx) + b = NewBinary(roblox.Studio, logOutput, &cfg, &pfx) } go func() { @@ -116,8 +115,8 @@ func main() { select {} // wait for window to close } - if _, err := os.Stat(filepath.Join(pfx.Dir, "drive_c", "windows")); err != nil { - log.Printf("Initializing wineprefix at %s", pfx.Dir) + if _, err := os.Stat(filepath.Join(pfx.Dir(), "drive_c", "windows")); err != nil { + log.Printf("Initializing wineprefix at %s", pfx.Dir()) b.Splash.Message("Initializing wineprefix") if err := PrefixInit(&pfx); err != nil { diff --git a/go.mod b/go.mod index a761b85d..3b0d9416 100644 --- a/go.mod +++ b/go.mod @@ -13,21 +13,18 @@ require ( dario.cat/mergo v1.0.0 gioui.org v0.3.0 github.com/hugolgst/rich-go v0.0.0-20230917173849-4a4fb1d3c362 - github.com/nxadm/tail v1.4.11 golang.org/x/sys v0.11.0 ) require ( gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect gioui.org/shader v1.0.6 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 // indirect golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 // indirect golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 // indirect golang.org/x/image v0.5.0 // indirect golang.org/x/text v0.7.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect ) retract ( diff --git a/go.sum b/go.sum index 828fcb81..da8ba644 100644 --- a/go.sum +++ b/go.sum @@ -14,15 +14,11 @@ github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo= github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI= github.com/hugolgst/rich-go v0.0.0-20230917173849-4a4fb1d3c362 h1:Q8D2HP1l2mOoeRVLhHjDhK8MRb7LkjESWRtd2gbauws= github.com/hugolgst/rich-go v0.0.0-20230917173849-4a4fb1d3c362/go.mod h1:nGaW7CGfNZnhtiFxMpc4OZdqIexGXjUlBnlmpZmjEKA= -github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= -github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/otiai10/copy v1.12.0 h1:cLMgSQnXBs1eehF0Wy/FAGsgDTDmAqFR7rQylBb1nDY= github.com/otiai10/copy v1.12.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= @@ -54,7 +50,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -71,8 +66,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/wine/dxvk/dxvk.go b/wine/dxvk/dxvk.go index 38f8cc3a..cc6b2b06 100644 --- a/wine/dxvk/dxvk.go +++ b/wine/dxvk/dxvk.go @@ -39,7 +39,7 @@ func Remove(pfx *wine.Prefix) error { log.Println("Removing DLL:", dllPath) - if err := os.Remove(filepath.Join(pfx.Dir, dllPath)); err != nil { + if err := os.Remove(filepath.Join(pfx.Dir(), dllPath)); err != nil { return err } } @@ -83,8 +83,8 @@ func Extract(name string, pfx *wine.Prefix) error { } destDir, ok := map[string]string{ - "x64": filepath.Join(pfx.Dir, "drive_c", "windows", "system32"), - "x32": filepath.Join(pfx.Dir, "drive_c", "windows", "syswow64"), + "x64": filepath.Join(pfx.Dir(), "drive_c", "windows", "system32"), + "x32": filepath.Join(pfx.Dir(), "drive_c", "windows", "syswow64"), }[filepath.Base(filepath.Dir(header.Name))] if !ok { diff --git a/wine/exec.go b/wine/exec.go index 459e7446..4ecbd92a 100644 --- a/wine/exec.go +++ b/wine/exec.go @@ -1,6 +1,8 @@ package wine import ( + "errors" + "io" "log" "os/exec" ) @@ -11,15 +13,40 @@ type Cmd struct { func (p *Prefix) Command(name string, arg ...string) *Cmd { cmd := exec.Command(name, arg...) - cmd.Stderr = p.Output - cmd.Stdout = p.Output cmd.Env = append(cmd.Environ(), - "WINEPREFIX="+p.Dir, + "WINEPREFIX="+p.dir, ) return &Cmd{cmd} } +func (c *Cmd) SetOutput(w io.Writer) error { + if c.Process != nil { + return errors.New("SetOutput after process started") + } + c.Stderr = w + c.Stdout = w + return nil +} + +func (c *Cmd) OutputPipe() (io.Reader, error) { + if err := c.SetOutput(nil); err != nil { + return nil, err + } + + e, err := c.StderrPipe() + if err != nil { + return nil, err + } + + o, err := c.StdoutPipe() + if err != nil { + return nil, err + } + + return io.MultiReader(e, o), nil +} + func (c *Cmd) Start() error { log.Printf("Starting command: %s", c.String()) diff --git a/wine/prefix.go b/wine/prefix.go index c4d4e2dc..a9a95b0a 100644 --- a/wine/prefix.go +++ b/wine/prefix.go @@ -1,23 +1,23 @@ package wine import ( - "io" "log" - "os" ) type Prefix struct { - Dir string - Output io.Writer + dir string } func New(dir string) Prefix { return Prefix{ - Dir: dir, - Output: os.Stderr, + dir: dir, } } +func (p *Prefix) Dir() string { + return p.dir +} + func (p *Prefix) Wine(exe string, arg ...string) *Cmd { arg = append([]string{exe}, arg...) diff --git a/wine/user.go b/wine/user.go index 19b0f916..26e09527 100644 --- a/wine/user.go +++ b/wine/user.go @@ -11,5 +11,5 @@ func (p *Prefix) AppDataDir() (string, error) { return "", err } - return filepath.Join(p.Dir, "drive_c", "users", user.Username, "AppData"), nil + return filepath.Join(p.dir, "drive_c", "users", user.Username, "AppData"), nil } From 7250d6a0647e628f4c1ca2415446ccd48aa35845 Mon Sep 17 00:00:00 2001 From: sewn Date: Sat, 21 Oct 2023 16:28:42 +0300 Subject: [PATCH 28/55] bloxstraprpc: minor changes --- bloxstraprpc/{logs.go => activity.go} | 65 +++++++++++----------- bloxstraprpc/{discord.go => discordrpc.go} | 12 ++-- bloxstraprpc/message.go | 15 ++--- cmd/vinegar/binary.go | 2 +- 4 files changed, 43 insertions(+), 51 deletions(-) rename bloxstraprpc/{logs.go => activity.go} (75%) rename bloxstraprpc/{discord.go => discordrpc.go} (92%) diff --git a/bloxstraprpc/logs.go b/bloxstraprpc/activity.go similarity index 75% rename from bloxstraprpc/logs.go rename to bloxstraprpc/activity.go index c940d50e..187b0f3c 100644 --- a/bloxstraprpc/logs.go +++ b/bloxstraprpc/activity.go @@ -39,21 +39,21 @@ type Activity struct { timeStartedUniverse time.Time currentUniverseID string - InGame bool - IsTeleport bool - ServerType - PlaceID string - JobID string - MAC string + ingame bool + teleported bool + server ServerType + placeID string + jobID string + mac string teleport bool reservedteleport bool } -func (a *Activity) HandleLog(line string) error { - if !a.InGame && a.PlaceID == "" { +func (a *Activity) HandleRobloxLog(line string) error { + if !a.ingame && a.placeID == "" { if strings.Contains(line, GameJoiningPrivateServerEntry) { - a.ServerType = Private + a.server = Private return nil } @@ -63,7 +63,7 @@ func (a *Activity) HandleLog(line string) error { } } - if !a.InGame && a.PlaceID != "" { + if !a.ingame && a.placeID != "" { if strings.Contains(line, GameJoiningUDMUXEntry) { a.handleUDMUX(line) return nil @@ -71,20 +71,19 @@ func (a *Activity) HandleLog(line string) error { if strings.Contains(line, GameJoinedEntry) { a.handleGameJoined(line) - return a.SetCurrentGame() } } - if a.InGame && a.PlaceID != "" { + if a.ingame && a.placeID != "" { if strings.Contains(line, GameDisconnectedEntry) { - log.Printf("Disconnected From Game (%s/%s/%s)", a.PlaceID, a.JobID, a.MAC) + log.Printf("Disconnected From Game (%s/%s/%s)", a.placeID, a.jobID, a.mac) a.Clear() return a.SetCurrentGame() } if strings.Contains(line, GameTeleportingEntry) { - log.Printf("Teleporting to server (%s/%s/%s)", a.PlaceID, a.JobID, a.MAC) + log.Printf("Teleporting to server (%s/%s/%s)", a.placeID, a.jobID, a.mac) a.teleport = true return nil } @@ -111,12 +110,12 @@ func (a *Activity) HandleLog(line string) error { func (a *Activity) handleUDMUX(line string) { m := GameJoiningUDMUXPattern.FindStringSubmatch(line) - if len(m) != 3 || m[2] != a.MAC { + if len(m) != 3 || m[2] != a.mac { return } - a.MAC = m[1] - log.Printf("Got game join UDMUX: %s", a.MAC) + a.mac = m[1] + log.Printf("Got game join UDMUX: %s", a.mac) } func (a *Activity) handleGameJoining(line string) { @@ -125,41 +124,41 @@ func (a *Activity) handleGameJoining(line string) { return } - a.InGame = false - a.JobID = m[1] - a.PlaceID = m[2] - a.MAC = m[3] + a.ingame = false + a.jobID = m[1] + a.placeID = m[2] + a.mac = m[3] if a.teleport { - a.IsTeleport = true + a.teleported = true a.teleport = false } if a.reservedteleport { - a.ServerType = Reserved + a.server = Reserved a.reservedteleport = false } - log.Printf("Joining Game (%s/%s/%s)", a.JobID, a.PlaceID, a.MAC) + log.Printf("Joining Game (%s/%s/%s)", a.jobID, a.placeID, a.mac) } func (a *Activity) handleGameJoined(line string) { m := GameJoinedEntryPattern.FindStringSubmatch(line) - if len(m) != 2 || m[1] != a.MAC { + if len(m) != 2 || m[1] != a.mac { return } - a.InGame = true - log.Printf("Joined Game (%s/%s/%s)", a.PlaceID, a.JobID, a.MAC) + a.ingame = true + log.Printf("Joined Game (%s/%s/%s)", a.placeID, a.jobID, a.mac) // handle rpc } func (a *Activity) Clear() { - a.InGame = false - a.PlaceID = "" - a.JobID = "" - a.MAC = "" - a.ServerType = Public - a.IsTeleport = false + a.teleported = false + a.ingame = false + a.placeID = "" + a.jobID = "" + a.mac = "" + a.server = Public a.presence = client.Activity{} } diff --git a/bloxstraprpc/discord.go b/bloxstraprpc/discordrpc.go similarity index 92% rename from bloxstraprpc/discord.go rename to bloxstraprpc/discordrpc.go index 3e76b8a4..050a653a 100644 --- a/bloxstraprpc/discord.go +++ b/bloxstraprpc/discordrpc.go @@ -23,7 +23,7 @@ func Logout() { } func (a *Activity) SetCurrentGame() error { - if !a.InGame { + if !a.ingame { log.Println("Not in game, clearing presence") a.presence = client.Activity{} } else { @@ -37,15 +37,15 @@ func (a *Activity) SetCurrentGame() error { func (a *Activity) SetPresence() error { var status string - log.Printf("Setting presence for Place ID %s", a.PlaceID) + log.Printf("Setting presence for Place ID %s", a.placeID) - uid, err := api.GetUniverseID(a.PlaceID) + uid, err := api.GetUniverseID(a.placeID) if err != nil { return err } log.Printf("Got Universe ID as %s", uid) - if !a.IsTeleport || uid != a.currentUniverseID { + if !a.teleported || uid != a.currentUniverseID { a.timeStartedUniverse = time.Now() } @@ -63,7 +63,7 @@ func (a *Activity) SetPresence() error { } log.Printf("Got Universe thumbnail as %s", tn.ImageURL) - switch a.ServerType { + switch a.server { case Public: status = "by " + gd.Creator.Name case Private: @@ -85,7 +85,7 @@ func (a *Activity) SetPresence() error { Buttons: []*client.Button{ { Label: "See game page", - Url: "https://www.roblox.com/games/" + a.PlaceID, + Url: "https://www.roblox.com/games/" + a.placeID, }, }, } diff --git a/bloxstraprpc/message.go b/bloxstraprpc/message.go index fd0f3e38..d042bdd5 100644 --- a/bloxstraprpc/message.go +++ b/bloxstraprpc/message.go @@ -31,10 +31,6 @@ type Message struct { func ParseMessage(line string) (Message, error) { var m Message - if !strings.Contains(line, GameMessageEntry) { - return m, nil - } - msg := line[strings.Index(line, GameMessageEntry)+len(GameMessageEntry)+1:] if err := json.Unmarshal([]byte(msg), &m); err != nil { @@ -42,15 +38,12 @@ func ParseMessage(line string) (Message, error) { } if m.Command == "" { - return m, errors.New("command is empty") - } - - if len(m.Data.Details) > 128 { - return m, errors.New("details cannot be longer than 128 characters") + return Message{}, errors.New("command is empty") } - if len(m.Data.State) > 128 { - return m, errors.New("details cannot be longer than 128 characters") + // discord RPC implementation requires a limit of 128 characters + if len(m.Data.Details) > 128 || len(m.Data.State) > 128 { + return Message{}, errors.New("details or state cannot be longer than 128 characters") } log.Printf("Received message: %+v", m) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index f849e789..0ada529e 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -219,7 +219,7 @@ func (b *Binary) HandleRobloxLog(line string) { } if b.Config.DiscordRPC { - if err := b.Activity.HandleLog(line); err != nil { + if err := b.Activity.HandleRobloxLog(line); err != nil { log.Printf("Failed to handle Discord RPC: %s", err) } } From afd9f1977f24042c155d74e6c4d0ea3b6e96f676 Mon Sep 17 00:00:00 2001 From: sewn Date: Sat, 21 Oct 2023 16:30:31 +0300 Subject: [PATCH 29/55] roblox/api: adjust api log message --- roblox/api/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roblox/api/api.go b/roblox/api/api.go index 9355843a..4d33e54c 100644 --- a/roblox/api/api.go +++ b/roblox/api/api.go @@ -19,7 +19,7 @@ func SetClient(client *http.Client) { } func Request(method, service, endpoint string, v interface{}) error { - log.Printf("Performing %s request on %s/%s", method, service, endpoint) + log.Printf("Performing Roblox API %s %s request on %s", method, service, endpoint) url := fmt.Sprintf(APIURL, service, endpoint) From 85535dcc92f871d9d1fa2f87656c65b29dcf3881 Mon Sep 17 00:00:00 2001 From: sewn Date: Sat, 21 Oct 2023 17:35:38 +0300 Subject: [PATCH 30/55] roblox/api: handle http errors --- roblox/api/api.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/roblox/api/api.go b/roblox/api/api.go index 4d33e54c..d487b213 100644 --- a/roblox/api/api.go +++ b/roblox/api/api.go @@ -36,13 +36,14 @@ func Request(method, service, endpoint string, v interface{}) error { } defer resp.Body.Close() - if resp.StatusCode >= 400 { + if resp.StatusCode != http.StatusOK { + // Return the given API error only if the decoder succeeded errsResp := new(errorsResponse) - if err := json.NewDecoder(resp.Body).Decode(errsResp); err != nil { - return err + if err := json.NewDecoder(resp.Body).Decode(errsResp); err == nil { + return errsResp } - return errsResp + return fmt.Errorf("%w: %s", ErrBadStatus, resp.Status) } if v != nil { From 864d3968dbfaaefbd5843b0699f945f9bccad97a Mon Sep 17 00:00:00 2001 From: sewn Date: Sat, 21 Oct 2023 22:09:24 +0300 Subject: [PATCH 31/55] ci/vendor: try different for release variable --- .github/workflows/vendor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/vendor.yml b/.github/workflows/vendor.yml index 16e0d992..1df2823f 100644 --- a/.github/workflows/vendor.yml +++ b/.github/workflows/vendor.yml @@ -18,7 +18,7 @@ jobs: run: go mod vendor - name: 'Package the source directory' run: | - RELEASE="vinegar-${{ github.ref_name }}" + RELEASE="vinegar-${{ github.event.release.tag_name }}" cd .. cp -r vinegar $RELEASE @@ -30,6 +30,6 @@ jobs: uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: "../vinegar-${{ github.ref_name }}.tar.xz" + file: "../vinegar-${{ github.event.release.tag_name }}.tar.xz" overwrite: true make_latest: false \ No newline at end of file From 9e4ab10ff125d9be2b415afa184743f25db48dd0 Mon Sep 17 00:00:00 2001 From: sewn Date: Sun, 22 Oct 2023 16:57:59 +0300 Subject: [PATCH 32/55] rework log handling slightly --- cmd/vinegar/binary.go | 87 ++++++++++------------------------------ cmd/vinegar/vinegar.go | 10 ++--- internal/logs/logs.go | 4 +- wine/{exec.go => cmd.go} | 32 ++++++++++----- wine/prefix.go | 9 ++++- wine/tricks.go | 5 +++ 6 files changed, 62 insertions(+), 85 deletions(-) rename wine/{exec.go => cmd.go} (55%) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index 0ada529e..deadbec5 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -11,7 +11,6 @@ import ( "path/filepath" "strings" "syscall" - "time" bsrpc "github.com/vinegarhq/vinegar/bloxstraprpc" "github.com/vinegarhq/vinegar/internal/config" @@ -30,8 +29,6 @@ const ( DialogInternalBrowserBrokenTitle = "WebView/InternalBrowser is broken" DialogUseBrowserMsg = "Use the browser for whatever you were doing just now." DialogQuickLoginMsg = "Use Quick Log In to authenticate ('Log In With Another Device' button)" - RobloxLogShutdownEntry = "[FLog::SingleSurfaceApp] shutDown:" - RobloxLogAbsoluteExitEntry = "[FLog::SingleSurfaceApp] unregisterMemoryPrioritizationCallback" ) type Binary struct { @@ -49,12 +46,10 @@ type Binary struct { // Logging Auth bool - Exited chan bool Activity bsrpc.Activity - Output io.Writer } -func NewBinary(bt roblox.BinaryType, out io.Writer, cfg *config.Config, pfx *wine.Prefix) Binary { +func NewBinary(bt roblox.BinaryType, cfg *config.Config, pfx *wine.Prefix) Binary { var bcfg config.Binary switch bt { @@ -74,13 +69,20 @@ func NewBinary(bt roblox.BinaryType, out io.Writer, cfg *config.Config, pfx *win Name: bt.BinaryName(), Type: bt, Prefix: pfx, - - Output: out, - Exited: make(chan bool, 1), } } func (b *Binary) Run(args ...string) error { + if b.Config.DiscordRPC { + if err := bsrpc.Login(); err != nil { + log.Printf("Failed to authenticate Discord RPC: %s, disabling RPC", err) + b.Config.DiscordRPC = false + } + + // NOTE: This will panic if logout fails + defer bsrpc.Logout() + } + // REQUIRED for HandleRobloxLog to function. os.Setenv("WINEDEBUG", os.Getenv("WINEDEBUG")+",warn+debugstr") @@ -93,68 +95,27 @@ func (b *Binary) Run(args ...string) error { return err } - log.Printf("Launching %s", b.Name) - b.Splash.Message("Launching " + b.Alias) - - // Launches into foreground - if err := cmd.Start(); err != nil { - return err - } - // Act as the signal holder, as roblox/wine will not do anything with the INT signal. // Additionally, if Vinegar got TERM, it will also immediately exit, but roblox // continues running. c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) - go func() { <-c log.Println("Killing Roblox") - // This way, cmd.Wait() will return and the wineprefix killer will be ran. + // This way, cmd.Run() will return and the wineprefix killer will be ran. cmd.Process.Kill() // Don't handle INT after it was recieved, this way if another signal was sent, // Vinegar will immediately exit. signal.Stop(c) }() - // This is for cases where Roblox itself does not exit by itself, which - // happens sometimes (#173). - go func() { - // Gets sent by the log handler - <-b.Exited - log.Println("Got Roblox shutdown") - // Give roblox two seconds to cleanup its garbage - time.Sleep(2 * time.Second) - // Send a signal to the signal holder to kill the wine process - c <- syscall.SIGINT - }() + go b.HandleOutput(o) - if b.Config.DiscordRPC { - if err := bsrpc.Login(); err != nil { - log.Printf("Failed to authenticate Discord RPC: %s, disabling RPC", err) - b.Config.DiscordRPC = false - } - - // NOTE: This will panic if logout fails - defer bsrpc.Logout() - } - - // Log handler, this *should* return once the output reader of the - // Wine/Roblox process is closed, indicating that it has exited. - // r := io.TeeReader(o, b.Output) - // This ^ can alternatively be used to achieve both parsing of Wine+Roblox logs - // while also sending it to the log output, but debugstr from Wine - // should be ignored for a cleaner log output, which by itself should only - // be used for HandleRobloxLog(), which is also sent to the log output... - // Although, there may be some wine logs AFTER wine exits, which is why - // this function only returns once there is nothing to read. Only downside - // to that is this the command will take some time to flush and close the - // output pipes. - if err := b.HandleWineLog(o); err != nil { - return err - } + log.Printf("Launching %s", b.Name) + b.Splash.Message("Launching " + b.Alias) - if err := cmd.Wait(); err != nil { + if err := cmd.Run(); err != nil { return fmt.Errorf("roblox process: %w", err) } @@ -170,7 +131,7 @@ func (b *Binary) Run(args ...string) error { return nil } -func (b *Binary) HandleWineLog(wr io.Reader) error { +func (b *Binary) HandleOutput(wr io.Reader) { s := bufio.NewScanner(wr) for s.Scan() { txt := s.Text() @@ -184,10 +145,8 @@ func (b *Binary) HandleWineLog(wr io.Reader) error { continue } - fmt.Fprintln(b.Output, txt) + fmt.Fprintln(b.Prefix.Output, txt) } - - return s.Err() } func (b *Binary) HandleRobloxLog(line string) { @@ -196,7 +155,7 @@ func (b *Binary) HandleRobloxLog(line string) { b.Splash.Close() } - fmt.Fprintln(b.Output, line) + fmt.Fprintln(b.Prefix.Output, line) if strings.Contains(line, "DID_LOG_IN") { b.Auth = true @@ -213,11 +172,6 @@ func (b *Binary) HandleRobloxLog(line string) { return } - if strings.Contains(line, RobloxLogShutdownEntry) { - b.Exited <- true - return - } - if b.Config.DiscordRPC { if err := b.Activity.HandleRobloxLog(line); err != nil { log.Printf("Failed to handle Discord RPC: %s", err) @@ -431,7 +385,7 @@ func (b *Binary) Command(args ...string) (*wine.Cmd, error) { mutexer := b.Prefix.Command("wine", filepath.Join(BinPrefix, "robloxmutexer.exe")) err := mutexer.Start() if err != nil { - return &wine.Cmd{}, err + return &wine.Cmd{}, fmt.Errorf("robloxmutexer: %w") } } @@ -441,6 +395,7 @@ func (b *Binary) Command(args ...string) (*wine.Cmd, error) { if len(launcher) >= 1 { cmd.Args = append(launcher, cmd.Args...) + // For safety, ensure that the launcher is in PATH launcherPath, err := exec.LookPath(launcher[0]) if err != nil { return &wine.Cmd{}, err diff --git a/cmd/vinegar/vinegar.go b/cmd/vinegar/vinegar.go index 2fcc9c39..8213e637 100644 --- a/cmd/vinegar/vinegar.go +++ b/cmd/vinegar/vinegar.go @@ -53,7 +53,7 @@ func main() { log.Fatal(err) } - pfx := wine.New(dirs.Prefix) + pfx := wine.New(dirs.Prefix, os.Stderr) // Always ensure its created, wine will complain if the root // directory doesnt exist if err := os.MkdirAll(dirs.Prefix, 0o755); err != nil { @@ -84,16 +84,16 @@ func main() { logFile := logs.File(cmd) logOutput := io.MultiWriter(logFile, os.Stderr) - b.Output = logOutput + pfx.Output = logOutput log.SetOutput(logOutput) defer logFile.Close() switch cmd { case "player": - b = NewBinary(roblox.Player, logOutput, &cfg, &pfx) + b = NewBinary(roblox.Player, &cfg, &pfx) case "studio": - b = NewBinary(roblox.Studio, logOutput, &cfg, &pfx) + b = NewBinary(roblox.Studio, &cfg, &pfx) } go func() { @@ -145,7 +145,7 @@ func PrefixInit(pfx *wine.Prefix) error { return err } - return pfx.RegistryAdd("HKEY_CURRENT_USER\\Control Panel\\Desktop", "LogPixels", wine.REG_DWORD, "97") + return pfx.SetDPI(97) } func Uninstall() { diff --git a/internal/logs/logs.go b/internal/logs/logs.go index 2edf0432..c5db1a23 100644 --- a/internal/logs/logs.go +++ b/internal/logs/logs.go @@ -20,8 +20,8 @@ func File(name string) *os.File { file, err := os.Create(path) if err != nil { - log.Printf("Failed to create %s log file: %s", name, err) - return nil + log.Printf("Failed to create %s log file: %s, using Stderr", name, err) + return os.Stderr } log.Printf("Logging to file: %s", path) diff --git a/wine/exec.go b/wine/cmd.go similarity index 55% rename from wine/exec.go rename to wine/cmd.go index 4ecbd92a..418f2b4c 100644 --- a/wine/exec.go +++ b/wine/cmd.go @@ -2,6 +2,7 @@ package wine import ( "errors" + "os" "io" "log" "os/exec" @@ -11,8 +12,20 @@ type Cmd struct { *exec.Cmd } +// Command returns a passthrough Cmd struct to execute the named +// program with the given arguments. +// The command's Stderr and Stdout will be set to their os counterparts +// if the prefix's Output is nil. func (p *Prefix) Command(name string, arg ...string) *Cmd { cmd := exec.Command(name, arg...) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if p.Output != nil { + cmd.Stdout = p.Output + cmd.Stderr = p.Output + } + cmd.Env = append(cmd.Environ(), "WINEPREFIX="+p.dir, ) @@ -20,19 +33,16 @@ func (p *Prefix) Command(name string, arg ...string) *Cmd { return &Cmd{cmd} } -func (c *Cmd) SetOutput(w io.Writer) error { - if c.Process != nil { - return errors.New("SetOutput after process started") - } - c.Stderr = w - c.Stdout = w - return nil -} - +// OutputPipe erturns a pipe that will be a MultiReader +// of StderrPipe and StdoutPipe, it will set both Stdout +// and Stderr to nil once ran. func (c *Cmd) OutputPipe() (io.Reader, error) { - if err := c.SetOutput(nil); err != nil { - return nil, err + if c.Process != nil { + return nil, errors.New("OutputPipe after process started") } + + c.Stdout = nil + c.Stderr = nil e, err := c.StderrPipe() if err != nil { diff --git a/wine/prefix.go b/wine/prefix.go index a9a95b0a..d584cb3c 100644 --- a/wine/prefix.go +++ b/wine/prefix.go @@ -1,15 +1,22 @@ package wine import ( + "io" "log" ) type Prefix struct { + // Output specifies the descendant prefix commmand's + // Stderr and Stdout together. + Output io.Writer + dir string } -func New(dir string) Prefix { + +func New(dir string, out io.Writer) Prefix { return Prefix{ + Output: out, dir: dir, } } diff --git a/wine/tricks.go b/wine/tricks.go index 98e45403..7679b7f3 100644 --- a/wine/tricks.go +++ b/wine/tricks.go @@ -2,6 +2,7 @@ package wine import ( "log" + "strconv" ) func (p *Prefix) DisableCrashDialogs() error { @@ -15,3 +16,7 @@ func (p *Prefix) Winetricks() error { return p.Command("winetricks").Run() } + +func (p *Prefix) SetDPI(dpi int) error { + return p.RegistryAdd("HKEY_CURRENT_USER\\Control Panel\\Desktop", "LogPixels", REG_DWORD, strconv.Itoa(dpi)) +} From 1463d85ef2c87ef1867b119971f97edf83d7e0b9 Mon Sep 17 00:00:00 2001 From: sewn Date: Sun, 22 Oct 2023 17:33:34 +0300 Subject: [PATCH 33/55] wine: ensure prefix variable cannot be tampered with --- wine/cmd.go | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/wine/cmd.go b/wine/cmd.go index 418f2b4c..7f971b42 100644 --- a/wine/cmd.go +++ b/wine/cmd.go @@ -10,15 +10,19 @@ import ( type Cmd struct { *exec.Cmd + + // in-order to ensure that the WINEPREFIX environment + // variable cannot be tampered with. + prefixDir string } // Command returns a passthrough Cmd struct to execute the named // program with the given arguments. +// // The command's Stderr and Stdout will be set to their os counterparts // if the prefix's Output is nil. func (p *Prefix) Command(name string, arg ...string) *Cmd { cmd := exec.Command(name, arg...) - cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if p.Output != nil { @@ -26,11 +30,17 @@ func (p *Prefix) Command(name string, arg ...string) *Cmd { cmd.Stderr = p.Output } - cmd.Env = append(cmd.Environ(), - "WINEPREFIX="+p.dir, - ) + return &Cmd{ + Cmd: cmd, + prefixDir: p.dir, + } +} - return &Cmd{cmd} +// SetOutput set's the command's standard output and error to +// the given io.Writer. +func (c *Cmd) SetOutput(o io.Writer) { + c.Stdout = o + c.Stderr = o } // OutputPipe erturns a pipe that will be a MultiReader @@ -41,8 +51,7 @@ func (c *Cmd) OutputPipe() (io.Reader, error) { return nil, errors.New("OutputPipe after process started") } - c.Stdout = nil - c.Stderr = nil + c.SetOutput(nil) e, err := c.StderrPipe() if err != nil { @@ -58,13 +67,18 @@ func (c *Cmd) OutputPipe() (io.Reader, error) { } func (c *Cmd) Start() error { + c.Env = append(c.Environ(), + "WINEPREFIX="+c.prefixDir, + ) + log.Printf("Starting command: %s", c.String()) return c.Cmd.Start() } func (c *Cmd) Run() error { - log.Printf("Running command: %s", c.String()) - - return c.Cmd.Run() + if err := c.Start(); err != nil { + return err + } + return c.Wait() } From 0b43ad513dd729f5a740481aff552dbbaeadf3da Mon Sep 17 00:00:00 2001 From: sewn Date: Sun, 22 Oct 2023 18:26:51 +0300 Subject: [PATCH 34/55] util: use slices for walkdirexcluded --- internal/config/state/cleaners.go | 5 ++--- util/paths.go | 34 ++++++------------------------- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/internal/config/state/cleaners.go b/internal/config/state/cleaners.go index 6705ec86..25162e17 100644 --- a/internal/config/state/cleaners.go +++ b/internal/config/state/cleaners.go @@ -3,7 +3,6 @@ package state import ( "log" "os" - "path/filepath" "github.com/vinegarhq/vinegar/internal/dirs" "github.com/vinegarhq/vinegar/util" @@ -19,7 +18,7 @@ func CleanPackages() error { return util.WalkDirExcluded(dirs.Downloads, pkgs, func(path string) error { log.Printf("Removing unused package %s", path) - return os.Remove(filepath.Join(dirs.Downloads, path)) + return os.Remove(path) }) } @@ -33,6 +32,6 @@ func CleanVersions() error { return util.WalkDirExcluded(dirs.Versions, vers, func(path string) error { log.Printf("Removing unused version directory %s", path) - return os.RemoveAll(filepath.Join(dirs.Versions, path)) + return os.RemoveAll(path) }) } diff --git a/util/paths.go b/util/paths.go index 74c7970a..5d0acf0a 100644 --- a/util/paths.go +++ b/util/paths.go @@ -1,50 +1,28 @@ package util import ( - "io/fs" "os" "path/filepath" - "time" + "slices" ) +// WalkDirExcluded will walk the file tree located at dir, calling +// onExcluded for every file or directory that does not have a name in included. func WalkDirExcluded(dir string, included []string, onExcluded func(string) error) error { files, err := os.ReadDir(dir) if err != nil { return err } -find: for _, file := range files { - for _, inc := range included { - if file.Name() == inc { - continue find - } + if slices.Contains(included, file.Name()) { + continue } - if err := onExcluded(file.Name()); err != nil { + if err := onExcluded(filepath.Join(dir, file.Name())); err != nil { return err } } return nil } - -func FindTimeFile(dir string, comparison *time.Time) (string, error) { - var name string - - err := filepath.Walk(dir, func(p string, i fs.FileInfo, err error) error { - if i.ModTime().After(*comparison) { - name = p - } - return nil - }) - if err != nil { - return "", err - } - - if name == "" { - return "", os.ErrNotExist - } - - return name, nil -} From c7aa21be0fb7ca81a72937703e4176b314c2c40a Mon Sep 17 00:00:00 2001 From: sewn Date: Sun, 22 Oct 2023 20:38:38 +0300 Subject: [PATCH 35/55] cmd/vinegar: defer autokill --- cmd/vinegar/binary.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index deadbec5..d36af1e8 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -115,19 +115,20 @@ func (b *Binary) Run(args ...string) error { log.Printf("Launching %s", b.Name) b.Splash.Message("Launching " + b.Alias) + defer func() { + if util.CommFound("Roblox") { + log.Println("Another Roblox instance is already running, not killing wineprefix") + } + + if b.Config.AutoKillPrefix { + b.Prefix.Kill() + } + }() + if err := cmd.Run(); err != nil { return fmt.Errorf("roblox process: %w", err) } - if util.CommFound("Roblox") { - log.Println("Another Roblox instance is already running, not killing wineprefix") - return nil - } - - if b.Config.AutoKillPrefix { - b.Prefix.Kill() - } - return nil } From a040bc5c4e5134c09229d8f38b1ec061fb248275 Mon Sep 17 00:00:00 2001 From: sewn Date: Sun, 22 Oct 2023 20:38:59 +0300 Subject: [PATCH 36/55] cmd/vinegar: return on if roblox is already running --- cmd/vinegar/binary.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index d36af1e8..acae7850 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -118,6 +118,7 @@ func (b *Binary) Run(args ...string) error { defer func() { if util.CommFound("Roblox") { log.Println("Another Roblox instance is already running, not killing wineprefix") + return } if b.Config.AutoKillPrefix { From 28936ec3cb36fe4ef65c1edcb9dd9c01b066070d Mon Sep 17 00:00:00 2001 From: sewn Date: Sun, 22 Oct 2023 20:40:54 +0300 Subject: [PATCH 37/55] update go version to 1.21 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 3b0d9416..e7321bd7 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/vinegarhq/vinegar -go 1.20 +go 1.21 require ( github.com/BurntSushi/toml v1.3.2 From d3a25f8f4674b2e34ac4230a848ae2416dae2fcf Mon Sep 17 00:00:00 2001 From: sewn Date: Sun, 22 Oct 2023 20:41:24 +0300 Subject: [PATCH 38/55] update go version to go 1.21 --- .github/workflows/build.yml | 4 ++++ .github/workflows/vendor.yml | 2 +- go.sum | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 97212d28..0ebefa1e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,5 +10,9 @@ jobs: steps: - name: 'Checkout Repository' uses: actions/checkout@v3 + - name: 'Setup Go' + uses: actions/setup-go@v4 + with: + go-version: '^1.21' - name: 'Build Vinegar' run: make VINEGAR_GOFLAGS="--tags nogui" diff --git a/.github/workflows/vendor.yml b/.github/workflows/vendor.yml index 1df2823f..368ab307 100644 --- a/.github/workflows/vendor.yml +++ b/.github/workflows/vendor.yml @@ -13,7 +13,7 @@ jobs: - name: 'Setup Go' uses: actions/setup-go@v4 with: - go-version: '^1.20' + go-version: '^1.21' - name: 'Make the vendor directory' run: go mod vendor - name: 'Package the source directory' diff --git a/go.sum b/go.sum index da8ba644..ad3053a7 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,7 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY= +eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA= gioui.org v0.3.0 h1:xZty/uLl1+/HNKpumX60JPQd46n8Zy6lc5T3IRMKoR4= gioui.org v0.3.0/go.mod h1:1H72sKEk/fNFV+l0JNeM2Dt3co3Y4uaQcD+I+/GQ0e4= gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= @@ -17,11 +18,13 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo= github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI= +github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/hugolgst/rich-go v0.0.0-20230917173849-4a4fb1d3c362 h1:Q8D2HP1l2mOoeRVLhHjDhK8MRb7LkjESWRtd2gbauws= github.com/hugolgst/rich-go v0.0.0-20230917173849-4a4fb1d3c362/go.mod h1:nGaW7CGfNZnhtiFxMpc4OZdqIexGXjUlBnlmpZmjEKA= github.com/otiai10/copy v1.12.0 h1:cLMgSQnXBs1eehF0Wy/FAGsgDTDmAqFR7rQylBb1nDY= github.com/otiai10/copy v1.12.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= +github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 1b92739b78aa58d4c0cd29436388663c9f74fa30 Mon Sep 17 00:00:00 2001 From: sewn Date: Mon, 23 Oct 2023 21:15:42 +0300 Subject: [PATCH 39/55] cmd/vinegar: attempt to recover from gui failure --- cmd/vinegar/vinegar.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/vinegar/vinegar.go b/cmd/vinegar/vinegar.go index 8213e637..f41facd7 100644 --- a/cmd/vinegar/vinegar.go +++ b/cmd/vinegar/vinegar.go @@ -97,9 +97,15 @@ func main() { } go func() { + defer func() { + if r := recover(); r != nil { + log.Println("WARNING: Recovered from splash panic", r) + } + }() + err := b.Splash.Run() if err != nil { - log.Fatal(err) + log.Printf("WARNING: Failed to run splash window: %s", err) } }() From 4c306311b411f07995808fd10016b9e858f5f0ca Mon Sep 17 00:00:00 2001 From: sewn Date: Mon, 23 Oct 2023 21:40:26 +0300 Subject: [PATCH 40/55] roblox/bootstrapper: check for live channel for cdn --- roblox/bootstrapper/manifest.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/roblox/bootstrapper/manifest.go b/roblox/bootstrapper/manifest.go index 083e7434..862aad57 100644 --- a/roblox/bootstrapper/manifest.go +++ b/roblox/bootstrapper/manifest.go @@ -20,7 +20,8 @@ func channelPath(channel string) string { // ClientSettings it will be lowercased, but not on the deploy mirror. channel = strings.ToLower(channel) - if channel == "" { + // Roblox CDN only accepts no channel if its live + if channel == "" || channel == "live" { return "/" } From 61e7aca06ee823869aa9113ab9551934e0fc6acf Mon Sep 17 00:00:00 2001 From: sewn Date: Mon, 23 Oct 2023 21:46:14 +0300 Subject: [PATCH 41/55] roblox: try to respect default channel --- roblox/bootstrapper/manifest.go | 4 ++-- roblox/version/version.go | 25 ++++++++----------------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/roblox/bootstrapper/manifest.go b/roblox/bootstrapper/manifest.go index 862aad57..e9c4e2da 100644 --- a/roblox/bootstrapper/manifest.go +++ b/roblox/bootstrapper/manifest.go @@ -20,8 +20,8 @@ func channelPath(channel string) string { // ClientSettings it will be lowercased, but not on the deploy mirror. channel = strings.ToLower(channel) - // Roblox CDN only accepts no channel if its live - if channel == "" || channel == "live" { + // Roblox CDN only accepts no channel if its the default channel + if channel == "" || channel == version.DefaultChannel { return "/" } diff --git a/roblox/version/version.go b/roblox/version/version.go index e5c958c0..88b7bd25 100644 --- a/roblox/version/version.go +++ b/roblox/version/version.go @@ -2,7 +2,6 @@ package version import ( "log" - "strings" "github.com/vinegarhq/vinegar/roblox" "github.com/vinegarhq/vinegar/roblox/api" @@ -24,20 +23,12 @@ type Version struct { GUID string } -func ChannelPath(channel string) string { - // Ensure that the channel is lowercased, since internally in - // ClientSettings it will be lowercased, but not on the deploy mirror. - channel = strings.ToLower(channel) - - if channel == DefaultChannel { - return "/" +func New(bt roblox.BinaryType, channel string, GUID string) Version { + if channel == "" { + channel = DefaultChannel } - return "/channel/" + channel + "/" -} - -func New(bt roblox.BinaryType, channel string, GUID string) Version { - log.Printf("Got %s version %s", bt.BinaryName(), GUID) + log.Printf("Got %s version %s with channel %s", bt.BinaryName(), GUID, channel) return Version{ Type: bt, @@ -47,11 +38,11 @@ func New(bt roblox.BinaryType, channel string, GUID string) Version { } func Fetch(bt roblox.BinaryType, channel string) (Version, error) { - c := "" - if c != "" { - c = "for channel " + channel + if channel == "" { + channel = DefaultChannel } - log.Printf("Fetching latest version of %s %s", bt.BinaryName(), c) + + log.Printf("Fetching latest version of %s for channel %s", bt.BinaryName(), channel) cv, err := api.GetClientVersion(bt, channel) if err != nil { From b8b47f543cbbd7e1a02c29991ed8f11d5a5a4d43 Mon Sep 17 00:00:00 2001 From: Jrelvas <55360900+Noted-Jrelvas@users.noreply.github.com> Date: Tue, 24 Oct 2023 06:37:13 +0100 Subject: [PATCH 42/55] Remove Vinegar branding from desktop entries (#234) --- desktop/roblox-app.desktop.in | 2 +- desktop/roblox-player.desktop.in | 2 +- desktop/roblox-studio.desktop.in | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/desktop/roblox-app.desktop.in b/desktop/roblox-app.desktop.in index 738c741c..c9e8cd8c 100644 --- a/desktop/roblox-app.desktop.in +++ b/desktop/roblox-app.desktop.in @@ -1,6 +1,6 @@ [Desktop Entry] Type=Application -Name=Roblox App (Vinegar) +Name=Roblox App Icon=$FLATPAK.player Exec=vinegar player -app Terminal=false diff --git a/desktop/roblox-player.desktop.in b/desktop/roblox-player.desktop.in index 567d9db6..4683262c 100644 --- a/desktop/roblox-player.desktop.in +++ b/desktop/roblox-player.desktop.in @@ -1,6 +1,6 @@ [Desktop Entry] Type=Application -Name=Roblox Player (Vinegar) +Name=Roblox Player NoDisplay=true Icon=$FLATPAK.player Exec=vinegar player %u diff --git a/desktop/roblox-studio.desktop.in b/desktop/roblox-studio.desktop.in index 71d421d0..72def383 100644 --- a/desktop/roblox-studio.desktop.in +++ b/desktop/roblox-studio.desktop.in @@ -1,6 +1,6 @@ [Desktop Entry] Type=Application -Name=Roblox Studio (Vinegar) +Name=Roblox Studio Icon=$FLATPAK.studio Exec=vinegar studio %u Terminal=false From 71f71eac8c379ecc160ca5830b992800ad3b7018 Mon Sep 17 00:00:00 2001 From: sewn Date: Tue, 17 Oct 2023 21:27:27 +0300 Subject: [PATCH 43/55] sysinfo: initial --- sysinfo/sysinfo.go | 111 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 sysinfo/sysinfo.go diff --git a/sysinfo/sysinfo.go b/sysinfo/sysinfo.go new file mode 100644 index 00000000..ab11fe32 --- /dev/null +++ b/sysinfo/sysinfo.go @@ -0,0 +1,111 @@ +package sysinfo + +import ( + "os" + "io/fs" + "strconv" + "bufio" + "regexp" + "syscall" + "strings" + "path/filepath" +) + +type Kernel struct { + Release string + Version string +} + +type CPU struct { + Model string + Flags []string +} + +type GPU struct { + Path string + Integrated bool + Index int + Driver string +} + +type GPUs []GPU + +func NewKernel() Kernel { + var un syscall.Utsname + _ = syscall.Uname(&un) + + unameString := func(unarr [65]int8) string { + var sb strings.Builder + for _, b := range unarr[:] { + if b == 0 { + break + } + sb.WriteByte(byte(b)) + } + return sb.String() + } + + return Kernel{ + Release: unameString(un.Release), + Version: unameString(un.Version), + } +} + +func NewCPU() (cpu CPU) { + column := regexp.MustCompile("\t+: ") + + f, _ := os.Open("/proc/cpuinfo") + defer f.Close() + + s := bufio.NewScanner(f) + + for s.Scan() { + sl := column.Split(s.Text(), 2) + if sl == nil { + continue + } + + // pfft, who needs multiple cpus? just return if we got all we need + if cpu.Model != "" && cpu.Flags != nil { + break + } + + switch sl[0] { + case "model name": + cpu.Model = sl[1] + case "flags": + cpu.Flags = strings.Split(sl[1], " ") + } + } + if s.Err() != nil { + return + } + + return +} + +func NewGPUs() (gpus GPUs) { + card := regexp.MustCompile(`card([0-9]+)(?:-eDP-\d+)?$`) + + filepath.Walk("/sys/class/drm", func(p string, i fs.FileInfo, err error) error { + var gpu GPU + + match := card.FindStringSubmatch(p) + if match == nil { + return nil + } + + if len(match) == 2 { + gpu.Integrated = true + } + + gpu.Index, _ = strconv.Atoi(match[1]) + gpu.Driver, _ = filepath.EvalSymlinks(filepath.Join(p, "device/driver")) + gpu.Path = p + + gpus = append(gpus, gpu) + return nil + }) + + return +} From b038673a68a357f041ce3c5184fe02fdeca73d46 Mon Sep 17 00:00:00 2001 From: Thelolguy1 Date: Tue, 17 Oct 2023 14:37:00 -0700 Subject: [PATCH 44/55] sysinfo.go: add avx and flatpak detect --- sysinfo/sysinfo.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sysinfo/sysinfo.go b/sysinfo/sysinfo.go index ab11fe32..70139d34 100644 --- a/sysinfo/sysinfo.go +++ b/sysinfo/sysinfo.go @@ -9,6 +9,7 @@ import ( "syscall" "strings" "path/filepath" + "slices" ) type Kernel struct { @@ -19,6 +20,7 @@ type Kernel struct { type CPU struct { Model string Flags []string + AVX bool } type GPU struct { @@ -77,6 +79,9 @@ func NewCPU() (cpu CPU) { cpu.Flags = strings.Split(sl[1], " ") } } + + cpu.AVX = slices.Contains(cpu.Flags, "avx") + if s.Err() != nil { return } @@ -109,3 +114,10 @@ func NewGPUs() (gpus GPUs) { return } + +func InFlatpak() bool { + if _, err := os.Stat("/.flatpak-info"); err == nil { + return true + } + return false +} From 67732d91d3d3682db3596b1151efbfe56a70170b Mon Sep 17 00:00:00 2001 From: Thelolguy1 Date: Tue, 17 Oct 2023 19:40:44 -0700 Subject: [PATCH 45/55] vinegar.go: add stub for reportinfo --- cmd/vinegar/vinegar.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/vinegar/vinegar.go b/cmd/vinegar/vinegar.go index f41facd7..32cf2e23 100644 --- a/cmd/vinegar/vinegar.go +++ b/cmd/vinegar/vinegar.go @@ -22,7 +22,7 @@ var BinPrefix string func usage() { fmt.Fprintln(os.Stderr, "usage: vinegar [-config filepath] player|studio [args...]") fmt.Fprintln(os.Stderr, "usage: vinegar [-config filepath] exec prog [args...]") - fmt.Fprintln(os.Stderr, " vinegar [-config filepath] edit|kill|uninstall|delete|install-webview2|winetricks") + fmt.Fprintln(os.Stderr, " vinegar [-config filepath] edit|kill|uninstall|delete|install-webview2|winetricks|reportinfo") os.Exit(1) } @@ -61,6 +61,8 @@ func main() { } switch cmd { + case "reportinfo": + // TODO: implement this function case "exec": if len(args) < 2 { usage() From 2d1e4a17e38c3f78b8f8b551123f5ad019c7baa9 Mon Sep 17 00:00:00 2001 From: Thelolguy1 Date: Tue, 17 Oct 2023 21:17:40 -0700 Subject: [PATCH 46/55] sysinfo: add wael's recommendation --- sysinfo/sysinfo.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sysinfo/sysinfo.go b/sysinfo/sysinfo.go index 70139d34..95996736 100644 --- a/sysinfo/sysinfo.go +++ b/sysinfo/sysinfo.go @@ -116,8 +116,6 @@ func NewGPUs() (gpus GPUs) { } func InFlatpak() bool { - if _, err := os.Stat("/.flatpak-info"); err == nil { - return true - } - return false + _, err := os.Stat("/.flatpak-info") + return err == nil } From 5c48e4a28cd09c00d05b8c0ac4ce67c44df0391c Mon Sep 17 00:00:00 2001 From: sewn Date: Wed, 18 Oct 2023 20:14:24 +0300 Subject: [PATCH 47/55] initial sysinfo support --- cmd/vinegar/vinegar.go | 40 +++++++++++++++++++++++++---- sysinfo/distro.go | 57 ++++++++++++++++++++++++++++++++++++++++++ sysinfo/sysinfo.go | 18 +++++++------ 3 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 sysinfo/distro.go diff --git a/cmd/vinegar/vinegar.go b/cmd/vinegar/vinegar.go index 32cf2e23..11dc1b07 100644 --- a/cmd/vinegar/vinegar.go +++ b/cmd/vinegar/vinegar.go @@ -7,12 +7,14 @@ import ( "log" "os" "path/filepath" + "slices" "github.com/vinegarhq/vinegar/internal/config" "github.com/vinegarhq/vinegar/internal/config/editor" "github.com/vinegarhq/vinegar/internal/config/state" "github.com/vinegarhq/vinegar/internal/dirs" "github.com/vinegarhq/vinegar/internal/logs" + "github.com/vinegarhq/vinegar/sysinfo" "github.com/vinegarhq/vinegar/roblox" "github.com/vinegarhq/vinegar/wine" ) @@ -22,7 +24,7 @@ var BinPrefix string func usage() { fmt.Fprintln(os.Stderr, "usage: vinegar [-config filepath] player|studio [args...]") fmt.Fprintln(os.Stderr, "usage: vinegar [-config filepath] exec prog [args...]") - fmt.Fprintln(os.Stderr, " vinegar [-config filepath] edit|kill|uninstall|delete|install-webview2|winetricks|reportinfo") + fmt.Fprintln(os.Stderr, " vinegar [-config filepath] edit|kill|uninstall|delete|install-webview2|winetricks|sysinfo") os.Exit(1) } @@ -47,7 +49,7 @@ func main() { } // These commands (except player & studio) don't require a configuration, // but they require a wineprefix, hence wineroot of configuration is required. - case "player", "studio", "exec", "kill", "install-webview2", "winetricks": + case "sysinfo", "player", "studio", "exec", "kill", "install-webview2", "winetricks": cfg, err := config.Load(*configPath) if err != nil { log.Fatal(err) @@ -61,14 +63,17 @@ func main() { } switch cmd { - case "reportinfo": - // TODO: implement this function + case "sysinfo": + Sysinfo(&pfx) case "exec": if len(args) < 2 { usage() } - if err := pfx.Wine(args[1], args[2:]...).Run(); err != nil { + cmd := pfx.Wine(args[1], args[2:]...) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + if err := cmd.Run(); err != nil { log.Fatal(err) } case "kill": @@ -183,3 +188,28 @@ func Delete() { log.Fatal(err) } } + +func Sysinfo(pfx *wine.Prefix) { + cpu := sysinfo.GetCPU() + avx := slices.Contains(cpu.Flags, "avx") + k := sysinfo.GetKernel() + d, err := sysinfo.GetDistro() + if err != nil { + log.Fatal(err) + } + + ver, err := pfx.Wine("--version").Output() + if err != nil { + log.Fatal(err) + } + + info := `## System information +* Distro: %s +* Processor: %s + * Supports AVX: %t +* Kernel: %s +* Wine: %s +` + + fmt.Printf(info, d, cpu.Model, avx, k, ver) +} diff --git a/sysinfo/distro.go b/sysinfo/distro.go new file mode 100644 index 00000000..85cf5ec3 --- /dev/null +++ b/sysinfo/distro.go @@ -0,0 +1,57 @@ +package sysinfo + +import ( + "os" + "bufio" + "strings" +) + +type Distro struct { + Name string + Version string +} + +func GetDistro() (Distro, error) { + var d Distro + + f, err := os.Open("/etc/os-release") + if err != nil { + return Distro{}, err + } + defer f.Close() + + s := bufio.NewScanner(f) + + for s.Scan() { + m := strings.SplitN(s.Text(), "=", 2) + if len(m) != 2 { + continue + } + + val := strings.Trim(m[1], "\"") + + switch m[0] { + case "PRETTY_NAME": + d.Name = val + case "VERSION_ID": + d.Version = val + } + } + if err := s.Err(); err != nil { + return Distro{}, err + } + + return d, nil +} + +func (d Distro) String() string { + if d.Name == "" { + d.Name = "Linux" + } + + if d.Version == "" { + d.Version = "Linux" + } + + return d.Name + " " + d.Version +} diff --git a/sysinfo/sysinfo.go b/sysinfo/sysinfo.go index 95996736..ea10f9b3 100644 --- a/sysinfo/sysinfo.go +++ b/sysinfo/sysinfo.go @@ -9,7 +9,6 @@ import ( "syscall" "strings" "path/filepath" - "slices" ) type Kernel struct { @@ -20,7 +19,6 @@ type Kernel struct { type CPU struct { Model string Flags []string - AVX bool } type GPU struct { @@ -32,7 +30,7 @@ type GPU struct { type GPUs []GPU -func NewKernel() Kernel { +func GetKernel() Kernel { var un syscall.Utsname _ = syscall.Uname(&un) @@ -53,7 +51,11 @@ func NewKernel() Kernel { } } -func NewCPU() (cpu CPU) { +func (k Kernel) String() string { + return k.Release + " " + k.Version +} + +func GetCPU() (cpu CPU) { column := regexp.MustCompile("\t+: ") f, _ := os.Open("/proc/cpuinfo") @@ -80,8 +82,6 @@ func NewCPU() (cpu CPU) { } } - cpu.AVX = slices.Contains(cpu.Flags, "avx") - if s.Err() != nil { return } @@ -89,7 +89,11 @@ func NewCPU() (cpu CPU) { return } -func NewGPUs() (gpus GPUs) { +func (cpu *CPU) String() string { + return cpu.Model +} + +func GetGPUs() (gpus GPUs) { card := regexp.MustCompile(`card([0-9]+)(?:-eDP-\d+)?$`) filepath.Walk("/sys/class/drm", func(p string, i fs.FileInfo, err error) error { From 6bc5502d3edda74c4dda00d2dee3fec12235439d Mon Sep 17 00:00:00 2001 From: sewn Date: Wed, 18 Oct 2023 20:19:06 +0300 Subject: [PATCH 48/55] ci: update build golang version --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0ebefa1e..33ed19e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,10 @@ jobs: build: runs-on: ubuntu-latest steps: + - name: 'Setup Go' + uses: actions/setup-go@v4 + with: + go-version: '^1.20' - name: 'Checkout Repository' uses: actions/checkout@v3 - name: 'Setup Go' From 24fb1978c525d46de19b061d5dd0156f8c78908b Mon Sep 17 00:00:00 2001 From: sewn Date: Fri, 20 Oct 2023 20:58:15 +0300 Subject: [PATCH 49/55] sysinfo: better gpu (card) handling --- sysinfo/gpu.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++ sysinfo/sysinfo.go | 38 ------------------------------- 2 files changed, 57 insertions(+), 38 deletions(-) create mode 100644 sysinfo/gpu.go diff --git a/sysinfo/gpu.go b/sysinfo/gpu.go new file mode 100644 index 00000000..d3b0d745 --- /dev/null +++ b/sysinfo/gpu.go @@ -0,0 +1,57 @@ +package sysinfo + +import ( + "os" + "strings" + "path" + "path/filepath" +) + +// ANY error here will be ignored. All of the filepath querying +// and such is done within /sys/, which is stored in memory. + +type Card struct { + Path string + Driver string + Index int + Embedded bool +} + +const drmPath = "/sys/class/drm" +var embeddedDisplays = []string{"eDP", "LVDS", "DP-2"} + +func Cards() (cards []Card) { + drmCards, _ := filepath.Glob(path.Join(drmPath, "card[0-9]")) + + for i, c := range drmCards { + d, _ := filepath.EvalSymlinks(path.Join(c, "device/driver")) + d = path.Base(d) + + cards = append(cards, Card{ + Path: c, + Driver: d, + Index: i, + Embedded: embedded(c), + }) + } + + return +} + +func embedded(cardPath string) (embed bool) { + filepath.Walk(drmPath, func(p string, f os.FileInfo, err error) error { + if !strings.HasPrefix(p, cardPath) { + return nil + } + + for _, hwd := range embeddedDisplays { + if strings.Contains(p, hwd) { + embed = true + } + } + + return nil + }) + + return +} diff --git a/sysinfo/sysinfo.go b/sysinfo/sysinfo.go index ea10f9b3..7197ce42 100644 --- a/sysinfo/sysinfo.go +++ b/sysinfo/sysinfo.go @@ -2,13 +2,10 @@ package sysinfo import ( "os" - "io/fs" - "strconv" "bufio" "regexp" "syscall" "strings" - "path/filepath" ) type Kernel struct { @@ -21,15 +18,6 @@ type CPU struct { Flags []string } -type GPU struct { - Path string - Integrated bool - Index int - Driver string -} - -type GPUs []GPU - func GetKernel() Kernel { var un syscall.Utsname _ = syscall.Uname(&un) @@ -93,32 +81,6 @@ func (cpu *CPU) String() string { return cpu.Model } -func GetGPUs() (gpus GPUs) { - card := regexp.MustCompile(`card([0-9]+)(?:-eDP-\d+)?$`) - - filepath.Walk("/sys/class/drm", func(p string, i fs.FileInfo, err error) error { - var gpu GPU - - match := card.FindStringSubmatch(p) - if match == nil { - return nil - } - - if len(match) == 2 { - gpu.Integrated = true - } - - gpu.Index, _ = strconv.Atoi(match[1]) - gpu.Driver, _ = filepath.EvalSymlinks(filepath.Join(p, "device/driver")) - gpu.Path = p - - gpus = append(gpus, gpu) - return nil - }) - - return -} - func InFlatpak() bool { _, err := os.Stat("/.flatpak-info") return err == nil From edc6798d4717ba8300292a581e0192c5b0b79575 Mon Sep 17 00:00:00 2001 From: sewn Date: Mon, 23 Oct 2023 22:55:27 +0300 Subject: [PATCH 50/55] sysinfo: globalize --- sysinfo/{gpu.go => card.go} | 14 +++--- sysinfo/cpu.go | 32 +++++++++++++ sysinfo/distro.go | 30 +++++++------ sysinfo/kernel.go | 36 +++++++++++++++ sysinfo/sysinfo.go | 90 ++++++------------------------------- 5 files changed, 104 insertions(+), 98 deletions(-) rename sysinfo/{gpu.go => card.go} (79%) create mode 100644 sysinfo/cpu.go create mode 100644 sysinfo/kernel.go diff --git a/sysinfo/gpu.go b/sysinfo/card.go similarity index 79% rename from sysinfo/gpu.go rename to sysinfo/card.go index d3b0d745..92615686 100644 --- a/sysinfo/gpu.go +++ b/sysinfo/card.go @@ -1,16 +1,15 @@ +//go:build linux && amd64 + package sysinfo import ( "os" - "strings" "path" "path/filepath" + "strings" ) -// ANY error here will be ignored. All of the filepath querying -// and such is done within /sys/, which is stored in memory. - -type Card struct { +type card struct { Path string Driver string Index int @@ -18,16 +17,17 @@ type Card struct { } const drmPath = "/sys/class/drm" + var embeddedDisplays = []string{"eDP", "LVDS", "DP-2"} -func Cards() (cards []Card) { +func getCards() (cs []card) { drmCards, _ := filepath.Glob(path.Join(drmPath, "card[0-9]")) for i, c := range drmCards { d, _ := filepath.EvalSymlinks(path.Join(c, "device/driver")) d = path.Base(d) - cards = append(cards, Card{ + cs = append(cs, card{ Path: c, Driver: d, Index: i, diff --git a/sysinfo/cpu.go b/sysinfo/cpu.go new file mode 100644 index 00000000..f8c5fc20 --- /dev/null +++ b/sysinfo/cpu.go @@ -0,0 +1,32 @@ +//go:build linux + +package sysinfo + +import ( + "bufio" + "os" + "regexp" +) + +func cpuModel() string { + column := regexp.MustCompile("\t+: ") + + f, _ := os.Open("/proc/cpuinfo") + defer f.Close() + + s := bufio.NewScanner(f) + + for s.Scan() { + sl := column.Split(s.Text(), 2) + if sl == nil { + continue + } + + // pfft, who needs multiple cpus? just return if we got all we need + if sl[0] == "model name" { + return sl[1] + } + } + + return "unknown cpu" +} diff --git a/sysinfo/distro.go b/sysinfo/distro.go index 85cf5ec3..46e036c5 100644 --- a/sysinfo/distro.go +++ b/sysinfo/distro.go @@ -1,27 +1,32 @@ +//go:build linux + package sysinfo import ( - "os" "bufio" + "os" "strings" ) -type Distro struct { - Name string +type distro struct { + Name string Version string } -func GetDistro() (Distro, error) { - var d Distro +func getDistro() (d distro) { + d = distro{ + Name: "unknown distro name", + Version: "unknown distro version", + } f, err := os.Open("/etc/os-release") if err != nil { - return Distro{}, err + return } defer f.Close() s := bufio.NewScanner(f) - + for s.Scan() { m := strings.SplitN(s.Text(), "=", 2) if len(m) != 2 { @@ -29,7 +34,7 @@ func GetDistro() (Distro, error) { } val := strings.Trim(m[1], "\"") - + switch m[0] { case "PRETTY_NAME": d.Name = val @@ -37,14 +42,11 @@ func GetDistro() (Distro, error) { d.Version = val } } - if err := s.Err(); err != nil { - return Distro{}, err - } - - return d, nil + + return } -func (d Distro) String() string { +func (d distro) String() string { if d.Name == "" { d.Name = "Linux" } diff --git a/sysinfo/kernel.go b/sysinfo/kernel.go new file mode 100644 index 00000000..ec56596e --- /dev/null +++ b/sysinfo/kernel.go @@ -0,0 +1,36 @@ +package sysinfo + +import ( + "strings" + "syscall" +) + +type kernel struct { + Release string + Version string +} + +func getKernel() kernel { + var un syscall.Utsname + _ = syscall.Uname(&un) + + return kernel{ + Release: unameString(un.Release), + Version: unameString(un.Version), + } +} + +func (k kernel) String() string { + return k.Release + " " + k.Version +} + +func unameString(unarr [65]int8) string { + var sb strings.Builder + for _, b := range unarr[:] { + if b == 0 { + break + } + sb.WriteByte(byte(b)) + } + return sb.String() +} diff --git a/sysinfo/sysinfo.go b/sysinfo/sysinfo.go index 7197ce42..b4b91ea1 100644 --- a/sysinfo/sysinfo.go +++ b/sysinfo/sysinfo.go @@ -2,86 +2,22 @@ package sysinfo import ( "os" - "bufio" - "regexp" - "syscall" - "strings" ) -type Kernel struct { - Release string - Version string -} - -type CPU struct { - Model string - Flags []string -} - -func GetKernel() Kernel { - var un syscall.Utsname - _ = syscall.Uname(&un) - - unameString := func(unarr [65]int8) string { - var sb strings.Builder - for _, b := range unarr[:] { - if b == 0 { - break - } - sb.WriteByte(byte(b)) - } - return sb.String() - } - - return Kernel{ - Release: unameString(un.Release), - Version: unameString(un.Version), - } -} - -func (k Kernel) String() string { - return k.Release + " " + k.Version -} - -func GetCPU() (cpu CPU) { - column := regexp.MustCompile("\t+: ") - - f, _ := os.Open("/proc/cpuinfo") - defer f.Close() - - s := bufio.NewScanner(f) - - for s.Scan() { - sl := column.Split(s.Text(), 2) - if sl == nil { - continue - } - - // pfft, who needs multiple cpus? just return if we got all we need - if cpu.Model != "" && cpu.Flags != nil { - break - } - - switch sl[0] { - case "model name": - cpu.Model = sl[1] - case "flags": - cpu.Flags = strings.Split(sl[1], " ") - } - } - - if s.Err() != nil { - return - } - - return -} +var ( + Kernel kernel + CPU string + Cards []card + Distro distro + InFlatpak bool +) -func (cpu *CPU) String() string { - return cpu.Model -} +func init() { + Kernel = getKernel() + CPU = cpuModel() + Cards = getCards() + Distro = getDistro() -func InFlatpak() bool { _, err := os.Stat("/.flatpak-info") - return err == nil + InFlatpak = err == nil } From 8f3c7156f8027c259fbf24f20191010d63ad5c99 Mon Sep 17 00:00:00 2001 From: sewn Date: Mon, 23 Oct 2023 23:05:12 +0300 Subject: [PATCH 51/55] cmd: drop stderr/out setting in Wine --- cmd/vinegar/vinegar.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cmd/vinegar/vinegar.go b/cmd/vinegar/vinegar.go index 11dc1b07..294ad7a8 100644 --- a/cmd/vinegar/vinegar.go +++ b/cmd/vinegar/vinegar.go @@ -70,10 +70,7 @@ func main() { usage() } - cmd := pfx.Wine(args[1], args[2:]...) - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - if err := cmd.Run(); err != nil { + if err := pfx.Wine(args[1], args[2:]...).Run(); err != nil { log.Fatal(err) } case "kill": From 84c323662a8780a291124ab91ade0a90b326448c Mon Sep 17 00:00:00 2001 From: sewn Date: Tue, 24 Oct 2023 09:19:54 +0300 Subject: [PATCH 52/55] cmd/vinegar: wait for roblox to die in procfs --- cmd/vinegar/binary.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index acae7850..e93373ab 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -5,7 +5,9 @@ import ( "fmt" "io" "log" + "strconv" "os" + "time" "os/exec" "os/signal" "path/filepath" @@ -116,6 +118,19 @@ func (b *Binary) Run(args ...string) error { b.Splash.Message("Launching " + b.Alias) defer func() { + for { + time.Sleep(100 * time.Millisecond) + + // This is because there may be a race condition between the process + // procfs depletion and the proccess getting killed. + // CommFound walks over procfs, so here ensure that the process no longer + // exists in procfs. + _, err := os.Stat(filepath.Join("/proc", strconv.Itoa(cmd.Process.Pid))) + if err != nil { + break + } + } + if util.CommFound("Roblox") { log.Println("Another Roblox instance is already running, not killing wineprefix") return From 7b92ea0a5d9636d0b60f0b3bbcfa95a9233eb99b Mon Sep 17 00:00:00 2001 From: sewn Date: Tue, 24 Oct 2023 09:26:19 +0300 Subject: [PATCH 53/55] sysinfo --- cmd/vinegar/binary.go | 6 +++--- cmd/vinegar/vinegar.go | 17 +++++------------ wine/cmd.go | 6 +++--- wine/prefix.go | 3 +-- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index e93373ab..436fcef4 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -5,14 +5,14 @@ import ( "fmt" "io" "log" - "strconv" "os" - "time" "os/exec" "os/signal" "path/filepath" + "strconv" "strings" "syscall" + "time" bsrpc "github.com/vinegarhq/vinegar/bloxstraprpc" "github.com/vinegarhq/vinegar/internal/config" @@ -135,7 +135,7 @@ func (b *Binary) Run(args ...string) error { log.Println("Another Roblox instance is already running, not killing wineprefix") return } - + if b.Config.AutoKillPrefix { b.Prefix.Kill() } diff --git a/cmd/vinegar/vinegar.go b/cmd/vinegar/vinegar.go index 294ad7a8..f0c21c9e 100644 --- a/cmd/vinegar/vinegar.go +++ b/cmd/vinegar/vinegar.go @@ -7,15 +7,14 @@ import ( "log" "os" "path/filepath" - "slices" "github.com/vinegarhq/vinegar/internal/config" "github.com/vinegarhq/vinegar/internal/config/editor" "github.com/vinegarhq/vinegar/internal/config/state" "github.com/vinegarhq/vinegar/internal/dirs" "github.com/vinegarhq/vinegar/internal/logs" - "github.com/vinegarhq/vinegar/sysinfo" "github.com/vinegarhq/vinegar/roblox" + "github.com/vinegarhq/vinegar/sysinfo" "github.com/vinegarhq/vinegar/wine" ) @@ -187,15 +186,9 @@ func Delete() { } func Sysinfo(pfx *wine.Prefix) { - cpu := sysinfo.GetCPU() - avx := slices.Contains(cpu.Flags, "avx") - k := sysinfo.GetKernel() - d, err := sysinfo.GetDistro() - if err != nil { - log.Fatal(err) - } - - ver, err := pfx.Wine("--version").Output() + cmd := pfx.Wine("--version") + cmd.Stdout = nil // required for Output() + ver, err := cmd.Output() if err != nil { log.Fatal(err) } @@ -208,5 +201,5 @@ func Sysinfo(pfx *wine.Prefix) { * Wine: %s ` - fmt.Printf(info, d, cpu.Model, avx, k, ver) + fmt.Printf(info, sysinfo.Distro, sysinfo.CPU, sysinfo.HasAVX, sysinfo.Kernel, ver) } diff --git a/wine/cmd.go b/wine/cmd.go index 7f971b42..ec77cc9a 100644 --- a/wine/cmd.go +++ b/wine/cmd.go @@ -2,9 +2,9 @@ package wine import ( "errors" - "os" "io" "log" + "os" "os/exec" ) @@ -31,7 +31,7 @@ func (p *Prefix) Command(name string, arg ...string) *Cmd { } return &Cmd{ - Cmd: cmd, + Cmd: cmd, prefixDir: p.dir, } } @@ -50,7 +50,7 @@ func (c *Cmd) OutputPipe() (io.Reader, error) { if c.Process != nil { return nil, errors.New("OutputPipe after process started") } - + c.SetOutput(nil) e, err := c.StderrPipe() diff --git a/wine/prefix.go b/wine/prefix.go index d584cb3c..1e9ad534 100644 --- a/wine/prefix.go +++ b/wine/prefix.go @@ -13,11 +13,10 @@ type Prefix struct { dir string } - func New(dir string, out io.Writer) Prefix { return Prefix{ Output: out, - dir: dir, + dir: dir, } } From c4d4385db58a0710ba78636fc6aad585b7e820a6 Mon Sep 17 00:00:00 2001 From: sewn Date: Tue, 24 Oct 2023 09:28:58 +0300 Subject: [PATCH 54/55] sysinfo: amd64 and HasAVX --- sysinfo/sysinfo.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sysinfo/sysinfo.go b/sysinfo/sysinfo.go index b4b91ea1..7934cedd 100644 --- a/sysinfo/sysinfo.go +++ b/sysinfo/sysinfo.go @@ -1,7 +1,10 @@ +//go:build amd64 package sysinfo import ( "os" + + "golang.org/x/sys/cpu" ) var ( @@ -9,6 +12,7 @@ var ( CPU string Cards []card Distro distro + HasAVX = cpu.X86.HasAVX InFlatpak bool ) From 8b061c5949c122c73455c1f4b76c4980c306324c Mon Sep 17 00:00:00 2001 From: sewn Date: Tue, 24 Oct 2023 10:05:32 +0300 Subject: [PATCH 55/55] cmd/vinegar: handle flatpak in sysinfo & extreme wine failure cases --- cmd/vinegar/binary.go | 16 +++++++++++++--- cmd/vinegar/vinegar.go | 6 ++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/cmd/vinegar/binary.go b/cmd/vinegar/binary.go index 436fcef4..39ff3091 100644 --- a/cmd/vinegar/binary.go +++ b/cmd/vinegar/binary.go @@ -104,9 +104,14 @@ func (b *Binary) Run(args ...string) error { signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c - log.Println("Killing Roblox") - // This way, cmd.Run() will return and the wineprefix killer will be ran. - cmd.Process.Kill() + + // Only kill the process if it even had a PID + if cmd.Process != nil { + log.Println("Killing Roblox") + // This way, cmd.Run() will return and the wineprefix killer will be ran. + cmd.Process.Kill() + } + // Don't handle INT after it was recieved, this way if another signal was sent, // Vinegar will immediately exit. signal.Stop(c) @@ -118,6 +123,11 @@ func (b *Binary) Run(args ...string) error { b.Splash.Message("Launching " + b.Alias) defer func() { + // Don't do anything if the process even ran correctly. + if cmd.Process == nil { + return + } + for { time.Sleep(100 * time.Millisecond) diff --git a/cmd/vinegar/vinegar.go b/cmd/vinegar/vinegar.go index f0c21c9e..d3693c02 100644 --- a/cmd/vinegar/vinegar.go +++ b/cmd/vinegar/vinegar.go @@ -198,8 +198,10 @@ func Sysinfo(pfx *wine.Prefix) { * Processor: %s * Supports AVX: %t * Kernel: %s -* Wine: %s -` +* Wine: %s` fmt.Printf(info, sysinfo.Distro, sysinfo.CPU, sysinfo.HasAVX, sysinfo.Kernel, ver) + if sysinfo.InFlatpak { + fmt.Println("* Flatpak: [x]") + } }