Skip to content

Commit

Permalink
Initialize remote servers in the background on load
Browse files Browse the repository at this point in the history
  • Loading branch information
mircearoata committed Feb 14, 2024
1 parent 379417e commit 5f04c00
Show file tree
Hide file tree
Showing 15 changed files with 385 additions and 274 deletions.
6 changes: 3 additions & 3 deletions backend/app/debug_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ func addMetadata(writer *zip.Writer) error {
var selectedMetadataInstall *MetadataInstallation
for _, install := range installs {
metadata := ficsitcli.FicsitCLI.GetInstallationsMetadata()[install]
if metadata == nil {
if metadata.Info == nil {
slog.Warn("failed to get metadata for installation", utils.SlogPath("path", install))
continue
}
i := &MetadataInstallation{
Installation: metadata,
Name: fmt.Sprintf("Satisfactory %s (%s)", metadata.Branch, metadata.Launcher),
Installation: metadata.Info,
Name: fmt.Sprintf("Satisfactory %s (%s)", metadata.Info.Branch, metadata.Info.Branch),
Profile: ficsitcli.FicsitCLI.GetInstallation(install).Profile,
}
i.Path = utils.RedactPath(i.Path)
Expand Down
138 changes: 33 additions & 105 deletions backend/ficsitcli/installs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,142 +5,70 @@ import (
"fmt"
"log/slog"
"os/exec"
"slices"

"github.com/satisfactorymodding/ficsit-cli/cli"
resolver "github.com/satisfactorymodding/ficsit-resolver"

"github.com/satisfactorymodding/SatisfactoryModManager/backend/installfinders"
"github.com/satisfactorymodding/SatisfactoryModManager/backend/installfinders/common"
"github.com/satisfactorymodding/SatisfactoryModManager/backend/utils"
)

func (f *ficsitCLI) initInstallations() error {
err := f.initLocalInstallationsMetadata()
if err != nil {
return fmt.Errorf("failed to initialize found installations: %w", err)
for _, install := range f.ficsitCli.Installations.Installations {
f.installationMetadata.Store(install.Path, installationMetadata{
State: InstallStateUnknown,
Info: nil,
})
}

err = f.initRemoteServerInstallationsMetadata()
err := f.initLocalInstallationsMetadata()
if err != nil {
return fmt.Errorf("failed to initialize remote server installations: %w", err)
}

filteredInstalls := f.GetInstallations()
if len(filteredInstalls) > 0 {
if !slices.Contains(filteredInstalls, f.ficsitCli.Installations.SelectedInstallation) {
f.ficsitCli.Installations.SelectedInstallation = filteredInstalls[0]
_ = f.ficsitCli.Installations.Save()
}
return fmt.Errorf("failed to initialize found installations: %w", err)
}

return nil
}

func (f *ficsitCLI) initLocalInstallationsMetadata() error {
installs, findErrors := installfinders.FindInstallations()

f.installFindErrors = findErrors
f.installationMetadata = make(map[string]*common.Installation)

fallbackProfile := f.GetFallbackProfile()
// This may take a while, so we do it in the background
go f.initRemoteServerInstallationsMetadata()

createdNewInstalls := false
for _, install := range installs {
ficsitCliInstall := f.ficsitCli.Installations.GetInstallation(install.Path)
if ficsitCliInstall == nil {
_, err := f.ficsitCli.Installations.AddInstallation(f.ficsitCli, install.Path, fallbackProfile)
if err != nil {
return fmt.Errorf("failed to add installation: %w", err)
}
createdNewInstalls = true
}
f.installationMetadata[install.Path] = install
}
// Even if the remote server metadata is not yet available, we can still do this
f.ensureSelectedInstallationIsValid()

if createdNewInstalls {
err := f.ficsitCli.Installations.Save()
if err != nil {
return fmt.Errorf("failed to save installations: %w", err)
}
}
return nil
}

func (f *ficsitCLI) initRemoteServerInstallationsMetadata() error {
for _, installation := range f.GetInstallations() {
err := f.checkAndAddExistingRemote(installation)
if err != nil {
slog.Warn("failed to check and add existing remote", slog.Any("error", err), utils.SlogPath("path", installation))
}
}
return nil
}

func isServerTarget(targetName string) bool {
return targetName == "WindowsServer" || targetName == "LinuxServer"
}

func (f *ficsitCLI) checkAndAddExistingRemote(path string) error {
slog.Debug("checking whether installation is remote", utils.SlogPath("path", path))
installation := f.ficsitCli.Installations.GetInstallation(path)
if installation == nil {
return nil
}
if _, ok := f.installationMetadata[installation.Path]; ok {
// Already have metadata for this install
return nil
}
platform, err := installation.GetPlatform(f.ficsitCli)
if err != nil {
// Maybe the server is unreachable at the moment
// We will keep this install for now
slog.Info("failed to get platform", slog.Any("error", err), utils.SlogPath("path", installation.Path))
} else if !isServerTarget(platform.TargetName) {
// Not a server, but a local install, should already have metadata
return nil
}
if err := f.AddRemoteServer(path); err != nil {
slog.Warn("could not connect to remote server, adding placeholder metadata", slog.Any("error", err), utils.SlogPath("path", installation.Path))

f.installationMetadata[path] = &common.Installation{
Path: installation.Path,
Location: common.LocationTypeRemote,
Launcher: f.getNextRemoteLauncherName(),
func (f *ficsitCLI) ensureSelectedInstallationIsValid() {
if !f.isValidInstall(f.ficsitCli.Installations.SelectedInstallation) {
filteredInstalls := f.GetInstallations()
if len(filteredInstalls) > 0 {
f.ficsitCli.Installations.SelectedInstallation = filteredInstalls[0]
_ = f.ficsitCli.Installations.Save()
f.EmitGlobals()
}
}
return nil
}

func (f *ficsitCLI) GetInstallations() []string {
installations := make([]string, 0, len(f.ficsitCli.Installations.Installations))
for _, installation := range f.ficsitCli.Installations.Installations {
// Keep installations that we have metadata for
if _, ok := f.installationMetadata[installation.Path]; !ok {
// Keep installations that are remote servers
// Even if we don't have metadata for them
platform, err := installation.GetPlatform(f.ficsitCli)
if err != nil {
// Maybe the server is unreachable at the moment
// We will keep this install for now
slog.Info("failed to get platform", slog.Any("error", err), utils.SlogPath("path", installation.Path))
} else if !isServerTarget(platform.TargetName) {
// Not a server, but a local install, should already have metadata
continue
}
if !f.isValidInstall(installation.Path) {
continue
}
installations = append(installations, installation.Path)
}
return installations
}

func (f *ficsitCLI) GetInstallationsMetadata() map[string]*common.Installation {
return f.installationMetadata
func (f *ficsitCLI) GetInstallationsMetadata() map[string]installationMetadata {
rawMap := make(map[string]installationMetadata, len(f.ficsitCli.Installations.Installations))
f.installationMetadata.Range(func(key string, value installationMetadata) bool {
rawMap[key] = value
return true
})
return rawMap
}

func (f *ficsitCLI) GetCurrentInstallationMetadata() *common.Installation {
// This function only exists so common.Installation is generated to typescript
return f.installationMetadata[f.ficsitCli.Installations.SelectedInstallation]
func (f *ficsitCLI) GetCurrentInstallationMetadata() installationMetadata {
meta, _ := f.installationMetadata.Load(f.ficsitCli.Installations.SelectedInstallation)
return meta
}

func (f *ficsitCLI) GetInvalidInstalls() []string {
Expand Down Expand Up @@ -294,12 +222,12 @@ func (f *ficsitCLI) LaunchGame() {
slog.Error("no installation selected")
return
}
metadata := f.installationMetadata[selectedInstallation.Path]
if metadata == nil {
metadata, ok := f.installationMetadata.Load(selectedInstallation.Path)
if !ok || metadata.Info == nil {
slog.Error("no metadata for installation")
return
}
cmd := exec.Command(metadata.LaunchPath[0], metadata.LaunchPath[1:]...)
cmd := exec.Command(metadata.Info.LaunchPath[0], metadata.Info.LaunchPath[1:]...)
out, err := cmd.CombinedOutput()
if err != nil {
slog.Error("failed to launch game", slog.Any("error", err), slog.String("cmd", cmd.String()), slog.String("output", string(out)))
Expand Down
192 changes: 192 additions & 0 deletions backend/ficsitcli/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package ficsitcli

import (
"errors"
"fmt"
"log/slog"
"strconv"
"strings"
"sync"

"github.com/satisfactorymodding/ficsit-cli/cli"

"github.com/satisfactorymodding/SatisfactoryModManager/backend/installfinders"
"github.com/satisfactorymodding/SatisfactoryModManager/backend/installfinders/common"
"github.com/satisfactorymodding/SatisfactoryModManager/backend/utils"
)

func (f *ficsitCLI) initLocalInstallationsMetadata() error {
installs, findErrors := installfinders.FindInstallations()

f.installFindErrors = findErrors

fallbackProfile := f.GetFallbackProfile()

createdNewInstalls := false
for _, install := range installs {
ficsitCliInstall := f.ficsitCli.Installations.GetInstallation(install.Path)
if ficsitCliInstall == nil {
_, err := f.ficsitCli.Installations.AddInstallation(f.ficsitCli, install.Path, fallbackProfile)
if err != nil {
return fmt.Errorf("failed to add installation: %w", err)
}
createdNewInstalls = true
}
f.installationMetadata.Store(install.Path, installationMetadata{
State: InstallStateValid,
Info: install,
})
}

if createdNewInstalls {
err := f.ficsitCli.Installations.Save()
if err != nil {
return fmt.Errorf("failed to save installations: %w", err)
}
}
return nil
}

func (f *ficsitCLI) initRemoteServerInstallationsMetadata() {
installationsToCheck := make([]*cli.Installation, 0, len(f.ficsitCli.Installations.Installations))
for _, installation := range f.ficsitCli.Installations.Installations {
if meta, ok := f.installationMetadata.Load(installation.Path); ok {
if meta.State != InstallStateUnknown {
// Already have metadata for this install
continue
}
}
installationsToCheck = append(installationsToCheck, installation)
}

if len(installationsToCheck) == 0 {
return
}

// Mark all installations as loading, since this may take a while

for _, installation := range installationsToCheck {
f.installationMetadata.Store(installation.Path, installationMetadata{
State: InstallStateLoading,
})
}

f.EmitGlobals()

var wg sync.WaitGroup
for _, installation := range installationsToCheck {
wg.Add(1)
go func(installation *cli.Installation) {
defer wg.Done()
f.fetchRemoteInstallationMetadata(installation)
}(installation)
}
wg.Wait()

meta, ok := f.installationMetadata.Load(f.ficsitCli.Installations.SelectedInstallation)
if !ok || meta.State == InstallStateInvalid {
f.ensureSelectedInstallationIsValid()
}

f.EmitGlobals()
}

func (f *ficsitCLI) fetchRemoteInstallationMetadata(installation *cli.Installation) {
defer f.EmitGlobals()
meta, err := f.getRemoteServerMetadata(installation)
if err != nil {
if errors.Is(err, ErrInstallNotServer) {
// If this installation is not a server, it is invalid
f.installationMetadata.Store(installation.Path, installationMetadata{
State: InstallStateInvalid,
})
return
}
// If we failed to get metadata, we will keep this install for now
f.installationMetadata.Store(installation.Path, installationMetadata{
State: InstallStateUnknown,
Info: nil,
})
slog.Warn("failed to get remote server metadata", slog.Any("error", err), utils.SlogPath("path", installation.Path))
return
}

f.installationMetadata.Store(installation.Path, installationMetadata{
State: InstallStateValid,
Info: meta,
})
}

func (f *ficsitCLI) FetchRemoteServerMetadata(path string) error {
installation := f.ficsitCli.Installations.GetInstallation(path)
if installation == nil {
return fmt.Errorf("installation not found")
}
if meta, ok := f.installationMetadata.Load(path); ok && meta.State != InstallStateUnknown {
return nil
}
f.installationMetadata.Store(path, installationMetadata{
State: InstallStateLoading,
})
f.EmitGlobals()
f.fetchRemoteInstallationMetadata(installation)
return nil
}

var ErrInstallNotServer = fmt.Errorf("installation is not a server")

func (f *ficsitCLI) getRemoteServerMetadata(installation *cli.Installation) (*common.Installation, error) {
gameVersion, err := installation.GetGameVersion(f.ficsitCli)
if err != nil {
return nil, fmt.Errorf("failed to get game version: %w", err)
}

platform, err := installation.GetPlatform(f.ficsitCli)
if err != nil {
return nil, fmt.Errorf("failed to get platform: %w", err)
}
var installType common.InstallType
switch platform.TargetName {
case "Windows":
installType = common.InstallTypeWindowsClient
case "WindowsServer":
installType = common.InstallTypeWindowsServer
case "LinuxServer":
installType = common.InstallTypeLinuxServer
}

if installType == common.InstallTypeWindowsClient {
return nil, ErrInstallNotServer
}

branch := common.BranchEarlyAccess // TODO: Do we have a way to detect this for remote installs?

return &common.Installation{
Path: installation.Path,
Type: installType,
Location: common.LocationTypeRemote,
Branch: branch,
Version: gameVersion,
Launcher: f.getNextRemoteLauncherName(),
}, nil
}

func (f *ficsitCLI) getNextRemoteLauncherName() string {
existingNumbers := make(map[int]bool)
for _, install := range f.GetRemoteInstallations() {
metadata, ok := f.installationMetadata.Load(install)
if ok && metadata.Info != nil {
if strings.HasPrefix(metadata.Info.Launcher, "Remote ") {
num, err := strconv.Atoi(strings.TrimPrefix(metadata.Info.Launcher, "Remote "))
if err == nil {
existingNumbers[num] = true
}
}
}
}
for i := 1; ; i++ {
if !existingNumbers[i] {
return "Remote " + strconv.Itoa(i)
}
}
}
Loading

0 comments on commit 5f04c00

Please sign in to comment.