""" Command Line Interface for Python Scheduler Allows starting daemon and managing jobs """ import sys import argparse import json from pathlib import Path from datetime import datetime # Add current directory to path sys.path.insert(0, str(Path(__file__).parent)) from core.database import Database from core.config import SchedulerConfig from core.logger import setup_logging from daemon.automation_daemon import AutomationDaemon from loguru import logger def cmd_start_daemon(args): """Start the automation daemon""" setup_logging('daemon') # Prevent multiple daemon instances via PID lock file lock_file = Path(__file__).parent / 'data' / 'daemon.lock' lock_file.parent.mkdir(parents=True, exist_ok=True) import os import psutil if lock_file.exists(): try: existing_pid = int(lock_file.read_text().strip()) if psutil.pid_exists(existing_pid): proc = psutil.Process(existing_pid) if any('start_daemon' in ' '.join(p) for p in [proc.cmdline()]): logger.warning(f"Daemon already running (PID {existing_pid}) — exit.") sys.exit(0) except (ValueError, psutil.NoSuchProcess, psutil.AccessDenied): pass lock_file.unlink(missing_ok=True) lock_file.write_text(str(os.getpid())) logger.info("Starting daemon from CLI") try: daemon = AutomationDaemon() daemon.start() finally: lock_file.unlink(missing_ok=True) def cmd_create_job(args): """Create a new job""" setup_logging('cli') config = SchedulerConfig() db = Database(config.database_path) # Parse schedule config if args.cron: schedule_type = 'cron' schedule_config = {'cron': args.cron} elif args.interval: schedule_type = 'interval' # Parse interval: "1h", "30m", "1d" interval_str = args.interval if interval_str.endswith('h'): schedule_config = {'hours': int(interval_str[:-1])} elif interval_str.endswith('m'): schedule_config = {'minutes': int(interval_str[:-1])} elif interval_str.endswith('d'): schedule_config = {'days': int(interval_str[:-1])} else: logger.error("Invalid interval format. Use: 1h, 30m, 1d, etc.") return else: logger.error("Must specify --cron or --interval") return # Parse arguments arguments = args.args if args.args else [] # Create job job_id = db.create_job( name=args.name, description=args.description, job_type=args.type, executable_path=args.executable, arguments=json.dumps(arguments), working_directory=args.working_dir, schedule_type=schedule_type, schedule_config=schedule_config, enabled=not args.disabled, timeout_seconds=args.timeout, retry_count=args.retry, retry_delay_seconds=args.retry_delay, criticality=args.criticality, tags=args.tags.split(',') if args.tags else [], show_console=args.show_console ) logger.info(f"Created job '{args.name}' with ID {job_id}") print(f"Job created successfully! Job ID: {job_id}") def cmd_list_jobs(args): """List all jobs""" setup_logging('cli') config = SchedulerConfig() db = Database(config.database_path) jobs = db.get_all_jobs(enabled_only=args.enabled_only) if not jobs: print("No jobs found") return print(f"\nTotal jobs: {len(jobs)}\n") print(f"{'ID':<5} {'Name':<30} {'Type':<10} {'Schedule':<20} {'Status':<10} {'Criticality':<12}") print("-" * 90) for job in jobs: schedule_config = json.loads(job['schedule_config']) schedule_str = schedule_config.get('cron', str(schedule_config))[:20] status = "ENABLED" if job['enabled'] else "DISABLED" print(f"{job['id']:<5} {job['name']:<30} {job['job_type']:<10} {schedule_str:<20} {status:<10} {job['criticality']:<12}") print() def cmd_show_job(args): """Show job details""" setup_logging('cli') config = SchedulerConfig() db = Database(config.database_path) job = db.get_job(args.job_id) if not job: print(f"Job ID {args.job_id} not found") return print(f"\n{'='*60}") print(f"Job Details - ID {job['id']}") print(f"{'='*60}") print(f"Name: {job['name']}") print(f"Description: {job['description'] or 'N/A'}") print(f"Type: {job['job_type']}") print(f"Executable: {job['executable_path']}") print(f"Arguments: {job['arguments'] or 'N/A'}") print(f"Working Directory: {job['working_directory'] or 'N/A'}") print(f"Schedule Type: {job['schedule_type']}") print(f"Schedule Config: {job['schedule_config']}") print(f"Enabled: {'YES' if job['enabled'] else 'NO'}") print(f"Timeout: {job['timeout_seconds']} seconds") print(f"Retry Count: {job['retry_count']}") print(f"Retry Delay: {job['retry_delay_seconds']} seconds") print(f"Criticality: {job['criticality']}") print(f"Tags: {job['tags']}") print(f"Created: {job['created_at']}") print(f"Updated: {job['updated_at']}") print(f"{'='*60}\n") def cmd_enable_job(args): """Enable a job""" setup_logging('cli') config = SchedulerConfig() db = Database(config.database_path) db.update_job(args.job_id, enabled=True) logger.info(f"Enabled job ID {args.job_id}") print(f"Job ID {args.job_id} enabled") def cmd_disable_job(args): """Disable a job""" setup_logging('cli') config = SchedulerConfig() db = Database(config.database_path) db.update_job(args.job_id, enabled=False) logger.info(f"Disabled job ID {args.job_id}") print(f"Job ID {args.job_id} disabled") def cmd_delete_job(args): """Delete a job""" setup_logging('cli') config = SchedulerConfig() db = Database(config.database_path) if not args.force: response = input(f"Are you sure you want to delete job ID {args.job_id}? (yes/no): ") if response.lower() != 'yes': print("Cancelled") return db.delete_job(args.job_id) logger.info(f"Deleted job ID {args.job_id}") print(f"Job ID {args.job_id} deleted") def cmd_run_job(args): """Run a job manually""" setup_logging('cli') config = SchedulerConfig() db = Database(config.database_path) from daemon.job_executor import JobExecutor executor = JobExecutor(db, config) execution_id = executor.execute_job(args.job_id, triggered_by='manual-cli') if execution_id: print(f"Job queued for execution. Execution ID: {execution_id}") print(f"Monitor execution with: python start_daemon.py history --execution-id {execution_id}") else: print("Failed to queue job") def cmd_create_group(args): """Create a new job group""" setup_logging('cli') config = SchedulerConfig() db = Database(config.database_path) from daemon.dependency_manager import DependencyManager from daemon.job_executor import JobExecutor executor = JobExecutor(db, config) dep_manager = DependencyManager(db, executor) try: group_id = dep_manager.create_job_chain( name=args.name, job_ids=args.job_ids, description=args.description, error_handling=args.error_handling ) print(f"Job group created successfully! Group ID: {group_id}") logger.info(f"Created job group '{args.name}' with ID {group_id}") except ValueError as e: print(f"Error: {e}") logger.error(f"Failed to create job group: {e}") def cmd_list_groups(args): """List all job groups""" setup_logging('cli') config = SchedulerConfig() db = Database(config.database_path) with db._get_connection() as conn: cursor = conn.execute("SELECT * FROM job_groups ORDER BY name") groups = [dict(row) for row in cursor.fetchall()] if not groups: print("No job groups found") return print(f"\nTotal groups: {len(groups)}\n") print(f"{'ID':<5} {'Name':<30} {'Error Handling':<20} {'Created':<20}") print("-" * 80) for group in groups: # Count members members = db.get_job_group_members(group['id']) name_with_count = f"{group['name']} ({len(members)} jobs)" print(f"{group['id']:<5} {name_with_count:<30} {group['error_handling']:<20} {group['created_at'][:19]:<20}") print() def cmd_show_group(args): """Show job group details""" setup_logging('cli') config = SchedulerConfig() db = Database(config.database_path) group = db.get_job_group(args.group_id) if not group: print(f"Job group ID {args.group_id} not found") return members = db.get_job_group_members(args.group_id) print(f"\n{'='*60}") print(f"Job Group Details - ID {group['id']}") print(f"{'='*60}") print(f"Name: {group['name']}") print(f"Description: {group['description'] or 'N/A'}") print(f"Error Handling: {group['error_handling']}") print(f"Created: {group['created_at']}") print(f"\nJobs in execution order:") print(f"{'Order':<8} {'Job ID':<8} {'Job Name':<30}") print("-" * 50) for member in members: print(f"{member['execution_order']:<8} {member['job_id']:<8} {member['job_name']:<30}") print(f"{'='*60}\n") def cmd_run_group(args): """Run a job group manually""" setup_logging('cli') config = SchedulerConfig() db = Database(config.database_path) from daemon.dependency_manager import DependencyManager from daemon.job_executor import JobExecutor executor = JobExecutor(db, config) dep_manager = DependencyManager(db, executor) print(f"Executing job group {args.group_id}...") result = dep_manager.execute_job_group(args.group_id, triggered_by='manual-cli') print(f"\n{'='*60}") print(f"Job Group Execution Results") print(f"{'='*60}") print(f"Group: {result.get('group_name')}") print(f"Success: {'YES' if result['success'] else 'NO'}") print(f"Total Jobs: {result['total_jobs']}") print(f"Executed: {result['executed_jobs']}") print(f"Completed: {result['completed_jobs']}") print(f"Failed: {result['failed_jobs']}") print(f"Duration: {result['duration_seconds']:.1f}s") print(f"\nJob Executions:") print(f"{'Job Name':<30} {'Status':<12} {'Duration':<10} {'Exit Code':<10}") print("-" * 65) for ex in result['executions']: duration_str = f"{ex['duration']:.1f}s" if ex['duration'] else "N/A" exit_code_str = str(ex['exit_code']) if ex['exit_code'] is not None else "N/A" print(f"{ex['job_name']:<30} {ex['status']:<12} {duration_str:<10} {exit_code_str:<10}") print(f"{'='*60}\n") def cmd_delete_group(args): """Delete a job group""" setup_logging('cli') config = SchedulerConfig() db = Database(config.database_path) group = db.get_job_group(args.group_id) if not group: print(f"Job group ID {args.group_id} not found") return if not args.force: response = input(f"Are you sure you want to delete job group '{group['name']}'? (yes/no): ") if response.lower() != 'yes': print("Cancelled") return with db._get_connection() as conn: conn.execute("DELETE FROM job_groups WHERE id = ?", (args.group_id,)) conn.commit() logger.info(f"Deleted job group ID {args.group_id}") print(f"Job group '{group['name']}' deleted") def cmd_history(args): """Show execution history""" setup_logging('cli') config = SchedulerConfig() db = Database(config.database_path) if args.execution_id: # Show specific execution execution = db.get_execution(args.execution_id) if not execution: print(f"Execution ID {args.execution_id} not found") return print(f"\n{'='*60}") print(f"Execution Details - ID {execution['id']}") print(f"{'='*60}") print(f"Job ID: {execution['job_id']}") print(f"Status: {execution['status']}") print(f"Start Time: {execution['start_time']}") print(f"End Time: {execution['end_time'] or 'N/A'}") print(f"Duration: {execution['duration_seconds'] or 'N/A'} seconds") print(f"Exit Code: {execution['exit_code'] if execution['exit_code'] is not None else 'N/A'}") print(f"Log File: {execution['log_file'] or 'N/A'}") print(f"Triggered By: {execution['triggered_by']}") print(f"Error: {execution['error_message'] or 'N/A'}") print(f"{'='*60}\n") else: # Show recent executions executions = db.get_recent_executions(limit=args.limit, job_id=args.job_id) if not executions: print("No executions found") return print(f"\nRecent Executions (limit {args.limit}):\n") print(f"{'ID':<6} {'Job Name':<30} {'Status':<10} {'Start Time':<20} {'Duration':<10}") print("-" * 80) for ex in executions: duration_str = f"{ex['duration_seconds']:.1f}s" if ex['duration_seconds'] else "N/A" start_time = ex['start_time'][:19] if ex['start_time'] else "N/A" status_icon = { 'completed': '[OK]', 'failed': '[FAIL]', 'running': '[RUN]', 'timeout': '[TIME]', 'cancelled': '[STOP]' }.get(ex['status'], '[?]') print(f"{ex['id']:<6} {ex['job_name']:<30} {status_icon} {ex['status']:<8} {start_time:<20} {duration_str:<10}") print() def main(): """Main CLI entry point""" parser = argparse.ArgumentParser(description='Frank - Automation Daemon') subparsers = parser.add_subparsers(dest='command', help='Available commands') # Start daemon command parser_start = subparsers.add_parser('start', help='Start the scheduler daemon') parser_start.set_defaults(func=cmd_start_daemon) # Create job command parser_create = subparsers.add_parser('create', help='Create a new job') parser_create.add_argument('name', help='Job name') parser_create.add_argument('executable', help='Path to executable (.bat, .py, etc.)') parser_create.add_argument('--description', '-d', help='Job description') parser_create.add_argument('--type', '-t', choices=['batch', 'script', 'python_module'], default='batch', help='Job type') parser_create.add_argument('--working-dir', '-w', help='Working directory') parser_create.add_argument('--args', nargs='*', help='Arguments to pass to executable') parser_create.add_argument('--cron', help='Cron expression (e.g., "0 7 * * mon-fri")') parser_create.add_argument('--interval', help='Interval (e.g., "1h", "30m", "1d")') parser_create.add_argument('--timeout', type=int, default=3600, help='Timeout in seconds (default: 3600)') parser_create.add_argument('--retry', type=int, default=0, help='Retry count (default: 0)') parser_create.add_argument('--retry-delay', type=int, default=300, help='Retry delay in seconds (default: 300)') parser_create.add_argument('--criticality', choices=['critical', 'high', 'normal', 'low'], default='normal', help='Job criticality') parser_create.add_argument('--tags', help='Comma-separated tags') parser_create.add_argument('--disabled', action='store_true', help='Create job disabled') parser_create.add_argument('--show-console', action='store_true', help='Show console window during execution (debug mode)') parser_create.set_defaults(func=cmd_create_job) # List jobs command parser_list = subparsers.add_parser('list', help='List all jobs') parser_list.add_argument('--enabled-only', action='store_true', help='Show only enabled jobs') parser_list.set_defaults(func=cmd_list_jobs) # Show job command parser_show = subparsers.add_parser('show', help='Show job details') parser_show.add_argument('job_id', type=int, help='Job ID') parser_show.set_defaults(func=cmd_show_job) # Enable job command parser_enable = subparsers.add_parser('enable', help='Enable a job') parser_enable.add_argument('job_id', type=int, help='Job ID') parser_enable.set_defaults(func=cmd_enable_job) # Disable job command parser_disable = subparsers.add_parser('disable', help='Disable a job') parser_disable.add_argument('job_id', type=int, help='Job ID') parser_disable.set_defaults(func=cmd_disable_job) # Delete job command parser_delete = subparsers.add_parser('delete', help='Delete a job') parser_delete.add_argument('job_id', type=int, help='Job ID') parser_delete.add_argument('--force', '-f', action='store_true', help='Skip confirmation') parser_delete.set_defaults(func=cmd_delete_job) # Run job command parser_run = subparsers.add_parser('run', help='Run a job manually') parser_run.add_argument('job_id', type=int, help='Job ID') parser_run.set_defaults(func=cmd_run_job) # History command parser_history = subparsers.add_parser('history', help='Show execution history') parser_history.add_argument('--job-id', type=int, help='Filter by job ID') parser_history.add_argument('--execution-id', type=int, help='Show specific execution') parser_history.add_argument('--limit', type=int, default=20, help='Number of recent executions (default: 20)') parser_history.set_defaults(func=cmd_history) # Job Group commands parser_create_group = subparsers.add_parser('create-group', help='Create a new job group') parser_create_group.add_argument('name', help='Group name') parser_create_group.add_argument('job_ids', type=int, nargs='+', help='Job IDs in execution order') parser_create_group.add_argument('--description', '-d', help='Group description') parser_create_group.add_argument('--error-handling', choices=['stop_on_error', 'continue', 'skip_remaining'], default='stop_on_error', help='Error handling strategy') parser_create_group.set_defaults(func=cmd_create_group) parser_list_groups = subparsers.add_parser('list-groups', help='List all job groups') parser_list_groups.set_defaults(func=cmd_list_groups) parser_show_group = subparsers.add_parser('show-group', help='Show job group details') parser_show_group.add_argument('group_id', type=int, help='Group ID') parser_show_group.set_defaults(func=cmd_show_group) parser_run_group = subparsers.add_parser('run-group', help='Run a job group manually') parser_run_group.add_argument('group_id', type=int, help='Group ID') parser_run_group.set_defaults(func=cmd_run_group) parser_delete_group = subparsers.add_parser('delete-group', help='Delete a job group') parser_delete_group.add_argument('group_id', type=int, help='Group ID') parser_delete_group.add_argument('--force', '-f', action='store_true', help='Skip confirmation') parser_delete_group.set_defaults(func=cmd_delete_group) # Parse arguments args = parser.parse_args() if not args.command: parser.print_help() sys.exit(1) # Execute command try: args.func(args) except Exception as e: print(f"Error: {e}") import traceback traceback.print_exc() sys.exit(1) if __name__ == '__main__': main()