Update .gitignore to include log files and database journal files. Modify go.mod to update dependencies for go-sqlite3 and cloud.google.com/go/compute/metadata. Enhance Makefile to include logging options for manager and runner commands. Introduce new job token handling in auth package and implement database migration scripts. Refactor manager and runner components to improve job processing and metadata extraction. Add support for video preview in frontend components and enhance WebSocket management for channel subscriptions.
This commit is contained in:
87
internal/runner/blender/binary.go
Normal file
87
internal/runner/blender/binary.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Package blender handles Blender binary management and execution.
|
||||
package blender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"jiggablend/internal/runner/api"
|
||||
"jiggablend/internal/runner/workspace"
|
||||
)
|
||||
|
||||
// Manager handles Blender binary downloads and management.
|
||||
type Manager struct {
|
||||
manager *api.ManagerClient
|
||||
workspaceDir string
|
||||
}
|
||||
|
||||
// NewManager creates a new Blender manager.
|
||||
func NewManager(managerClient *api.ManagerClient, workspaceDir string) *Manager {
|
||||
return &Manager{
|
||||
manager: managerClient,
|
||||
workspaceDir: workspaceDir,
|
||||
}
|
||||
}
|
||||
|
||||
// GetBinaryPath returns the path to the Blender binary for a specific version.
|
||||
// Downloads from manager and extracts if not already present.
|
||||
func (m *Manager) GetBinaryPath(version string) (string, error) {
|
||||
blenderDir := filepath.Join(m.workspaceDir, "blender-versions")
|
||||
if err := os.MkdirAll(blenderDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create blender directory: %w", err)
|
||||
}
|
||||
|
||||
// Check if already installed - look for version folder first
|
||||
versionDir := filepath.Join(blenderDir, version)
|
||||
binaryPath := filepath.Join(versionDir, "blender")
|
||||
|
||||
// Check if version folder exists and contains the binary
|
||||
if versionInfo, err := os.Stat(versionDir); err == nil && versionInfo.IsDir() {
|
||||
// Version folder exists, check if binary is present
|
||||
if binaryInfo, err := os.Stat(binaryPath); err == nil {
|
||||
// Verify it's actually a file (not a directory)
|
||||
if !binaryInfo.IsDir() {
|
||||
log.Printf("Found existing Blender %s installation at %s", version, binaryPath)
|
||||
return binaryPath, nil
|
||||
}
|
||||
}
|
||||
// Version folder exists but binary is missing - might be incomplete installation
|
||||
log.Printf("Version folder %s exists but binary not found, will re-download", versionDir)
|
||||
}
|
||||
|
||||
// Download from manager
|
||||
log.Printf("Downloading Blender %s from manager", version)
|
||||
|
||||
reader, err := m.manager.DownloadBlender(version)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// Manager serves pre-decompressed .tar files - extract directly
|
||||
log.Printf("Extracting Blender %s...", version)
|
||||
if err := workspace.ExtractTarStripPrefix(reader, versionDir); err != nil {
|
||||
return "", fmt.Errorf("failed to extract blender: %w", err)
|
||||
}
|
||||
|
||||
// Verify binary exists
|
||||
if _, err := os.Stat(binaryPath); err != nil {
|
||||
return "", fmt.Errorf("blender binary not found after extraction")
|
||||
}
|
||||
|
||||
log.Printf("Blender %s installed at %s", version, binaryPath)
|
||||
return binaryPath, nil
|
||||
}
|
||||
|
||||
// GetBinaryForJob returns the Blender binary path for a job.
|
||||
// Uses the version from metadata or falls back to system blender.
|
||||
func (m *Manager) GetBinaryForJob(version string) (string, error) {
|
||||
if version == "" {
|
||||
return "blender", nil // System blender
|
||||
}
|
||||
|
||||
return m.GetBinaryPath(version)
|
||||
}
|
||||
|
||||
100
internal/runner/blender/logfilter.go
Normal file
100
internal/runner/blender/logfilter.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package blender
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"jiggablend/pkg/types"
|
||||
)
|
||||
|
||||
// FilterLog checks if a Blender log line should be filtered or downgraded.
|
||||
// Returns (shouldFilter, logLevel) - if shouldFilter is true, the log should be skipped.
|
||||
func FilterLog(line string) (shouldFilter bool, logLevel types.LogLevel) {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// Filter out empty lines
|
||||
if trimmed == "" {
|
||||
return true, types.LogLevelInfo
|
||||
}
|
||||
|
||||
// Filter out separator lines
|
||||
if trimmed == "--------------------------------------------------------------------" ||
|
||||
(strings.HasPrefix(trimmed, "-----") && strings.Contains(trimmed, "----")) {
|
||||
return true, types.LogLevelInfo
|
||||
}
|
||||
|
||||
// Filter out trace headers
|
||||
upperLine := strings.ToUpper(trimmed)
|
||||
upperOriginal := strings.ToUpper(line)
|
||||
|
||||
if trimmed == "Trace:" ||
|
||||
trimmed == "Depth Type Name" ||
|
||||
trimmed == "----- ---- ----" ||
|
||||
line == "Depth Type Name" ||
|
||||
line == "----- ---- ----" ||
|
||||
(strings.Contains(upperLine, "DEPTH") && strings.Contains(upperLine, "TYPE") && strings.Contains(upperLine, "NAME")) ||
|
||||
(strings.Contains(upperOriginal, "DEPTH") && strings.Contains(upperOriginal, "TYPE") && strings.Contains(upperOriginal, "NAME")) ||
|
||||
strings.Contains(line, "Depth Type Name") ||
|
||||
strings.Contains(line, "----- ---- ----") ||
|
||||
strings.HasPrefix(trimmed, "-----") ||
|
||||
regexp.MustCompile(`^[-]+\s+[-]+\s+[-]+$`).MatchString(trimmed) {
|
||||
return true, types.LogLevelInfo
|
||||
}
|
||||
|
||||
// Completely filter out dependency graph messages (they're just noise)
|
||||
dependencyGraphPatterns := []string{
|
||||
"Failed to add relation",
|
||||
"Could not find op_from",
|
||||
"OperationKey",
|
||||
"find_node_operation: Failed for",
|
||||
"BONE_DONE",
|
||||
"component name:",
|
||||
"operation code:",
|
||||
"rope_ctrl_rot_",
|
||||
}
|
||||
|
||||
for _, pattern := range dependencyGraphPatterns {
|
||||
if strings.Contains(line, pattern) {
|
||||
return true, types.LogLevelInfo
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out animation system warnings (invalid drivers are common and harmless)
|
||||
animationSystemPatterns := []string{
|
||||
"BKE_animsys_eval_driver: invalid driver",
|
||||
"bke.anim_sys",
|
||||
"rotation_quaternion[",
|
||||
"constraints[",
|
||||
".influence[0]",
|
||||
"pose.bones[",
|
||||
}
|
||||
|
||||
for _, pattern := range animationSystemPatterns {
|
||||
if strings.Contains(line, pattern) {
|
||||
return true, types.LogLevelInfo
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out modifier warnings (common when vertices change)
|
||||
modifierPatterns := []string{
|
||||
"BKE_modifier_set_error",
|
||||
"bke.modifier",
|
||||
"Vertices changed from",
|
||||
"Modifier:",
|
||||
}
|
||||
|
||||
for _, pattern := range modifierPatterns {
|
||||
if strings.Contains(line, pattern) {
|
||||
return true, types.LogLevelInfo
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out lines that are just numbers or trace depth indicators
|
||||
// Pattern: number, word, word (e.g., "1 Object timer_box_franck")
|
||||
if matched, _ := regexp.MatchString(`^\d+\s+\w+\s+\w+`, trimmed); matched {
|
||||
return true, types.LogLevelInfo
|
||||
}
|
||||
|
||||
return false, types.LogLevelInfo
|
||||
}
|
||||
|
||||
143
internal/runner/blender/version.go
Normal file
143
internal/runner/blender/version.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package blender
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// ParseVersionFromFile parses the Blender version that a .blend file was saved with.
|
||||
// Returns major and minor version numbers.
|
||||
func ParseVersionFromFile(blendPath string) (major, minor int, err error) {
|
||||
file, err := os.Open(blendPath)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to open blend file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read the first 12 bytes of the blend file header
|
||||
// Format: BLENDER-v<major><minor><patch> or BLENDER_v<major><minor><patch>
|
||||
// The header is: "BLENDER" (7 bytes) + pointer size (1 byte: '-' for 64-bit, '_' for 32-bit)
|
||||
// + endianness (1 byte: 'v' for little-endian, 'V' for big-endian)
|
||||
// + version (3 bytes: e.g., "402" for 4.02)
|
||||
header := make([]byte, 12)
|
||||
n, err := file.Read(header)
|
||||
if err != nil || n < 12 {
|
||||
return 0, 0, fmt.Errorf("failed to read blend file header: %w", err)
|
||||
}
|
||||
|
||||
// Check for BLENDER magic
|
||||
if string(header[:7]) != "BLENDER" {
|
||||
// Might be compressed - try to decompress
|
||||
file.Seek(0, 0)
|
||||
return parseCompressedVersion(file)
|
||||
}
|
||||
|
||||
// Parse version from bytes 9-11 (3 digits)
|
||||
versionStr := string(header[9:12])
|
||||
|
||||
// Version format changed in Blender 3.0
|
||||
// Pre-3.0: "279" = 2.79, "280" = 2.80
|
||||
// 3.0+: "300" = 3.0, "402" = 4.02, "410" = 4.10
|
||||
if len(versionStr) == 3 {
|
||||
// First digit is major version
|
||||
fmt.Sscanf(string(versionStr[0]), "%d", &major)
|
||||
// Next two digits are minor version
|
||||
fmt.Sscanf(versionStr[1:3], "%d", &minor)
|
||||
}
|
||||
|
||||
return major, minor, nil
|
||||
}
|
||||
|
||||
// parseCompressedVersion handles gzip and zstd compressed blend files.
|
||||
func parseCompressedVersion(file *os.File) (major, minor int, err error) {
|
||||
magic := make([]byte, 4)
|
||||
if _, err := file.Read(magic); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
file.Seek(0, 0)
|
||||
|
||||
if magic[0] == 0x1f && magic[1] == 0x8b {
|
||||
// gzip compressed
|
||||
gzReader, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to create gzip reader: %w", err)
|
||||
}
|
||||
defer gzReader.Close()
|
||||
|
||||
header := make([]byte, 12)
|
||||
n, err := gzReader.Read(header)
|
||||
if err != nil || n < 12 {
|
||||
return 0, 0, fmt.Errorf("failed to read compressed blend header: %w", err)
|
||||
}
|
||||
|
||||
if string(header[:7]) != "BLENDER" {
|
||||
return 0, 0, fmt.Errorf("invalid blend file format")
|
||||
}
|
||||
|
||||
versionStr := string(header[9:12])
|
||||
if len(versionStr) == 3 {
|
||||
fmt.Sscanf(string(versionStr[0]), "%d", &major)
|
||||
fmt.Sscanf(versionStr[1:3], "%d", &minor)
|
||||
}
|
||||
|
||||
return major, minor, nil
|
||||
}
|
||||
|
||||
// Check for zstd magic (Blender 3.0+): 0x28 0xB5 0x2F 0xFD
|
||||
if magic[0] == 0x28 && magic[1] == 0xb5 && magic[2] == 0x2f && magic[3] == 0xfd {
|
||||
return parseZstdVersion(file)
|
||||
}
|
||||
|
||||
return 0, 0, fmt.Errorf("unknown blend file format")
|
||||
}
|
||||
|
||||
// parseZstdVersion handles zstd-compressed blend files (Blender 3.0+).
|
||||
// Uses zstd command line tool since Go doesn't have native zstd support.
|
||||
func parseZstdVersion(file *os.File) (major, minor int, err error) {
|
||||
file.Seek(0, 0)
|
||||
|
||||
cmd := exec.Command("zstd", "-d", "-c")
|
||||
cmd.Stdin = file
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to create zstd stdout pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return 0, 0, fmt.Errorf("failed to start zstd decompression: %w", err)
|
||||
}
|
||||
|
||||
// Read just the header (12 bytes)
|
||||
header := make([]byte, 12)
|
||||
n, readErr := io.ReadFull(stdout, header)
|
||||
|
||||
// Kill the process early - we only need the header
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
|
||||
if readErr != nil || n < 12 {
|
||||
return 0, 0, fmt.Errorf("failed to read zstd compressed blend header: %v", readErr)
|
||||
}
|
||||
|
||||
if string(header[:7]) != "BLENDER" {
|
||||
return 0, 0, fmt.Errorf("invalid blend file format in zstd archive")
|
||||
}
|
||||
|
||||
versionStr := string(header[9:12])
|
||||
if len(versionStr) == 3 {
|
||||
fmt.Sscanf(string(versionStr[0]), "%d", &major)
|
||||
fmt.Sscanf(versionStr[1:3], "%d", &minor)
|
||||
}
|
||||
|
||||
return major, minor, nil
|
||||
}
|
||||
|
||||
// VersionString returns a formatted version string like "4.2".
|
||||
func VersionString(major, minor int) string {
|
||||
return fmt.Sprintf("%d.%d", major, minor)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user