
For some reason, calling `AssocQueryStringW` on Windows Home returns error code 122, "The data area passed to a system call is too small", even when the data area is large enough. Furthermore, the API actually describes that in such cases `S_FALSE` is supposed to be returned, with `*pcchOut` set to the required size. Because of this apparent violation of the documentation, and because it just works, Flamenco now ignores this particular error and just returns the obtained string.
153 lines
4.9 KiB
Go
153 lines
4.9 KiB
Go
package find_blender
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.blender.org/flamenco/pkg/api"
|
|
"git.blender.org/flamenco/pkg/crosspath"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
var (
|
|
ErrNotAvailable = errors.New("not available on this platform")
|
|
ErrAssociationNotFound = errors.New("no program is associated with .blend files")
|
|
ErrNotBlender = errors.New("not a Blender executable")
|
|
ErrTimedOut = errors.New("version check took too long")
|
|
)
|
|
|
|
// blenderVersionTimeout is how long `blender --version` is allowed to take,
|
|
// before timing out. This can be much slower than expected, when loading
|
|
// Blender from shared storage on a not-so-fast NAS.
|
|
const blenderVersionTimeout = 10 * time.Second
|
|
|
|
type CheckBlenderResult struct {
|
|
Input string // What was the original 'exename' CheckBlender was told to find.
|
|
FoundLocation string
|
|
BlenderVersion string
|
|
Source api.BlenderPathSource
|
|
}
|
|
|
|
// Find returns the path of a `blender` executable,
|
|
// If there is one associated with .blend files, and the current platform is
|
|
// supported to query those, that one is used. Otherwise $PATH is searched.
|
|
func Find(ctx context.Context) (CheckBlenderResult, error) {
|
|
return CheckBlender(ctx, "")
|
|
}
|
|
|
|
// FileAssociation returns the full path of a Blender executable, by inspecting file association with .blend files.
|
|
// `ErrNotAvailable` is returned if no "blender finder" is available for the current platform.
|
|
func FileAssociation() (string, error) {
|
|
// findBlender() is implemented in one of the platform-dependent files.
|
|
return fileAssociation()
|
|
}
|
|
|
|
func CheckBlender(ctx context.Context, exename string) (CheckBlenderResult, error) {
|
|
if exename == "" {
|
|
// exename is not given, see if we can use .blend file association.
|
|
fullPath, err := fileAssociation()
|
|
switch {
|
|
case errors.Is(err, ErrNotAvailable):
|
|
// Association finder not available, act as if "blender" was given as exename.
|
|
return CheckBlender(ctx, "blender")
|
|
case err != nil:
|
|
// Some other error occurred, better to report it.
|
|
return CheckBlenderResult{}, fmt.Errorf("error finding .blend file association: %w", err)
|
|
default:
|
|
// The full path was found, report the Blender version.
|
|
return getResultWithVersion(ctx, exename, fullPath, api.BlenderPathSourceFileAssociation)
|
|
}
|
|
}
|
|
|
|
if crosspath.Dir(exename) != "." {
|
|
// exename is some form of path, see if it works for us.
|
|
return checkBlenderAtPath(ctx, exename)
|
|
}
|
|
|
|
// Try to find exename on $PATH
|
|
fullPath, err := exec.LookPath(exename)
|
|
if err != nil {
|
|
return CheckBlenderResult{}, err
|
|
}
|
|
return getResultWithVersion(ctx, exename, fullPath, api.BlenderPathSourcePathEnvvar)
|
|
}
|
|
|
|
func checkBlenderAtPath(ctx context.Context, path string) (CheckBlenderResult, error) {
|
|
stat, err := os.Stat(path)
|
|
if err != nil {
|
|
return CheckBlenderResult{}, err
|
|
}
|
|
if !stat.IsDir() {
|
|
// Simple case, it's not a directory so let's just try and execute it.
|
|
return getResultWithVersion(ctx, path, path, api.BlenderPathSourceInputPath)
|
|
}
|
|
|
|
// Try appending the Blender executable name.
|
|
log.Debug().
|
|
Str("path", path).
|
|
Str("executable", blenderExeName).
|
|
Msg("blender finder: given path is directory, going to find Blender executable")
|
|
exepath := filepath.Join(path, blenderExeName)
|
|
return getResultWithVersion(ctx, path, exepath, api.BlenderPathSourceInputPath)
|
|
}
|
|
|
|
// getResultWithVersion tries to run the command to get Blender's version.
|
|
// The result is returned as a `CheckBlenderResult` struct.
|
|
func getResultWithVersion(
|
|
ctx context.Context,
|
|
input,
|
|
commandline string,
|
|
source api.BlenderPathSource,
|
|
) (CheckBlenderResult, error) {
|
|
result := CheckBlenderResult{
|
|
Input: input,
|
|
FoundLocation: commandline,
|
|
Source: source,
|
|
}
|
|
|
|
version, err := getBlenderVersion(ctx, commandline)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
result.BlenderVersion = version
|
|
return result, nil
|
|
}
|
|
|
|
func getBlenderVersion(ctx context.Context, commandline string) (string, error) {
|
|
logger := log.With().Str("commandline", commandline).Logger()
|
|
|
|
// Make sure that command execution doesn't hang indefinitely.
|
|
cmdCtx, cmdCtxCancel := context.WithTimeout(ctx, blenderVersionTimeout)
|
|
defer cmdCtxCancel()
|
|
|
|
cmd := exec.CommandContext(cmdCtx, commandline, "--version")
|
|
stdoutStderr, err := cmd.CombinedOutput()
|
|
switch {
|
|
case errors.Is(cmdCtx.Err(), context.DeadlineExceeded):
|
|
logger.Warn().Stringer("timeout", blenderVersionTimeout).Msg("command timed out")
|
|
return "", fmt.Errorf("%s: %w", commandline, ErrTimedOut)
|
|
case err != nil:
|
|
logger.Info().Err(err).Str("output", string(stdoutStderr)).Msg("error running command")
|
|
return "", err
|
|
}
|
|
|
|
version := string(stdoutStderr)
|
|
lines := strings.Split(version, "\n")
|
|
for idx := range lines {
|
|
line := strings.TrimSpace(lines[idx])
|
|
if strings.HasPrefix(line, "Blender ") {
|
|
return line, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("%s: %w", commandline, ErrNotBlender)
|
|
}
|