flamenco/internal/find_blender/find_blender.go
Sybren A. Stüvel 161a7f7cb3 Less dramatic logging when Blender cannot be found
Avoid the word "error" in logging when Blender cannot be found. Typically
these are warnings, and having the word "error" there makes people think
otherwise.
2022-09-22 12:37:46 +02:00

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("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)
}