pax_global_header00006660000000000000000000000064125252023160014510gustar00rootroot0000000000000052 comment=c43b4f61e69e1e777f9d218ddf0e694a8ded0de8 ctop-0.4.1/000077500000000000000000000000001252520231600124575ustar00rootroot00000000000000ctop-0.4.1/.gitignore000066400000000000000000000001031252520231600144410ustar00rootroot00000000000000# Temp files *.swp *.pyc # Build artefacts build *.egg-info dist ctop-0.4.1/CHANGELOG000066400000000000000000000026561252520231600137020ustar00rootroot00000000000000CHANGELOG ######### v0.4.1 (2015-05-14) ====== - feat: Python 3 support (@NicolasLM) - feat: fold/unfold cgroup tree - feat: hint/error message when no cgroup is found and boot2docker is installed - fix \#3: crash when no cgroup is located - fix: docker detection + actions under systemd v0.4.0 (2015-04-19) ====== - feat: detect container system type (docker, systemd, lxc) - feat: attach, enter, stop, kill supported containers - feat: select container / follow container - fix: mutiple crashes related to small screens - fix: tree render - enhancement: rewrite README - enhancement: drop 'docopt' dependency v0.3.1 ====== - enhancement: drop 'psutil' dependency - enhancement: python 2.6 compatibility - fix: phantom container lines after cgroup termination - doc: add screenshot v0.3.0 ====== - add command line - add choose columns, sort columns from command line - add choose refresh rate from command line - add tree view + toggle from cmdline + toggle from keyboard - add pause mode to ease text selections - add status bar - add CHANGELOG - fix parse of 1-line dict-like cgroup pseudo-files - drop peak memory usage column, meaningless in practise v0.2.0 ====== - add blkio bw column - add time column - enhancement: header contrast - enhancement: remove 1st header line - enhancement: header contrast - add sort toggle on click - highlight sort column - exit with 'q' v0.1.0 ====== Initial release ctop-0.4.1/Dockerfile000066400000000000000000000002031252520231600144440ustar00rootroot00000000000000FROM python:3 MAINTAINER Jean-Tiare Le Bigot WORKDIR /src ADD . /src RUN python ./setup.py install CMD ["ctop"] ctop-0.4.1/LICENSE000066400000000000000000000020521252520231600134630ustar00rootroot00000000000000Copyright (c) 2014, Jean-Tiare Le Bigot. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ctop-0.4.1/README.rst000066400000000000000000000113261252520231600141510ustar00rootroot00000000000000CTOP ==== A command line / text based Linux Containers monitoring tool that works just like you expect. .. image:: https://github.com/yadutaf/ctop/raw/master/screenshots/screenshot.png Introduction ------------ ``ctop`` will help you see what's going on at the container level. Basically, containers are a logical group of processes isolated using kernel's cgroups and namespaces. Recently, they have been made popular by Docker and they are also heavily used under the hood by systemd and a load of container tools like lxc, rocket, lmctfy and many others. Under the hood, ctop will collect all metrics it can from cgroups in realtime and render them to instantly give you an overview of the global system health. It currently collects metrics related to cpu, memory and block IO usage as well as metadata such as owning user (mostly for systemd based containers), uptime and attempts to guess the container managing technology behind. When the container technology has been successfully guessed, additional features are exposed like attaching to container (basically, it opens a shell in the container context) and stopping it. ``ctop`` author uses it on production system to quicky detect biggest memory users in low memory situations. Features -------- - collect cpu, memory and blkio metrics - collect metadata like task count, owning user, container technology - sort by any column - optionally display logical/tree view - optionally fold/unfold sub cgroup tree - optionally follow selected cgroup/container - optionnaly pause the refresh (typically, to select text) - detects Docker, LXC, unprivileged LXC and systemd based containers - supports advanced features for Docker and LXC based containers - open a shell/attach to supported container types for further diagnose - stop/kill supported container types - click to sort / reverse - click to select cgroup - no external dependencies beyond Python >= 2.6 or Python >= 3.0 Installation ------------ As a monitoring tool, ``ctop`` tries to be as dicrete as possible. Nonetheless it still has some expectations. It will need at least Python 2.6 with builtin curses support to run. This is usually found with Debian 6 and newer. This said, the recommended installation method relies on pip .. code:: bash pip install ctop ctop If using pip is not an option, which is often the case on production systems, you may also directly grab the self-contained source file directly from github and run it in place. All you'll need is Python 2.6 (Debian Squeeze): .. code:: bash wget https://raw.githubusercontent.com/yadutaf/ctop/master/cgroup_top.py -O ctop chmod +x ctop ./ctop Alternatively, if you are a Boot2docker user, you may install a Dockerized version of ctop instead. Please note that this is experimental and that you may not be able to controll / attach to your containers from ctop using this method: .. code:: bash docker pull yadutaf/ctop docker run --volume=/sys/fs/cgroup:/sys/fs/cgroup:ro -it --rm yadutaf/ctop # Optionally, to resolve uids to usernames, add '--volume /etc/passwd:/etc/passwd:ro' Usage ----- **Command line**: Monitor local cgroups as used by Docker, LXC, SystemD, ... Usage: ctop [--tree] [--refresh=] [--columns=] [--sort-col=] [--follow=] [--fold=, ...] ctop (-h | --help) Options: --tree Show tree view by default. --fold= Start with cgroup path folded --follow= Follow/highlight cgroup at path. --refresh= Refresh display every [default: 1]. --columns= List of optional columns to display. Always includes 'name'. [default: owner,processes,memory,cpu-sys,cpu-user,blkio,cpu-time]. --sort-col= Select column to sort by initially. Can be changed dynamically. [default: cpu-user] -h --help Show this screen. **Control**: - press ``p`` to toggle/pause the refresh and select text. - press ``f`` to let selected line follow / stay on the same container. Default: Don't follow. - press ``q`` or ``Ctrl+C`` to quit. - press ``F5`` to toggle tree/list view. Default: list view. - press ``↑`` and ``↓`` to navigate between containers. - press ``+`` or ``-`` to toggle child cgroup folding - click on title line to select sort column / reverse sort order. - click on any container line to select it. Additionally, for supported container types (Currently Docker and LXC): - press ``a`` to attach to console output. - press ``e`` to open a shell in the container context. Aka 'enter' container. - press ``s`` to stop the container (SIGTERM). - press ``k`` to kill the container (SIGKILL). Requirements ------------ * python >=2.6 or python >=3.0, with builtin curses support Licence ------- MIT ctop-0.4.1/bin/000077500000000000000000000000001252520231600132275ustar00rootroot00000000000000ctop-0.4.1/bin/ctop000077500000000000000000000000741252520231600141230ustar00rootroot00000000000000#!/usr/bin/env python import cgroup_top cgroup_top.main() ctop-0.4.1/cgroup_top.py000077500000000000000000000677701252520231600152360ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- ''' Monitor local cgroups as used by Docker, LXC, SystemD, ... Usage: ctop [--tree] [--refresh=] [--columns=] [--sort-col=] [--follow=] [--fold=, ...] ctop (-h | --help) Options: --tree Show tree view by default. --fold= Start with cgroup path folded --follow= Follow/highlight cgroup at path. --refresh= Refresh display every [default: 1]. --columns= List of optional columns to display. Always includes 'name'. [default: owner,processes,memory,cpu-sys,cpu-user,blkio,cpu-time]. --sort-col= Select column to sort by initially. Can be changed dynamically. [default: cpu-user] -h --help Show this screen. ''' from __future__ import print_function import os import re import sys import stat import pwd import time import pty import subprocess import multiprocessing from collections import defaultdict from collections import namedtuple from optparse import OptionParser try: import curses, _curses except ImportError: print("Curse is not available on this system. Exiting.", file=sys.stderr) sys.exit(0) def cmd_exists(cmd): return subprocess.call(["/bin/which", cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 HAS_LXC = cmd_exists('lxc-start') HAS_DOCKER = cmd_exists('docker') HIDE_EMPTY_CGROUP = True CGROUP_MOUNTPOINTS={} CONFIGURATION = { 'sort_by': 'cpu_total', 'sort_asc': False, 'tree': False, 'follow': False, 'pause_refresh': False, 'refresh_interval': 1.0, 'columns': [], 'selected_line': None, 'selected_line_num': 0, 'selected_line_name': '/', 'cgroups': [], 'fold': [], } Column = namedtuple('Column', ['title', 'width', 'align', 'col_fmt', 'col_data', 'col_sort']) COLUMNS = [] COLUMNS_MANDATORY = ['name'] COLUMNS_AVAILABLE = { 'owner': Column("OWNER", 10, '<', '{0:%ss}', 'owner', 'owner'), 'type': Column("TYPE", 10, '<', '{0:%ss}', 'type', 'type'), 'processes': Column("PROC", 4, '>', '{0:%sd}', 'tasks', 'tasks'), 'memory': Column("MEMORY", 17, '^', '{0:%ss}', 'memory_cur_str', 'memory_cur_bytes'), 'cpu-sys': Column("SYST", 5, '^', '{0: >%s.1%%}', 'cpu_syst', 'cpu_total'), 'cpu-user': Column("USER", 5, '^', '{0: >%s.1%%}', 'cpu_user', 'cpu_total'), 'blkio': Column("BLKIO", 10, '^', '{0: >%s}', 'blkio_bw', 'blkio_bw_bytes'), 'cpu-time': Column("TIME+", 14, '^', '{0: >%ss}', 'cpu_total_str', 'cpu_total_seconds'), 'name': Column("CGROUP", '', '<', '{0:%ss}', 'cgroup', 'cgroup'), } # TODO: # - visual CPU/memory usage # - auto-color # - persist preferences # - dynamic column width # - handle small screens # - massive refactoring. This code U-G-L-Y ## Utils def to_human(num, suffix='B'): num = int(num) for unit in [' ','K','M','G','T','P','E','Z']: if abs(num) < 1024.0: return "{0:.1f}{1}{2}".format(num, unit, suffix) num /= 1024.0 return "{0:5.1d}{1}{2}" % (num, 'Y', suffix) def div(num, by): res = num / by mod = num % by return res, mod def to_human_time(seconds): minutes, seconds = div(seconds, 60) hours, minutes = div(minutes, 60) days, hours = div(hours, 24) if days: return '%3dd %02d:%02d.%02d' % (days, hours, minutes, seconds) else: return '%02d:%02d.%02d' % (hours, minutes, seconds) def get_total_memory(): ''' Get total memory from /proc if available. ''' try: with open('/proc/meminfo') as f: content = f.read() except OSError: content = '' for line in content.split('\n'): fields = re.split(' +', line) if fields[0].strip() == "MemTotal:": return int(fields[1])*1024 return -1 def run(user, cmd, interactive=False): ''' Run ``cmd`` as ``user``. If ``interactive`` is True, save any curses status and synchronously run the command in foreground. Otherwise, run the command in background, discarding any output. special user -2 means: current user ''' prefix = [] cur_uid = os.getuid() try: cur_user = pwd.getpwuid(cur_uid).pw_name except: cur_user = cur_uid if user != cur_user and user != -2: if cur_uid == 0: prefix = ['su', user] if user == 'root': prefix = ['sudo'] else: prefix = ['sudo', '-u', user] if interactive: # Prepare screen for interactive command curses.savetty() curses.nocbreak() curses.echo() curses.endwin() # Run command pty.spawn(prefix+cmd) # Restore screen curses.start_color() # load colors curses.use_default_colors() curses.noecho() # do not echo text curses.cbreak() # do not wait for "enter" curses.curs_set(0) # hide cursor curses.mousemask(curses.ALL_MOUSE_EVENTS) curses.resetty() else: with open('/dev/null', 'w') as dev_null: subprocess.Popen( prefix+cmd, stdout=dev_null, stderr=dev_null, close_fds=True, ) class Cgroup(object): def __init__(self, path, base_path): self.path = path self.base_path = base_path @property def name(self): return self.path[len(self.base_path):] or '/' @property def owner(self): path = os.path.join(self.base_path, self.path, 'tasks') uid = os.stat(path).st_uid try: return pwd.getpwuid(uid).pw_name except: return uid @property def type(self): path = self.name # Guess cgroup owner if path.startswith('/docker/') or path.startswith('/system.slice/docker-'): return 'docker' elif path.startswith('/lxc/'): return 'lxc' elif path.startswith('/user.slice/'): _, parent, name = path.rsplit('/', 2) if parent.endswith('.scope'): if os.path.isdir('/home/%s/.local/share/lxc/%s' % (self.owner, name)): return 'lxc-user' return 'systemd' elif path == '/user.slice' or path == '/system.slice' or path.startswith('/system.slice/'): return 'systemd' else: return '-' def _coerce(self, value): try: return int(value) except: pass try: return float(value) except: pass return value def __getitem__(self, name): path = os.path.join(self.base_path, self.path, name) with open(path) as f: content = f.read().strip() if name == 'tasks' or '\n' in content or ' ' in content: content = content.split('\n') if ' ' in content[0]: content = dict((re.split(' +', l, 1) for l in content)) for k, v in content.items(): content[k] = self._coerce(v) else: content = [self._coerce(v) for v in content] else: content = self._coerce(content) return content def cgroups(base_path): ''' Generator of cgroups under path ``name`` ''' for cgroup_path, dirs, files in os.walk(base_path): yield Cgroup(cgroup_path, base_path) ## Grab cgroup data def init(): # Get all cgroup subsystems avalaible on this system with open("/proc/cgroups") as f: cgroups = f.read().strip() subsystems = [] for cgroup in cgroups.split('\n'): if cgroup[0] == '#': continue subsystems.append(cgroup.split()[0]) # Match cgroup mountpoints to susbsytems. Always take the first matching with open("/proc/mounts") as f: mounts = f.read().strip() for mount in mounts.split('\n'): mount = mount.split(' ') if mount[2] != "cgroup": continue for arg in mount[3].split(','): if arg in subsystems and arg not in CGROUP_MOUNTPOINTS: CGROUP_MOUNTPOINTS[arg] = mount[1] def collect(measures): cur = defaultdict(dict) prev = measures['data'] # Collect global data if 'cpuacct' in CGROUP_MOUNTPOINTS: # list all "folders" under mountpoint for cgroup in cgroups(CGROUP_MOUNTPOINTS['cpuacct']): # Collect tasks cur[cgroup.name]['tasks'] = cgroup['tasks'] # Collect user cur[cgroup.name]['owner'] = cgroup.owner # Collect cgroup type cur[cgroup.name]['type'] = cgroup.type # Collect memory statistics if 'memory' in CGROUP_MOUNTPOINTS: # list all "folders" under mountpoint for cgroup in cgroups(CGROUP_MOUNTPOINTS['memory']): cur[cgroup.name]['memory.usage_in_bytes'] = cgroup['memory.usage_in_bytes'] cur[cgroup.name]['memory.limit_in_bytes'] = min(int(cgroup['memory.limit_in_bytes']), measures['global']['total_memory']) # Collect CPU statistics if 'cpuacct' in CGROUP_MOUNTPOINTS: # list all "folders" under mountpoint for cgroup in cgroups(CGROUP_MOUNTPOINTS['cpuacct']): # Collect CPU stats cur[cgroup.name]['cpuacct.stat'] = cgroup['cpuacct.stat'] cur[cgroup.name]['cpuacct.stat.diff'] = {'user':0, 'system':0} # Collect CPU increase on run > 1 if cgroup.name in prev: for key, value in cur[cgroup.name]['cpuacct.stat'].items(): cur[cgroup.name]['cpuacct.stat.diff'][key] = value - prev[cgroup.name]['cpuacct.stat'][key] # Collect BlockIO statistics if 'blkio' in CGROUP_MOUNTPOINTS: # list all "folders" under mountpoint for cgroup in cgroups(CGROUP_MOUNTPOINTS['blkio']): # Collect BlockIO stats cur[cgroup.name]['blkio.throttle.io_service_bytes'] = cgroup['blkio.throttle.io_service_bytes'] cur[cgroup.name]['blkio.throttle.io_service_bytes.diff'] = {'total':0} # Collect BlockIO increase on run > 1 if cgroup.name in prev: cur_val = cur[cgroup.name]['blkio.throttle.io_service_bytes']['Total'] prev_val = prev[cgroup.name]['blkio.throttle.io_service_bytes']['Total'] cur[cgroup.name]['blkio.throttle.io_service_bytes.diff']['total'] = cur_val - prev_val # Sanity check: any data at all ? if not len(cur): raise KeyboardInterrupt() # Apply measures['data'] = cur def built_statistics(measures, conf): # Time prev_time = measures['global'].get('time', -1) cur_time = time.time() time_delta = cur_time - prev_time measures['global']['time'] = cur_time cpu_to_percent = measures['global']['scheduler_frequency'] * measures['global']['total_cpu'] * time_delta # Build data lines results = [] for cgroup, data in measures['data'].items(): cpu_usage = data.get('cpuacct.stat.diff', {}) line = { 'owner': str(data.get('owner', 'nobody')), 'type': str(data.get('type', 'cgroup')), 'tasks': len(data['tasks']), 'memory_cur_bytes': data.get('memory.usage_in_bytes', 0), 'memory_limit_bytes': data.get('memory.limit_in_bytes', measures['global']['total_memory']), 'cpu_total_seconds': data.get('cpuacct.stat', {}).get('system', 0) + data.get('cpuacct.stat', {}).get('user', 0), 'cpu_syst': cpu_usage.get('system', 0) / cpu_to_percent, 'cpu_user': cpu_usage.get('user', 0) / cpu_to_percent, 'blkio_bw_bytes': data.get('blkio.throttle.io_service_bytes.diff', {}).get('total', 0), 'cgroup': cgroup, } line['cpu_total'] = line['cpu_syst'] + line['cpu_user'], line['cpu_total_str'] = to_human_time(line['cpu_total_seconds']) line['memory_cur_percent'] = line['memory_cur_bytes'] / line['memory_limit_bytes'] line['memory_cur_str'] = "{0: >7}/{1: <7}".format(to_human(line['memory_cur_bytes']), to_human(line['memory_limit_bytes'])) line['blkio_bw'] = to_human(line['blkio_bw_bytes'], 'B/s') results.append(line) return results def render_tree(results, tree, level=0, prefix=[], node='/'): # Exit condition if node not in tree: return # Iteration for i, line in enumerate(tree[node]): cgroup = line['cgroup'] # Build name if i == len(tree[node]) - 1: line['_tree'] = prefix + [curses.ACS_LLCORNER, curses.ACS_HLINE, ' '] _child_prefix = prefix + [' ', ' ', ' '] else: line['_tree'] = prefix + [curses.ACS_LTEE, curses.ACS_HLINE, ' '] _child_prefix = prefix + [curses.ACS_VLINE, ' ', ' '] # Commit, fold or recurse results.append(line) if cgroup not in CONFIGURATION['fold']: render_tree(results, tree, level+1, _child_prefix, cgroup) else: line['_tree'] [-2] = '+' def prepare_tree(results): # Build tree tree = {} rendered = [] for line in results: cgroup = line['cgroup'] parent = os.path.dirname(cgroup) # Root cgroup ? if parent == cgroup: rendered.append(line) continue # Insert in hierarchie as needed if parent not in tree: tree[parent] = [] tree[parent].append(line) # Render tree, starting from root render_tree(rendered, tree) return rendered def display(scr, results, conf): # Sort results = sorted(results, key=lambda line: line.get(conf['sort_by'], 0), reverse=not conf['sort_asc']) if CONFIGURATION['tree']: results = prepare_tree(results) CONFIGURATION['cgroups'] = [cgroup['cgroup'] for cgroup in results] # Ensure selected line name synced with num if CONFIGURATION['follow']: while True: try: i = CONFIGURATION['cgroups'].index(CONFIGURATION['selected_line_name']) CONFIGURATION['selected_line_num'] = i break except: CONFIGURATION['selected_line_name'] = os.path.dirname(CONFIGURATION['selected_line_name']) else: CONFIGURATION['selected_line_num'] = min(len(results)-1, CONFIGURATION['selected_line_num']) CONFIGURATION['selected_line_name'] = CONFIGURATION['cgroups'][CONFIGURATION['selected_line_num']] CONFIGURATION['selected_line'] = results[CONFIGURATION['selected_line_num']] # Display statistics scr.clear() height, width = scr.getmaxyx() # Title line && templates x = 0 line_tpl = [] scr.addstr(0, 0, ' '*width, curses.color_pair(1)) for col in COLUMNS: # Build templates title_fmt = '{0:%s%ss}' % (col.align, col.width) line_tpl.append(col.col_fmt % (col.width)) # Build title line color = 2 if col.col_sort == conf['sort_by'] else 1 try: scr.addstr(0, x, title_fmt.format(col.title)+' ', curses.color_pair(color)) except: # Handle narrow screens break if col.width: x += col.width + 1 # Content lineno = 1 for line in results: y = 0 if lineno-1 == CONFIGURATION['selected_line_num']: col_reg, col_tree = curses.color_pair(2), curses.color_pair(2) else: col_reg, col_tree = colors = curses.color_pair(0), curses.color_pair(4) # Draw line background try: scr.addstr(lineno, 0, ' '*width, col_reg) except: # Handle small screens break # Draw line content try: for col in COLUMNS: cell_tpl = col.col_fmt % (col.width if col.width else 1) data_point = line.get(col.col_data, '') if col.title == 'CGROUP' and CONFIGURATION['tree']: data_point = os.path.basename(data_point) or '[root]' for c in line.get('_tree', []): scr.addch(c, col_tree) y+=1 scr.addstr(lineno, y, cell_tpl.format(data_point)+' ', col_reg) if col.width: y += col.width + 1 except: # Handle narrow screens pass lineno += 1 else: # Make sure last line did not wrap, clear it if needed try: scr.addstr(lineno, 0, ' '*width) except: pass # status line try: color = curses.color_pair(2) try: scr.addstr(height-1, 0, ' '*(width), color) except: # Last char wraps, on purpose: draw full line pass selected = results[CONFIGURATION['selected_line_num']] scr.addstr(height-1, 0, " CTOP ", color) scr.addch(curses.ACS_VLINE, color) scr.addstr(" [P]ause: "+('On ' if CONFIGURATION['pause_refresh'] else 'Off '), color) scr.addch(curses.ACS_VLINE, color) scr.addstr(" [F]ollow: "+('On ' if CONFIGURATION['follow'] else 'Off ') , color) scr.addch(curses.ACS_VLINE, color) scr.addstr(" [F5] Toggle %s view "%('list' if CONFIGURATION['tree'] else 'tree'), color) scr.addch(curses.ACS_VLINE, color) # Fold control if CONFIGURATION['tree']: scr.addstr(" [+/-] %s "%('unfold' if selected['cgroup'] in CONFIGURATION['fold'] else 'fold'), color) scr.addch(curses.ACS_VLINE, color) # Do we have any actions available for *selected* line ? selected_type = selected['type'] if selected_type == 'docker' and HAS_DOCKER or \ selected_type in ['lxc', 'lxc-user'] and HAS_LXC: scr.addstr(" [A]ttach, [E]nter, [S]top, [K]ill ", color) scr.addch(curses.ACS_VLINE, color) scr.addstr(" [Q]uit", color) except: # Handle narrow screens pass scr.refresh() def set_sort_col(sort_by): if CONFIGURATION['sort_by'] == sort_by: CONFIGURATION['sort_asc'] = not CONFIGURATION['sort_asc'] else: CONFIGURATION['sort_by'] = sort_by def on_keyboard(c): '''Handle keyborad shortcuts''' if c == ord('q'): raise KeyboardInterrupt() elif c == ord('p'): CONFIGURATION['pause_refresh'] = not CONFIGURATION['pause_refresh'] elif c == ord('f'): CONFIGURATION['follow'] = not CONFIGURATION['follow'] return 2 elif c == ord('+') or c == ord('-'): cgroup = CONFIGURATION['selected_line']['cgroup'] if cgroup in CONFIGURATION['fold']: CONFIGURATION['fold'].remove(cgroup) else: CONFIGURATION['fold'].append(cgroup) return 2 elif c == ord('a'): selected = CONFIGURATION['selected_line'] selected_name = os.path.basename(selected['cgroup']) if selected['type'] == 'docker' and HAS_DOCKER: if selected_name.startswith('docker-'): selected_name = selected_name[7:-6] run(-2, ['docker', 'attach', selected_name], interactive=True) elif selected['type'] in ['lxc', 'lxc-user'] and HAS_LXC: run(selected['owner'], ['lxc-console', '--name', selected_name, '--', '/bin/bash'], interactive=True) return 2 elif c == ord('e'): selected = CONFIGURATION['selected_line'] selected_name = os.path.basename(selected['cgroup']) if selected['type'] == 'docker' and HAS_DOCKER: if selected_name.startswith('docker-'): selected_name = selected_name[7:-6] run(-2, ['docker', 'exec', '-it', selected_name, '/bin/bash'], interactive=True) elif selected['type'] in ['lxc', 'lxc-user'] and HAS_LXC: run(selected['owner'], ['lxc-attach', '--name', selected_name, '--', '/bin/bash'], interactive=True) return 2 elif c == ord('s'): selected = CONFIGURATION['selected_line'] selected_name = os.path.basename(selected['cgroup']) if selected['type'] == 'docker' and HAS_DOCKER: if selected_name.startswith('docker-'): selected_name = selected_name[7:-6] run(-2, ['docker', 'stop', selected_name]) elif selected['type'] in ['lxc', 'lxc-user'] and HAS_LXC: run(selected['owner'], ['lxc-stop', '--name', selected_name, '--nokill', '--nowait']) return 1 elif c == ord('k'): selected = CONFIGURATION['selected_line'] selected_name = os.path.basename(selected['cgroup']) if selected['type'] == 'docker' and HAS_DOCKER: if selected_name.startswith('docker-'): selected_name = selected_name[7:-6] run(-2, ['docker', 'stop', '-t', '0', selected_name]) elif selected['type'] in ['lxc', 'lxc-user'] and HAS_LXC: run(selected['owner'], ['lxc-stop', '-k', '--name', selected_name, '--nowait']) return 2 elif c == 269: # F5 CONFIGURATION['tree'] = not CONFIGURATION['tree'] return 2 elif c == curses.KEY_DOWN: if CONFIGURATION['follow']: i = CONFIGURATION['cgroups'].index(CONFIGURATION['selected_line_name']) else: i = CONFIGURATION['selected_line_num'] i = min(i+1, len(CONFIGURATION['cgroups'])-1) CONFIGURATION['selected_line_num'] = i CONFIGURATION['selected_line_name'] = CONFIGURATION['cgroups'][i] return 2 elif c == curses.KEY_UP: if CONFIGURATION['follow']: i = CONFIGURATION['cgroups'].index(CONFIGURATION['selected_line_name']) else: i = CONFIGURATION['selected_line_num'] i = max(i-1, 0) CONFIGURATION['selected_line_num'] = i CONFIGURATION['selected_line_name'] = CONFIGURATION['cgroups'][i] return 2 return 1 def on_mouse(): '''Update selected line / sort''' _, x, y, z, bstate = curses.getmouse() # Left button click ? if bstate & curses.BUTTON1_CLICKED: # Is it title line ? if y == 0: # Determine sort column based on offset / col width x_max = 0 for col in COLUMNS: if not col.width: set_sort_col(col.col_sort) elif x < x_max+col.width: set_sort_col(col.col_sort) else: x_max += col.width + 1 continue return 2 # Is it a cgroup line ? elif y <= len(CONFIGURATION['cgroups']): if CONFIGURATION['follow']: CONFIGURATION['selected_line_name'] = CONFIGURATION['cgroups'][y-1] else: CONFIGURATION['selected_line_num'] = y-1 return 2 return 1 def on_resize(): '''Redraw screen, do not refresh''' return 1 def event_listener(scr, timeout): ''' Wait for curses events on screen ``scr`` at mot ``timeout`` ms return - 1 OK - 2 redraw - 0 error ''' try: scr.timeout(timeout) c = scr.getch() if c == -1: return 1 elif c == curses.KEY_MOUSE: return on_mouse() elif c == curses.KEY_RESIZE: return on_resize() else: return on_keyboard(c) except _curses.error: return 0 def rebuild_columns(): del COLUMNS[:] for col in CONFIGURATION['columns']+COLUMNS_MANDATORY: COLUMNS.append(COLUMNS_AVAILABLE[col]) def diagnose(): devnull = open(os.devnull, 'w') if os.path.isfile('/.dockerenv'): print(""" Hint: It seems you are running inside a Docker container. Please make sure to expose host's cgroups with '--volume=/sys/fs/cgroup:/sys/fs/cgroup:ro'""", file=sys.stderr) if cmd_exists('boot2docker'): print(""" Hint: It seems you have 'boot2docker' installed. To monitor Docker containers in 'boot2docker' run CTOP inside the VM itself with: $ docker run --volume=/sys/fs/cgroup:/sys/fs/cgroup:ro -it --rm yadutaf/ctop""", file=sys.stderr) devnull.close() def main(): # Parse arguments parser = OptionParser() parser.add_option("--tree", action="store_true", default=False, help="show tree view by default") parser.add_option("--refresh", action="store", type="int", default=1, help="Refresh display every ") parser.add_option("--follow", action="store", type="string", default="", help="Follow cgroup path") parser.add_option("--fold", action="append", help="Fold cgroup sub tree") parser.add_option("--columns", action="store", type="string", default="owner,type,processes,memory,cpu-sys,cpu-user,blkio,cpu-time", help="List of optional columns to display. Always includes 'name'") parser.add_option("--sort-col", action="store", type="string", default="cpu-user", help="Select column to sort by initially. Can be changed dynamically.") options, args = parser.parse_args() CONFIGURATION['tree'] = options.tree CONFIGURATION['refresh_interval'] = float(options.refresh) CONFIGURATION['columns'] = [] CONFIGURATION['fold'] = options.fold or list() if options.follow: CONFIGURATION['selected_line_name'] = options.follow CONFIGURATION['follow'] = True for col in options.columns.split(','): col = col.strip() if col in COLUMNS_MANDATORY: continue if not col in COLUMNS_AVAILABLE: print("Invalid column name", col, file=sys.stderr) print(__doc__) sys.exit(1) CONFIGURATION['columns'].append(col) rebuild_columns() if options.sort_col not in COLUMNS_AVAILABLE: print("Invalid sort column name", options.sort_col, file=sys.stderr) print(__doc__) sys.exit(1) CONFIGURATION['sort_by'] = COLUMNS_AVAILABLE[options.sort_col].col_sort # Initialization, global system data measures = { 'data': defaultdict(dict), 'global': { 'total_cpu': multiprocessing.cpu_count(), 'total_memory': get_total_memory(), 'scheduler_frequency': os.sysconf('SC_CLK_TCK'), } } init() if not CGROUP_MOUNTPOINTS: print("[ERROR] Failed to locate cgroup mountpoints.", file=sys.stderr) diagnose() sys.exit(1) results = None try: # Curse initialization stdscr = curses.initscr() curses.start_color() # load colors curses.use_default_colors() curses.noecho() # do not echo text curses.cbreak() # do not wait for "enter" curses.curs_set(0) # hide cursor stdscr.keypad(1) # parse keypad controll sequences curses.mousemask(curses.ALL_MOUSE_EVENTS) # Curses colors curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_GREEN) # header curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_CYAN) # focused header / line curses.init_pair(3, curses.COLOR_WHITE, -1) # regular curses.init_pair(4, curses.COLOR_CYAN, -1) # tree # Main loop while True: collect(measures) results = built_statistics(measures, CONFIGURATION) display(stdscr, results, CONFIGURATION) sleep_start = time.time() while CONFIGURATION['pause_refresh'] or time.time() < sleep_start + CONFIGURATION['refresh_interval']: if CONFIGURATION['pause_refresh']: to_sleep = -1 else: to_sleep = int((sleep_start + CONFIGURATION['refresh_interval'] - time.time())*1000) ret = event_listener(stdscr, to_sleep) if ret == 2: display(stdscr, results, CONFIGURATION) except KeyboardInterrupt: pass finally: curses.nocbreak() stdscr.keypad(0) curses.echo() curses.endwin() # If we found only root cgroup, me may be expecting to run in a boot2docker instance if results is not None and len(results) < 2: print("[WARN] Failed to find any relevant cgroup/container.", file=sys.stderr) diagnose() if __name__ == "__main__": main() ctop-0.4.1/screenshots/000077500000000000000000000000001252520231600150175ustar00rootroot00000000000000ctop-0.4.1/screenshots/screenshot.png000066400000000000000000003336301252520231600177120ustar00rootroot00000000000000PNG  IHDR &YbKGD pHYs  tIMEm& IDATxwxE3n/齑; *( kEX+k({IvKn2)ސ`@Qr;;sFBOݯM?i@҆OgD.z84\?Z>79څB!t"gl9i?'I ?8E ===B!BV ]v&oe4jU .N 8!B!.IeYy.\Ta+bhB!BuJAeB(SSYQ"B!8,sk$R*IM*Jx%IA2 9fD!B!tP%nʩNk9.IŨۧ]JW/3tZ:Ќ6#%%9!!l2'$͖wx ̈N?(p9%/Bϋy^Y ~gB!Y%I2\5aNQp:sT.:>P?0 NMGږ3Rʎ3J׉40 c Q:pFCEm(rJLjLdJDIԱDfس޲nƜv-GyyN/,:]Nb$֊.qي4G-K>B!йLQ[Z w,q{ ش0'|T$͌<>)buNBӷ}z^Y!_duֆgx=]Dy~nP/3Z=.b~= [֬\**DTMdoחմnkmF,HD^_ܵ&ǤeŖ͠퐒ϝ#B!Ynh},pKJ[j? v4*.KR >_koeYRD[5~g\fm˹.3WꑦnV)cuyj ˅Y PXlN)S *edAPR󊒢òN3-HHùWh| motCofz<]n"'=*QYӆX{}FBz;`I4.zMcZ 6=lةV^I`D .!*\3u2@E/.<^tK\ϱT{V뭪]'++]>I"2ZiTT){2%P|Ic)yZ7 ≥Z |U.r򍜊"]DK@JZB4<`wxi;o`ʨ^ [C{Ťx7K27F6QI욇* kpJt]@1]3XY||˥TJ<¸zco}6ҜK淨YjNT2/z)i'g;.`kj Q̺ZE+\܋nЬ@p!VK]y`S>tKv G lPugd.u?;HEyIt{ {e9o4bB!ENJA+{w'^}om80R_@mv\СCR fsM5~t6T1zyq>w 4CVg2?!={浯'9A<.4\E,DS7**A߮hu4˫kݍ37 :e@>=&YE$_yUX7 phlxkZjܤ`俨n>ZÒaEwXd0h҃ڙSw\-޻_Q;聙W|~c{07z׼{~qJ]N+~s@meߦ㐔wpyPiEo}o \4Rix̪). !BNhyK$U̲. NLjI S?!!Rq\fm6p[k^1bn|?5ǘ9g#}/kxQqJ6̹9De.m) R V(ֈlXLTs\Sf0\ZW7}y큵IS$]:) n!A[(+<a -ۼش4>W8Y(~l$Z:M\1ߕu߇dX2|>>ܲ,Hpِ6d_cQ6Pb=$WTPiKܐ+"J$}+ ; /*j;\{$B!t^;NerR?^1S=!_۞B/29eBXkT@&8.ar'}lƶ @;NRGv-e7=4!E V7.o\~CX CDVfOL|EPR@tQjW[@|RU壠inUokxŵ_2 )Yz_J*o,KIq YJJZ㬩,@NU}2@RwC,UdES<ۉB!aglydFPef OrKN{8khR\hP?9L%QOlNeZ1#coY$Ij9c,zǁ^[m_.8ch8, `ݏ6T =N(Gr;^?%,p2%Z;pijZcl"(lz:YnZ-y2ĩiʴ~ԩKJ qwVLL'J-֤j^&,D BqѰD+ 7_J^e->Xս.lz{cu;R@*q>]]v QĖxp= (%kjn(\Q;V{зו7ۻDᙣiQQaT8D!B!RKQ0vyN@EB޺8(7L-lՋ0LuulEQReXv pft\nOJb9kWyc;eKAQa%ݻj}NSRBTDXphK߶m/td{ǘijג% 4VU+[UUmjָ.svrmCzDۡt?85A85+i}zL6ZeuIlކ/}_ n%qѹXF~Ӻmtͱ҉ >%CIf+;lમVfoAfؾ BGȶ?ޒVt#.B޺Wdz^*Hj%h0B_U BюMJpltYⶖfپP l29VIbYi*s&4CÚmr!$6:̤.>+hpVj!BuRR2~(?|fP3M0$%$GޫX[ΨR 䦵-..fp|oJT1 !:F5#K/cp=bNKHI9եǑ`Vn1P***2i`l2 6|S1#_=)֛x'ՈR KP ,DŽVK ״bjV7p*4Ⱥ8] 1OW|ӣ> D"&x9^V(DxAD*)D{5ap_Iln>jғ󮉀E\K j.G0D%V#xZ.5reeru% ZUwHñ?B!B%EQ?ɶ v`^ !_QDrda[*縖 F <ϋvT|7E } : ƇuW:@Wq.Y.I'_[Ws'tS`mYDX*@=^w4O?0ߺ4k_VQUh"ܫ>^TH՞k_{}ӆ мn-ڨgzAhzrqqZҀIfA}(=fEiY/sPhG*$SBxBG[mqj%3 5l]q G!BIXUVRdJ)%0 qZ5}I5,yf3j431d/PP/e ;2}Ky`otՅt. >Ҭ? ol=lL=bETz*cB!PvRr.E!B!l B!B3"{;B!B!v"B!NQ'B!B33^xǭq E^ư"B!jϰE!B!tqM r[!'9󹜏:$,7aĦ;h#鎼[vl!ЙĴR6xrI?!Ac9S 2ͽLgomiiZ꯹Ҙ%޳: zDN\Hz/\h5Bϰ!P$}G3ufO; _/ gx$tP_,Lt**ܙ$ՙkYDb. }Ig3jp7=q%齨SNs[5TT*C4^꺿tҽE=j81 rNv7b; q:xNTڹpc_9]oY$j~۱{+N_GZzw¤;UY,Ð!OΎ{b`F-B ]4'-Ej<:f4993ԸWJC{IxW&_[pp%!W7[k!!t^Pˑ7[W%].SEC@P?GNto^!u6-S)5"B4sR:z\tEt*nǓup[ʲEW#;~8dҘZ?-mV+U/.}}a0ǘ_EJC?L8p9$rO/sm?1@^D[ktMShӕ2f϶WjtҠd>O}TR%~Z4;>Qqt!w:wq<\4 _rKU1^ 83hz~y}*@#deKw87X'>S a'BBLwtgܡSʫ'&9Hqٯ>(0֤7L887{Ĭ/_g}廙}J Tr 췎M;KVрuTF+Lă'i\ޖ!)jD^+pNY=Q.4'j<ů_3Pdb06mfO2/d?WǥOUCxj$\ֿV0!$ψϜ5⿬ i}-۝mI9BLkέY'f?5qn9lO닗4{I81ŲTdc8+U'aY_ gjM+|PmK3sH|eBP_9QA1zo@Jl$"%e$]9/YZWQs3xbrËRHnYh4~{o_DU>)wzoB[v*!1Ϳ7^fmhl[|{ B={ a-CШ/OJlJZήI0-TS9r7eꦉӆ]*:V-9ɕR*k&imd \Uyf] wJ]Fk2ysl*ޣT $.<4J.O1NgzcuCiz~0ڊ( IDATCL}C!m{$VUlVt^1jTXVD[eV(]KP .yɮ=U.wJ~{p%w/:) Dkn\nNMb2>Y*T;䎉}BcB!t!n6pw;U+>/(cwon?.$R`q:xd@@r:! N<ݦ @2*:@F)T[{d sӨL0j9̠crqoAy$_jyhG)BJaH;"(O6ЅwX%3L]4'xVϝqV n4&7[#'{xИ+\k/S*n8M~9/ѹ? a'.*>Y_{ qjBv6Ox"йLQk{RGƨa[jݧx,h?v6DKyksɁRoQlM{+=FT @Ku}ŽINf ˴zBqDhګlt'0ێ(J2 7y=Bra1 +s?.FAaϩnXIU1^lݧ1lőM'pۗdSrVRYǿC7ȇ_z_6)Bx)Д*O\bDkO=RvCw}ZJ6Ij~yZ P裕=Ypf~uV59 󙏽Z{P ~?=n7ؓn;'.*=Ƃq_H\} g*=n!BB$<ƶ756.hSh&n2}ozz|KMZ⯞J]߿ol?u@IU#z7@$u<5qU jipWTuTSM2,Gme[_k)otI^Ҷ̓u?B| 7&8F*N$]zjy l@ \7yM;)~VFT:vR9\G;8vYr9Ⱥ*abFzaΗ'zO%NP V(hH^+\>PTTyuNSV K׃4[Yo:<5q &'P6gYל3a^^ӲL /vߦy圫h;ON{ԣA5 4 3^mgކ{\/BԲUBt^b= ol8`c|n~4乚Cx"(QǃBp\u^R;݅]LT9$ʂdl"jhkqDe-:ckjnV(1(VYjuRʫN{x{^rKHWR@p%#$DWIWG0?QϺNNxȉ!戼R.Yi^@eU"uXxihjK#&G3L`ע-CLGuN\g09U BϰS%1b/ TnF2lgD@HNOgc+4[bkm to#ˆ|k; PW'gHN7+bX5LeZCinvXG,@gPq^"l\- 8}8j]]{WwYee~`DT|f;b(q.`( \ @{;%c7P(5|3vpo`&oleV%J}H׵fYsspؔ%Yn}%j/ FM@K+>c9PҰ_ BMhη:mC~u{'jROݿy͹-1*]m/%!0~!Px1Y*k7u$S 4RN  =jO49}J.sn2Im \^MVPR˨|Q ϟ8;OΒmIB!JRm]Fs jWS]cB9Ba'ÎK?ր$$E{,<^ <`Tc nuZ[VtMfɯ>#Zc\/ԗZ*ߌCr>5z ?F^*l*?+ξ#h;:O1B=C"^V:q۝zۅM"bc{NIRL"5W]W)D>~_2{ח'dP6xqщ{,D6N&<sRs6b9*] @x~d ۶<`')RB9Ddt)L@7*jo.cU@c+Ts'R!:#~ڔP,W bs?!B g0 !B!v"B!a'B!BΉӦ`B!B!x!B!;BS`]}]5 >gg83v';[ bnؚpo|&!B5T 曱Fy _ڳۙQ9cJg/c-;Ǵ&;Dr+_fpfIap1˲2 ?{ў*;c/y ?)VոGg8 >zս-%6ͯ)8ΨZżt4:6p.ȶnLC*@&ݿKK:^M" |ć!@mNs/}XJ>R^ N맫<0+|?ﮗ B«:8HmofycV'iO|4=9G}Wb=uľ  oOX%y;3v31JOۭa珱ֶ6 Qsc ړW/]׌YǏ陵Sztv\}+Pv.:? P>ah"N_'4ov?U{_ZZ!N_\»)9 +{\{n"zvnxW .X\` MQ&~~騨׀jP]/69'=waͫW>f`U8iӰ9uJ?>8>kq]ه!^>e`]4xfEI氻1 Nd=R%o4?{6״c8#CꕟPJ) ;j!_\է_[wMsyÂ\DDk榯P".wH?=}Z )B^]e֏n5}p*0/uqO;'k=@|.f?Y:tUDץ *Jjo +'oώ?ërǢҿ}~Th'S>|~GԀT3J?\VNG07Sα7Ih!'+>r^LЅF7X);>_)|Uk̷oc/>wsƝ[GO۟|7 &ɷv3p6|§ncFX]+7>SuekNR9SnxJ7U+ÿ(ܿ#:w |7uv3ךWw=os v<-76k=nq7Yv m̤d>^ّt%Fuebɯ6=|Cɭq2p8*]soW*`7|XR0TWBs+2u;$ݰۙr{2ݟ/<|}üKv(>]+w 6/6N<<'Pvs.(%l۞QTxlda ~Xqœ \-[9'>vUI|z9JJ>?pbwx K~ͽ8wV߳艁&QO^,`}YcE.qM}qzMN<+F>_.g;'WZ>mLߞ< y͝u%hN^ȴ(`: w;~5$w̋si7>{m[]oyg--ۘ!aM=QlINw8VK1%M4qRʺ@"=#L,cuObzc4E^Q 0u&衶. .22/;͒'y;zqFߵ|w,~DFcA!qͩſkm7o-vj`.W(ZylBX>"WvdER;Tʄw >4 z*fP *3DDGw IDAT9H6:NÇїe*ޣkvb/x`0 FoXj]3->~i>iY9t!@^QdžogCc \y10zc֧}m{AV{\3w8k~.mW9 `{QAs#E DH⎫̧W>; >[g8ڹGI^決:s"T?^\;B/-}cwG֮4B<&;@(>]_t'3p+t~ b产NO7A΂~dν녠7eCt'.ţ>7M[Pj2κr[=8s߻٨wy}Ĺ/蝺y}}mA:ŧ ?< -k›f}%;͸İg3P|[sN|ۛWu\}W%q+ߟB>9zi@yQӷ7;tI00%9>=h{kC g\8cbUuti`[~`+w?G|4owG!tV~Bc?J\Zk„߸Mbl̵3m-1|!!B! <ۉB!B!:%ʣqߎB!:(a=;B!vF߻dy Ba?NUv3;ù澞B?IOؾ#Baj̕5ߌ5#q 鳗ʖcZrG?W ׿pfgTbUI: 1˲2 ?{6lnőGzeF?#Oq}u/mp ,پ{Fl?(<;ÙaSҦʗ2;}nRw ;>ɷ:g3;|s&ƪTi׽ugâ>~f. bÂh(%jTԨQc%hlĆDEY˲m4(; Qw=s9w}kz]&=̎S=&=M?tC}Ğt*e{vԚGO"aۿ?K$a\:;,z㰄$Ua" >\q+/ '#,9YfH(;Kko{)RY\i;?wمarر)+6OYvN;yuifpչt9Ls9q!B[j[>+,, U﫧9؃V.ڽ1r;fwa+J)GW)4ɒ?|}Ww;v'MR3=ѤgSBg'ׯIOPk7A[vQ`==E8h/A xs!G}}wS&J";v9T"J=1ՠ_ߛd龫z(f㻍;~%T"J_܍:4]\.1^aQf ^) 7oCag'N[Q| p>=Emw!7|jө5I")M}(z~pm''AmM}f P=K%S]k!~تſN_-(2<*J%{i.3{ǾK<}tlӦ3evCX1 H$I I >K׈O-k)S]|˟E!7Lo9u3Wwzh=]?{4]t$}ҒϬ77.r*eVZX/l/_ը[01?5-yW NFW~w-nsچHE Y@@ K*=0{ogb&>{o?خLsWl۳Ȣ'd&RT( CV, vfm~x}7-X:ٿڶ;e*K;= jO5ǽu_qa'n?2ӉC+oT1Z!RhQ )z\ӜWݿ=hrgwC%ykK]r+f:GJМM[B3OC^cG;^̝L(@ v^uJ'z)ݞQ{!{k-6mC連 s{ʯL}VfX'}[Nvz>خ ^ZBDB*πU41׻WcgenO]X@K+@ iP)Yr Nt^_584O Ha_{v"  V}SsSk)/ /m\fNwO7jY`N 0:QNYt~iCю[$ %BN_'ScUI'3no uퟫwe$r/oYGWMVhkmGy;G8̈́ R$IJ!*IB%WbYsHi*;E]pvp#cA~޼0axGDgqNk39jK j*xآ3|~u8S9/\?9XOyQ)ͦJ~䙌 @$ɚ_$ ETwEU!*ɀq3[٠WTeDdZ ]Z[1!Ose&ϼ JJbz9WϿw/aڒ8aK5ꇨK@UPsBYVn e4ӰK{->ZB&r kg n@w(hԵt~)/9{}|P!.'tPQ=}?>L 'ۧ~=ݏ꒐߶glmȻ1(AMI>f \cBly^3fTu^A{ D ćO %YUfZW\㹸ɳҊT_A` V}ERpm=)i?ʍW<8chmRefoFiNS]uMp7mk z;|=/^-MÛ{9ִ-mӷeѳ?OT֡zef=M ] @?{_qR}hCw?ګe%Y2I/% G;~Տ[]i8eI(UⳫc`t5+RP,}NBJӢ=<~n:QNĞTuÊណ-\߱kd12K'nf)^+fѺC=~];!f~J;%({`0-[{ ve50}yFϽ6.mۘ0}+Wv[u a.׷g^,޸ń77e8No ܼߢ~թGAKoeP`‚PRXl`g>ƴ8%kvWiz;Y;8y[yڵ뷭Չ@ ݡ$Yd}޽m7[VNv"mC6ute\fg9pMK6ĿY\̫ͫU iIlP"jI#7g-p9!0g,[|5[ձ^EGo `|c1nR'pgZ  Ϗ~kHth\DqݵS7Veo3+.>pz(L'b.{T'}j|D弘Q9ћ7diXUO<7ҙgU >c=fKoY*:9OZ@w\}2u;^]`[K|_=G{އ/8Ia~B hq 㐭~0a(^9nWPGK墽k83 6/;Is\k r;4j|˜K[Tx]|R&5o(㶥c̘Uy?7t@ wFVhXn;&ַacFz>m#+ &5M uQ:fnC D cdbgfP_f 2+(G ?G @e`;L 9׹њDhQUf- @ !#.G@ >]@|&`Bx&ڌHZD9@ Pى@ 6$0@_!10P@ Sv2-6UYyrZ~+"l%u0lf_aB@ '~"T$uc<@Rp' 8].>ϠOIkSfiYpڝ2P3OFP*5҉5=o;uфB(<98tA$^_LW"88M{P*6n!}I% 'W 3WO3wײGGIEpڌY >Vune"ay8:mO?9O KJLpdԞ=f~#jL~N]XNl,cwU}cw~҄MI 8-gꇥ*>=UR4QQю'Cs<@ PssJ͔;6y@$&ܼzc\7kDGDHE²w|!u¥CX`T$3N]FNFWyk qyrwǙ;e"$9N,Jm+Pnԡirٍ_0ڜlҨ7PX# ¬SlD~;ܢϼaOGQG8sf]Di +]ArU 8yfĦ}:x~sM߭zgK"7ޱO۴L>l $I$Hëϒ53>C=4so/J߼31\ݵq픟TRG6 Da\0.!eYZYZGWE745V/.oD$VOP;-_#Eaia۞EG= <%=|0| BQbi'&n4k뛿iZH|2=aNv{ e.Fsqc<ՒG3NjtpH{sO}jϚrZNFL'N=ՠx|z:㻎#]c# <XΓ][}f}ߗ[0@4SsZl?s廵DїV|rN,}z2++1^{ .~9?^Gs]E-r6m ,Kӟ<( (SfdaV^j⓿Pd:l;+f[/%$e{ea\dx4اvVz ^)o-xa ׺ͬBibX̾~1?\UI= lURPSca܏2 7 IDAT[J;nob6mCԑ s{ʯ~V-KdsO~3l}]'F[33:~`m%Yʳ[w Lzyf9xU?BqWOB/WH@`aU{~N/N9[qP]|iIj(s.͌i_NY?mő} ZGհxҌ4Vxu~u2s{ 2!֜Hkh9I|^ww[q @m9iXvM.K-QrFo<ڇģE])nSTgWR'3no uퟫwe$r/oYGWMVhkOמ@(eJĪr$Tr%,UkQ }ucÍoy„e42q6z3oFs|zi׺ngCw+"V];c{ vy -=;o2CM\=vkX#w!\(W=?t_ڋS%:|@T<;Ǧtڿ=z\tYwx!N`[d 6Q+Vif1U_7^;72_4QGsb^82OA aJ~䙌k~%W_8adHj7ĝ뵮$Ij f kR!'qTJSHssw]ϬÙ*uvyrqzʃhoO˞$I5TIkJ5z]]T% x =z퟉SK 3&)4Qb[ϼ JJbz9WϿw/a\8aK5u!*PU$FYVn e4ӰK{->ZB&r kg n@w(hԵ>νw! *o+7~>TպjOi=un=>a-KG/N6>UF,9n䃔}0Hssj|IGX 7I?uwyۏ DKL,MЫ;^ `[vc43%%19%19-5AՀk=.~( O6aܓx5 1=e^HkQ%[YV,s7 F۠׸L򤐨2CyHT+$[`P}])6ՙio;j}wweEk-,3{C(/(W7JOs/mʅþifο]6_ %s}.omCGS?~4=u岤,n-,y¤RگhSƽΒ]2dT!ݨ}W^)RЌ Ƕ-VQvqQƓd?q~Hӟ%)kNB!zf:1Ok꼢~~]jdspr4-Jlӊ|@ MI<:zZl1'u䁺(%Cne:zlQ'qA_: qSfznieоEϲ_Dqi߬/d!o! ^8КOi܋o޶ ӷrukױ^˰1~};{v;+^LxsS4Fhm7l_qpSsd[&IB9ƵvjťkozqEpkKpa!V/y䯎%]:b7Z;tG/“zS׵aڮIn:s@*d V**yLz(QzQ%x4f_q폋RMQ0t\[pCHrkԴR%YUUnvnoo? ԣǠ۷2yq(0UN:/x!Ŗ_ vcL+ZZxMz_tQuQm1`Ase]{ ~ۚK ħBMFRr"/_m qRdB%ͳ/m}+l{y3;<5^}}!ݺx|` lׅ1&D: ?q)k"Yw(yy~Ӓ oWW^y4 nZrbC`:Y0jc3s/N!eǰc"dPdeaiϴ0@{ḁ ՍhLmrN` w~D2'.u'Ưe?ivS#,Ō,|'!K7Xm0幡'_q5[ |g_UxׁYLCzNͥzsأK@ vlL!#]F,2Qxwԍ*cgxԐ TΔؐPN?kCB.:=` \ϢsT ܶp*O.@  CX眍 Xێ mؘ^(5~~vs9)n;T,I@ Z \S_BTscdbgfP_f 2+(G ?G hxv|s @yl޸Jda;L 9׹њDhQUf- @ NE#T @ DcAF 35d?}B5@|]|Gg3[H1a8N9[|mr+frZ~+"l%u0lf_aB@ '(@ f[vЀ;u^uS ޾v=yNRWm럌T$, =k HXOqoZ<5,I(24=o;uфB(<987'{=x~0^n;OZ,1V(=Ij9z 7) bZx-g|tT$>i͘u cxzQwzM]&=̎4x1ig]Wc ||"!g[#:Q968h/AN5gM,mL]$\^3[r9q!ߔ_TGC=BW09Tq͔Si,;'ƍں4c{|3^\z젡1rO-f=MQKko{)RY\ը訿hD74ƽUqr mᕚ)շg96y@$&ܼzc\7kDGDHE²w|!u¥CX`T$3N]FNFWyk qyrwǙ;e"$9N,Jm+Pnԡirٍ_0ڜlҨ7PX# ¬SlD~;ܢϼaOGQG8s>-UΆFGGٿ/if,EIa\0.1۵ qaNyJ O%C󻟻onސO/ڵtjOrS_%^:+\nIP{n3{t8HL@=vt[5+EGE^ D JCvoXy݌:Dncߥ>:iә2}$I$I$W%kg|p=Qz覧.ms[w:ƙt"}OIm!W¸a\B\I3ݏn<'*ucכs/!D#NEy.]~՞jx~=iXCx0ug[P?۹dz_{llm;h /3=EC~\םO}le~_K  ]:\:y_($$A;\ֿ30zWp6LOY8ڶ-Y=Hr0?ꊆ9'ܽ=yQa {'M]Ŷ ۏ CM1 Z}Meg:5<~>&Ⱈc_Z9O<8!EħӠ6_ze_v׷?NfTJya; _ֽc߽UsC>a۞EG= <%=|0| BQbi'&n4k뛿iZH|2=aNv{ e.Fsqc1ЌZ4~:\*SntԳ_ '/F9R'j70AmǖI ~^=`9OwmKY_ne{*vڿ6Lk_{[])3nOnW$+ :~E^]BRiPf>Ʊ&YecOykľ8 9o˧/8mf]WL_U-)2YY2h+ꮟBO͝L(@ v^uJ'z)ݞQ{!{k-6mCӻvbdcoco]'F ;SX=lsK -!H"!`g*`ݫn1WgݏUHn`!ɋJ.el@, K}upOpq>ʜ{6~V.^ydl4œ8Ko [^# x)]+:0⇏t=ZSB-q(ϧ_ ە;./=5| @ tZvl;8{y>mi&LegbQ3&NaLΪRKT\=>fahAW3Yt!eۛFG]t K'q9[Vaӵ'J$*\$ \ ,.eϹ& OrTT?6m7/L)#`L!㻕\+>֓~';j>Seơ"Gm Aʹp3oFs|zi׺ng^Toہ:1=k_IU}K <\p4p~Ka-y|P'WVӟ:I I^N.+ҞӋ{*0'5اJt1ywM{R ` Cy KS`r 9C^"Ռ2lL=R'=MO*R"+u2eh`$\rg.5Rcm{ɇz]=X \<)$lҀ!-{e^T+$Ѐ~-F;P!ʒsM7$*!9ӜbN~nS.M[7e횷)2>[w}˩C=@1ahǏN,KۂEI_io_[2}0≂K/N6>ۮXR%е3A괿(IQJfkO~KfUk-c`<w3yVZvCJӢz ým3o֮cK7k;] urojŁT1@TU*08PУJth͜Փl;;,+)&m*Ў-^ztY&/YEOCh7?i$ܥV tlu dWh|hü-[<\l{O\ʸGS_~Ϗߏvmazag"| @ h&#)^9a/68)2@HiٗUؙQȾsېng<]>B˜K[T|]|Eh;NBFX4e.8کKU6κOWF9S'v=|b6.{*^n0Q(7q1f̪<CTz^'Wf.Xp-/n@ ,ݐԬ&l w8Fdf9p1g\P(X6s<)oRKgMGv6"d4Oӽ,2gR *#ے_U/OqjK-biTakTef!>I^nF v_z&tN]Q$Xc_! #> Ɗ&7 8 [7f V$0z}&DUUkrè u9$?i_n{L*׫d%F J7&$uLLt.5wXmp=T2ULMM'gtU:)F2d\}?,kemjb_T :IM$JM# X*W% r:~!Be'ig;ki $</mC\WvTBSmՕaJ^c %Ftޮi0I"[Dg;ǭɎHfS&٨b^S~zpln3jU)hTR1`Vow ަOf?+V AE?bLJZ (S8_6EFbP5$n=:1]E>d`1qeC qH tHSvFU{UZ;W]GMIFN<Zd[sNg+۳<)uY4_uA&+eUH\]UJwij3ce>3l;4Հlz>=@eLԥW.DY)ԤO5qilvr(l;ʙ@ D.;)lӗ2^%,(jp&_^nĐ4u|a"l8`jN&L +\-0Զj̓Sum:P2V|y~r\Fq՗vՉwN`Ĭ\|k@J`RY5otj.3;2cTrtg]eU^1-0b*Ih?BHDa@ճCԾeL* ԩJA\V{;c%YZV=P#R|ӉG@XvB~he'&+'UIQo@\Y3r W jbZzW.:e[V>0HUؕ]]jLlS#T +Ֆ* i e۸(oy!Ybm)sp)5 z{M씻zLXZdgE̲ R152/#DtNou~B?Ȫ=+IRƏy$s H5.H׳$1eE$X{f nԗ Wff2V&3%pHIFIɓh*"ϯ x:H5ǫf}3 ұ3&˴%܇r 4UZFa ҾhjUR*QkXߎUN 0Ӽ(ĕ"- 1ל- aKGH&92Oq#Yx7.K,ГRSoTDҲ{g^F/3sAQsMB @g2L3 8<mU}){SU*#wMV:+O2S[/8<\zÑ,p¨y9k@ H2U3H܈O4@OӁdԠd(9>#$xn5۞u!RUuP-SIpa+q?ROeO?fCb:0F#I h:e'7WªR>4.i(" VH'5`{\*3HPQ62Hwb`,>#+ TϺ v@rM%mL%m*iZb%gy1<>r^RðSտJӰWٵ- j\Q}*nl<˪[Yw󪊌+m[׹[GH#e2 X$VUB kiJd< 5!A3U~h:Eb޳,t̜0H"BOS?K(,2uH+ˡo4x?RN֚1T3Ժk^_g_@|lۢ6zfEb8HH=#.J0 $Q@+2ZI-iB}a%RɑJKYMn.%6 pr2Ic~Uiђ|al%QXeqb離V/649S.eVMdNa!DJVq^Q)CM,",(=[?? YN?G23$ge>m ʻ8Y 2IJz\V.-ሳD&s6,YX`$ȌKaZ,g"p )Ua'KVmYz%E ֿ2z Ҷ&pNvճ2y6@ўYpH1H#Y fSיSīer沖ƌBB Nɳ BAa90v6~jmĸ@ Ơu ƪ>,?f &e@ʹY ACQypō@ qnp?HMrM+-2eT"Y&+̩ky9pemeuz]wNc|GF<|bU;)#;YJW_;󸚶/{pT)E QDg"d "y{4J]JN<[ y]{k9ݲ\CjۡXcgPc֠'q'럿Z @ n)Ms3$2j_ɰkͫ 7z5DDi &kV[I :C[&@(4Xrqs@ FAT Y(ڧA !XJ͑@ *;Mqz@ @ bb@ D :!6 o !C&@ e@P@ŚO;@);Yp9|.bk㢧oqTIσ7t&KvAAA Bqh< @ Zxɋv`w}*W%߾~#=АwҳCl yw*uYUJ9ҩm{1 |.c_K片 Mo5Mw|X > '߈C勧>)RE%;(OYZE=FUr3l8R\4B6dnJ6*~WNwrwK7yW< @ *;Yc4\Ns&x9_'sHT(i> ݖ`Z.ߟ`ZVoD?scO0fJUGνpy*'{qcV`}} t{<.n=a2.z># m)$2 L]]gŠ~- [}<{צc!ws/ۜ?d{z`Nƿˊ>X&ů8)'/ xSQ|p#:h ~/[Q Uel]sC2X;%ckǯ ,zII夤V TFAd٦tMcÂ>*@bo,z82Yгל4F{Sτq#O{67XY^y  ' &%U5I@fFUmݾ=ә ^n%^>!QǜڳB4>( tCi=|\nJ2Q ,^Tp9wH?^?Y0׃> @貓wͱ{gkܘߣn 1׽fQ 8ӭVS'laow n\mn}jsNkn#DBS/}8b*a:Mj2!/?^S% ~o/Ue1u0~A"O_γ{Ol_%OLoy>z)`t $B4 D"jGs5ٵ9݂s|.֡߭4>œbDv牣[y_l`*fM|]Y'cs,Գobml Q2mJ++J{ϧI.؞ ̮#,ZGfX_fSZu1׫I+yn'^pmQoxHWp}~=[7jyq~^S9> ؍i4 )7UF^oD|#Ϗ*ͯkǖc˅d |/< ns};q"alݣ@ -w46\;Y67ȡx$#VoRo3MvN* wLJ|rqRcS&?UV{}6DΣ8N^afZ?'k5I csI*#Hʌ6a]qB6xa4a!Trd0+DR(?S;vm&޾̕=4ddf7vZ8ĜMGrL7YWܐppT ɩPJs#8&ӗ_H덉odAd̏s7콗[)g?|P$5n؋YID^ӠB':XLiTX 6ٿ{Q(UJ,&yaKU@sވ?E5" |"+)n􀠜{EKJ~!oseg0I N,2`ilҧkYmid秂e5LJ]}+϶yI7<{xs;MWS#\?cj9`j㾾IO~-_w<c;Y,ݺܺA;KILWhc+1/chL1 B+|SB1vk8`}E_|qBf_/_uu|yF|v7R8j"*@ey_ljiM,?L{'@ZhבJ6r)hV>/62O@eyq%"IMq_#0dh{l$88B*'[d8գ 9*#I+>?&6=xÕuSF.^p,r܎>mtZSJ^7+,o:C2-`);MO zu`@4 %`Lv~vbR#**2QBaq7oX^YT,MKPeAUqDNy\m.NqjPϼm !\θ k%{`9I<덏o #=Nur+ 6Xsfa[9~!?"Rn*xua9=^@][qAj>lnF8S:´B#kj5s$((߻~LUV'v̙˲&!+{%5мU`Niciډ$SviK`‡$5ey5@i5qJ/? 4ՠMe} S.QdqQ@X!F+uJs:bZ$N'^>xEo<1]S+,D&V̶W2ַ?ק9i[ IDATOmtdD 50*rvW^‹cڀ!Vr{Ѽj;O?oAŸXq$ g혳vݧ̒]&NIso\ȯ:AoM?rΏ}zlo|?+X}6qݛ>E_cP ꬤ|-yzZ*ɈxI(Lt ,^fۨw 31jL( ?QqY0#Ny@-{:uzZ9rS@ u6s.~}>8['!~oVp.'9F7tϿqscQtF\")弒+͎|N|qk \Pab}5 /{yi]?V%ĻG5o]}^z6po_q Q%t ꂕ&\@?W?:7iٷ8^Y"MsYqO vSn;ԝRranߟ2IW]EA=o%G^hL!w^"x9((vzuǟhj 9gvGܠw:e1f}!MwJD)7"aKCPfۤWv_}8A }-u*/fK~]0-%לTfGXhsa!՜?Lrqs@ oXvn:?&7gŖE-95aϾwŸ(f\tgG@ ۖD=ٝ# _0c@΂@ @ ! 2pwLr @e'2@ -oiv(TL@ Ҳ5@ r./d㢧6U{R _ ÒąhP}n4:A *;yѮ>OK}Ff;nv١?eS(̹ȗ.g|)?mݪ @sWrR9|.Ƀ U)V\𹡛tzw8|n!e7"v9P h2Ғ9|nBvf{<8|nl%f@i&ޯ7 .26^5̮Sv=*>eMoYI[Dt&OY޸Tp9뎝i)~"A#_q䪰>vƦ 4 eӘG IynlɛP{l?z9龀o/=::8ʩO>6<`C:/X1}ߖ4/~lK'l!rnG{?ޱ94J/99wo*{LV֦O*Onw#)Y{҅r-Z=CZ>?BYCf&q (4Uc˷u~cZqg|fOtzwo@θgh,D;.Xc묟>4\Ns&x9_'sHT(i> ݖ`Z.ߟ`ZVoD?scO0fJׄUGνpy*'{qcV`}} t{<.n=a2.z># m)$2 L]]gŠ~- [}aYֳTNJ*'%r_8M}_YѠ(N>jn459DMA4scjudvK`׎_+ƿNmJw.y->61>,2,av⭇#%?={p͹LAci(;u+8Lx7ǁlyS啧P  p`^R[ Ϗ_w$qOa OvnߞEky޻6 V۷7ʐ'NPX& r/1QLt#cyDՀ$𹜷wM`vƸ'?Sv6Pod:ΌO~n>ĞN5>&b#|Ӻ6`\FuT4VLZLa% @vd*%:lsUe{9Yb"Ï԰ fβsNS}̆nK/acz8폗, 3‚_;KcYL= _Fȓj~5=|'#ypnq0ܿUmN0QUC۶-~NUœbDv牣[y_lp`*fM|]Y'cs,3ʠ8iRK+26ק v.z5is1x:}ح1K# ~`;Z7 Y ϏgF-O4{ HӀݘFo?Ȑx;~Tl|J柤~[CQyjUơz P{Н܏b CƬaɒcG؉g%VdDFꎷ0U+#_oeO~4zpޥSI w*; x =r*;G.ǾD=M~hm5Gq̴N&Jk33("nTFʓm992o&l>hpC:YQ0ZC\=h9O~36&V07$,;"Cr$}c3O;VgU͟x?SUFE~}βw7\r 蚆m]va9r\͙ \b% qЬ.⁚:UNy6f~CV_Tm8_%Rp}~=e=E&qGO~#OSNU5uͻC2snQ?3"hj&%=}ֻqr9gebэ5jgV,S˟/KZ #BV-40 @4Y@鮃ΔVA5ybO]5b4:؛;Y5?7ፘ#\T#"JPX 2g􀠜{EKJ~!oseg0I N,yJ^,́wMt4-tr(ޯd~ӷ-)bjl6 B+|SB1vk8`}E_|qBf_/_cۑ7.[i!T^ZZZAS2oz% U8㦔0@RY&+yrɯy>̼⺥f uN=o8pi^gj;;8 .0U<WKFTU[QEře0 ,nZA ް> Xbe˂J]7-Qkՠ45yۂkZdO'd񦴳qGTv[iP 45Ś4 %S OyVFADqUBIՅDc6&Svdmpz7ɒțȯ?x%+FŤD (tfnN/Cuh7xqBJ4;BtMSpCT9cK$S5_уcOL3yeMh ^CW4K?мU#O67OȍJI^x 0T/Mnʋ"_xK6`30+t5x^iui+B **% =1 4>)HuHG]k@/V!''C^,?~#,׬2~TxibgcWq$Fv7.mN^~k}?kOԜ&{>%-^WѽZUv A^;%uZ봌ZJ,VoA\LIM WB"):z'i_4ҷ$)& 2ފO?Pe3XO][%/AblY+21jL bk}-dǎҏoVp.'9F7tϿqscQtF\")弒+͎|N|qk \Pab}5 /{yi]>#= d~_i+KAX޷DF6#?}VKGV ]F]/+ 7~)wxi ٷʵ4A=u.@"'Ѧuk|y1;O> Q%t ꂕ&\F6 _gle>xH`ˊ{}Θq ]O/}k3S\7d)\W"s*8ٽmuzgʯEr;lO4h3#T k}\wE/dm?{= rNPp>SB$ g~@ _Qw:e1fpwS&n~!D3 M#/Ƒ5 II2 xjDLǙ17'3pY@|u*/fK~]0-%לTfGXhsa!՜?Lrqs@ oXvn:?&7gŖE-95aϾwŸ(f\tgG@ ۖD=ٝ# _0c@΂@ Bq瞳0=@ ~Y@( hy @ P)A D(;Yp9|.bBn!<.zZ{jCQ'E?Й5P;, K\؉@ F@odc2̾[oXgo}PXλn^S;v}+:˩ c676R3u4oqav#6QQ/[mzGEDg4iZܼJgOze!|5'i`яܘҵ>ՑǢs/x;|+eب=Xc!2v:Ὸxl~O%` ˇzdB[ Sfי".z+|(k/޵Xݜoԭ3iM1Vn:y Uel]sC2X;%ckǯ ,zII夤V TFCd٦tMcÂ>*@bo,z82Yгל4FAӌ AܚEUx}I@fFUmݾ=`;ݶK|B<9gυ =;+~BX*II}򲀇ˊ?%7ʐ'NPX& r/1QLt#cyDՀ$𹜷wM{IU W١uzI1e)7}3=;k}TM3?[4Gum%6 J4VLZLa% @vd*%:lsUe{9Yb"Ï԰ fβsNS}̆nK/acz8폗T 3‚_;KcYL= _Fȓj~5=|'#yUsBۘg<>XS*fM|]Y'cs,Գobml Q2mJ++J{ϧI.؞ ̮#,ZGf է%ziµ{GKF9{<1#]Q|??)zhnLٷ܌\dȔ4e]\}JZb\NC[idŇ#Oos(j}=Om84|{^Oj~qWa{ݘ;,e~;\_Nܿ|8[(g*U=tm'Dہ}YtƳʹ~w锽jRe'v IDATGcok#esCʯ) O2n)6dhg䡢p7Ȕ\(Q'%>-?%i\=moGkcA<f%s"0@~:CwX}&EARy2 'Gfmyܤ^MnH'/>ԵԀW\56(^fuԴԩM%/}c3O;VgU͟x?ߩUkAع6y;Qѵ_' Ca[x]$`o=WsfqjSog9dEWo]~Y"G\(-0t4=۔7vZ8ĜMGrL7YٙN~>7?[^S'?޼;$.3'!3{I?+B>k+;Pb/Z#g['=/yvɜ9O~36&VE~B־,gXj .̏ [Md'dk~GVp;S8k &fw/ xjB$/li]u7wj~n1GFDZ/"p@te:_s{OP{Iׯ\]C mNl?L&%OVˀ9γIfNn'!MT o"1-kbe%XZLbVnb/۱ LW~kc2_vhL1slѴI{&PҀ>-p\0vk8`}E|qOuׯO;cC]\J!Zq_O Z0$!@Nj"*@ey_ljiML4~^&OS>)$WRb؎IEf)i=}VTc\G +%(hV>/⊩ۘrcu=kOH'տD)< D%p6SN rI?KDS@P}?7Ðv֖]ڮu>+G^lpf|T AĪDly2T/|V$ ʢ*`i_:>- +%M%/⚂Wi8pi^gj;;8 .0Em015ibÈ}+8 uԠAPXM+֧ō 6m 'EZ8t5{g޶ZG\θ qǛX.!qOksi[I(b٩_nA!Իk,4~^&O?[IAjUBIՅwqY,_ny/2'w~"U @ jPLU : [Pݨq ^GT\V(^: 5MyQqj猩.LU|E=i3y%5/x _Q,@V<<ӌNJ'y]t>Rk@/V!'cNFSۡçho(sECM\1C+*&9vzbK}2`Z]J5:ʢJ =2BfL7 ~O /i kr{Q?C }Dsӧe9iSظфvL\>>>kǢxn_]JIN4>(dӝ3qgidϧdx2j5w>\!z'~i2rxj1+X$)Faɿz'i_4ҷ$)& 2ފO?Pe3XO][%/Abl.QqF15PM``vGà0-G[C늍 blݐVs 5gSkDYI!M;u17wc[oUYvnf߽Xf=uՠSTML6jۄLt㈮asׯ5!r7neE(ݶJqpۓw!&&-&ӥӼhq!5BBD$B0~.+)di*;3'IW7ݼGweLs럫 2Ժ۾8) ?|Jr-/90ﮍ, :z$W};|^3^O;vQ?)CX2p֚Tۗ?v&M-c}!7?A R{ůGpZK7Y$ıc܃*ĥK iagN2 'oXx.Hn9d z&q5_ZhXm=X_ ^^ڳvsψw#kߺ1%m߾xuǟ-qDɛO1~txcfT2/٤s`]U4ڕ҅M)`T|?}/ENvzǡ(.q'1Gk<nioՕk+i{](ENMb%wX,%2}M<.Տ>zfZ#W$٧v8 ;oO}/Xranߟ<. s\T)6oҧ7Ň #S2$DT&q{P*hѕ_u(vzuǟhj 9gvG.)o`EjTMh' E9'nm8ڍNE3?A w:e1f}!MwJD)7"aK愤ۤWvItEhL̘Ύ~8G@ sbg%F.يTْkN*uێ#W,4˹jΟoz_ C89@ 7,;S7ttJPb} C[.E1[C'?; @ ߶$*Ir@ 89d#@F @ Dk@ D , MV@ ZD|s9H/_iL7==ƨړoLM%.DG@ wy@ Sɋv`w}Zp^72k1f߭KUY,>(,K]7ߩr;ӾF{]Tx1Te@oDrŢ=MuX\RCP \r\NiV-bBi\|l}7inw#8שNLvhJy,9S6b͕ڱ[.lK'ξl!Hބ 8cgj)U2ڦQғGLsYD5ߙ#Vž8,_\ٴq4 b\0 %*ޓ2Yr0~٧d`m}t1.}hWl' ~;X+3> ?!@ ͵d H.~~r=:}V|ßԶ!Q |}v4w[>?[iyP~iY-ύI?)]bWy,:‰穜WRƍZ݃=u(,iTЊ | G&/0umv +r·2`oQH]&KJN n?ƍ61>,2,av⭇#%?={p͹LAci4͸P(@A8AP0i/ʭYTG$) dinT۳h Ëm'$S{\гi'ů8)'/ x_[} ydye۩{<.u DW,=":ύNZ ALy{t&cK/ZǙOS~mG[@ ]sj79%݂s|.֡߭4hΧdI96Xe>j=/K +F Ұ=n?Ѝ}c'_d>l{@[CE~4zpޥSI Dƀ߆zG<9t_S#d$ MSmɐCEn>)YiV#Q8NJ|[~Jq*zJ~Xy+LKD`r-0'tLz﹃$deFAN̰.EqI!00ܐN_*}kZklP½"iSJ^:hE%fv8?]?_D2@vijjmTtI},{x%'Ѐieƛ9|.՜٠>-k\-YjY}Q}Gy<~_HQ|?/)~ ;3L/ͿyO6Mݳ1gӑӍ'|VvLJ|>%O9Vԉ7I:)Fe̞jУϫ񚬻s؋YID^C?Rkpa~W(j&#oA Nwv4p%M>͠^ wՄI^Ҵ`od܄7bpQ (B_Dbt 7ۿ6= (q_z'qA?ۜlپ8L=S%KdMRKsg>]+jK#/>OB^D>c[`1ֺҭέK㱴4~./_(cޯd) abx>tf`D|4آiM}Z`m\eqD-Is➂ _/|~5S.IH79İ521. SzGǸVJPT ~Ѭ|_&D) D%p6YN rI?KDS@P}?7Ðv֖]ڮu>+G^lpf|T AĪDly2T/|V$ ʢ*`i_:>- +%M%/⚂Wi8pi^gj;;8 .0Em015mbÈ}+8 uԠAPXM+֧ō 6m 'EZ8t5{g޶ZG\θ qǛX.!qOksi[I(b٩_nA!ԻOk,4~^&O?[IAjUBIՅin~Q0WbTL*HngjPLU : [Pݨq ^GT\V(^: 5Mù Qqj猩.LU|E=i3y%5/x _Q,@V<<ӌNJ'y]t>Rk@/V!'cNFSۡçhohnECM\1C+*&9vzbK}2`Z]J5:ʢJ =2BfL7 ~O /i kr{Q?C }Dsӧe9iSظфvL\>>>kǢxn_]JIN4>(dӝ3qgidϧdx2j5w>\!z'~i2rx0+XvY//v_ۓ zkoE-ѨlYik$#%"hiG.fn p*>۬GtiFmqinqѕ=l5.f|bdμymi@~(:}; dO,6NS!|oDIjYc_~qߗԤ5޸mMޣv*]N(¡2oO-t8LFׯOƅq H" F( ٧П_?x$]ݴGwMt1v0͡r7ȼQsnXP<>to)鷼60w]k\Uwd{غz=gEY(kIE2Ŵ]lIGa +E_/[B٧ \}$p>gtOB;J?vY=k\;K\۟0Ov+c y<1Go9rJ7;j9WŽ5pA֣0=kw=|x(0fk#_Oq^Ҧ+/QaqzyG4;ȟ/9^/M>fMuO(;都߼$t6DP ]EQvXeeDD #T^ nt xΝ;ΛyoVB]sVR@ߵ zo֮?_ -XxiVWUbjܖl9lX56.V(AΉ%΂^RE2}Z2GG|_f-7chHw?2*01p^^Լ ؿrml7dr"s'괆bE(QyKH -W{1=<9DR-ATߵ~缩ͤەm!MgFdӶ#aֱ+ք)tMN9q^u,O@ Sb9`XsE.ӎRݖkN*}ǮV-6ͻjΟ7A- A89@ w,;S3?{"~M8QЛ}Ũ$nDz+gGw@ Duۼn?Ϛ"gA _63@ '@( h{ @ ZPAoim M8l.͍r1t 3;Ske2rkglSw9滽Os&hq=yb%7Og\Z?}?i2BHt-}q/¾`:z6ܣ)$ }CJ8ln$Ixe}%dE^E'w_ D]8k`Wᗏt6``agJqWtdl^Kif\(   (TL#~N24c7JgΝf۩Mt^qVNn 2i'/jqI'\$S d ѵ65?'c5 a+r?9vC{;}`h]GXv+})eAN/zNӺ+~jTZ97NIH :cI@ ? sbl71ۮCw+ZlhǙLe::us& ?x kYUX-oTM':[jT1[sژn? -8h>vwY!l1b߯ HIKQ~۷~'j:T=W Kn`z׭ϸ{V=gU67SF~FdW\5RBMigl_Rm4%]R߫uX|iiKV CcEz<1+]?o:{hn ynG-1`Heg.,yS}JZEvѝX$Ň#Oos(Ϲy"kɢ>>\ؙK;2AP.י|BT%'Bq~aW0b:&!?A ovom("[\$ ?͠^wu"A^Ҵzjco7BpA ( |W@BJt w>ߐ? yrltJB , +s5DiE5I*u~gj ӵ4-~S|x8^/w3[Sy,.&1 _Ktl}Y+lIJnQ 8-g>lgi\l^23@T Pѧ z5;>p0DЖc?.Y uJ;c6^[AY)`/G'Św/fpkT$;m Uj3**_%D*hL>t~uU*KʬZNZcA'jDUBWˇOyNqnr?PQbWjS-8cp-'xs;MO OH'տi< řeSǥ~ D#.@(?\# hDK [^z&_ *yѕf5ɵI =NY2KjRyŔL)J^mn\pձ~nZl`hh@'gG\ʸ''qeig aRj y ̮TӫP@hhj(|^&O?[ IA4]-h^W\̸*A=gu{J`RŨXܞ!+F|!AWf4VX^kb ~؂8SFbFYasJcNyvpko~(ypINӗ_Qۜ|S< ߮;[}ZQX@ȮPjY4mTtFk|s/ T0徫j%Xc\_#_Q|ޭTTxQUV6\7.ژ3e;C)˫3 y]0--FaҌ|P bssȼ\` @dWI6Z/uk}3.m5ת^֊ 7JpN8?Ege#{MC#̛֧u!q!|BH  y |0#~.)dߥ;ϗzy^H)q7%[2<opp?aihK @sb`;7@Lֆǥ'/Fˍ8Z"w}%=y>2Jo1L F8,:On\T(g۵Mdg?I+9I5\]Ymx:}=J|0TA5AKf {S~tygon܋M%"2?A 願*7tm=̈ ;cڶ|8?3۟:vŕRYњ51;v2dΉ=ͬcyBd #зj}+2uќvlה\sR;vjiTs쿙jyW@ c ;>?gmĉ޼;.F%q;]M'?;H @ ߷$uwY}֔u9 @ E2/@F @Dk@ D * xOkk@ md8\as9lnׯ4lOٙ\cT-灛ӿEj! ɋѠ hq@ UvƮSg [闯ᵩ7oe7m2A;"/RY,)(Ln߼[aso:v}+NQaW{2d*4w//p[(hO[@N׸9l.]yd )s;rrj~1{-p;gT k>e0.3'<*eEo[ȳLbؙ1\,ܑi*a/ō'#sZ@ lQi} U9)UR]JkKt:LP?{]Yx89 8s&m)\ns z:t?#xoHhiʁ'-m^:vOQ:xܢwP˻tLV,SZmoi#mHs>23#&^ݰ|ߖœs3mg* ڼ@:v4=?"I)7`\n&s9h'Ҡ$.Qz4=yt:xY:iZˉK>4Ո!^Vw:6K+WGס0Mf^尹/%^WWW^CՆ{>=>#D^BcbH M}6i޷tнﲄȚܫ s 0R<Q_*2>xN?r,LX)gitvZkAS%$1n۬.Ue;_XTk>ٌ AԞI{}I@ffFUչ3l;·\x?K;Y Po8/}NKg?yYT7w'[$S d ѵ65?'c5 a+r?9vC{;}`[G]O:'1)qi]3=aTܲ?[4\yuneV M 3j@{̙STXC,,%fWaQP5Opj>nqK >CS/w,lkc98̆sȗ^mعJߩgҳ4s=~ '/EKmMSc\u6T /}_>~>@6YV{LV}W۰Lɯe~ z]KwٗX+pjLtB4iJ⻤WLFϱf #ӖEyi]rEwaNkOΧdIM9A7[d3~ʨ/qk_9W>fsɛF>77-?z[ OK&;;/I֣9#:O.q$ @ ~Pwԇ6F;Y.'PnOw=_}r7CvM%wJɸqpXǏioi1OS'+j)~{}FDޣvAqvF_g6/d̞?+ '+3: wvd,w  éc d*ԖiN HӨ+UM5ŃhL2\ypRֱ2eRR5G \~Z%iھKeM2?-`4ktw%ml.͍^ןѬ>mk\+k\{Mma譓+;%O)Vij%d%^ #1 @,w{kcEY_\$44{QLyaKꩍS@pY?4y\ y+)C@^ɱ)7 [O(],fH'YE13Os:K'MT"zklŎ/oL籸4,\n,_,ұmf %+i F(wzDS)@iF68.Jo{LA[G\'gq:s֩#*!}Ξ;=֦/hXE ] Sm&T@\Z^E-S2oJeI58QI+c4D*Bj)i?T֍RNPTjT?J,I'iT:4gacXZt:NfO XvJ_JZA\$5 @ noAC'ZO]z3BQa3ꯠi@oXv-:ӊǪ'J=Gv?Rs/bvfEKOhonwSx8_+wbk}a=:ջj/3*JʦCETsЕgn#~7xTeD Li~N6n|';S0h^a ҡU Zx]N¸n^ ]l@Rs?[xEvAzkQTX}y9$d"nY3r><$ 1Ӓd~҄$Kԟ5H_$E~ZDQ[ @rR 5Xjv(!eW#i=W61Yu% (*X*10q;c&@Q0hZ=WmYRzCMz IDAT;W9IWow"έ>һ ˴OtI~e8N์.+;=zײ>bd̹ 7 EI۠{/k;7aɕ8D} k2WG֧>3rі]sznp#SStn_6ظٴ:4xi}Z2_(* ȹ>r@fٙkԺv# 6w{\v0QN r*v/_]ࡤ^Y{ڸ<_)̺,xcj{#{A[o uSmX}8/Ky~6Y|)w|`Y _mbjܖl9lX56.V(AΉ%΂^RE2}Z2GG|_f-7chHw?2X]YeE O.Rm %o; 'XS9P/ϸkld)\W$s;q_{/B j^_6 \9uZC1{G]䝽Jp/ƻ79@@ blZ`vsfʶ}3#2|~i^0ovVgWJqd sDkBl:ަA(:'K6 5@V2L"aEsڱ]SrIeuܪŦyWsPf:o_!(G ?G e'`j&'wR8UoDAo^ˮpЦa~ds@ [vn,F>k:@ "̿lfr@ge#Fp@ Tv"h@Yէ@ 6Qv2G.㰹67W 6'Lm1}ߢ hP}a8A Ŀ*;kcש{Ԉ6ƠF,}oޭⰹ7Ӿu'x]Ⱛmu2gc8ln}v}Z IH^aT}k܏6î`N湝Hes9Il5o?Kr3v[aPV2F{^֢-}Dt&LLuyJ2~yjVT-^6O<,?\hAl&2[D9Rΐ$@ r 0sRN4{~+}3~uz~ pr8q4L~cMo/Rvӹ.A&u#fGH6ӔgOZۼtLߣFu>uEǡv+7w7Do>Y n\p㍫G0ې>}ddgE}K+SZrrw{ XaM1 K[{(]ߙYN$O0a.7cI^KIl|iPL(D={^_ArWFyz:^wYBVdMqUtr Nù f)N/~y~H$qOn O3vtܙIkDw^>A1ǝ߿jD f~_*NKSP){ڟ<|*A:YCav>8>d?Lz|\N_j4\yuneV 8SuU,VRncdbg/a6 2i}S0Wq W[b*vѼc9f x^d6tGmPN?+$𕞭X=P?m)ORf(3Pΰ>Qi97 Q;-1-7-i|e]-H{.(H~^,ٓ~TdeFΎepR~a8uTr ST:IIAvu%FxzIrK#N:6XFVp^Yl_jt J j.ul7 c\[DGkӀa67z]Fqүl3l1r5N`rrtqO^{Yx֊ ܛoc uxXLY-~,pŁ]BD?uoS2oU~qx{PBv^RqSUW ~^?<@dQ\M̂|vԥ~_}Cmz_̵eIOBf[[+WAEBO3<]d4؛97?鍐#\P/ J'5dEH|лl=Q0`hQlv$yJ_>t<#tr >AkA5.9NKV{ILWƒ_4}Y+lIJnQ 8-g>lgi\l^23@T Pѧ z5;>p0DЖc?.Y uJ;c6^[AY)`/G'Św/fpP޼+ό}+EoUj3**_%D*hL>t~uU*KʬZNZcA'jDUBWˇOyNqnr?PQb1E?.?!iT:4gacXZt:NfO XvJ_JZA\$5 @ noAC'ZO]z3BQa3 vf6ȵ>R=98.K;cy"ͱ̆BbftP29m>Yϋ0WAuI\ke̡i{"9s3r|v<{/hm ?'{r7dhg ƙ)9{_9 A:J0m@6ޮ l SXa\7 .ttx]S6\ 9IM|J-["Awõ(*>꼂wK27댬Y9y EpsKՆi)|M~҄dKҟ5H_$E~ZDQ[ @rR 5Xjv(!eW#i=W61Yu% (*X*10q;c&@Q0hZ=WmYRzCM;W9IWow"έ>һ ˴OtI~e8N์.+;=zײ>bd̹ 7 EI۠{/k;7aɕ8D~}Je\׿:>AkCxgwkńI%8T'ڟwƽͦ֡ MӺѸz>@PA$>hE{?2 zo9fҧw>0v0QN r*v/_]ࡤ^Yᅷ{ڸ<9:+">|.Xy ge)fU|7kן/n,4+בRZ1A5nnK6dx,VxE~\+ g֍}/{)ewwnKx5OkCRlFw -ɻ><@fY&{# ]uMB uN.*3Z~"k?I+9I5\]Ymx:}=J(!K4>!AA[b{x~spOBz(FFo;M=l&ݮlGi:3"ΘzsO sggu]qG@ ?G&D_M6Nm2snOd3XY@m*s/b ]45-לTF]ǭZlwu1՜?oZvrqs@ XvfbrN-EϏYFI[NMEq1*۱ m:G@f@ ?G e'Q6b$볦Y@/f&zV6@ m 'ZQ; Fq @@ T[}dOK@@ `8\as9lnW2t 3;Ske>_-ֆ$$0P kǘKr3v[aHVDTq޶og'>QIE/k-zП)MDgTQreL3PiThAl&2[_\PzKrԼ>oG6y4Zl4_ˀ%.jnkv|BPՀ@m9ɜ"^| 1fl20k;6R;/ IrGz߃G3̏۫c[CS5aqc}.r*81k0} d慐Z^}@xupE=>PmGS;RH%:V.p;+Hoi}QR1uYelY]3,1>91$ Q+ri $N=,w#c t VxxtEٯ28//7Pm:Y/΄ۗNUe;_XTk>E^E'w_ D]8k` ANLKR{&UoH$qOn O3vtܙIk|\NOIzȟ,7w{\N\}k?JZ݊w_J|Y{Ӵre:/ͧɮ9o烣k9lk~NjDWs$'Z. .aw%~ vZ:;-ˢZ@~ӺILy~wJb\EZmLbOAegmc;VX1'hv:v[bFSLe::us& ?x kYUX-oTM':[jT1[sژn?`-8h>vwY!l1b߯ HIKQ~۷~'j:T=W KnP[藦Tz>MI|a-=urTyQAĘi&Aw )w)#?uu@Aoy?m}igk־028[mi%1]qjcɫGm< b!XoyJs}{oD'7s}p^ăQK -3U5ju'$YhΩowy>z/BP'v>HS2$ۦ -?eC85t?\;M{̙;`ss#+В'[N.s9;GP}ڗ?M{ 'x8pzUR~@ ?l;C# ,|h(ܻǯ>JTCAB;æDd8r8.$AvZb[nZ ʺZu0R'% 3:{=A\_ҙ=I@Q78WJA*OVftYN'%hSK%tPֱ4 sm ݒmT͑.Cy!V} A0c| k|PzIrK#N:6XFVp^YlAK?]à#-(m gs9lnfi-1B IDATEMSjK4'$EiԕԂUJy!6#^S[zNYq*G G"#K?ejgjlnFQOX%NF f )5*(LdLZ>RAw2guUu՟7M|JR/osJK 85Nyʜ{Ǒ(,5Yώ/o]py&[8F2Iڗ?I^\Yfꘄ@ dп;ڽlqT_\$44W{QLyaKꩍS@pY?4y\ y+)C@^ɱ)7 [O(],fH'YE13Os:K'Q 8-g>lgi\*j Mf;[R^߉\ǦitV{ILWƒŢr#)wzDS)@iF-~S|1 v%$s^+>dySNQɵU;w6}dv?3:>SrlK'o9{oz\PPw4%G} &$Z>/ϧd)buʒ2kpViЉQUvS~G-ǭ7>Ԩ~X_O5_4L I4M8 Ңq2{ @ òSBT"@oʏ[Dp=~3:|k fovit塃/rMrdy2tӮ}V# Z֤ .(ɩL٩?{0hGy /u^.OQ)>V'36ޏCyDln o U|~i'o^V4.%5Pb}e k&ԔVUzSk}d}h03ZȞ"ɡO?2Aby)P9]s/bvtk;4)/9s3r|v<{&GliB?'{r7w3( */۩a@6mf/]9&Oᔧi8B93e=bA=4g$ϧdr+ {?\+y'$q_zA7'Tm A:JHП}ӴߒO(z+6?A UvB]NJK#]N%,j$m:6~f*X*10q;c&@Q0hZ=WmYRzCM;W9IWow"έ>n2ӫ:elһaGEuPt9hˮV=wvVLT!c̹lTϵzpG(J{XFعcdOV~ $!ӿ:ۗ46m6m o0oZE'\Fy5u}%!((@[.-"Np[Z@Qn@ QAAY" !Qc?'/'wƽoth­.\ہD8BP(@a9?g'٧Lյq8~ #o:pxAP&88m~VW˯^ԉ]93hĨi}΂!o 7|J2-'zpm6Wz} 5ڛ`9wJixjÆMr9|h3y|r#տKןaݍ.qQ H*eO|g_Y;菓vZY^}G74ر[}i\EK_-wzN9p&":q qٽŠE[k|Ů+T1:fQp=7^؋Ɩ7{qN{֞ Z򜤫>Rʈo>uwm%O+sxmbs-$o1cΎ _G*[}FG2 @ j;ľrEGv[2:@ rs 6fqWW \#cgw"K!v"Z cȮȪcԾgP޾_fqo;V^2q}ht>}Mwm8aDqen41|@Q6\BV:N.aĘ};eطD h>߈N>r5ͪeFgĄKGKBX6?{hGn9rEZ*z>EWu%s٬OmT1*dq٬P#Zw yQhgMG޾׈j;'T҂EFe7wS@6dpQsD5̨Rnvv{v3)x Tv?^ꓗ{8l={¦[Sw\O鋒dC Nϝ:|տ?G{Ezk6U߉2} k|8g4kEޜ#^{+*i;R{.F1ۑ>}G\QԷg՜4W9њxEbg~nzLzXiUK?#%l^|WC3Q2he!;/e!.Xۄ@ 4BCMdʀ_iO?_}j/IYڇ9(/>YܗR,Aׅ]C6ңyz_G 4$4%fsZfq3E윤*{zsyͺ10)uXs'6ˎK=<ݐ!okxM3 [+Ebed22fppAuLVF&+#ҐD*)ɛm>ҬGÁ a(|tz_CJ^e#~RuZ9 ?^Bz/Pي߹u":yzJc[7R:-9RD;тf**OAAA$Τʽ>ҿC?'{r'#1S.Z[tUh6^QUhw¤5ggZQ=J\N~{ȁl='^R,$zkӒN M:8WU?MGɲǨ[ϣ _fq_e@5独K9?Mrb/v{e=8sp+H@ rvRTx^p=>~a:2\J,50qm (:fvź t׽` rOG,N`pdJJ^OfcO?&HY'mUYQOTf@Q1 LS=ZDc75DΜr|bډ :i\Y%j*q(xS##m1/sNkt0/O|Txc݈.T``^'eci}ҡ./Q@.9 #uL6yhA{Ŋu'-xrc~wSE-㞜%&]T^Ԝ w03c %>!lR꫶d͐b4J %qU,(-[2{@VΛ"V]~9f@ UHΩ;1ڠ5؛jGM3=C#|Y*E&=%ژX:-aZ[a xJles عziMCQ5sǪ5h yB4 \!B+}bonQ[饍>l?-|;s>~_Vڸ>i;%ɡ|utWJX-{;* l>x}9DWC.j]Ay>;7Ϸ^WVL-Gl([X1N,<:MQ{ýik}#{Je{fa`q:SG,i&] Յ< uIi\ jrZo=R(ˌ,?~:!떰>n xCi њV/m4@ rvyo5VŞq ɯp_E4z-eh&Z-",όˌy_lQ-yiƏOд<8N? QYp ZҲJJŶСP @]OCH«G_4n]Tm$cT@86,Muo B5 :u?xJsks_`CzkrDtNZ`ܔg>RYҸ@FcnݤwZ.$'(*B( ȹ>=ϥ=9@AoƟrgU k.^`gjeNs 1j~߃ r[!|Qum\}7δ=l$x%n^By y{bRG(s!/E6~^4wPs' EL |ޚMttao@WHZX "ՠU ugF)؝})~4-v^77g2z4o齰-NjrLD  u؛8'm܍=kTAyNU )]uF~noYQaŢǛn<V\1^{\` eX#otZB*EN}x E}Ȫɪi Zgј;}Fإ ࿋ #C~\@\o? V.<'3B&}1cΎ _GO_zVy1Lg|s~.-*31?r~} k"3?TeWJY"*j:,Y|z*o[rB ɸg}uoңoR8W2G|Σ ೯^ IDATX;,oxښ}w8Hkl٧R֥Ѣ@WPL Z֡̌X;nK stOXu]@~uxegFGӆ@ oB&hTFk,e{?g:PT##@P[jò]+E-;( @ lCϙCv@ @  hP@ 7gu&BTk j; h@>CUh]Iv}ڋA241uidt@ kDb.Td ND ` Yuyl |k,m^ GFֲYu^66g(a;QnDmML]eT}k܏lͪhA 컐ⲓ vh?!1fEg>gq972QZ7m0/>3'&^:bMo_ȳܤ}dW#"-fu=EE9,n\}j3w˓w@m'!kISQjH :9>`G̟fMțskcop5zM%=X@6dp$sD5̨rlj'ۃN[Vw]>a_U$肋>ۯTի'z&hy4Y`s=cuHs>2+E}q{6XY-Ns {+S?$qOn xEUݥ TI:`wp㔸 ]Z:T!%5Oȓ3Iv?ȶɺHrQUFs-=KY98M{+@ruyo^iIqwI@ mvRTx^p=>~a:2\J,50qm $Z:fvź t׽` rOG,N`pdJJ^O)fcO?&HY'mUYQOTf@Q1 LS=ZDc75DΜr|bډ }gަGEỸ眮Ա=Fl(J}]I᧠7myu8 H%(-l<%U&X7 }&GH[nZ4/jU.g[gNZt9G^Ƈw4}2MIo~<߹v{&TƇ#ͧdI규pA*>$uЙ1vHMS6W o^f:vվteOjk0rT^U3EV D*?βSKq 81098ey?r[`t0?{78'Rh&QzS׹ˆ:s1(j$"J&Hڌ#+v߈aa8t%$2q3b$)j+R5G#up|W*p:O L~lPjԶLZ1\vjyԡ'fd.z/OŇGV 6U[Ge~]8f_c~3ҀeeB,.}єѬ>k^̫;̆kpMmq؝k:>^n7_(ȿ?[U=j;sٕ^GB+-My>%O.kCNhyd7W(-[2{@VΛ"V]~9!;4r*4 p-ibRhRErN=@pquP@(2.HiRo _#@TWng+#SoٍeK'YQ5sǪ5h yB4 \!B+}bonQ[د ҷ%P' |-1fvr%}I3iYl Q5Mʜ5sQ:'ޛل4Ӻ̲ISҌ>p^0~VH9Pl=#~.[k'qȷ>Vk˵U4EQ1{5cމA!D%TūT׼QjSSji"y +y!?C8J(gBZ1,**k"t.*9O(]yRVNǔMSӭUNbPPUSX-`= _ٌ|Oso ]G+sSkuMii"6{w |Ӧx\e/i~N>n.~ag0;t>۪]u^ޅk> t 囆ՠW_#!myZ`]9ςQ_ߔ2t_>+Ev[o$V7pKuOshc~fvcuh}-h^OP(T bQ4Fs}ϥ=9>vVЛ_>v2G~wUd>y+UkDcūW%ubѴS2oE2:ܙzk71rHogXmeqwKmcqYh4.y!DA@5b>\e%4MKi{8i =U ξ#*GF {yQ)DR\'YsҶݳlJ5$]r߄uF~noYQaŢV/n<V9Wó&6mu0]ƪo^w4GЖ~\@\o? V.<'3>dd4-hX%\]+02gvM>W(ΗԔ>mVpƶz}Ik,e]:-j;;_Z]',?}ª89[BuWG dϋ;9' E_@-P>qR€ls~LGjqs@ v z [mXkU6{GTw@ m9s@ QWzV6@  '>@m'@ GA \TmA~n+CS@FG -F$BE@ oDƐ]UǨ}60͗ݽ}vHН|jd-UehSy6+Kf?@E{ކU @w(fq٬[Hp Y,.; hc]|ssc*5|#3so|bBY#V9h濎x˙#h3y|-˱yer%Ӓd  2Fn{i/ȷ۸D65f~>./6rHF;+[SNsiv nƎ 4Dzf667hs{9_ ~s4BCMdʀEL{ʸJKf 1\vB!kvҾRg_ }a/Y"( NNנe'ʱx`VNφ dedV2H}R%奇6;U_3<)>5)40,Us_Dt6]Do  ' &T%u&Uw?'{ras^-z 0F1Uv.LZv$Zy;8qJwZ]kWYLVFw~IoMS)d̩i/m.dvrկQ\v|n㒹lֻgN֦˞/wMLZyI/敖WqwȏĞѦm'EuW'َq#U) 4R:ZFΐ+:fvź t׽` rOG,N`pdQJ^Ow2f Ǟ\TM Nڪ b;?-7M{d|+=n1k"P;[9)|:?MuyYwq9]cm%g{لQ@Ao/KKLnD*0L0S/ ȪO{hA{Ŋu'-xrc~w#~tܓ{xQU:z11L|=D>̀!iJLmHw:-eJBwS2$[N c}\HrJ^;i)[膎޷]R/3;j_ _[&sǗ?Qk^̫;̆kpMmq؝k:>^n7_(ȿ&v͒e٤zi5/ sVM|X-muH6KOp~s$#h)du^r:E'Ghْ[r/ɜ/'94r*4 p-ibRhRErN=@pqu%! B)}[uϛzU~>ro8,\TI?eК{ҹM(w slBfi]-PbB}y%S+{U;'6E^)a@e<4`$ZAA<]NgKFX_ t alwM ޼>n^p ~nJwv/uj4{<,phwPN\#~.cܓ0qގd<:bx;[agÐ^#b7;K IfUӫФ@hi?˵k[)~K!AMNj}Rk Қ{KU@ ,^Q_oC$(*F4o+ܗa& F5G3c|=Zg8Xi )T'p<0aQiVXWIkwPyF drj>h4n:ݺ}Ms|@H()`?usJr_lF'E ֹd٣ԕՉ9ѩ4sysb~=|;`i_zuA VI\?m43C6v?&n s۹5ݶt=ݡY\WE_ai/ϋؼ<s1I T qjFu߽goi oH1J7L@Q0N]܋d@>tq~{jxA糺8؅[7]#@ } 7 EAFFCF2(s**BTΟx[և|pz |J1frui:;}vmln ϷE ! p bP,7 F(r'qITnf~K4 eތ?/9j3ϸF:_֘Oi:l$Çk<'h)"O33RVe3s |W̊ + zt̹zc6n{aމۻu [U߼*wĵُ#[@`s:#(CVMVM_ВKO_zVy1L,ȻK{2}oRw~=Av;`Y+ɡ0i 8'AOd\x`wn؏_Id;sXcmt9TXʺt ZԊ|Az5}|iuU׫!}O.lu) @DYę3v k|0|~NgUWqd sķ+<4:A|]tww)sN5Ŀ Z}ڥ&K٨ϙj+@ l+<ڰ,n׊lt( 29@ j;s&F@/+f ; l@  ¯f6P j; h@FG2&@ PٚQb_#{P ML]@ n; @ CvEV]=ۀ4_vZ6{۱?p#1Cw򑫑lV].MJ,/N6hzBSW(U":7i+EE9,n\}jƿD2ۯTի'z&hy4Y`s=cuHs>W+S-oQ}"IV#=%{ "74Br,n5?'c5 L\TL~Ӟ~2nza<8&NH;dNKa}1!|zf`. NNנe'IyQv9+8:WXwr'wMǽ7>T=D>+ }3ӐlZ%WlVɽZhyl_i>%OR;R1y%$Ό쵓FjZn}(2ӱӬhm蟤h]mY2٫j3ShU۩,; mS*/nXm1Jw?ɽy2in=,Ł&Z! :j 7yY (3w6pH"jT8ّbvZ/{No@aQB"+#)7#YzJb"UsČ1ˆ-Y7 !w:Z ePFm+yɤ;eG{bOfXy9z_|y+jtPջuTֱ:(ׅm%<7+ ZPYP&P0Y܇MӾk̼l 9嶫|rlqOSZ'J#!|t'8m?Y9umg?e[ 9?\hlɀ[Y9oXїw?~J] 9QGBs41)u @UHΩ;1ڠ5؛jGM3=CV, "as #S>LK}+~QI_9|Nd7V;U/MdMRG}ֈנ. (s. Dk銽 EoEFcpJߖ@G=\/{{S$$e'G_֖4)sۖΕ~h F(>lh{og2Lz'heN}`*_os֡2a{G\N=kxgdžb ƥY?㟖vaï[ilAN8@SSTWZUK-?e[Һ ڼjG}݄CT6NZEPTk%$ !$]$K@X!@XS7Ҧq2; @ ZvPnly=;mzޔiǠHx xdAK[-?[$G`:6d?j}*GZ*5V>PbB}y%S+{U;'6E^)a@e<4`$RA<]NgKFX_ t alwM ޼>n^p ~nJwv/uj4{<,phwPN\#~.cܓAxÈˋ,7p_r)0]fzPpt^2\ꬔk*/J!5hw9"fPWV'濍N]0nJ3^ [O1'y,mKE ֞^Olq9֧}9iܓA+3f7pޘzre8Nra _{DsMImi-H{oDPTA q!{**(!^eJi P){$ͼD5)Z|=q|Yŋ)Z}XÛͱ\9ڧk]DWd$' Sc v]7wj})Y/G{?k^KZ{9IyZƼu ׌@!q+/[8OuU=cʻwI7`IԮ[<V>XM<|qbySy1FM>j 1Bx}!VMx _㑿Q%\[}6n=pVQHyԖ^K6/ygpp#x X_,t^XJc Vש4D 1 < s{ =>\U8*SRRKlU{9zvqx|Ѩti[2<23BxTH_"_u`UQ~*3L:ߵrYmF;V.'1N#2D{觯/T>azf.2KZ !+c^E5n9MWѬq`_!"ԕǴ:^^rǖoޟ!?w9{*]ޤ_=e˯(ф.f=T$YhZyj؟wt덣{{2xއݥ/PyGk yGqofy{Ni37W4=Iw vJmĈu{ϔeQ?HJLq}ߎv7] o;m|~a .Cie~ ?^$OUF/<}ݦ_پb2=[ܷɋ>vj ׻FLi×JGr}Z%M;s0_P0?êrR%Fڽ!Rs<}֣B$xxdIvlԖ9 D+m?Ͼw]>ޝ6<=0eYe ,kɗ8H <ϧܟaN;'JLmeՐ%Z|j]*u|37 k7w>:26zw~+pe&QOL۸z3|Xc+1k_ ?J[³KW)䲼s?N]ןrn|B{]|LԒ~"u`i'!ܗK7Oi=d8mfI8?~][vmw#l:P5QP6`ITϮ>_Al57caWwgߴ*BByWnU6j#MzD8odP5,}*ΈI.A)s;;b)ZV~quxyɘg+U3ХԝCn8m_W#ly>t0i\xښy=gHQFW49Z&xS>_N6AG v*|—U[}j3|?}?N]Q6`ď D8aӱyA1k3|ҋjG۷|ǚo~>Kzشz׊ay;^J[}wJ"}aKoZ#<;7[RYt{9QJbfWj2O{g O$cZ|s1F!)8o~PYwxw\㘟whkz>f ZN(-_K(p)B.SF\ky˘gUYܱC'מٽ{/M1cK9œY\¦_e$\qU h ŧNqT ol}ө cՙUc6;\qZo"n&ذ_P}[md~,13C-K2wot8v2A|/(ϔdž/^wYciE"]Wy367[Rx-@!lv3+/0=Õ9.{zѵ}3&9[f BlinauxqVSC5>co_Kt~d{ʉ|;uzu2s1ށro!:\g)TOSF^*CZSPl<'}P;Y}9_"99*qgXa1/9vș4U'imW˷i/}D:8 'kT[s̊l4!+_SY=L3R8GF3*Ba IDATuA\+6~\˯Fz I|Awz'uz*Nu52dǦHL)+ m#9Oi~fƮN\[* ãO-!!|BI; DjSzaƈ X,%SYRHΥEϼ<1QI=6^?RF65{noAYũkD+ q\ U2^8$ǿxo٪N_}s$@~tӤ/%V5Jc ?nwCl8aa\OShb;_qγmb5 &XlX1"1T;uc0gVSfsjsMՏh.$zB!nBa1v;z|]|\#3]7Ū7Soumc?mp2nxEe wi?r#Uз_$E"ݼA-7s2!-Z8Ϳb.j %څlѦeF! xoƭNY>*wo8^R%?-sƒ y瓏lߗT-IFȿIG-Ux!@Ͻ1Ԫ5akΛl޴|v^؊Y/vB1d_fN]r}>xU9N~Ը=#ɛBmLM<2co)NOo*VqG~(P1o,;ڔo/j~^ؑms?\!ܗ[cBeUH3Ʈ#S e?q.ҚQƳG}e ^%|{8B~-a;$O|md ydKg1<{к=ȝy벿_EF]Ϳ<ϣTf->yǡ斧>1~ }|t8B.S:z|QƢZOBcmn-׾gŲJ Ӷѻ^֜۩C2Z_'ƵrYmF0g^lWscSۡya$Gv5\V|iڗZnF`||0w]jfc_99uǯHԘ[o?~y<ϣ\>Z|,gsCr=u<4^_nzn܎;l߼jg~tzgVPf|ڠy΃z@є޺//,iߎW1ǐ oד9}8!Ķw[gF݊ NQR*hĔ9?|y/+קLz=Pb CXeY ||sBi_VV iXŧm=ݥ'=R]c}B)/lē܄  l!|ї3[oKhx}B/dg~Kυʂ{ 7Tjn8zۮh}.M{݆ {u^m]pͳC6FE)䲲Ė<5|7~CN]i!< |=軙[~đMe,INm|ZF5^rR7z(턫)?жS -}t$CfSk/M#zuel&|7&s Q j@L|?}?N]Q6`ďB6vj>/{1߾;ּ|_]Ʀ׻V WBܒ﫽SDvP] )="72Alx־]g$?rZD[9Н\{2= bˇ]t- !f;:d3rԓk7Mx.dh!DwVeA<+U3ХԝCn8m_W#ly>t0i\xښy=gHQFW4Z&xSOvvsM"{Dݸph7Z;wF;{ϊU3Պ6/iw}ka]f+[Bh8KzkOq;5G|Z_[md~,13C[i+/:#0ݦL~?.3jG.i2Q֜/c?E^1$"<|i<ꗓm#_1?Hָ~sf7˸VY|OGkaM-akDD򵄈)29mĵ#;7[RYt{9QJbf7s^U;tr9{ݻG0Ҵ3jxyS1><>-W%nZeץw33oe In={_ZNWAiЎ͊{<w~ÖغX?iX_q} xyhhF)єӎOg̈?=G[Wy367[Rx-@!lv3+ ;Qx4k>"Vq-[+6~\˯F-%`e0C7˿Zz'uz*Nu50u}{޾M֜-?mɷ%k:?3og|}uz1s1ށro!:\Hg[ȭMs>R*U0a59\yMhKƬ}Ŝv^3,􊰘;xL->j2pʚWΎ⎾yvߞvvn$x]d0\7OPdvzhBx\+c'Bm3Ye +Fڧk+@--*%n &v^(97soZj5Q3!/&5IV>I}UGnۆesO0FM7׌?sڷV>I),)$ԢGgr D(vo)#=wo7YoAY DW_P}%m~j qk RJHh R 9*C? DjSzaƈ X,%kR WX&9{Vu#a-&}q(A9œYUD|?^ s 5C@F Z| XRE;W+IɬriѴMPKufj?3"Wwm^}[&a1v;z{.̹ȓfsnr[ !Dy3U۸k{XFҐHj _ l=<=Ln{ͣYFˊlgY`gW3,ʒܚ:Uoړr:1"̻ZwyӲ,FYO!ݺ$͕%q0czK?s99CSYZ 0 X*OsIO[_Arb^LazFCWڕ 0<#'yi_襖=ߞW6{o/q0}ܠCZyj9^USNnxn%Kf4q;m}EL)?7|('m<78>->&ƭ1$q+/[8OuU=r$4D,5 PdD(pāTvUFyy5wJXUmӃm[reێҞO4t; wo݀kuh^RϠЖ͛kĖS.j %څlѦeF-NGOO?IyI#&̓wt~Wq⅘R!*ػyI+FJ~%Os=F ֦s֮Oa)jBN;^ukWer{[ezخ[/_VsP>gkOEjn # ~狾?effi2w;-vJ;CN׻mÿ޲j 9Zğ>i۩e1^Zi.xhSCQ/s՜\kOYisHŽf_.g !b:~͐?($}l'}cYXW,#Yq ڤf }f.\ٯ#Yf(0qDE\s_~t;)H޳PK^`Vml;1mS1$o ]n:۴O#c>=½ҬzCط/{iǸ/~%DvYEz~ ؕgLaU8;Mݗ#hS:EhǛw |Ɏ85Ѧ|;}QŽmze 5{ wV\'nʥg?ؖg(NJҰDlvidcBⷼdӜeu^G?<_yԯ'l~8XR'QAΙ8ӆ!s_LZ}PӎCtzY]\~?Lu^܁S+B*OhR/B[G?>8rzꍧ.-/wyﺘw<8\?Jy zGN|Mc7-2Bi1>u1^Z)b9IǍ~8N<%1~|j|o爿|&:kQ{ #7vP+ Q0Ecսbw;)KUi' @ H;i'N@ H;vN@ vNv"v i' H;i' κdIDAT@ H;i'N@ H;vNv i' H;i' @ H;i'N@ vNv i' H;i' @ H;vN@ vNv i' H;i'N@ H;vN@ vNv i' @ H;i'N@ H;vN@ vNv< BӜ8l@g+- 8@,]ǥzB]ѨNZ̊sh:G/ OouoDs3k>!|ȔzY5/yg 3I;Y~N{Tm[Wfhbu7;LT+vr9H@&r2U c>Z["Ϻte#^ 8\XϏXSݤ=?f6;Nh\ܞ59Laqe(njR>;6*Ėy|gT26M@Ϗ9zqxf_}ٿhE]Ǔ,v c}"ͺdtwaUҔ+Ra IuwHi lDk}=9`%/H^zBa =2:4]O֎y|y}%x2նJD۟6IPt F:w#qu u2B_oo~ޯصvF!f.Yv&ϮH[)$/85A#Z5;G2Sx-hDi25ClCy%w"e;z|0˥JzȵsrZ!"tB/fN⢯_嘤jJ-Ks #Y}Y٪ Qݽ.kJm\`R$*j%D%fe0V|Ik띪h/_pI.|WI~E_W\rjy::>XJ^"eͧSGe+{]!L+Pl֖|+K%">+RR%\8)[*p /tLX9;|J+?ygGѕ6!ԾYH7N)&RENc‘f/}sMDmY Ϻ+3lVzJ&-o_hVK?z8mr b[Rݣ!ݡi'mCR su-%s[h<*;o^M8k!!۔9 V4jRflݹ>gkvZ#?#Կȑ_>qeW?uHŻ@,H{bFAe^v0Visx?{J4;R5G9Xs|Q uCu\t U=[qx*6!GuOgmJZ2U!}Lyz[ ^{q櫃C/u rQj 0Rji]"кذń(oXb3OrƆU|Wv*J7uڊO;xX5 ,,;IϴHr=q˶`;8jǬg;&FsǪFмɱ.ZUٕ; ,ǩęv%ItBc0Op j%O.d9 R/eڧqV$1%EۣoI Z*ꦍFOlv˰§8V'ES'EVENˤ?rl>0d,NCrg_qA)("k=<2_xD7yf9& *{|CU-TYBayʆ`PW;904seDXOm'D/(|x>at-$ΕZT$I)J]52iamWV jـgV*TP <|=:vqJB4 *KYyT;:QA%bJIsolZ m袴2~3K>s@e#f=Ԝo֫TYk)/H_rZU[i񩱝:~2B5?/G`S,O(5L=YרoۭRtfGq (,U?XQ `j ϒgX"p*RGk֮jaUQ(_wTr"k]avhi'c]ѡ6?]"If_׭k2&+VYwc t!*QZ9hxҜJD;~ i%Teَ *.9)G\TmYBȫy"ayumђqӗ9pEX'rK::!\X w29Fi!V(GbTs;8|[bIs_2.8eYݸ*!|JXiI(k&NN;Z*J;1OzG.3ZWs@o63-(҂hnOmx'Y=j*>Qx|jni*.g8FXDJtOK )*ٺ5Pvf{?r7պ*'maF92\R %Qx&[ 9kB_ʺ&ROнUbqN|]\u]Q026O,bUm*v); ^t=qȍ]2z`o GӫBOq+;My[ܸ$ȅ?_%~ڥR0VU:!PiX/ ]48)pφ+ ʻt}DJzZGOlG' '4V3 B/Ǻ\n(' r׻2>Kةܖ8_.xmphRAŰ ! e;֩',Pnre˓Ԫnzc;|$]l<ך}/F#gc-<Ź 3L$J=)Zun}?4~wkλ_?r%F%m|@v%& `iv%rZҮIENDB`ctop-0.4.1/setup.py000066400000000000000000000017361252520231600142000ustar00rootroot00000000000000#!/usr/bin/env python import os from setuptools import setup with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as f: readme = f.read() setup( name='ctop', version='0.4.1', description='A lightweight top like monitor for linux CGroups', long_description=readme, author='Jean-Tiare Le Bigot', author_email='jt@yadutaf.fr', url='https://github.com/yadutaf/ctop', py_modules=['cgroup_top'], scripts=['bin/ctop'], license='MIT', platforms = 'any', classifiers=[ 'Environment :: Console', 'Environment :: Console :: Curses', 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Topic :: System :: Monitoring', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', ], )