import subprocess
import tempfile
import os
import shutil
from pathlib import Path
from typing import Optional, Union, List, Tuple
import dotenv
from .exceptions import (
AsepriteNotFoundError,
CommandExecutionError,
LuaScriptError,
FileNotFoundError as AsepriteFileNotFoundError
)
from .validation import validate_file_path
from .config import get_config
from .logging import get_logger, Timer
dotenv.load_dotenv()
logger = get_logger(__name__)
class AsepriteCommand:
"""Helper class for running Aseprite commands."""
def __init__(self):
"""Initialize AsepriteCommand with validated executable path."""
self.config = get_config()
logger.info("Initializing AsepriteCommand")
self._aseprite_path = self._find_aseprite()
logger.info(f"Found Aseprite at: {self._aseprite_path}")
def _find_aseprite(self) -> str:
"""Find Aseprite executable path.
Returns:
str: Path to Aseprite executable
Raises:
AsepriteNotFoundError: If Aseprite cannot be found
"""
# First check configuration
if self.config.aseprite_path:
if os.path.isfile(self.config.aseprite_path) and os.access(self.config.aseprite_path, os.X_OK):
return self.config.aseprite_path
else:
raise AsepriteNotFoundError(self.config.aseprite_path)
# Then check environment variable
env_path = os.getenv('ASEPRITE_PATH')
if env_path:
if os.path.isfile(env_path) and os.access(env_path, os.X_OK):
return env_path
else:
raise AsepriteNotFoundError(env_path)
# Check if aseprite is in PATH
aseprite_in_path = shutil.which('aseprite')
if aseprite_in_path:
return aseprite_in_path
# Check common installation locations
common_paths = [
r"C:\Program Files\Aseprite\Aseprite.exe",
r"C:\Program Files (x86)\Aseprite\Aseprite.exe",
"/Applications/Aseprite.app/Contents/MacOS/aseprite",
"/usr/bin/aseprite",
"/usr/local/bin/aseprite"
]
for path in common_paths:
if os.path.isfile(path) and os.access(path, os.X_OK):
return path
raise AsepriteNotFoundError()
def run_command(self, args: List[str]) -> Tuple[bool, str]:
"""Run an Aseprite command with proper error handling.
Args:
args: List of command arguments
Returns:
tuple: (success, output) where success is a boolean and output is the command output
Raises:
CommandExecutionError: If command execution fails
"""
cmd = [self._aseprite_path] + args
with Timer("run_command", command=' '.join(args[:3] + ['...'] if len(args) > 3 else args)):
try:
logger.debug(f"Running command: {' '.join(cmd)}")
result = subprocess.run(
cmd,
check=True,
capture_output=True,
text=True,
timeout=self.config.lua.timeout
)
logger.debug(f"Command completed successfully")
return True, result.stdout
except subprocess.CalledProcessError as e:
logger.error(f"Command failed with exit code {e.returncode}",
command=cmd, stderr=e.stderr)
raise CommandExecutionError(cmd, e.returncode, e.stderr or e.stdout)
except subprocess.TimeoutExpired:
logger.error(f"Command timed out", command=cmd, timeout=self.config.lua.timeout)
raise CommandExecutionError(cmd, -1, f"Command timed out after {self.config.lua.timeout} seconds")
def execute_lua_script(self, script_content: str, filename: Optional[str] = None) -> Tuple[bool, str]:
"""Execute a Lua script in Aseprite.
Args:
script_content: Lua script code to execute
filename: Optional filename to open before executing script
Returns:
tuple: (success, output)
Raises:
LuaScriptError: If script execution fails
AsepriteFileNotFoundError: If specified file doesn't exist
"""
# Validate script size
script_size = len(script_content.encode('utf-8'))
if script_size > self.config.lua.max_script_size:
logger.error(f"Lua script too large", size=script_size, max_size=self.config.lua.max_script_size)
raise LuaScriptError(
script_content[:100] + "...",
f"Script exceeds maximum size of {self.config.lua.max_script_size} bytes"
)
logger.debug(f"Executing Lua script", size=script_size, has_file=bool(filename))
# Validate file if provided
if filename:
try:
file_path = validate_file_path(filename, must_exist=True)
# Check if path is allowed
if not self.config.is_path_allowed(file_path):
raise LuaScriptError(
script_content[:100] + "...",
f"Access to path '{file_path}' is not allowed by security configuration"
)
filename = str(file_path)
except Exception as e:
if isinstance(e, LuaScriptError):
raise
raise AsepriteFileNotFoundError(filename)
# Create a temporary file for the script
try:
with tempfile.NamedTemporaryFile(suffix='.lua', delete=False, mode='w') as tmp:
tmp.write(script_content)
script_path = tmp.name
except Exception as e:
raise LuaScriptError(script_content[:100] + "...", f"Failed to create temp file: {e}")
try:
args = ["--batch"]
if filename:
args.append(filename)
args.extend(["--script", script_path])
with Timer("execute_lua_script", script_size=len(script_content), file=filename):
success, output = self.run_command(args)
if not success:
logger.error("Lua script execution failed", output=output)
raise LuaScriptError(script_content[:100] + "...", output)
logger.debug("Lua script executed successfully")
return success, output
except CommandExecutionError as e:
logger.error("Lua script command error", error=str(e))
raise LuaScriptError(script_content[:100] + "...", e.details['stderr'])
finally:
# Clean up the temporary script file
try:
os.remove(script_path)
except:
pass # Ignore cleanup errors
# Global instance for backward compatibility
_global_command = None
def get_command() -> AsepriteCommand:
"""Get global AsepriteCommand instance."""
global _global_command
if _global_command is None:
_global_command = AsepriteCommand()
return _global_command