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:
parent
bf47afc32b
commit
4181708709
@ -10,6 +10,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/google/shlex"
|
"github.com/google/shlex"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
@ -109,6 +110,21 @@ func (ce *CommandExecutor) cmdBlenderRenderCommand(
|
|||||||
return nil, err
|
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 := make([]string, 0)
|
||||||
cliArgs = append(cliArgs, parameters.argsBefore...)
|
cliArgs = append(cliArgs, parameters.argsBefore...)
|
||||||
cliArgs = append(cliArgs, parameters.blendfile)
|
cliArgs = append(cliArgs, parameters.blendfile)
|
||||||
|
11
internal/worker/command_blender_nonwindows.go
Normal file
11
internal/worker/command_blender_nonwindows.go
Normal 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")
|
||||||
|
}
|
119
internal/worker/command_blender_windows.go
Normal file
119
internal/worker/command_blender_windows.go
Normal 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
|
21
internal/worker/command_blender_windows_test.go
Normal file
21
internal/worker/command_blender_windows_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user