Worker: automatically find Blender on Windows via file association

On Windows, when the Manager doesn't explicitly point at a Blender to use,
use the the application associated with `.blend` files instead.
This commit is contained in:
Sybren A. Stüvel 2022-04-09 15:05:34 +02:00
parent bf47afc32b
commit 4181708709
4 changed files with 167 additions and 0 deletions

View File

@ -10,6 +10,7 @@ import (
"fmt"
"io"
"os/exec"
"path/filepath"
"github.com/google/shlex"
"github.com/rs/zerolog"
@ -109,6 +110,21 @@ func (ce *CommandExecutor) cmdBlenderRenderCommand(
return nil, err
}
if filepath.Dir(parameters.exe) == "." {
// No directory path given. Check that the executable can be found on the
// path.
if _, err := exec.LookPath(parameters.exe); err != nil {
// Attempt a platform-specific way to find which Blender executable to
// use. If Blender cannot not be found, just use the configured command
// and let the OS produce the errors.
path, err := FindBlender()
if err == nil {
logger.Info().Str("path", path).Msg("found Blender")
parameters.exe = path
}
}
}
cliArgs := make([]string, 0)
cliArgs = append(cliArgs, parameters.argsBefore...)
cliArgs = append(cliArgs, parameters.blendfile)

View File

@ -0,0 +1,11 @@
//go:build !windows
// SPDX-License-Identifier: GPL-3.0-or-later
package worker
import "errors"
// FindBlender returns an error, as the file association lookup is only available on Windows.
func FindBlender() (string, error) {
return "", errors.New("file association lookup is only available on Windows")
}

View File

@ -0,0 +1,119 @@
//go:build windows
// SPDX-License-Identifier: GPL-3.0-or-later
package worker
import (
"fmt"
"syscall"
"unsafe"
)
// FindBlender returns the path of `blender.exe` associated with ".blend" files.
func FindBlender() (string, error) {
// Load library.
libname := "shlwapi.dll"
libshlwapi, err := syscall.LoadLibrary(libname)
if err != nil {
return "", fmt.Errorf("loading %s: %w", libname, err)
}
defer func() { _ = syscall.FreeLibrary(libshlwapi) }()
// Find function.
funcname := "AssocQueryStringW"
assocQueryString, err := syscall.GetProcAddress(libshlwapi, funcname)
if err != nil {
return "", fmt.Errorf("finding function %s in %s: %w", funcname, libname, err)
}
// https://docs.microsoft.com/en-gb/windows/win32/api/shlwapi/nf-shlwapi-assocquerystringw
pszAssoc, err := syscall.UTF16PtrFromString(".blend")
if err != nil {
return "", fmt.Errorf("converting string to UTF16: %w", err)
}
pszExtra, err := syscall.UTF16PtrFromString("open")
if err != nil {
return "", fmt.Errorf("converting string to UTF16: %w", err)
}
var cchOut uint32 = 65535
buf := make([]uint16, cchOut)
pszOut := unsafe.Pointer(&buf[0])
result1, _, err := syscall.SyscallN(
assocQueryString,
uintptr(ASSOCF_INIT_DEFAULTTOSTAR), // [in] ASSOCF flags
uintptr(ASSOCSTR_EXECUTABLE), // [in] ASSOCSTR str
uintptr(unsafe.Pointer(pszAssoc)), // [in] LPCWSTR pszAssoc
uintptr(unsafe.Pointer(pszExtra)), // [in, optional] LPCWSTR pszExtra
uintptr(pszOut), // [out, optional] LPWSTR pszOut
uintptr(unsafe.Pointer(&cchOut)), // [in, out] DWORD *pcchOut
)
if err != nil {
// This can be a syscall.Errno with code 0, indicating things are actually fine.
if errno, ok := err.(syscall.Errno); ok && errno == 0 {
// So this is fine.
} else {
return "", fmt.Errorf("error calling AssocQueryStringW from shlwapi.dll: %w", err)
}
}
if result1 != 0 {
return "", fmt.Errorf("unknown result %d calling AssocQueryStringW from shlwapi.dll: %w", result1, err)
}
exe := syscall.UTF16ToString(buf)
return exe, nil
}
// Source: https://docs.microsoft.com/en-us/windows/win32/shell/assocf_str
const (
ASSOCF_NONE = ASSOCF(0x00000000)
ASSOCF_INIT_NOREMAPCLSID = ASSOCF(0x00000001)
ASSOCF_INIT_BYEXENAME = ASSOCF(0x00000002)
ASSOCF_OPEN_BYEXENAME = ASSOCF(0x00000002)
ASSOCF_INIT_DEFAULTTOSTAR = ASSOCF(0x00000004)
ASSOCF_INIT_DEFAULTTOFOLDER = ASSOCF(0x00000008)
ASSOCF_NOUSERSETTINGS = ASSOCF(0x00000010)
ASSOCF_NOTRUNCATE = ASSOCF(0x00000020)
ASSOCF_VERIFY = ASSOCF(0x00000040)
ASSOCF_REMAPRUNDLL = ASSOCF(0x00000080)
ASSOCF_NOFIXUPS = ASSOCF(0x00000100)
ASSOCF_IGNOREBASECLASS = ASSOCF(0x00000200)
ASSOCF_INIT_IGNOREUNKNOWN = ASSOCF(0x00000400)
ASSOCF_INIT_FIXED_PROGID = ASSOCF(0x00000800)
ASSOCF_IS_PROTOCOL = ASSOCF(0x00001000)
ASSOCF_INIT_FOR_FILE = ASSOCF(0x00002000)
)
type ASSOCF uint32
// Source: https://docs.microsoft.com/en-us/windows/win32/api/shlwapi/ne-shlwapi-assocstr
const (
ASSOCSTR_COMMAND ASSOCSTR = iota + 1
ASSOCSTR_EXECUTABLE
ASSOCSTR_FRIENDLYDOCNAME
ASSOCSTR_FRIENDLYAPPNAME
ASSOCSTR_NOOPEN
ASSOCSTR_SHELLNEWVALUE
ASSOCSTR_DDECOMMAND
ASSOCSTR_DDEIFEXEC
ASSOCSTR_DDEAPPLICATION
ASSOCSTR_DDETOPIC
ASSOCSTR_INFOTIP
ASSOCSTR_QUICKTIP
ASSOCSTR_TILEINFO
ASSOCSTR_CONTENTTYPE
ASSOCSTR_DEFAULTICON
ASSOCSTR_SHELLEXTENSION
ASSOCSTR_DROPTARGET
ASSOCSTR_DELEGATEEXECUTE
ASSOCSTR_SUPPORTED_URI_PROTOCOLS
ASSOCSTR_PROGID
ASSOCSTR_APPID
ASSOCSTR_APPPUBLISHER
ASSOCSTR_APPICONREFERENCE
ASSOCSTR_MAX
)
type ASSOCSTR uint32

View File

@ -0,0 +1,21 @@
//go:build windows
// SPDX-License-Identifier: GPL-3.0-or-later
package worker
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestFindBlender is a "weak" test, which actually accepts both happy and unhappy flows.
// It would be too fragile to always require a file association to be set up with Blender.
func TestFindBlender(t *testing.T) {
exe, err := FindBlender()
if err == nil {
assert.NotEmpty(t, exe)
} else {
assert.Empty(t, exe)
}
}