#! /usr/bin/python3
# -*- python -*-
# -*- coding: utf-8 -*-
#   tuna - Application Tuning GUI
#   Copyright (C) 2008-2011 Arnaldo Carvalho de Melo <acme@redhat.com>
#   Copyright (C) 2015-2026 John Kacur <jkacur@redhat.com>
#
# SPDX-License-Identifier: GPL-2.0-only

""" tuna - Application Tuning Program"""

import argparse
import os
import sys
import errno
import re
import fnmatch
import gettext
import locale
from functools import reduce
import tuna.new_eth as ethtool
import tuna.tuna_sched as tuna_sched
import procfs
from tuna import tuna, sysfs, utils
from tuna import cpuset
from tuna.version import TUNA_VERSION
import logging
import time
import shutil
import tuna.cpupower as cpw

def get_loglevel(level):
    """Convert a logging level string to a Python logging level constant.

    Accepts either numeric (0-4) or string (DEBUG, INFO, WARNING, ERROR) formats.
    Numeric values are multiplied by 10 to match Python logging constants.

    Args:
        level: Logging level as string. Can be:
              - Numeric: "0" (NOTSET), "1" (DEBUG), "2" (INFO), "3" (WARNING), "4" (ERROR)
              - String: "DEBUG", "INFO", "WARNING", "ERROR" (case-insensitive)

    Returns:
        Integer logging level constant compatible with Python's logging module.
        - Numeric input: Returns level * 10 (e.g., "1" -> 10 for DEBUG)
        - String input: Returns the uppercase string (e.g., "debug" -> "DEBUG")

    Raises:
        ValueError: If level is "CRITICAL" (not supported by tuna)

    Note:
        Python logging levels: 0=NOTSET, 10=DEBUG, 20=INFO, 30=WARNING, 40=ERROR, 50=CRITICAL
        CRITICAL level is explicitly rejected as tuna does not support it.
    """
    if level.isdigit() and int(level) in range(0,5):
        # logging built-in module levels:
        # 0 - NOTSET
        # 10 - DEBUG
        # 20 - INFO
        # 30 - WARNING
        # 40 - ERROR
        return int(level) * 10
    level_str = level.upper()
    if level_str == "CRITICAL":
        raise ValueError("CRITICAL level is not supported by tuna")
    return level_str

def setup_logging(logger_name):

    logger = logging.getLogger(logger_name)
    logger.setLevel(logging.DEBUG)
    logger.propagate = False
    return logger

def add_handler(loglevel, tofile=False):

    formatter = logging.Formatter('[%(levelname)s] %(message)s')

    if tofile:
        lognum = 1
        date = time.strftime("%Y%m%d")
        while os.path.exists("tuna-{}-{}".format(date, lognum)):
            lognum += 1

        path = "tuna-{}-{}/log/Log".format(date,lognum)
        os.makedirs(os.path.dirname(path), exist_ok=True, mode=0o777)
        handler = logging.FileHandler(path)
    else:
        handler = logging.StreamHandler(sys.stderr)

    handler.setFormatter(formatter)
    handler.setLevel(loglevel)

    return handler

try:
    import inet_diag
    have_inet_diag = True
except ImportError:
    have_inet_diag = False

class HelpMessageParser(argparse.ArgumentParser):
    def error(self, message):
        sys.stderr.write(f'error: {message}\n')
        self.print_help()
        sys.exit(2)

def get_default_cpuset_profile():
    """Get default cpuset profile filename based on execution context.

    Returns:
        str: Default profile path
        - './cpuset-profile.yaml' when running from dev (tuna-cmd.py or tuna-cmd)
        - '/etc/tuna/cpuset-profile.yaml' when running from production (tuna)
    """
    script_name = os.path.basename(sys.argv[0])
    if script_name in ('tuna-cmd.py', 'tuna-cmd'):
        return 'cpuset-profile.yaml'
    else:
        return '/etc/tuna/cpuset-profile.yaml'

def resolve_cpuset_profile_path(filename):
    """Resolve cpuset profile path by searching in standard locations.

    Args:
        filename: User-provided filename or path

    Returns:
        str: Resolved absolute path if file exists, otherwise original filename

    Search order for relative paths:
        1. Current directory (./filename)
        2. /etc/tuna/filename
    Absolute paths are returned as-is.
    """
    # If absolute path, use it directly
    if os.path.isabs(filename):
        return filename

    # Try current directory first
    if os.path.exists(filename):
        return filename

    # Try /etc/tuna/ as fallback
    etc_path = os.path.join('/etc/tuna', filename)
    if os.path.exists(etc_path):
        return etc_path

    # Return original filename (will produce meaningful error if not found)
    return filename

def gen_parser():


    POS = {
            "cpu_list": dict(metavar='CPU-LIST', type=tuna.cpustring_to_list, help="CPU-LIST affected by commands"),
            "thread_list": dict(metavar='THREAD-LIST', nargs='+', help="THREAD-LIST affected by commands"),
            "filename": dict(metavar='FILENAME', type=str, help="Save kthreads sched tunables to this file"),
            "run_command": dict(metavar='COMMAND', type=str, help="fork a new process and run the \"COMMAND\""),
            "priority": dict(type=tuna.get_policy_and_rtprio, metavar="POLICY:RTPRIO", help="Set thread scheduler tunables: POLICY and RTPRIO"),
          }

    MODS = {
            "logging": dict(dest='loglevel', metavar='LOG-LEVEL', type=get_loglevel, help="Log application details to file for given LOG-LEVEL"),
            "debug" : dict(action='store_true', dest='debug', help='Print DEBUG level logging details to console'),
            "version": dict(action='version', version=TUNA_VERSION, help="show version"),
            "warn": dict(action='store_true', dest='warn', help='Warn when thread/IRQ patterns match nothing'),
            "threads": dict(dest='thread_list', default='', metavar='THREAD-LIST', type=str, help="THREAD-LIST affected by commands"),
            "irqs": dict(dest='irq_list', default='', metavar='IRQ-LIST', type=str, help="IRQ-LIST affect by commands"),
            "cpus": dict(dest='cpu_list', default=[], metavar='CPU-LIST', type=tuna.cpustring_to_list, help='CPU-LIST affected by commands'),
            "sockets": dict(dest='sockets', default='', metavar='CPU-SOCKET-LIST', type=str, help="CPU-SOCKET-LIST affected by commands"),
            "show_sockets": dict(action='store_true', help='Show network sockets in use by threads'),
            "cgroups": dict(action='store_true', dest='cgroups', help='Display the processes with the type of cgroups they are in'),
            "spaced": dict(action='store_false', dest='compact', help='Display spaced view for cgroups'),
            "affect_children": dict(action='store_true', help="Operation will affect children threads"),
            "nohz_full": dict(action='store_true', help="CPUs in nohz_full kernel command line will be affected by operations"),
            "no_uthreads": dict(action='store_false', dest='uthreads', help="Operations will not affect user threads"),
            "no_kthreads": dict(action='store_false', dest='kthreads', help="Operations will not affect kernel threads"),
            "disable_perf": dict(action='store_true', help="Explicitly disable usage of perf in GUI for process view"),
            "refresh": dict(default=2500, metavar='MSEC', type=int, help="Refresh the GUI every MSEC milliseconds"),
            "priority": dict(default=(None, None), metavar="POLICY:RTPRIO", type=tuna.get_policy_and_rtprio, help="Set thread scheduler tunables: POLICY and RTPRIO"),
            "background": dict(action='store_true', help="Run command as background task"),
            "idle_info": dict(dest='idle_info', action='store_const', const=True, help='Print general idle information for the selected CPUs, including index values for IDLE-STATE.'),
            "idle_state_disabled_status": dict(dest='idle_state_disabled_status', metavar='IDLE-STATE', type=int, help='Print whether IDLE-STATE is enabled on the selected CPUs.'),
            "disable_idle_state": dict(dest='disable_idle_state', metavar='IDLE-STATE', type=int, help='Disable IDLE-STATE on the selected CPUs.'),
            "enable_idle_state": dict(dest='enable_idle_state', metavar='IDLE-STATE', type=int, help='Enable IDLE-STATE on the selected CPUs.')
    }

    parser = HelpMessageParser(description="tuna - Application Tuning Program")

    parser._positionals.title = "commands"
    parser.add_argument('-v', '--version', **MODS['version'])
    parser.add_argument('-L', '--logging', **MODS['logging'])
    parser.add_argument('-D', '--debug', **MODS['debug'])
    parser.add_argument('-w', '--warn', **MODS['warn'])

    subparser = parser.add_subparsers(dest='command', parser_class=HelpMessageParser)

    idle_set = subparser.add_parser('cpu_power',
                                    description='Manage CPU idle state disabling (requires libcpupower and it\'s Python bindings)',
                                    help='Set all idle states on a given CPU-LIST.')
    isolate = subparser.add_parser('isolate', description="Move all allowed threads and IRQs away from CPU-LIST",
                                    help="Move all allowed threads and IRQs away from CPU-LIST")
    include = subparser.add_parser('include', description="Allow all threads to run on CPU-LIST",
                                     help="Allow all threads to run on CPU-LIST")
    move = subparser.add_parser('move', description="Move selected threads/IRQs to specified CPUs, sockets, or cpusets",
                                    help="Move selected threads/IRQs to specified CPUs, sockets, or cpusets")
    spread = subparser.add_parser('spread', description="Spread selected entities to CPU-LIST",
                                    help="Spread selected entities over CPU-LIST")
    priority = subparser.add_parser('priority', description="Set thread scheduler tunables: POLICY and RTPRIO",
                                    help="Set thread scheduler tunables: POLICY and RTPRIO")
    run = subparser.add_parser('run', description="Fork a new process and run the COMMAND",
                                help="Fork a new process and run the COMMAND")
    show_threads = subparser.add_parser('show_threads', description='Show thread list', help='Show thread list')
    show_irqs = subparser.add_parser('show_irqs', description='Show IRQ list', help='Show IRQ list')
    show_configs = subparser.add_parser('show_configs', description='List preloaded profiles', help='List preloaded profiles')
    what_is = subparser.add_parser('what_is', description='Provides help about selected entities', help='Provides help about selected entities')
    gui = subparser.add_parser('gui', description="Start the GUI", help="Start the GUI")
    cpuset = subparser.add_parser('cpuset', description='Manage CPU sets (cgroup v2)', help='Manage CPU sets')

    isolate_group = isolate.add_mutually_exclusive_group(required=True)
    isolate_group.add_argument('-c', '--cpus', **MODS['cpus'])
    isolate_group.add_argument('-S', '--sockets', **MODS['sockets'])
    isolate_group.add_argument('-N', '--nohz_full', **MODS['nohz_full'])
    isolate.add_argument('--cpuset', type=str, nargs='?', const='tuna_isolated',
                        metavar='NAME', default=None,
                        help='Use cpusets for isolation with optional name (default: tuna_isolated, partition=isolated)')
    isolate.add_argument('--cpuset-housekeeping', nargs='+', metavar='...',
                        help='Create housekeeping cpuset: [NAME] CPU-LIST (default name: tuna_housekeeping, partition=member)')
    isolate.add_argument('--housekeeping-isolated', action='store_true',
                        help='Make housekeeping cpuset use partition=isolated instead of member (requires --cpuset-housekeeping)')

    include_group = include.add_mutually_exclusive_group(required=True)
    include_group.add_argument('-c', '--cpus', **MODS['cpus'])
    include_group.add_argument('-S', '--sockets', **MODS['sockets'])
    include_group.add_argument('-N', '--nohz_full', **MODS['nohz_full'])

    move_group = move.add_mutually_exclusive_group(required=True)
    move_group.add_argument('-c', '--cpus', **MODS['cpus'])
    move_group.add_argument('-S', '--sockets', **MODS['sockets'])
    move_group.add_argument('-N', '--nohz_full', **MODS['nohz_full'])
    move_group.add_argument('--cpuset', type=str, metavar='CPUSET-NAME', help='Move threads/IRQs into cpuset (cgroup v2) instead of setting CPU affinity')
    move.add_argument('-t', '--threads', **MODS['threads'])
    move.add_argument('-q', '--irqs', **MODS['irqs'])
    move.add_argument('-U', '--no_uthreads', **MODS['no_uthreads'])
    move.add_argument('-K', '--no_kthreads', **MODS['no_kthreads'])

    spread_group = spread.add_mutually_exclusive_group(required=True)
    spread_group.add_argument('-c', '--cpus', **MODS['cpus'])
    spread_group.add_argument('-S', '--sockets', **MODS['sockets'])
    spread_group.add_argument('-N', '--nohz_full', **MODS['nohz_full'])
    spread.add_argument('-t', '--threads', **MODS['threads'])
    spread.add_argument('-q', '--irqs', **MODS['irqs'])
    spread.add_argument('-U', '--no_uthreads', **MODS['no_uthreads'])
    spread.add_argument('-K', '--no_kthreads', **MODS['no_kthreads'])

    priority.add_argument('priority', **POS['priority'])
    priority.add_argument('-t', '--threads', **MODS['threads'], required=True)
    priority.add_argument('-C', '--affect_children', **MODS['affect_children'])

    run.add_argument('run_command', **POS['run_command'])
    run_group = run.add_mutually_exclusive_group(required=False)
    run_group.add_argument('-c', '--cpus', **MODS['cpus'])
    run_group.add_argument('-S', '--sockets', **MODS['sockets'])
    run_group.add_argument('-N', '--nohz_full', **MODS['nohz_full'])
    run_group.add_argument('--cpuset', type=str, metavar='CPUSET-NAME', help='Run process in specified cpuset (cgroup v2) instead of setting CPU affinity')
    run.add_argument('-p', '--priority', **MODS['priority'])
    run.add_argument('-b', '--background', **MODS['background'])

    show_threads_group1 = show_threads.add_mutually_exclusive_group(required=False)
    show_threads_group1.add_argument('-c', '--cpus', **MODS['cpus'])
    show_threads_group1.add_argument('-N', '--nohz_full', **MODS['nohz_full'])
    show_threads_group1.add_argument('-S', '--sockets', **MODS['sockets'])
    show_threads_group2 = show_threads.add_mutually_exclusive_group(required=False)
    show_threads_group2.add_argument('-t', '--threads', **MODS['threads'])
    show_threads_group2.add_argument('-q', '--irqs', **MODS['irqs'])
    show_threads.add_argument('-U', '--no_uthreads', **MODS['no_uthreads'])
    show_threads.add_argument('-K', '--no_kthreads', **MODS['no_kthreads'])
    show_threads.add_argument('-C', '--affect_children', **MODS['affect_children'])

    if have_inet_diag:
        show_threads.add_argument('-n', '--show_sockets', **MODS['show_sockets'])
    show_threads.add_argument('-G', '--cgroups', **MODS['cgroups'])
    show_threads.add_argument('--cpuset', type=str, metavar='CPUSET-NAME', help='Show only threads in the specified cpuset')
    show_threads.add_argument('-z', '--spaced', **MODS['spaced'])


    show_irqs_group = show_irqs.add_mutually_exclusive_group(required=False)
    show_irqs_group.add_argument('-c', '--cpus', **MODS['cpus'])
    show_irqs_group.add_argument('-N', '--nohz_full', **MODS['nohz_full'])
    show_irqs_group.add_argument('-S', '--sockets', **MODS['sockets'])
    show_irqs.add_argument('-q', '--irqs', **MODS['irqs'])

    idle_set_group = idle_set.add_mutually_exclusive_group(required=True)
    idle_set_group.add_argument('-i', '--idle-info', **MODS['idle_info'])
    idle_set_group.add_argument('-s', '--status', **MODS['idle_state_disabled_status'])
    idle_set_group.add_argument('-d', '--disable', **MODS['disable_idle_state'])
    idle_set_group.add_argument('-e', '--enable', **MODS['enable_idle_state'])
    idle_set.add_argument('-c', '--cpus', **MODS['cpus'])

    what_is.add_argument('thread_list', **POS['thread_list'])

    gui.add_argument('-d', '--disable_perf', **MODS['disable_perf'])
    gui.add_argument('-R', '--refresh', **MODS['refresh'])
    gui_group = gui.add_mutually_exclusive_group(required=False)
    gui_group.add_argument('-c', '--cpus', **MODS['cpus'])
    gui_group.add_argument('-N', '--nohz_full', **MODS['nohz_full'])
    gui_group.add_argument('-S', '--sockets', **MODS['sockets'])
    gui.add_argument('-U', '--no_uthreads', **MODS['no_uthreads'])
    gui.add_argument('-K', '--no_kthreads', **MODS['no_kthreads'])

    # Cpuset nested subcommands
    cpuset_subparser = cpuset.add_subparsers(dest='cpuset_command', required=True, parser_class=HelpMessageParser)

    # cpuset create
    cpuset_create = cpuset_subparser.add_parser('create', description='Create a new cpuset', help='Create a new cpuset')
    cpuset_create.add_argument('-c', '--cpus', **MODS['cpus'], required=True)
    cpuset_create.add_argument('-n', '--name', type=str, metavar='NAME', help='Cpuset name (default: auto-generate tunaN)')
    cpuset_create.add_argument('-i', '--isolated', action='store_true', help='Set CPU partition to isolated')
    cpuset_create.add_argument('-m', '--memory-nodes', type=str, metavar='MEM-NODES', dest='memory_nodes',
                                help='Memory nodes (default: auto-detect from CPUs, e.g., "0" or "0-1")')

    # cpuset list
    cpuset_list = cpuset_subparser.add_parser('list', description='List cpusets', help='List cpusets')
    cpuset_list.add_argument('-p', '--pattern', type=str, metavar='PATTERN', help='Filter by glob pattern (e.g., tuna*)')
    cpuset_list.add_argument('-v', '--verbose', action='store_true', help='Show detailed information (CPUs, tasks, partition type)')
    cpuset_list.add_argument('--show-empty', action='store_true', help='Include cpusets with no CPUs assigned (default: skip empty)')
    cpuset_list.add_argument('--show-system', action='store_true', help='Include systemd-managed cpusets (default: skip system)')
    cpuset_list.add_argument('--no-recursive', dest='recursive', action='store_false', default=True, help='Only search top-level cpusets')

    # cpuset destroy
    cpuset_destroy = cpuset_subparser.add_parser('destroy', description='Destroy cpuset(s)', help='Destroy cpuset(s)')
    cpuset_destroy.add_argument('names', nargs='*', type=str, metavar='NAME', help='Cpuset name(s) to destroy (one or more, OR use --pattern)')
    cpuset_destroy.add_argument('-p', '--pattern', type=str, metavar='PATTERN', help='Glob pattern to destroy multiple cpusets (e.g., tuna*)')
    cpuset_destroy.add_argument('-f', '--force', action='store_true', help='Migrate tasks to root cgroup before destroying')
    cpuset_destroy.add_argument('--include-empty', action='store_true', help='When using --pattern, include cpusets with no CPUs assigned (default: skip empty)')
    cpuset_destroy.add_argument('--no-recursive', dest='recursive', action='store_false', default=True, help='Only destroy top-level cpusets when using --pattern')

    # cpuset move
    cpuset_move = cpuset_subparser.add_parser('move', description='Move processes to a cpuset', help='Move processes to a cpuset')
    cpuset_move.add_argument('name', type=str, metavar='NAME', help='Cpuset name to move processes to')
    cpuset_move.add_argument('-t', '--threads', **MODS['threads'])
    cpuset_move.add_argument('-p', '--pids', dest='pid_list', type=str, metavar='PID-LIST', help='Comma-separated list of PIDs to move')

    # cpuset show
    cpuset_show = cpuset_subparser.add_parser('show', description='Show detailed information about a cpuset', help='Show cpuset details')
    cpuset_show.add_argument('name', type=str, metavar='NAME', help='Cpuset name to show')
    cpuset_show.add_argument('--show-tasks', action='store_true', help='Show detailed list of tasks/processes')

    # cpuset status
    cpuset_status = cpuset_subparser.add_parser('status', description='Show system-wide cpuset overview', help='Show cpuset status')
    cpuset_status.add_argument('-p', '--pattern', type=str, metavar='PATTERN', help='Filter by glob pattern (e.g., tuna*)')
    cpuset_status.add_argument('--show-empty', action='store_true', help='Include cpusets with no CPUs assigned (default: skip empty)')
    cpuset_status.add_argument('--show-system', action='store_true', help='Include systemd-managed cpusets (default: skip system)')
    cpuset_status.add_argument('--no-recursive', dest='recursive', action='store_false', default=True, help='Only show top-level cpusets')

    # cpuset modify
    cpuset_modify = cpuset_subparser.add_parser('modify', description='Modify an existing cpuset', help='Modify cpuset configuration')
    cpuset_modify.add_argument('name', type=str, metavar='NAME', help='Cpuset name to modify')
    cpuset_modify.add_argument('--add-cpus', dest='add_cpu_list', type=str, metavar='CPU-LIST',
                                help='CPUs to add to the cpuset (e.g., 4-7 or 4,5,6,7)')
    cpuset_modify.add_argument('--remove-cpus', dest='remove_cpu_list', type=str, metavar='CPU-LIST',
                                help='CPUs to remove from the cpuset (e.g., 0-1 or 0,1)')
    cpuset_modify.add_argument('-m', '--memory-nodes', type=str, metavar='MEM-NODES', dest='memory_nodes',
                                help='Set memory nodes (e.g., "0" or "0-1")')
    partition_group = cpuset_modify.add_mutually_exclusive_group()
    partition_group.add_argument('--isolated', action='store_true', help='Set CPU partition to isolated')
    partition_group.add_argument('--no-isolated', action='store_true', help='Set CPU partition to member (not isolated)')

    # cpuset save
    cpuset_save = cpuset_subparser.add_parser('save', description='Save cpuset configuration to YAML profile', help='Save cpuset configuration')
    default_profile = get_default_cpuset_profile()
    cpuset_save.add_argument('filename', nargs='?', default=default_profile,
                             help=f'Output YAML file (default: {default_profile})')

    # cpuset apply
    cpuset_apply = cpuset_subparser.add_parser('apply', description='Apply cpuset configuration from YAML profile', help='Apply cpuset configuration')
    cpuset_apply.add_argument('filename', nargs='?', default=default_profile,
                              help=f'Input YAML file (default: {default_profile})')
    cpuset_apply.add_argument('-v', '--verbose', action='store_true',
                              help='Print detailed progress messages')

    return parser


def thread_help(tid, ps):
    if tid not in ps:
        print(f"tuna: thread {tid} doesn't exist!")
        return

    pinfo = ps[tid]
    cmdline = procfs.process_cmdline(pinfo)
    help, title = tuna.kthread_help_plain_text(tid, cmdline)
    print(title, "\n")
    if help.isspace():
        help = "No help description available."
    print(help)


def save_cpusets_cmd(filename):
    """Handler for 'tuna cpuset save' command - saves cpuset configuration to YAML.

    Args:
        filename: Output YAML file path
    """
    from tuna import profile

    # Check cgroup v2 support
    ci = cpuset.CpusetsInit()
    if not ci.supported:
        print("Error: cgroup v2 cpusets not supported on this system", file=sys.stderr)
        sys.exit(1)

    # Save cpusets using profile module (reuses get_all_cpusets_info)
    num_saved = profile.save_cpusets(filename, get_all_cpusets_info)

    if num_saved > 0:
        print(f"Saved {num_saved} cpuset(s) to {filename}")
    else:
        print("No cpusets to save", file=sys.stderr)


def apply_cpusets_cmd(filename, verbose=False):
    """Handler for 'tuna cpuset apply' command - applies cpuset configuration from YAML.

    Args:
        filename: Input YAML file path
        verbose: If True, print detailed progress messages
    """
    from tuna import profile

    # Check cgroup v2 support
    ci = cpuset.CpusetsInit()
    if not ci.supported:
        print("Error: cgroup v2 cpusets not supported on this system", file=sys.stderr)
        sys.exit(1)

    # Resolve filename (search current dir, then /etc/tuna/)
    resolved_path = resolve_cpuset_profile_path(filename)

    # Apply cpusets using profile module (passes cpuset module for code reuse)
    num_applied, num_warnings, num_errors = profile.apply_cpusets(resolved_path, cpuset, verbose)

    # Print summary
    if num_applied > 0:
        print(f"Applied {num_applied} cpuset(s) from {filename}")
    if num_warnings > 0:
        print(f"Warnings: {num_warnings}", file=sys.stderr)
    if num_errors > 0:
        print(f"Errors: {num_errors}", file=sys.stderr)
        sys.exit(1)


def extract_cpuset_name(cgroup_path):
    """
    Extract cpuset/cgroup name from full cgroup path for display.

    Examples:
        "0::/tuna0" → "tuna0"
        "0::/" → ""
        "0::/system.slice/ssh.service" → "system.slice"
        "0::/user.slice/user-1000.slice/session-2.scope" → "user.slice"

    Args:
        cgroup_path: Full cgroup path from /proc/[pid]/cgroup

    Returns:
        First component of the cgroup path (cpuset name or top-level cgroup)
    """
    if not cgroup_path:
        return ""

    # Remove "0::" prefix
    if cgroup_path.startswith("0::"):
        cgroup_path = cgroup_path[3:]

    # Remove leading slash
    if cgroup_path.startswith("/"):
        cgroup_path = cgroup_path[1:]

    # Return empty string for root cgroup
    if not cgroup_path:
        return ""

    # Return first component (cpuset name or top-level cgroup like "system.slice")
    return cgroup_path.split('/')[0]


def ps_show_header(has_ctxt_switch_info, cgroups=False):
    print("%7s %6s %5s %7s       %s" %
          (" ", " ", " ", _("thread"),
           has_ctxt_switch_info and "ctxt_switches" or ""))
    print("%7s %6s %5s %7s%s %15s" % ("pid", "SCHED_", "rtpri", "affinity",
                                      has_ctxt_switch_info and " %9s %12s" % (
                                          "voluntary", "nonvoluntary")
                                      or "", "cmd"), end=' ')
    print(" %7s" % ("cpuset") if cgroups else "")


def ps_show_sockets(pid, ps, inodes, inode_re, indent=0):
    header_printed = False
    dirname = f"/proc/{pid}/fd"
    try:
        filenames = os.listdir(dirname)
    except OSError:  # Process died
        return
    sindent = " " * indent
    for filename in filenames:
        pathname = os.path.join(dirname, filename)
        try:
            linkto = os.readlink(pathname)
        except OSError:  # Process died
            continue
        inode_match = inode_re.match(linkto)
        if not inode_match:
            continue
        inode = int(inode_match.group(1))
        if inode not in inodes:
            continue
        if not header_printed:
            print("%s%-10s %-6s %-6s %15s:%-5s %15s:%-5s" %
                  (sindent, "State", "Recv-Q", "Send-Q",
                   "Local Address", "Port",
                   "Peer Address", "Port"))
            header_printed = True
        s = inodes[inode]
        print("%s%-10s %-6d %-6d %15s:%-5d %15s:%-5d" %
              (sindent, s.state(),
               s.receive_queue(), s.write_queue(),
               s.saddr(), s.sport(), s.daddr(), s.dport()))


def format_affinity(affinity):
    if len(affinity) <= 4:
        return ",".join(str(a) for a in affinity)

    return ",".join(str(hex(a)) for a in procfs.hexbitmask(affinity, utils.get_nr_cpus()))

def ps_show_thread(pid, affect_children, ps, has_ctxt_switch_info, sock_inodes,
                   sock_inode_re, cgroups, irqs, columns=None, compact=True):
    try:
        affinity = format_affinity(os.sched_getaffinity(pid))
    except OSError as e:
        if e.args[0] == errno.ESRCH:
            return
        raise e

    sched = tuna_sched.sched_str(os.sched_getscheduler(pid))[6:]
    rtprio = int(ps[pid]["stat"]["rt_priority"])
    cgroup_path = ps[pid]["cgroups"]
    cpuset_name = extract_cpuset_name(cgroup_path)
    cmd = ps[pid]["stat"]["comm"]
    users = ""
    if tuna.is_irq_thread(cmd):
        try:
            users = irqs[tuna.irq_thread_number(cmd)]["users"]
            for u in users:
                if u in utils.get_nics():
                    users[users.index(u)] = "%s(%s)" % (
                        u, ethtool.get_module(u))
            users = ",".join(users)
        except (KeyError, ValueError, LookupError):
            users = "Not found in /proc/interrupts!"

    ctxt_switch_info = ""
    if has_ctxt_switch_info:
        voluntary_ctxt_switches = int(
            ps[pid]["status"]["voluntary_ctxt_switches"])
        nonvoluntary_ctxt_switches = int(
            ps[pid]["status"]["nonvoluntary_ctxt_switches"])
        ctxt_switch_info = " %9d %12s" % (voluntary_ctxt_switches,
                                          nonvoluntary_ctxt_switches)

    # Indent affected children
    s1 = " %-5d " % pid if affect_children else "  %-5d" % pid
    print(s1, end=' ')
    s2 = "%6s %5d %8s%s %15s     %s" % (sched, rtprio, affinity,
                                     ctxt_switch_info, cmd, users)
    print(s2, end=' ')

    if cgroups:
        length = int(columns) - len(s1 + " ") - len(s2 + " ")
        if len(" %9s" % cpuset_name) <= length:
            print("%s" % cpuset_name)
        else:
            print("\n %s" % cpuset_name + ("" if compact else "\n"))
    else:
        print()

    if sock_inodes:
        ps_show_sockets(pid, ps, sock_inodes, sock_inode_re,
                        affect_children and 3 or 4)
    if affect_children and "threads" in ps[pid]:
        for tid in list(ps[pid]["threads"].keys()):
            ps_show_thread(tid, False, ps[pid]["threads"],
                           has_ctxt_switch_info,
                           sock_inodes, sock_inode_re, cgroups, irqs, columns, compact)


def ps_show(ps, affect_children, thread_list, cpu_list,
            irq_list_numbers, show_uthreads, show_kthreads,
            has_ctxt_switch_info, sock_inodes, sock_inode_re, cgroups, compact, match_requested, irqs, cpuset_filter=None):

    ps_list = []
    for pid in list(ps.keys()):
        iskth = tuna.iskthread(pid)
        if not show_uthreads and not iskth:
            continue
        if not show_kthreads and iskth:
            continue
        in_irq_list = False
        if irq_list_numbers:
            if tuna.is_hardirq_handler(ps, pid):
                try:
                    irq = int(ps[pid]["stat"]["comm"][4:])
                    if irq not in irq_list_numbers:
                        if not thread_list:
                            continue
                    else:
                        in_irq_list = True
                except (ValueError, KeyError):
                    pass
            elif not thread_list:
                continue
        if not in_irq_list and thread_list and pid not in thread_list:
            continue
        try:
            affinity = os.sched_getaffinity(pid)
        except OSError as e:
            if e.args[0] == errno.ESRCH:
                continue
            raise e
        if cpu_list and not set(cpu_list).intersection(set(affinity)):
            continue
        # Filter by cpuset if requested
        if cpuset_filter:
            # Extract cpuset name from cgroup path
            # Format: "0::/cpuset_name" or "0::/" for root
            cgroup_path = ps[pid]["cgroups"]
            if cgroup_path.startswith("0::"):
                cgroup_path = cgroup_path[3:]  # Remove "0::" prefix
            # Remove leading slash to get cpuset name
            if cgroup_path.startswith("/"):
                cgroup_path = cgroup_path[1:]
            # Get the first component (cpuset name)
            # For nested cpusets like "parent/child", this gets "parent"
            cpuset_name = cgroup_path.split('/')[0] if cgroup_path else ""
            # Check if process is in the requested cpuset
            if cpuset_name != cpuset_filter:
                continue
        if match_requested and thread_list and pid in thread_list:
            ps_list.append(pid)
        elif not match_requested:
            ps_list.append(pid)


    ps_list.sort()


    # Width of terminal in columns
    columns = 80
    if cgroups:
        if os.isatty(sys.stdout.fileno()):
            columns = shutil.get_terminal_size().columns

    for pid in ps_list:
        ps_show_thread(pid, affect_children, ps, has_ctxt_switch_info,
                       sock_inodes, sock_inode_re, cgroups, irqs, columns, compact)


def load_socktype(socktype, inodes):
    idiag = inet_diag.create(socktype=socktype)
    while True:
        try:
            s = idiag.get()
        except (OSError, StopIteration):
            break
        inodes[s.inode()] = s


def load_sockets():
    inodes = {}
    for socktype in (inet_diag.TCPDIAG_GETSOCK, inet_diag.DCCPDIAG_GETSOCK):
        load_socktype(socktype, inodes)
    return inodes


def do_ps(thread_list, cpu_list, irq_list, show_uthreads, show_kthreads,
          affect_children, show_sockets, cgroups, compact, match_requested, cpuset_filter=None):
    ps = procfs.pidstats()
    if affect_children:
        ps.reload_threads()

    irqs = procfs.interrupts()

    sock_inodes = None
    sock_inode_re = None
    if show_sockets:
        sock_inodes = load_sockets()
        sock_inode_re = re.compile(r"socket:\[(\d+)\]")

    has_ctxt_switch_info = "voluntary_ctxt_switches" in ps[1]["status"]
    try:
        if sys.stdout.isatty():
            ps_show_header(has_ctxt_switch_info, cgroups)
        ps_show(ps, affect_children, thread_list,
                cpu_list, irq_list, show_uthreads, show_kthreads,
                has_ctxt_switch_info, sock_inodes, sock_inode_re, cgroups, compact, match_requested, irqs, cpuset_filter)
    except IOError:
        # 'tuna -P | head' for instance
        pass


def find_drivers_by_users(users):
    nics = utils.get_nics()
    drivers = []
    for u in users:
        try:
            idx = u.index('-')
            u = u[:idx]
        except ValueError:
            pass
        if u in nics:
            driver = ethtool.get_module(u)
            if driver not in drivers:
                drivers.append(driver)

    return drivers


def show_irqs(irq_list, cpu_list, match_requested):
    irqs = procfs.interrupts()

    if sys.stdout.isatty():
        print("%4s %-16s %8s" % ("#", _("users"), _("affinity"),))
    sorted_irqs = []
    for k in list(irqs.keys()):
        try:
            irqn = int(k)
            affinity = irqs[irqn]["affinity"]
        except:
            continue
        if irq_list and irqn not in irq_list:
            continue

        if cpu_list and not set(cpu_list).intersection(set(affinity)):
            continue

        if match_requested and irq_list and irqn in irq_list:
            sorted_irqs.append(irqn)
        elif not match_requested:
            sorted_irqs.append(irqn)

    sorted_irqs.sort()
    for irq in sorted_irqs:
        affinity = format_affinity(irqs[irq]["affinity"])
        users = irqs[irq]["users"]
        print("%4d %-16s %8s" % (irq, ",".join(users), affinity), end=' ')
        drivers = find_drivers_by_users(users)
        if drivers:
            print(" %s" % ",".join(drivers))
        else:
            print()


def do_list_op(op, current_list, op_list):
    if not current_list:
        current_list = []
    if op == '+':
        return list(set(current_list + op_list))
    if op == '-':
        return list(set(current_list) - set(op_list))
    return list(set(op_list))

def threadstring_to_list(threadstr, ps=None):
    """Convert a thread specifier string to a list of thread IDs.

    Accepts flexible input formats with comma and/or space separators.
    Thread specifiers can be numeric PIDs or name patterns with wildcards.

    Args:
        threadstr: String of thread specifiers. Can be:
                  - Numeric PIDs: "1", "1,2,3", "1, 2, 3", "1 2 3"
                  - Thread name patterns: "systemd*", "kworker/*"
                  - Mixed: "1,systemd*,100", "1 systemd* 100"
                  Patterns use shell-style wildcards (* and ?).
        ps: Optional procfs.pidstats() object. If None, will be created
            on demand when needed to resolve name patterns.

    Returns:
        Tuple of (thread_list, match_requested) where:
        - thread_list: List of integer PIDs that match the specifiers
        - match_requested: Boolean indicating if any filtering was requested
                          (True if threadstr is non-empty, False otherwise)

    Note:
        The match_requested flag is used by display functions to determine
        whether to show only matched threads or all threads in the system.
        Empty input returns ([], False) to indicate "show all threads".
    """
    match_requested = bool(threadstr)
    thread_list = []
    if not threadstr:
        return thread_list, match_requested
    # Split by both comma and whitespace to handle "811,950" and "811, 950" and "811 950"
    import re
    thread_strings = list(set(s.strip() for s in re.split(r'[,\s]+', threadstr) if s.strip()))
    for s in thread_strings:
        if s.isdigit():
            thread_list.append(int(s))
        else:
            if ps is None:
                ps = procfs.pidstats()
            try:
                thread_list += ps.find_by_regex(re.compile(fnmatch.translate(s)))
            except re.error:
                thread_list += ps.find_by_name(s)
    return thread_list, match_requested

def irqstring_to_list(irqstr):
    """Convert an IRQ specifier string to a list of IRQ numbers.

    Accepts flexible input formats with comma and/or space separators.
    IRQ specifiers can be numeric IRQ numbers or IRQ user patterns with wildcards.

    Args:
        irqstr: String of IRQ specifiers. Can be:
                - Numeric IRQ numbers: "50", "50,51,52", "50, 51, 52", "50 51 52"
                - IRQ user patterns: "eth0*", "nvme*"
                - Mixed: "50,eth0*,100", "50 eth0* 100"
                Patterns use shell-style wildcards (* and ?) matched against
                IRQ user names from /proc/interrupts.

    Returns:
        Tuple of (irq_list, match_requested) where:
        - irq_list: List of integer IRQ numbers that match the specifiers
        - match_requested: Boolean indicating if any filtering was requested
                          (True if irqstr is non-empty, False otherwise)

    Note:
        The match_requested flag is used by display functions to determine
        whether to show only matched IRQs or all IRQs in the system.
        Empty input returns ([], False) to indicate "show all IRQs".
    """
    match_requested = bool(irqstr)
    irq_list = []
    if not irqstr:
        return irq_list, match_requested
    # Split by both comma and whitespace
    import re
    irq_strings = list(set(s.strip() for s in re.split(r'[,\s]+', irqstr) if s.strip()))
    for s in irq_strings:
        if s.isdigit():
            irq_list.append(int(s))
        else:
            # find_by_user_regex returns a list of strings corresponding to irq number
            irq_list_str = procfs.interrupts().find_by_user_regex(re.compile(fnmatch.translate(s)))
            irq_list += [int(i) for i in irq_list_str if i.isdigit()]
    return irq_list, match_requested

def socketstring_to_list(socketstr):
    """Convert a CPU socket specifier string to a list of CPU numbers.

    Accepts flexible input formats with comma and/or space separators.
    Socket specifiers are socket IDs that get expanded to all CPUs in those sockets.

    Args:
        socketstr: String of socket specifiers. Can be:
                  - Single socket: "0"
                  - Multiple sockets: "0,1", "0, 1", "0 1"
                  Socket IDs must be valid socket numbers present in the system.

    Returns:
        List of integer CPU numbers belonging to the specified sockets.
        CPUs are returned in the order they appear in sysfs.

    Raises:
        SystemExit: If any socket ID is invalid. Prints available sockets
                   and exits with code 2.

    Note:
        This function queries sysfs to discover CPU topology and validate
        socket IDs. Invalid socket IDs cause immediate program termination
        with an error message showing valid options.
    """
    cpu_list = []
    # Split by both comma and whitespace
    import re
    socket_strings = list(set(s.strip() for s in re.split(r'[,\s]+', socketstr) if s.strip()))
    cpu_info = sysfs.cpus()

    for s in socket_strings:
        if s not in cpu_info.sockets:
            print("tuna: invalid socket %(socket)s sockets available: %(available)s" %
                    {"socket": s,"available": ",".join(list(cpu_info.sockets.keys()))})
            sys.exit(2)
        cpu_list += [int(cpu.name[3:]) for cpu in cpu_info.sockets[s]]
    return cpu_list

def pick_op(argument):
    """Extract an operation prefix from an argument string.

    Checks if the argument starts with a '+' or '-' operation prefix.
    If present, returns the operation and the remaining string.

    Args:
        argument: String to check for operation prefix.

    Returns:
        Tuple of (operation, remaining_string) where:
        - operation: '+' for addition, '-' for removal, None for replacement
        - remaining_string: The argument with the operation prefix removed,
                           or the original argument if no prefix was found

    Examples:
        >>> pick_op("+1,2,3")
        ('+', '1,2,3')
        >>> pick_op("-0,1")
        ('-', '0,1')
        >>> pick_op("1,2,3")
        (None, '1,2,3')
        >>> pick_op("")
        (None, '')
    """
    if argument == "":
        return (None, argument)
    if argument[0] in ('+', '-'):
        return (argument[0], argument[1:])
    return (None, argument)


def i18n_init():
    (app, localedir) = ('tuna', '/usr/share/locale')
    locale.setlocale(locale.LC_ALL, '')
    gettext.bindtextdomain(app, localedir)
    gettext.textdomain(app)
    gettext.install(app, localedir)


def list_config():
    from tuna.config import Config
    config = Config()
    print(_("Preloaded config files:"))
    for value in config.populate():
        print(value)
    sys.exit(1)

def nohz_full_to_cpu():

    try:
        return tuna.nohz_full_list()
    except:
        print("tuna: --nohz_full " +
              _(" needs nohz_full=cpulist on the kernel command line"))
        sys.exit(2)


def get_next_tuna_cpuset_name():
    """Find next available tunaN name for auto-generated cpusets.

    Scans existing cpusets matching pattern 'tuna[0-9]*' and returns
    the first available number in the sequence (fills gaps).

    Returns:
        str: Next available cpuset name (e.g., 'tuna0', 'tuna1')
    """
    existing = cpuset.list_cpusets(pattern='tuna[0-9]*', recursive=False)

    # Extract numbers from tuna0, tuna1, etc.
    numbers = []
    for name in existing:
        match = re.match(r'tuna(\d+)', name)
        if match:
            numbers.append(int(match.group(1)))

    # Find first gap in sequence, or use 0 if none exist
    if not numbers:
        return 'tuna0'

    # Find first gap in sequence
    for i in range(max(numbers) + 2):
        if i not in numbers:
            return f'tuna{i}'


def get_cpuset_info(cpuset_name):
    """Get detailed information about a cpuset.

    Args:
        cpuset_name: Name of the cpuset (e.g., 'tuna0')

    Returns:
        dict: Cpuset information with keys:
            - name: cpuset name
            - path: full path to cpuset
            - cpus: CPU list string (e.g., "0-3")
            - mems: Memory nodes string (e.g., "0")
            - tasks: List of PIDs
            - task_count: Number of tasks
            - partition: Partition type (isolated/member/root/n/a)
            - blocklisted_pids: List of (pid, comm) tuples for blocklisted processes
            - error: Error message if failed to read info

    """
    cs_path = os.path.join('/sys/fs/cgroup', cpuset_name)
    info = {
        'name': cpuset_name,
        'path': cs_path,
        'cpus': '',
        'mems': '',
        'tasks': [],
        'task_count': 0,
        'partition': 'n/a',
        'blocklisted_pids': [],
        'error': None
    }

    try:
        # Read CPUs
        with open(os.path.join(cs_path, 'cpuset.cpus'), 'r', encoding='utf-8') as f:
            info['cpus'] = f.read().strip()

        # Read memory nodes
        with open(os.path.join(cs_path, 'cpuset.mems'), 'r', encoding='utf-8') as f:
            info['mems'] = f.read().strip()

        # Read tasks
        with open(os.path.join(cs_path, 'cgroup.procs'), 'r', encoding='utf-8') as f:
            info['tasks'] = [line.strip() for line in f if line.strip()]
            info['task_count'] = len(info['tasks'])

        # Read partition type (if available)
        partition_file = os.path.join(cs_path, 'cpuset.cpus.partition')
        if os.path.exists(partition_file):
            with open(partition_file, 'r', encoding='utf-8') as f:
                info['partition'] = f.read().strip()

        # Check for blocklisted processes (skip system cgroups)
        if not cpuset_name.endswith(('.mount', '.scope', '.slice')):
            for pid_str in info['tasks']:
                try:
                    pid = int(pid_str)
                    if cpuset.Cpuset._is_process_blocklisted(pid):
                        comm = cpuset.Cpuset._get_process_name(pid)
                        info['blocklisted_pids'].append((pid, comm))
                except ValueError:
                    pass

    except (OSError, IOError) as e:
        info['error'] = str(e)

    return info


def get_all_cpusets_info(pattern=None, recursive=True, skip_empty=False, skip_system=False):
    """Get information about all cpusets matching criteria.

    Args:
        pattern: Glob pattern to filter cpusets (None for all)
        recursive: If True, search nested cpusets
        skip_empty: If True, skip cpusets with no CPUs assigned
        skip_system: If True, skip systemd-managed cpusets (.slice, .scope, .mount, .service)

    Returns:
        list: List of cpuset info dictionaries (from get_cpuset_info)
    """
    cpusets = cpuset.list_cpusets(pattern=pattern, recursive=recursive)
    infos = []

    # System cpuset suffixes to filter
    system_suffixes = ('.slice', '.scope', '.mount', '.service')

    for cs_name in cpusets:
        info = get_cpuset_info(cs_name)

        # Skip empty cpusets if requested
        if skip_empty and not info['cpus']:
            continue

        # Skip system cpusets if requested
        if skip_system and cs_name.endswith(system_suffixes):
            continue

        infos.append(info)

    return infos


def cpuset_create(cpu_list, name, isolated, memory_nodes=None):
    """Handler for 'tuna cpuset create' command.

    Args:
        cpu_list: List of CPU numbers to assign to cpuset
        name: Cpuset name (None for auto-generated tunaN)
        isolated: If True, set CPU partition to isolated
        memory_nodes: Memory nodes string (None for auto-detect from CPUs)
    """
    # Check cgroup v2 support
    ci = cpuset.CpusetsInit()
    if not ci.supported:
        print("Error: cgroup v2 cpusets not supported on this system", file=sys.stderr)
        sys.exit(1)

    # Auto-generate name if not provided
    if name is None:
        name = get_next_tuna_cpuset_name()

    # Auto-detect NUMA memory nodes if not specified
    if memory_nodes is None:
        memory_nodes = cpuset.get_numa_nodes_for_cpus(cpu_list)

    # Convert CPU list to string format (e.g., [0,1,2,3] -> "0-3")
    cpu_str = tuna.collapse_cpulist(cpu_list)

    try:
        # Create cpuset
        cs = cpuset.Cpuset(name)

        # Configure it (must set memnode before CPUs in cgroup v2)
        cs.write_memnode(memory_nodes)
        cs.assign_cpus(cpu_str)

        # Set isolated partition if requested
        if isolated:
            cs.set_cpu_exclusive()

        print(f"Created cpuset '{name}' with CPUs {cpu_str}, memory nodes {memory_nodes}" +
              (f" (isolated)" if isolated else ""))

    except (OSError, ValueError) as e:
        print(f"Error creating cpuset: {e}", file=sys.stderr)
        sys.exit(1)


def cpuset_list(pattern, verbose, show_empty, show_system, recursive):
    """Handler for 'tuna cpuset list' command.

    Args:
        pattern: Glob pattern to filter cpusets (None for all)
        verbose: If True, show detailed info (CPUs, memory nodes, tasks, partition)
        show_empty: If True, show cpusets with no CPUs assigned (default: skip empty)
        show_system: If True, show systemd-managed cpusets (default: skip system)
        recursive: If True, search nested cpusets
    """
    # Check cgroup v2 support
    ci = cpuset.CpusetsInit()
    if not ci.supported:
        print("Error: cgroup v2 cpusets not supported on this system", file=sys.stderr)
        sys.exit(1)

    # Get cpuset info using centralized function (skip_* params are inverse of show_*)
    infos = get_all_cpusets_info(pattern=pattern, recursive=recursive,
                                  skip_empty=not show_empty, skip_system=not show_system)

    if not infos:
        if pattern:
            print(f"No cpusets found matching pattern '{pattern}'")
        else:
            print("No cpusets found")
        return

    if verbose:
        # Verbose output: NAME, CPUS, MEMS, TASKS, PARTITION
        print(f"{'NAME':<20} {'CPUS':<15} {'MEMS':<10} {'TASKS':<7} {'PARTITION':<10}")
        print("-" * 65)

        for info in infos:
            cpus = info['cpus'] or "(none)"
            mems = info['mems'] or "(none)"
            print(f"{info['name']:<20} {cpus:<15} {mems:<10} {info['task_count']:<7} {info['partition']:<10}")
    else:
        # Simple output: just names
        for info in infos:
            print(info['name'])

    # Check for blocklisted processes (already collected by get_cpuset_info)
    warnings = []
    for info in infos:
        if info['blocklisted_pids']:
            for pid, comm in info['blocklisted_pids']:
                warnings.append(f"⚠️  WARNING: Blocklisted process PID {pid} ({comm}) found in cpuset '{info['name']}'")

    # Display warnings if any
    if warnings:
        print()
        for warning in warnings:
            print(warning, file=sys.stderr)
        print("\nBlocklisted processes should not be in custom cpusets!", file=sys.stderr)
        print("This may indicate a problem with blocklist protection.", file=sys.stderr)


def cpuset_destroy(names, pattern, force, include_empty, recursive):
    """Handler for 'tuna cpuset destroy' command.

    Args:
        names: List of cpuset names to destroy (mutually exclusive with pattern)
        pattern: Glob pattern for cpusets to destroy (mutually exclusive with names)
        force: If True, migrate tasks to root before destroying
        include_empty: If True, include cpusets with no CPUs assigned (default: skip empty, only used with pattern)
        recursive: If True, find and clean nested cpusets (only used with pattern)
    """
    # Check cgroup v2 support
    ci = cpuset.CpusetsInit()
    if not ci.supported:
        print("Error: cgroup v2 cpusets not supported on this system", file=sys.stderr)
        sys.exit(1)

    # Validate: must have either names OR pattern, not both, not neither
    if names and pattern:
        print("Error: specify either NAME(s) or --pattern, not both", file=sys.stderr)
        sys.exit(2)
    if not names and not pattern:
        print("Error: must specify either NAME(s) or --pattern", file=sys.stderr)
        sys.exit(2)

    try:
        if names:
            # Destroy specified cpusets by name (one or more)
            for name in names:
                cpuset.destroy_cpuset(name, force=force)
                print(f"Destroyed cpuset '{name}'")
        else:
            # Pattern-based cleanup
            if not include_empty:
                # Filter out empty cpusets before destroying
                all_cpusets = cpuset.list_cpusets(pattern=pattern, recursive=recursive)
                cpusets_to_destroy = []

                for cs_name in all_cpusets:
                    cs_path = os.path.join('/sys/fs/cgroup', cs_name)
                    try:
                        with open(os.path.join(cs_path, 'cpuset.cpus'), 'r', encoding='utf-8') as f:
                            cpus = f.read().strip()
                            if cpus:  # Only destroy if CPUs are assigned
                                cpusets_to_destroy.append(cs_name)
                    except (OSError, IOError):
                        # If we can't read it, skip it
                        pass

                # Destroy each non-empty cpuset individually
                if not cpusets_to_destroy:
                    print(f"No non-empty cpusets found matching pattern '{pattern}'")
                    return

                # Sort in reverse order by depth to ensure children destroyed before parents
                cpusets_by_depth = sorted(cpusets_to_destroy, key=lambda x: x.count(os.sep), reverse=True)

                for cs_name in cpusets_by_depth:
                    try:
                        cpuset.destroy_cpuset(cs_name, force=force)
                        print(f"Destroyed cpuset '{cs_name}'")
                    except (OSError, ValueError) as e:
                        print(f"Error destroying cpuset '{cs_name}': {e}", file=sys.stderr)
            else:
                # Use cleanup_cpusets for normal pattern-based destroy
                cpuset.cleanup_cpusets(pattern, force=force, recursive=recursive)
    except (OSError, ValueError) as e:
        print(f"Error destroying cpuset(s): {e}", file=sys.stderr)
        sys.exit(1)


def cpuset_move(name, thread_list, pid_list):
    """Handler for 'tuna cpuset move' command.

    Args:
        name: Cpuset name to move processes to
        thread_list: Thread list (already converted to list of PIDs by main())
        pid_list: PID list string (comma-separated PIDs only)
    """
    # Check cgroup v2 support
    ci = cpuset.CpusetsInit()
    if not ci.supported:
        print("Error: cgroup v2 cpusets not supported on this system", file=sys.stderr)
        sys.exit(1)

    # Validate: must specify at least one of thread_list or pid_list
    if not thread_list and not pid_list:
        print("Error: must specify either --threads or --pids", file=sys.stderr)
        sys.exit(2)

    # Check if cpuset exists
    cpuset_path = os.path.join('/sys/fs/cgroup', name)
    if not os.path.exists(cpuset_path):
        print(f"Error: cpuset '{name}' does not exist", file=sys.stderr)
        sys.exit(1)

    # Create cpuset object for the target cpuset
    try:
        cs = cpuset.Cpuset(name, existing_ok=True)
    except (OSError, ValueError) as e:
        print(f"Error accessing cpuset '{name}': {e}", file=sys.stderr)
        sys.exit(1)

    # Build list of PIDs to move
    pids = []

    # Process thread_list if provided (already converted to list of PIDs by main())
    if thread_list:
        # thread_list is already a list of integers from main()'s conversion
        pids.extend(thread_list)

    # Process pid_list if provided (simple comma-separated PIDs)
    if pid_list:
        import re
        pid_strings = [s.strip() for s in re.split(r'[,\s]+', pid_list) if s.strip()]
        for pid_str in pid_strings:
            if pid_str.isdigit():
                pids.append(int(pid_str))
            else:
                print(f"Warning: ignoring invalid PID '{pid_str}'", file=sys.stderr)

    # Remove duplicates
    pids = list(set(pids))

    if not pids:
        print("Error: no valid PIDs to move", file=sys.stderr)
        sys.exit(2)

    # Move each PID to the cpuset
    success_count = 0
    fail_count = 0
    blocked_count = 0

    for pid in pids:
        # Check if process is on the blocklist before attempting to move
        if cpuset.Cpuset._is_process_blocklisted(pid):
            comm = cpuset.Cpuset._get_process_name(pid)
            print(f"Skipping PID {pid} ({comm}): critical system process", file=sys.stderr)
            fail_count += 1
            blocked_count += 1
        elif cs.write_pid(pid):
            success_count += 1
        else:
            # write_pid returns False on failure (permission, process gone, etc.)
            print(f"Warning: failed to move PID {pid} to cpuset '{name}'", file=sys.stderr)
            fail_count += 1

    # Print summary
    print(f"Moved {success_count} process(es) to cpuset '{name}'", end='')
    if fail_count > 0:
        print(f" ({fail_count} failed)")
    else:
        print()

    # Exit with error code if all moves failed
    if success_count == 0:
        sys.exit(1)


def cpuset_show(name, show_tasks):
    """Handler for 'tuna cpuset show' command.

    Args:
        name: Cpuset name to show details for
        show_tasks: If True, show detailed task list
    """
    # Check cgroup v2 support
    ci = cpuset.CpusetsInit()
    if not ci.supported:
        print("Error: cgroup v2 cpusets not supported on this system", file=sys.stderr)
        sys.exit(1)

    # Check if cpuset exists
    cpuset_path = os.path.join('/sys/fs/cgroup', name)
    if not os.path.exists(cpuset_path):
        print(f"Error: cpuset '{name}' does not exist", file=sys.stderr)
        sys.exit(1)

    # Get cpuset info
    info = get_cpuset_info(name)

    if info['error']:
        print(f"Error reading cpuset info: {info['error']}", file=sys.stderr)
        sys.exit(1)

    # Print detailed information
    print(f"Cpuset: {name}")
    print(f"Path: {info['path']}")
    print(f"CPUs: {info['cpus'] or '(none)'}")
    print(f"Memory Nodes: {info['mems'] or '(none)'}")
    print(f"Partition: {info['partition']}")
    print(f"Task Count: {info['task_count']}")

    # Show task details if requested
    if show_tasks and info['tasks']:
        print(f"\nTasks ({len(info['tasks'])} total):")
        print(f"{'PID':<10} {'COMMAND':<30} {'STATUS':<10}")
        print("-" * 52)

        for pid_str in info['tasks']:
            try:
                pid = int(pid_str)
                comm = cpuset.Cpuset._get_process_name(pid)
                is_blocked = cpuset.Cpuset._is_process_blocklisted(pid)
                status = "BLOCKED" if is_blocked else "ok"
                print(f"{pid:<10} {comm:<30} {status:<10}")
            except (ValueError, OSError):
                print(f"{pid_str:<10} {'(error)':<30} {'error':<10}")

    # Show blocklist warnings
    if info['blocklisted_pids']:
        print()
        print("⚠️  WARNINGS:")
        for pid, comm in info['blocklisted_pids']:
            print(f"  - Blocklisted process PID {pid} ({comm})", file=sys.stderr)
        print("  These critical system processes should not be in custom cpusets!", file=sys.stderr)


def cpuset_status(pattern, show_empty, show_system, recursive):
    """Handler for 'tuna cpuset status' command.

    Args:
        pattern: Glob pattern to filter cpusets (None for all)
        show_empty: If True, show cpusets with no CPUs assigned (default: skip empty)
        show_system: If True, show systemd-managed cpusets (default: skip system)
        recursive: If True, search nested cpusets
    """
    # Check cgroup v2 support
    ci = cpuset.CpusetsInit()
    if not ci.supported:
        print("Error: cgroup v2 cpusets not supported on this system", file=sys.stderr)
        sys.exit(1)

    # Get all cpusets info (skip_* params are inverse of show_*)
    infos = get_all_cpusets_info(pattern=pattern, recursive=recursive,
                                  skip_empty=not show_empty, skip_system=not show_system)

    if not infos:
        if pattern:
            print(f"No cpusets found matching pattern '{pattern}'")
        else:
            print("No cpusets found")
        return

    # Calculate system-wide statistics
    total_cpus_assigned = set()
    total_tasks = 0
    isolated_cpusets = []
    cpusets_with_tasks = []

    for info in infos:
        if info['cpus']:
            # Parse CPU list to get individual CPUs
            try:
                cpu_list = tuna.expand_cpulist(info['cpus'])
                total_cpus_assigned.update(cpu_list)
            except (ValueError, IndexError):
                pass

        total_tasks += info['task_count']

        if info['partition'] == 'isolated':
            isolated_cpusets.append(info['name'])

        if info['task_count'] > 0:
            cpusets_with_tasks.append(info['name'])

    # Print system overview
    print("=" * 70)
    print("SYSTEM CPUSET OVERVIEW")
    print("=" * 70)
    print(f"Total cpusets: {len(infos)}")
    print(f"CPUs assigned: {len(total_cpus_assigned)}")
    print(f"Total tasks: {total_tasks}")
    print(f"Isolated cpusets: {len(isolated_cpusets)}")
    print()

    # Print cpuset summary table
    print(f"{'NAME':<25} {'CPUS':<20} {'MEMS':<8} {'TASKS':<7} {'PARTITION':<10}")
    print("-" * 75)

    for info in infos:
        cpus_display = info['cpus'] or "(none)"
        mems_display = info['mems'] or "(none)"

        # Truncate long names with ellipsis
        name_display = info['name']
        if len(name_display) > 24:
            name_display = name_display[:21] + "..."

        # Truncate long CPU lists
        if len(cpus_display) > 19:
            cpus_display = cpus_display[:16] + "..."

        print(f"{name_display:<25} {cpus_display:<20} {mems_display:<8} {info['task_count']:<7} {info['partition']:<10}")

    # Show isolated cpusets if any
    if isolated_cpusets:
        print()
        print(f"Isolated cpusets ({len(isolated_cpusets)}):")
        for name in isolated_cpusets:
            print(f"  - {name}")

    # Check for blocklisted processes across all cpusets
    warnings = []
    for info in infos:
        if info['blocklisted_pids']:
            for pid, comm in info['blocklisted_pids']:
                warnings.append((info['name'], pid, comm))

    if warnings:
        print()
        print("⚠️  WARNINGS - Blocklisted processes found:")
        for cpuset_name, pid, comm in warnings:
            print(f"  - {cpuset_name}: PID {pid} ({comm})", file=sys.stderr)
        print("  These critical system processes should not be in custom cpusets!", file=sys.stderr)


def cpuset_modify(name, add_cpus, remove_cpus, memory_nodes, isolated, no_isolated):
    """Handler for 'tuna cpuset modify' command.

    Args:
        name: Cpuset name to modify
        add_cpus: List of CPU numbers to add (or None)
        remove_cpus: List of CPU numbers to remove (or None)
        memory_nodes: Memory nodes string to set (or None to keep current)
        isolated: If True, set partition to isolated
        no_isolated: If True, set partition to member
    """
    # Check cgroup v2 support
    ci = cpuset.CpusetsInit()
    if not ci.supported:
        print("Error: cgroup v2 cpusets not supported on this system", file=sys.stderr)
        sys.exit(1)

    # Validate name
    cpuset_path = os.path.join('/sys/fs/cgroup', name)
    if not os.path.exists(cpuset_path):
        print(f"Error: cpuset '{name}' does not exist", file=sys.stderr)
        sys.exit(1)

    # At least one modification must be specified
    if not any([add_cpus, remove_cpus, memory_nodes is not None, isolated, no_isolated]):
        print("Error: at least one modification option must be specified", file=sys.stderr)
        print("  --add-cpus, --remove-cpus, --memory-nodes, --isolated, or --no-isolated", file=sys.stderr)
        sys.exit(1)

    # Can't specify both --isolated and --no-isolated
    if isolated and no_isolated:
        print("Error: --isolated and --no-isolated are mutually exclusive", file=sys.stderr)
        sys.exit(1)

    try:
        # Create Cpuset object for existing cpuset
        cs = cpuset.Cpuset(name, existing_ok=True)

        changes = []

        # Modify CPUs if requested
        if add_cpus or remove_cpus:
            old_cpus = cs.get_cpus()
            cs.modify_cpus(add_cpus=add_cpus, remove_cpus=remove_cpus)
            new_cpus = cs.get_cpus()
            changes.append(f"CPUs: {old_cpus or '(none)'} → {new_cpus or '(none)'}")

        # Modify memory nodes if requested
        if memory_nodes is not None:
            old_mems = cs.get_memnode()
            cs.write_memnode(memory_nodes)
            changes.append(f"Memory nodes: {old_mems or '(none)'} → {memory_nodes}")

        # Modify partition type if requested
        if isolated:
            old_partition = cs.get_partition_type()
            cs.set_cpu_exclusive()
            changes.append(f"Partition: {old_partition} → isolated")
        elif no_isolated:
            old_partition = cs.get_partition_type()
            cs.unset_cpu_exclusive()
            changes.append(f"Partition: {old_partition} → member")

        # Print summary of changes
        print(f"Modified cpuset '{name}':")
        for change in changes:
            print(f"  {change}")

    except (OSError, ValueError) as e:
        print(f"Error modifying cpuset: {e}", file=sys.stderr)
        sys.exit(1)


def main():
    i18n_init()
    parser = gen_parser()
    # Set all necessary defaults for gui subparser if no arguments provided
    args = parser.parse_args() if len(sys.argv) > 1 else parser.parse_args(['gui'])

    if args.debug:
        my_logger = setup_logging("my_logger")
        my_logger.addHandler(add_handler("DEBUG", tofile=False))
        my_logger.info("Debug option set")

    if args.loglevel:
        if not args.debug:
            my_logger = setup_logging("my_logger")
        try:
            my_logger.addHandler(add_handler(args.loglevel, tofile=True))
            my_logger.info("Logging option set at log level {}".format(args.loglevel))

        except ValueError as e:
            print(e, "tuna: --logging requires valid logging level\n")
            print("Valid log levels: NOTSET, DEBUG, INFO, WARNING, ERROR")
            print("Log levels may be specified numerically (0-4)\n")

    # Convert string arguments to lists after parsing
    match_requested = False
    ps = None

    # Convert thread_list from string to list
    if 'thread_list' in vars(args) and args.thread_list:
        # Join multiple arguments into a single string (handles "811, 950" without quotes)
        if isinstance(args.thread_list, list):
            thread_str = ' '.join(args.thread_list)
        else:
            thread_str = args.thread_list
        args.thread_list, match_requested = threadstring_to_list(thread_str, ps)

    # Convert irq_list from string to list
    if 'irq_list' in vars(args) and args.irq_list:
        if isinstance(args.irq_list, str):
            args.irq_list, irq_match_requested = irqstring_to_list(args.irq_list)
            match_requested = match_requested or irq_match_requested

    # Convert socket strings to cpu lists
    if hasattr(args, 'sockets') and args.sockets:
        args.cpu_list = socketstring_to_list(args.sockets)

    if args.command == 'cpu_power':
        if not cpw.have_cpupower:
            print(f"Error: libcpupower bindings are not detected; please install libcpupower bindings from at least kernel {cpw.cpupower_required_kernel}.", file=sys.stderr)
            sys.exit(1)

        my_cpupower = cpw.Cpupower(args.cpu_list)
        ret = my_cpupower.idle_set_handler(args)
        if ret > 0:
            sys.exit(ret)

    if 'irq_list' in vars(args):
        ps = procfs.pidstats()
        if tuna.has_threaded_irqs(ps):
            for irq in args.irq_list:
                irq_re = tuna.threaded_irq_re(irq)
                irq_threads = ps.find_by_regex(irq_re)
                if irq_threads:
                    # Change the affinity of the thread too
                    # as we can't rely on changing the irq
                    # affinity changing the affinity of the
                    # thread or vice versa. We need to change
                    # both.
                    if 'thread_list' in vars(args):
                        args.thread_list += irq_threads

    if 'nohz_full' in vars(args) and args.nohz_full:
        args.cpu_list = nohz_full_to_cpu()

    # Warn if user-specified patterns matched nothing (only if --warn)
    if match_requested and args.warn:
        if args.command in ['show_threads', 'move', 'm', 'spread', 'x', 'priority', 'p', 'what_is', 'W']:
            if 'thread_list' in vars(args) and isinstance(args.thread_list, list) and not args.thread_list:
                print("[WARNING] thread pattern matched 0 threads", file=sys.stderr)
        if args.command in ['show_irqs', 'move', 'm', 'spread', 'x']:
            if 'irq_list' in vars(args) and isinstance(args.irq_list, list) and not args.irq_list:
                print("[WARNING] IRQ pattern matched 0 IRQs", file=sys.stderr)

    # Validate converted arguments
    if args.command in ['move', 'm', 'spread', 'x', 'isolate', 'i', 'include', 'I', 'run', 'r', 'save', 's', 'show_threads', 'show_irqs', 'gui', 'g', 'cpu_power']:
        if 'cpu_list' in vars(args) and args.cpu_list:
            nr_cpus = utils.get_nr_cpus()
            invalid_cpus = [c for c in args.cpu_list if c >= nr_cpus]
            if invalid_cpus:
                print(f"[ERROR] Invalid CPU numbers: {invalid_cpus}. System has {nr_cpus} CPUs (0-{nr_cpus-1})", file=sys.stderr)
                sys.exit(2)

    if args.command in ['priority', 'p', 'run', 'r']:
        if 'priority' in vars(args) and args.priority:
            policy, rtprio = args.priority
            # Only validate rtprio if a policy was specified or if rtprio is non-zero
            if policy is not None or rtprio != 0:
                # RT policies (FIFO, RR) require priority 1-99
                # SCHED_FIFO = 1, SCHED_RR = 2 in Linux
                if policy in (1, 2) and rtprio is not None:
                    if rtprio < 1 or rtprio > 99:
                        print(f"[ERROR] Invalid RT priority: {rtprio}. RT priorities must be 1-99", file=sys.stderr)
                        sys.exit(2)
                # If only priority specified (no policy), assume RT and validate
                elif policy is None and rtprio is not None and rtprio != 0:
                    if rtprio < 1 or rtprio > 99:
                        print(f"[ERROR] Invalid RT priority: {rtprio}. RT priorities must be 1-99", file=sys.stderr)
                        sys.exit(2)

    if args.command in ['include', 'I']:
        tuna.include_cpus(args.cpu_list, utils.get_nr_cpus())

    elif args.command in ['isolate', 'i']:
        # Parse --cpuset-housekeeping arguments
        housekeeping_name = None
        housekeeping_cpus = None

        if args.cpuset_housekeeping:
            # Validate that --cpuset is also specified
            if not args.cpuset:
                print("Error: --cpuset-housekeeping requires --cpuset", file=sys.stderr)
                sys.exit(1)

            # Parse: [NAME] CPU-LIST or just CPU-LIST
            if len(args.cpuset_housekeeping) == 1:
                housekeeping_name = 'tuna_housekeeping'
                housekeeping_cpus = args.cpuset_housekeeping[0]
            elif len(args.cpuset_housekeeping) == 2:
                housekeeping_name = args.cpuset_housekeeping[0]
                housekeeping_cpus = args.cpuset_housekeeping[1]
            else:
                print("Error: --cpuset-housekeeping takes 1 or 2 arguments: [NAME] CPU-LIST", file=sys.stderr)
                sys.exit(1)

        # Traditional affinity-based isolation (existing behavior)
        tuna.isolate_cpus(args.cpu_list, utils.get_nr_cpus())

        # Create cpusets if requested
        if args.cpuset:
            try:
                # Check cgroup v2 support
                cpusets_init = cpuset.CpusetsInit()
                if not cpusets_init.supported:
                    print("Error: cgroup v2 cpuset controller not available", file=sys.stderr)
                    sys.exit(1)

                # Get NUMA nodes for isolated CPUs
                isolated_numa = cpuset.get_numa_nodes_for_cpus(args.cpu_list)

                # Create isolated cpuset (partition=isolated, empty)
                print(f"Creating isolated cpuset '{args.cpuset}' with CPUs {tuna.collapse_cpulist(args.cpu_list)}")
                isolated_cpuset = cpuset.Cpuset(args.cpuset)
                isolated_cpuset.write_memnode(isolated_numa)
                isolated_cpuset.assign_cpus(tuna.collapse_cpulist(args.cpu_list))
                isolated_cpuset.write_cpu_exclusive(True)  # partition=isolated
                print(f"Isolated cpuset '{args.cpuset}' created successfully")

                # Create housekeeping cpuset if requested
                if housekeeping_cpus:
                    # Parse housekeeping CPU list from string to list
                    hk_cpu_list = tuna.cpustring_to_list(housekeeping_cpus)
                    hk_numa = cpuset.get_numa_nodes_for_cpus(hk_cpu_list)

                    print(f"Creating housekeeping cpuset '{housekeeping_name}' with CPUs {housekeeping_cpus}")
                    hk_cpuset = cpuset.Cpuset(housekeeping_name)
                    hk_cpuset.write_memnode(hk_numa)
                    hk_cpuset.assign_cpus(housekeeping_cpus)
                    # Default to partition=member (False) unless --housekeeping-isolated specified
                    hk_isolated = getattr(args, 'housekeeping_isolated', False)
                    hk_cpuset.write_cpu_exclusive(hk_isolated)

                    # Migrate processes from root to housekeeping
                    print(f"Migrating processes to housekeeping cpuset...")
                    tm = cpuset.TaskMigrate(cpusets_init, hk_cpuset)
                    migrated, failed = tm.migrate()
                    print(f"Migrated {migrated} processes to '{housekeeping_name}' ({failed} kernel threads skipped)")

            except Exception as e:
                print(f"Error creating cpusets: {e}", file=sys.stderr)
                sys.exit(1)

    elif args.command in ['run', 'r']:
        # Check if using cpuset mode
        cpuset_name = None
        if hasattr(args, 'cpuset') and args.cpuset:
            # Verify cpuset exists
            cpuset_path = os.path.join('/sys/fs/cgroup', args.cpuset)
            if not os.path.exists(cpuset_path):
                print(f"Error: cpuset '{args.cpuset}' does not exist", file=sys.stderr)
                sys.exit(1)
            cpuset_name = args.cpuset

        tuna.run_command(args.run_command, args.priority[0], args.priority[1], args.cpu_list, args.background, cpuset_name)

    elif args.command in ['priority', 'p']:

        try:
            tuna.threads_set_priority(args.thread_list, args.priority, args.affect_children)
        except OSError as err:
            print(f"tuna: {err}")
            sys.exit(2)

    elif args.command in ['show_configs']:
        list_config()

    elif args.command in ['show_threads']:
        # Skip display if pattern matched nothing (warning already shown)
        has_content = False
        if match_requested:
            if 'thread_list' in vars(args) and isinstance(args.thread_list, list) and args.thread_list:
                has_content = True
            if 'irq_list' in vars(args) and isinstance(args.irq_list, list) and args.irq_list:
                has_content = True
        else:
            has_content = True  # No filtering, will show content

        if has_content:
            do_ps(args.thread_list, args.cpu_list, args.irq_list, args.uthreads,
                    args.kthreads, args.affect_children, args.show_sockets if "show_sockets" in args else None, args.cgroups, args.compact, match_requested, args.cpuset if "cpuset" in args else None)

    elif args.command in ['show_irqs']:
        # Skip display if pattern matched nothing (warning already shown)
        has_content = False
        if match_requested:
            if 'irq_list' in vars(args) and isinstance(args.irq_list, list) and args.irq_list:
                has_content = True
        else:
            has_content = True  # No filtering, will show content

        if has_content:
            show_irqs(args.irq_list, args.cpu_list, match_requested)

    elif args.command in ['move', 'm', 'spread', 'x']:
        spread = args.command in ['spread', 'x']

        if not (args.thread_list or args.irq_list):
            parser.error(f"tuna: {args.command} requires a thread/irq list!\n")

        # Check if using cpuset mode
        if hasattr(args, 'cpuset') and args.cpuset:
            # Cpuset mode: move entities to a cpuset instead of setting affinity
            if spread:
                print("Error: --cpuset cannot be used with spread command", file=sys.stderr)
                sys.exit(2)

            # Verify cpuset exists
            cpuset_path = os.path.join('/sys/fs/cgroup', args.cpuset)
            if not os.path.exists(cpuset_path):
                print(f"Error: cpuset '{args.cpuset}' does not exist", file=sys.stderr)
                sys.exit(1)

            # Create cpuset object
            try:
                cs = cpuset.Cpuset(args.cpuset, existing_ok=True)
            except (OSError, ValueError) as e:
                print(f"Error accessing cpuset '{args.cpuset}': {e}", file=sys.stderr)
                sys.exit(1)

            success_count = 0
            fail_count = 0

            # Move threads to cpuset
            if args.thread_list:
                for pid in args.thread_list:
                    if cs.write_pid(pid):
                        success_count += 1
                    else:
                        fail_count += 1

            # Move IRQ threads to cpuset
            if args.irq_list:
                if not ps:
                    ps = procfs.pidstats()
                irqs = procfs.interrupts()

                for irq in args.irq_list:
                    # Find IRQ thread for this IRQ number
                    irq_re = tuna.threaded_irq_re(irq)
                    irq_threads = ps.find_by_regex(irq_re)

                    for irq_pid in irq_threads:
                        if cs.write_pid(irq_pid):
                            success_count += 1
                        else:
                            fail_count += 1

            # Print summary
            print(f"Moved {success_count} task(s) to cpuset '{args.cpuset}'", end='')
            if fail_count > 0:
                print(f" ({fail_count} failed)")
            else:
                print()

        else:
            # Original affinity mode
            if args.thread_list:
                tuna.move_threads_to_cpu(args.cpu_list, args.thread_list, args.uthreads, args.kthreads, spread=spread)

            if args.irq_list:
                tuna.move_irqs_to_cpu(args.cpu_list, args.irq_list, spread=spread)

    elif args.command in ['W', 'what_is']:
        if not ps:
            ps = procfs.pidstats()
        for i, tid in enumerate(args.thread_list):
            thread_help(tid, ps)
            # Add blank line between threads (but not after the last one)
            if i < len(args.thread_list) - 1:
                print()

    elif args.command in ['g', 'gui']:
        # Don't try to start the gui if no display is available
        display = os.getenv("DISPLAY")
        if not display:
            parser.print_help()
            return

        # Disable IBUS to avoid warnings when running as root
        os.environ['GTK_IM_MODULE'] = 'gtk-im-context-simple'

        # Disable translations - po files are outdated and GUI is being redesigned
        os.environ['LANGUAGE'] = 'C'

        try:
            from tuna import tuna_gui
        except ImportError:
            # gui packages not installed
            print(_('tuna: packages needed for the GUI missing.'))
            print(_('      Make sure xauth, pygtk2-libglade are installed.'))
            parser.print_help()
            return
        except RuntimeError:
            print("tuna: machine needs to be authorized via xhost or ssh -X?")
            return

        try:
            app = tuna_gui.main_gui(args.kthreads, args.uthreads, args.cpu_list, args.refresh, args.disable_perf)
            app.run()
        except KeyboardInterrupt:
            pass

    elif args.command == 'cpuset':
        # Handle cpuset subcommands
        if args.cpuset_command == 'create':
            cpuset_create(args.cpu_list, args.name, args.isolated, args.memory_nodes)

        elif args.cpuset_command == 'list':
            cpuset_list(args.pattern, args.verbose, args.show_empty, args.show_system, args.recursive)

        elif args.cpuset_command == 'destroy':
            cpuset_destroy(args.names, args.pattern, args.force, args.include_empty, args.recursive)

        elif args.cpuset_command == 'move':
            cpuset_move(args.name, args.thread_list, args.pid_list)

        elif args.cpuset_command == 'show':
            cpuset_show(args.name, args.show_tasks)

        elif args.cpuset_command == 'status':
            cpuset_status(args.pattern, args.show_empty, args.show_system, args.recursive)

        elif args.cpuset_command == 'modify':
            # Parse CPU lists if provided
            add_cpus = None
            remove_cpus = None
            if args.add_cpu_list:
                add_cpus = tuna.cpustring_to_list(args.add_cpu_list)
            if args.remove_cpu_list:
                remove_cpus = tuna.cpustring_to_list(args.remove_cpu_list)

            cpuset_modify(args.name, add_cpus, remove_cpus, args.memory_nodes,
                         args.isolated, args.no_isolated)

        elif args.cpuset_command == 'save':
            save_cpusets_cmd(args.filename)

        elif args.cpuset_command == 'apply':
            apply_cpusets_cmd(args.filename, args.verbose)


if __name__ == '__main__':
    main()
