h h?@ABCDEFGHIJNAZ@A?Nn+ANn+AA?@ABCNAg[@=ZA=ZAjN? # Mike Fulbright # Jakub Jelinek # Jeremy Katz # Erik Troan # Matt Wilson # # ... And many others # # Copyright 1999-2003 Red Hat, Inc. # # This software may be freely redistributed under the terms of the GNU # library public license. # # You should have received a copy of the GNU Library Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # This toplevel file is a little messy at the moment... import sys, os # keep up with process ID of miniwm if we start it miniwm_pid = None # helper function to duplicate diagnostic output def dup_log(format, *args): if args: sys.stdout.write ("%s\n" % (format % args)) else: sys.stdout.write ("%s\n" % format) apply(log, (format,) + args) # start miniWM def startMiniWM(root='/'): childpid = os.fork() if not childpid: if os.access("./mini-wm", os.X_OK): cmd = "./mini-wm" elif os.access(root + "/usr/bin/mini-wm", os.X_OK): cmd = root + "/usr/bin/mini-wm" else: return None args = [cmd, '--display', ':1'] os.execv(args[0], args) sys.exit (1) return childpid # startup vnc X server def startVNCServer(vncpassword=None, root='/', vncconnecthost=None, vncconnectport=None): def set_vnc_password(root, passwd, passwd_file): (pid, fd) = os.forkpty() if not pid: os.execv(root + "/usr/bin/vncpasswd", [root + "/usr/bin/vncpasswd", passwd_file]) sys.exit(1) # read password prompt os.read(fd, 1000) # write password os.write(fd, passwd + "\n") # read challenge again, and newline os.read(fd, 1000) os.read(fd, 1000) # write password again os.write(fd, passwd + "\n") # read remaining output os.read(fd, 1000) # wait for status try: (pid, status) = os.waitpid(pid, 0) except OSError, (errno, msg): print __name__, "waitpid:", msg return status dup_log(_("Starting VNC...")) # figure out host info connxinfo = None srvname = None try: import network # try to load /tmp/netinfo and see if we can sniff out network info netinfo = network.Network() srvname = None if netinfo.hostname != "localhost.localdomain": srvname = "%s" % (netinfo.hostname,) else: for dev in netinfo.netdevices.keys(): try: ip = isys.getIPAddress(dev) log("ip of %s is %s" %(dev, ip)) except Exception, e: log("Got an exception trying to get the ip addr of %s: " "%s" %(dev, e)) continue if ip == '127.0.0.1' or ip is None: continue srvname = ip break if srvname is not None: connxinfo = "%s:1" % (srvname,) except: log("Unable to determine VNC server network info") # figure out product info if srvname is not None: desktopname = _("%s %s installation on host %s") % (product.productName, product.productVersion, srvname) else: desktopname = _("%s %s installation") % (product.productName, product.productVersion) vncpid = os.fork() if not vncpid: args = [ root + "/usr/bin/Xvnc", ":1", "-nevershared", "-depth", "16", "-geometry", "800x600", "IdleTimeout=0", "-auth", "/dev/null" "DisconnectClients=false", "desktop=%s" % (desktopname,)] # set passwd if necessary if vncpassword is not None: try: rc = set_vnc_password(root, vncpassword, "/tmp/vncpasswd_file") except Exception, e: dup_log("Unknown exception setting vnc password.") log("Exception was: %s" %(e,)) rc = 1 if rc: dup_log(_("Unable to set vnc password - using no password!")) dup_log(_("Make sure your password is at least 6 characters in length.")) else: args = args + ["-rfbauth", "/tmp/vncpasswd_file"] else: # needed if no password specified args = args + ["SecurityTypes=None",] tmplogFile = "/tmp/vncserver.log" try: err = os.open(tmplogFile, os.O_RDWR | os.O_CREAT) if err < 0: sys.stderr.write("error opening %s\n", tmplogFile) else: os.dup2(err, 2) os.close(err) except: # oh well pass os.execv(args[0], args) sys.exit (1) if vncpassword is None: dup_log(_("\n\nWARNING!!! VNC server running with NO PASSWORD!\n" "You can use the vncpassword= boot option\n" "if you would like to secure the server.\n\n")) dup_log(_("The VNC server now running.")) if vncconnecthost is not None: dup_log("Attempting to connect to vnc client on host %s..." % (vncconnecthost,)) hostarg = vncconnecthost if vncconnectport is not None: hostarg = hostarg + ":" + vncconnectport argv = ["/usr/bin/vncconfig", "-display", ":1", "-connect", hostarg] ntries = 0 while 1: output=iutil.execWithCapture(argv[0], argv, catchfd=2) outfields = string.split(string.strip(output), ' ') if outfields[0] == "connecting" and outfields[-1] == "failed": ntries += 1 if ntries > 50: dup_log("Giving up attempting to connect after 50 tries!\n") if connxinfo is not None: dup_log("Please manually connect your vnc client to %s to begin the install." % (connxinfo,)) else: dup_log("Please manually connect your vnc client to begin the install.") break dup_log(output) dup_log("Will try to connect again in 15 seconds...") time.sleep(15) continue else: dup_log("Connected!") break else: if connxinfo is not None: dup_log("Please connect to %s to begin the install..." % (connxinfo,)) else: dup_log("Please connect to begin the install...") os.environ["DISPLAY"]=":1" doStartupX11Actions() # function to handle X startup special issues for anaconda def doStartupX11Actions(): global miniwm_pid # now start up mini-wm try: miniwm_pid = startMiniWM() log("Started mini-wm") except: miniwm_pid = None log("Unable to start mini-wm") # test to setup dpi # cant do this if miniwm didnt run because otherwise when # we open and close an X connection in the xutils calls # the X server will exit since this is the first X # connection (if miniwm isnt running) if miniwm_pid is not None: import xutils try: if xutils.screenWidth() > 640: dpi = "96" else: dpi = "75" xutils.setRootResource('Xcursor.size', '24') xutils.setRootResource('Xcursor.theme', 'Bluecurve') xutils.setRootResource('Xcursor.theme_core', 'true') xutils.setRootResource('Xft.antialias', '1') xutils.setRootResource('Xft.dpi', dpi) xutils.setRootResource('Xft.hinting', '1') xutils.setRootResource('Xft.hintstyle', 'hintslight') xutils.setRootResource('Xft.rgba', 'none') except: sys.stderr.write("X SERVER STARTED, THEN FAILED"); raise RuntimeError, "X server failed to start" def doShutdownX11Actions(): global miniwm_pid if miniwm_pid is not None: try: os.kill(miniwm_pid, 15) os.waitpid(miniwm_pid, 0) except: pass def setupRhplUpdates(): try: os.mkdir("/tmp/updates") except: pass try: os.mkdir("/tmp/updates/rhpl") except: pass if os.access("/mnt/source/RHupdates/rhpl", os.X_OK): for f in os.listdir("/mnt/source/RHupdates/rhpl"): os.symlink("/mnt/source/RHupdates/rhpl/%s" %(f,), "/tmp/updates/rhpl/%s" %(f,)) if os.access("/usr/lib64/python2.2/site-packages/rhpl", os.X_OK): libdir = "lib64" else: libdir = "lib" for f in os.listdir("/usr/%s/python2.2/site-packages/rhpl" %(libdir,)): if os.access("/tmp/updates/rhpl/%s" %(f,), os.R_OK): continue elif f.endswith(".pyc") and os.access("/tmp/updates/rhpl/%s" %(f[:-1],), os.R_OK): # dont copy .pyc files we are replacing with updates continue os.symlink("/usr/%s/python2.2/site-packages/rhpl/%s" %(libdir, f), "/tmp/updates/rhpl/%s" %(f,)) # For anaconda in test mode if (os.path.exists('isys')): sys.path.append('isys') sys.path.append('textw') sys.path.append('iw') else: #CJS check for textw and iw patches for both the nfs and cdrom patch area if (os.path.exists('/mnt/source/RHupdates/textw')): sys.path.append('/mnt/source/RHupdates/textw') if (os.path.exists('/mnt/source/RHupdates/iw')): sys.path.append('/mnt/source/RHupdates/iw') if (os.path.exists('/tmp/updates/textw')): sys.path.append('/tmp/updates/textw') if (os.path.exists('/tmp/updates/iw')): sys.path.append('/tmp/updates/iw') #CJS end sys.path.append('/usr/lib/anaconda') sys.path.append('/usr/lib/anaconda/textw') sys.path.append('/usr/lib/anaconda/iw') if (os.path.exists('booty')): sys.path.append('booty') sys.path.append('booty/edd') else: sys.path.append('/usr/lib/booty') sys.path.append('/usr/share/redhat-config-keyboard') try: import updates_disk_hook except ImportError: pass # pull this in to get product name and versioning import product # do this early to keep our import footprint as small as possible # Python passed my path as argv[0]! # if sys.argv[0][-7:] == "syslogd": if len(sys.argv) > 1: if sys.argv[1] == "--syslogd": from syslogd import Syslogd root = sys.argv[2] output = sys.argv[3] syslog = Syslogd (root, open (output, "a")) # this never returns #CJS need to parse cmdline to determine where to install from cmdline = open("/proc/cmdline", "r").read() loc = cmdline.find("site=") if loc > 0 : stuff = cmdline[loc + 5:] loc2 = stuff.find(" ") loc3 = stuff[:loc2].find("/") if (loc3 > 0): product.productSite = stuff[:loc2] else: product.productSite = product.productSiteDir + "/" + stuff[:loc2] # #CJS now look for RHupdates so that each group can have their own anaconda stuff if (os.path.exists("/mnt/source/" + product.productSite + "/RHupdates")): sys.path.append("/mnt/source" + product.productSite + "/RHupdates") if (os.path.exists("/mnt/source/" + product.productSite + "/RHupdates/iw")): sys.path.append("/mnt/source/" + product.productSite + "/RHupdates/iw") if (os.path.exists("/mnt/source/" + product.productSite + "/RHupdates/textw")): sys.path.append("/mnt/source/" + product.productSite + "/RHupdates/textw") #CJS end of parse cmdline and fix up stuff depending on it # this handles setting up RHupdates for rhpl to minimize the set needed if (os.access("/mnt/source/RHupdates/rhpl", os.X_OK) or os.access("/tmp/updates/rhpl", os.X_OK)): setupRhplUpdates() import signal, traceback, string, isys, iutil, time from exception import handleException import dispatch from flags import flags from anaconda_log import anaconda_log from rhpl.log import log from rhpl.translate import _, textdomain, addPoPath if os.path.isdir("/mnt/source/RHupdates/po"): log("adding RHupdates/po") addPoPath("/mnt/source/RHupdates/po") if os.path.isdir("/tmp/updates/po"): log("adding /tmp/updates/po") addPoPath("/tmp/updates/po") textdomain("anaconda") # reset python's default SIGINT handler signal.signal(signal.SIGINT, signal.SIG_DFL) # Silly GNOME stuff if os.environ.has_key('HOME'): os.environ['XAUTHORITY'] = os.environ['HOME'] + '/.Xauthority' os.environ['HOME'] = '/tmp' os.environ['LC_NUMERIC'] = 'C' if os.environ.has_key ("ANACONDAARGS"): theargs = string.split (os.environ["ANACONDAARGS"]) else: theargs = sys.argv[1:] # we can't let the LD_PRELOAD hang around because it will leak into # rpm %post and the like. ick :/ if os.environ.has_key("LD_PRELOAD"): del os.environ["LD_PRELOAD"] # we need to do this really early so we make sure its done before rpm # is imported iutil.writeRpmPlatform() try: (args, extra) = isys.getopt(theargs, 'CGTRxtdr:fm:', [ 'text', 'test', 'debug', 'nofallback', 'method=', 'rootpath=', 'pcic=', "overhead=", 'testpath=', 'mountfs', 'traceonly', 'kickstart=', 'lang=', 'keymap=', 'kbdtype=', 'module=', 'class !"#$%&'()*+,-./0123=', 'expert', 'serial', 'lowres', 'nofb', 'rescue', 'nomount', 'autostep', 'resolution=', 'skipddc', 'vnc', 'vncconnect=', 'cmdline', 'headless']) except TypeError, msg: sys.stderr.write("Error %s\n:" % msg) sys.exit(-1) if extra: sys.stderr.write("Unexpected arguments: %s\n" % extra) sys.exit(-1) # Save the arguments in case we need to reexec anaconda for kon os.environ["ANACONDAARGS"] = string.join(sys.argv[1:]) # remove the arguments - gnome_init doesn't understand them savedargs = sys.argv[1:] sys.argv = sys.argv[:1] sys.argc = 1 # Parameters for the main anaconda routine # rootPath = '/mnt/sysimage' # where to instal packages extraModules = [] # kernel modules to use display_mode = 'g' # try GUI by default debug = 0 # start up pdb immediately traceOnly = 0 # don't run, just list modules we use nofallback = 0 # if GUI mode fails, exit rescue = 0 # run in rescue mode rescue_nomount = 0 # don't automatically mount device in rescue runres = '800x600' # resolution to run the GUI install in skipddc = 0 # if true skip ddcprobe (locks some machines) instClass = None # the install class to use progmode = 'install' # 'rescue', or 'install' method = None # URL representation of install method logFile = None # may be a file object or a file name # should we ever try to probe for X stuff? this will give us a convenient # out eventually to circumvent all probing and just fall back to text mode # on hardware where we break things if we probe isHeadless = 0 # probing for hardware on an s390 seems silly... if iutil.getArch() == "s390": isHeadless = 1 # # xcfg - xserver info (?) # mousehw - mouseinfo info # videohw - videocard info # monitorhw - monitor info # lang - language to use for install/machine default # keymap - kbd map # xcfg = None monitorhw = None videohw = None mousehw = None lang = None method = None keymap = None kbdtype = None progmode = None customClass = None kbd = None ksfile = None vncpassword = None vncconnecthost = None vncconnectport = None # # parse off command line arguments # for n in args: (str, arg) = n if (str == '--class'): customClass = arg elif (str == '-d' or str == '--debug'): debug = 1 elif (str == '--expert'): flags.expert = 1 elif (str == '--keymap'): keymap = arg elif (str == '--kickstart'): from kickstart import Kickstart ksfile = arg instClass = Kickstart(ksfile, flags.serial) elif (str == '--lang'): lang = arg elif (str == '--lowres'): runres = '640x480' elif (str == '-m' or str == '--method'): method = arg if method[0] == '@': # ftp installs pass the password via a file in /tmp so # ps doesn't show it filename = method[1:] method = open(filename, "r").readline() method = method[:len(method) - 1] os.unlink(filename) elif (str == '--module'): (path, name) = string.split(arg, ":") extraModules.append((path, name)) elif (str == '--nofallback'): nofallback = 1 elif (str == "--nomount"): rescue_nomount = 1 elif (str == '--rescue'): progmode = 'rescue' elif (str == '--resolution'): # run native X server at specified resolution, ignore fb runres = arg elif (str == "--skipddc"): skipddc = 1 elif (str == "--autostep"): flags.autostep = 1 elif (str == '-r' or str == '--rootpath'): rootPath = arg flags.setupFilesystems = 0 logFile = sys.stderr elif (str == '--traceonly'): traceOnly = 1 elif (str == '--serial'): flags.serial = 1 elif (str == '-t' or str == '--test'): flags.test = 1 flags.setupFilesystems = 0 logFile = "/tmp/anaconda-debug.log" elif (str == '-T' or str == '--text'): display_mode = 't' elif (str == "-C" or str == "--cmdline"): display_mode = 'c' elif (str == '--kbdtype'): kbdtype = arg elif (str == '--headless'): isHeadless = 1 elif (str == '--vnc'): flags.usevnc = 1 # see if there is a vnc password file try: pfile = open("/tmp/vncpassword.dat", "r") vncpassword=pfile.readline().strip() pfile.close() os.unlink("/tmp/vncpassword.dat") except: vncpassword=None pass # check length of vnc password if vncpassword is not None and len(vncpassword) < 6: from snack import * screen = SnackScreen() ButtonChoiceWindow(screen, _('VNC Password Error'), _('You need to specify a vnc password of at least 6 characters long.\n\n' 'Press to reboot your system.\n'), buttons = (_("OK"),)) screen.finish() sys.exit(0) elif (str == '--vncconnect'): cargs = string.split(arg, ":") vncconnecthost = cargs[0] if len(cargs) > 1: if len(cargs[1]) > 0: vncconnectport = cargs[1] log("product.productSite is %s " % product.productSite) log("product.productDefault is %s " % product.productDefault) # # must specify install, rescue mode # if (progmode == 'rescue'): if (not method): sys.stderr.write('--method required for rescue mode\n') sys.exit(1) import rescue, instdata, configFileData anaconda_log.open (logFile) log.handler=anaconda_log configFile = configFileData.configFileData() configFileData = configFile.getConfigData() id = instdata.InstallData([], "fd0", configFileData, method) rescue.runRescue(rootPath, not rescue_nomount, id) # shouldn't get back here sys.exit(1) else: if (not method): sys.stderr.write('no install method specified\n') sys.exit(1) anaconda_log.open (logFile) log.handler = anaconda_log # # Here we have a hook to pull in second half of kickstart file via https # if desired. # if ksfile is not None: from kickstart import pullRemainingKickstartConfig, KSAppendException from kickstart import parseKickstartVNC try: rc=pullRemainingKickstartConfig(ksfile) except KSAppendException, msg: rc = msg except: rc = _("Unknown Error") if rc is not None: dup_log(_("Error pulling second part of kickstart config: %s!") % (rc,)) sys.exit(1) # now see if they enabled vnc via the kickstart file. Note that command # line options for password, connect host and port override values in # kickstart file (ksusevnc, ksvncpasswd, ksvnchost, ksvncport) = parseKickstartVNC(ksfile) if ksusevnc: flags.usevnc = 1 if vncpassword == None: vncpassword = ksvncpasswd if vncconnecthost == None: vncconnecthost = ksvnchost if vncconnectport == None: vncconnectport = ksvncport if (debug): import pdb pdb.set_trace() # let people be stupid ## # don't let folks do anything stupid on !s390 ## if (not flags.test and os.getpid() > 90 and flags.setupFilesystems and ## not iutil.getArch() == "s390"): ## sys.stderr.write( ## "You're running me on a live system! that's incredibly stupid.\n") ## sys.exit(1) import isys import instdata import floppy if not isHeadless: import xsetup import rhpl.xhwstate as xhwstate import rhpl.keyboard as keyboard # handle traceonly and exit if traceOnly: if display_mode == 'g': sys.stderr.write("traceonly is only supported for text mode\n") sys.exit(0) # prints a list of all the modules imported from text import InstallInterface from text import stepToClasses import pdb import warnings import image import harddrive import urlinstall import mimetools import mimetypes import syslogd import installclass import re import rescue import configFileData import kickstart import whiteout import findpackageset import libxml2 import cmdline installclass.availableClasses() if display_mode == 't': for step in stepToClasses.keys(): if stepToClasses[step]: (mod, klass) = stepToClasses[step] exec "import %s" % mod for module in sys.__dict__['modules'].keys (): if module not in [ "__builtin__", "__main__" ]: foo = repr (sys.__dict__['modules'][module]) bar = string.split (foo, "'") if len (bar) > 3: print bar[3] sys.exit(0) # # override display mode if machine cannot nicely run X # if (not flags.test): if (iutil.memInstalled() < isys.MIN_GUI_RAM): dup_log(_("You do not have enough RAM to use the graphical " "installer. Starting text mode.")) display_mode = 't' time.sleep(2) if iutil.memInstalled() < isys.MIN_RAM: from snack import * screen = SnackScreen() ButtonChoiceWindow(screen, _('Fatal Error'), _('You do not have enough RAM to install Red Hat ' 'Linux on this machine.\n' '\n' 'Press to reboot your system.\n'), buttons = (_("OK"),)) screen.finish() sys.exit(0) # # handle class passed from loader # if customClass: import installclass classes = installclass.availableClasses(showHidden=1) for (className, objectClass, logo) in classes: if className == customClass: instClass = objectClass(flags.expert) if not instClass: sys.stderr.write("installation class %s not available\n" % customClass) sys.stderr.write("\navailable classes:\n") for (className, objectClass, logo) in classes: sys.stderr.write("\t%s\n" % className) sys.exit(1) # # if no instClass declared by user figure it out based on other cmdline args # if not instClass: from installclass import DefaultInstall, availableClasses instClass = DefaultInstall(flags.expert) if len(availableClasses(showHidden = 1)) < 2: (cname, cobject, clogo) = availableClasses(showHidden = 1)[0] log("%s is only installclass, using it" %(cname,)) instClass = cobject(flags.expert) # this lets install classes force text mode instlls if instClass.forceTextMode: dup_log(_("Install class forcing text mode installation")) display_mode = 't' # # find out what video hardware is available to run installer # # XXX kind of hacky - need to remember if we're running on an existing # X display later to avoid some initilization steps if os.environ.has_key('DISPLAY') and display_mode == 'g': x_already_set = 1 else: x_already_set = 0 if not isHeadless: # # Probe what is available for X and setup a hardware state # # try to probe interesting hw import rhpl.xserver as xserver skipddcprobe = (skipddc or (x_already_set and flags.test)) skipmouseprobe = not (not os.environ.has_key('DISPLAY') or flags.setupFilesystems) (videohw, monitorhw, mousehw) = xserver.probeHW(skipDDCProbe=skipddcprobe, skipMouseProbe = skipmouseprobe) # if the len(videocards) is zero, then let's assume we're isHeadless if len(videohw.videocards) == 0: print _("No video hardware found, assuming headless") videohw = None monitorhw = None mousehw = None isHeadless = 1 else: # setup a X hw state for use later with configuration. try: xcfg = xhwstate.XF86HardwareState(defcard=videohw, defmon=monitorhw) except Exception, e: print _("Unable to instantiate a X hardware state object.") xcfg = None else: videohw = None monitorhw = None mousehw = None xcfg = None # keyboard kbd = keyboard.Keyboard() if keymap: kbd.set(keymap) # # delay to let use see status of attempt to probe hw # time.sleep(3) # # now determine if we're going to run in GUI or TUI mode # if (display_mode == 'g' and method and (method.startswith('ftp://') or method.startswith('http://') or method.startswith('hd://') or method.startswith('oldhd://'))): dup_log(_("Graphical installation not available for %s installs. " "Starting text mode.") % (string.split(method, ':')[0],)) display_mode = 't' time.sleep(2) if not isHeadless: # if no mouse we force text mode mousedev = mousehw.get() if ksfile is None and not flags.usevnc and display_mode == 'g' and mousedev[0] == "No - mouse": # ask for the mouse type import rhpl.mouse as mouse if mouse.mouseWindow(mousehw) == 0: dup_log(_("No mouse was detected. A mouse is required for " "graphical installation. Starting text mode.")) display_mode = 't' time.sleep(2) else: dup_log(_("Using mouse type: %s"), mousehw.shortDescription()) else: # s390/iSeries checks if display_mode == 'g' and not (os.environ.has_key('DISPLAY') or flags.usevnc): dup_log("DISPLAY variable not set. Starting text mode!") display_mode = 't' time.sleep(2) # if they want us to use VNC do that now if display_mode == 'g' and flags.usevnc: # dont run vncpassword if in test mode if flags.test: vncpassword = None startVNCServer(vncpassword=vncpassword, vncconnecthost=vncconnecthost, vncconnectport=vncconnectport) if display_mode == 'g' and not os.environ.has_key('DISPLAY'): import rhpl.monitor as monitor # if no monitor probed lets guess based on runres hsync = monitorhw.getMonitorHorizSync() vsync = monitorhw.getMonitorVertSync() res_supported = monitor.monitor_supports_mode(hsync, vsync, runres) if not res_supported: import rhpl.guesslcd as guesslcd (hsync, vsync) = guesslcd.getSyncForRes(runres) monitorhw.setSpecs(hsync, vsync) # XXX - need to fix # # messy hack for how rhpl.xhwstate works # current it wants to use probed values which we dont have so we'll # fake them hsync = monitorhw.getMonitorHorizSync() vsync = monitorhw.getMonitorVertSync() monitorhw.orig_monHoriz = hsync monitorhw.orig_monVert = vsync # make sure we can write log to ramfs if os.access("/tmp/ramfs", os.W_OK): xlogfile = "/tmp/ramfs/X.log" else: xlogfile = None xsetup_failed = xserver.startXServer(videohw, monitorhw, mousehw, kbd, runres, xStartedCB=doStartupX11Actions, xQuitCB=doShutdownX11Actions, logfile=xlogfile) if xsetup_failed: dup_log(" X startup failed, falling back to text mode") display_mode = 't' time.sleep(2) # # read in anaconda configuration file # import configFileData configFile = configFileData.configFileData() configFileData = configFile.getConfigData() # setup links required for all install types for i in ( "services", "protocol", "nsswitch.conf", "joe"): try: os.symlink ("../mnt/runtime/etc/" + i, "/etc/" + i) except: pass # # setup links required by graphical mode if installing and verify display mode # if (display_mode == 'g'): if not flags.test and flags.setupFilesystems: for i in ( "imrc", "im_palette.pal", "gtk-2.0", "pango", "fonts", "fb.modes"): try: if os.path.exists("/mnt/runtime/etc/%s" %(i,)): os.symlink ("../mnt/runtime/etc/" + i, "/etc/" + i) except: pass # display splash screen if nofallback: from splashscreen import splashScreenShow splashScreenShow(configFileData) from gui import InstallInterface else: try: from splashscreen import splashScreenShow splashScreenShow(configFileData) from gui import InstallInterface except Exception, e: # if we're not going to really go into GUI mode, we need to get # back to vc1 where the text install is going to pop up. if not x_already_set: isys.vtActivate (1) dup_log("GUI installer startup failed, falling back to text mode.") display_mode = 't' if 'DISPLAY' in os.environ.keys(): del os.environ['DISPLAY'] time.sleep(2) if (display_mode == 't'): from text import InstallInterface if (display_mode == 'c'): from cmdline import InstallInterface # go ahead and set up the interface intf = InstallInterface () # imports after setting up the path if method: if method.startswith('cdrom://'): from image import CdromInstallMethod methodobj = CdromInstallMethod(method[8:], intf.messageWindow, intf.progressWindow, rootPath) elif method.startswith('nfs:/'): from image import NfsInstallMethod methodobj = NfsInstallMethod(method[5:], rootPath) elif method.startswith('nfsiso:/'): from image import NfsIsoInstallMethod methodobj = NfsIsoInstallMethod(method[8:], intf.messageWindow, rootPath) elif method.startswith('ftp://') or method.startswith('http://'): from urlinstall import UrlInstallMethod methodobj = UrlInstallMethod(method, rootPath) elif method.startswith('hd://'): tmpmethod = method[5:] i = string.index(tmpmethod, ":") drive = tmpmethod[0:i] tmpmethod = tmpmethod[i+1:] i = string.index(tmpmethod, "/") type = tmpmethod[0:i] dir = tmpmethod[i+1:] from harddrive import HardDriveInstallMethod methodobj = HardDriveInstallMethod(drive, type, dir, intf.messageWindow, rootPath) elif method.startswith('oldhd://'): tmpmethod = method[8:] i = string.index(tmpmethod, ":") drive = tmpmethod[0:i] tmpmethod = tmpmethod[i+1:] i = string.index(tmpmethod, "/") type = tmpmethod[0:i] dir = tmpmethod[i+1:] from harddrive import OldHardDriveInstallMethod methodobj = OldHardDriveInstallMethod(drive, type, dir, rootPath) else: print "unknown install method:", method sys.exit(1) floppyDevice = floppy.probeFloppyDevice() # create device nodes for detected devices if we're not running in test mode if not flags.test and flags.setupFilesystems: iutil.makeDriveDeviceNodes() id = instClass.installDataClass(extraModules, floppyDevice, configFileData, method) id.x_already_set = x_already_set if mousehw: id.setMouse(mousehw) if videohw: id.setVideoCard(videohw) if monitorhw: id.setMonitor(monitorhw) # # not sure what to do here - somehow we didnt detect anything # if xcfg is None and not isHeadless: try: xcfg = xhwstate.XF86HardwareState() except Exception, e: print _("Unable to instantiate a X hardware state object.") xcfg = None if xcfg is not None: # # XXX - hack - here we want to enforce frame buffer requirement on ppc # pick 640x480x8bpp cause that is what frame buffer inits to # when the machine reboots if iutil.getArch() == "ppc": cardname = "Framebuffer driver (generic)" xcfg.set_videocard_card(cardname) xcfg.set_videocard_name(cardname) xcfg.set_colordepth(8) xcfg.set_resolution("640x480") xsetup = xsetup.XSetup(xcfg) id.setXSetup(xsetup) if kbd: id.setKeyboard(kbd) instClass.setInstallData(id) dispatch = dispatch.Dispatcher(intf, id, methodobj, rootPath) if lang: dispatch.skipStep("language", permanent = 1) instClass.setLanguage(id, lang) if keymap: dispatch.skipStep("keyboard", permanent = 1) instClass.setKeyboard(id, keymap) # set up the headless case if isHeadless == 1: id.setHeadless(isHeadless) instClass.setAsHeadless(dispatch, isHeadless) instClass.setSteps(dispatch) # We shouldn't need this again # XXX #del id # # XXX This is surely broken # #if iutil.getArch() == "sparc": # import kudzu # mice = kudzu.probe (kudzu.CLASS_MOUSE, kudzu.BUS_UNSPEC, kudzu.PROBE_ONE); # if mice: # (mouseDev, driver, descr) = mice[0] # if mouseDev == 'sunmouse': # instClass.addToSkipList("mouse") # instClass.setMouseType("Sun - Mouse", "sunmouse") # comment out the next line to make exceptions non-fatal sys.excepthook = lambda type, value, tb, dispatch=dispatch, intf=intf: handleException(dispatch, intf, (type, value, tb)) try: intf.run(id, dispatch, configFileData) except SystemExit, code: intf.shutdown() except: handleException(dispatch, intf, sys.exc_info()) del intf . .. post.shand mousedev[0] == "No - mouse": # ask for the mouse type import rhpl.mouse as mouse if mouse.mouseWindow(mousehw) == 0: dup_log(_("No mouse was detected. A mouse is required for " "graphical installation. Starting text mode.")) display_mode = 't' time.sleep(2) else: dup_log(_("Using mouse type: %s"), mousehw.shortDescription()) else: # s390/iSeries checks if display_mode == 'g' and not (os.environ.has_key('DISPLAY') or flags.usevnc): dup_log("DISPLAY variable not set. Starting text mode!") display_mode = 't' time.sleep(2) # if they want us to use VNC do that now if display_mode == 'g' and flags.usevnc: # dont run vncpassword if in test mode if flags.test: vncpassword = None startVNCServer(vncpassword=vncpassword, vncconnecthost=vncconnecthost, vncconnectport=vncconnectport) if display_mode == 'g' and not os.environ.hasVERSION=lts30 SECUREFTPSERVER=linux1.fnal.gov # CHROOT="/mnt/sysimage/" RUN="$CHROOT/usr/sbin/chroot /mnt/sysimage/" INSTALLIMAGE="/mnt/source/" PATH=$PATH:/$CHROOT/bin:/$CHROOT/usr/bin:/$CHROOT/sbin:/$CHROOT/usr/sbin if [ -s $CHROOT/tmp/upgrade.log ] ; then METHOD=UPGRADE else METHOD=INSTALL fi mount | grep -q lts30test if [ $? -eq 0 ] ; then RELEASE=lts30test else mount | grep -q lts30rolling if [ $? -eq 0 ] ; then RELEASE=lts30rolling else RELEASE=$VERSION fi fi echo $RELEASE > /dev/tty5 mount | grep -q nfs if [ $? -ne 0 ] ; then MEDIA=CDROM else MEDIA=NFS fi export CHROOT export RUN export INSTALLIMAGE export PATH export METHOD echo "Installing Post Install RPMS" >/dev/tty5 echo "Log file in /tmp/postinstall.log" >/dev/tty5 # # Determine which workgroup we are if [ -s $CHROOT/etc/workgroup ]; then WORKGROUP=`cat $CHROOT/etc/workgroup` else WORKGROUP=`cat $CHROOT/etc/workgroup` echo $WORKGROUP >/dev/tty5 echo "CUSTOM" > $CHROOT/etc/workgroup WORKGROUP=`cat $CHROOT/etc/workgroup` fi echo "Fermi Linux $RELEASE $METHOD for $WORKGROUP via $MEDIA on `$RUN /bin/date`" > /tmp/banner grep -q "NOTICE TO USERS" $CHROOT/etc/motd if [ $? -ne 0 ] ; then cat $CHROOT/etc/motd $INSTALLIMAGE/Fermi/common/configfiles/fermibanner > /tmp/fermibanner cp /tmp/fermibanner $CHROOT/etc/motd fi if [ -s $CHROOT/etc/motd.rpmsave ] ; then grep "Fermi " $CHROOT/etc/motd.rpmsave > /tmp/fermilinux cat /tmp/banner /tmp/fermilinux | uniq > /tmp/bannerlinux cp /tmp/bannerlinux /tmp/banner fi cat /tmp/banner $CHROOT/etc/motd > /tmp/tmpbanner cp /tmp/banner $CHROOT/etc/FermiLinuxHistory $CHROOT/bin/cp /tmp/tmpbanner $CHROOT/etc/motd # need to rerun authconfig to fix pam system-auth $RUN /usr/sbin/authconfig --kickstart cd $CHROOT if [ -x $CHROOT/etc/$WORKGROUP/scripts/before.rpms.sh ] ; then ($RUN /etc/$WORKGROUP/scripts/before.rpms.sh > $CHROOT/etc/$WORKGROUP/before.rpms.log 2>&1 ) fi cd $CHROOT ls etc/$WORKGROUP/RPMS/*.rpm > $CHROOT/tmp/rpmfiles 2> /dev/null if [ -s $CHROOT/tmp/rpmfiles ] ; then cd $CHROOT/etc/$WORKGROUP/RPMS /$CHROOT/bin/rpm --root $CHROOT -U --force --nodeps *.rpm >& $CHROOT/etc/$WORKGROUP/workgroup.rpm.log fi cd $CHROOT if [ -d $CHROOT/etc/$WORKGROUP/RPMSI ] ; then ls $CHROOT/etc/$WORKGROUP/RPMSI/*.rpm > $CHROOT/tmp/rpmfiles.i 2> /dev/null if [ -s $CHROOT/tmp/rpmfiles.i ] ; then cd $CHROOT/etc/$WORKGROUP/RPMSI /$CHROOT/bin/rpm --root $CHROOT -i --force --nodeps *.rpm >> $CHROOT/etc/$WORKGROUP/workgroup.rpm.log 2>&1 fi fi $CHROOT/bin/touch $CHROOT/etc/$WORKGROUP/after.rpms.log if [ -x $CHROOT/etc/$WORKGROUP/scripts/after.rpms.nochroot.sh ] ; then $CHROOT/etc/$WORKGROUP/scripts/after.rpms.nochroot.sh >> $CHROOT/etc/$WORKGROUP/after.rpms.log 2>&1 else echo "No after.rpms.nochroot.sh found" >> $CHROOT/tmp/after.rpms.log 2>&1 fi if [ -x $CHROOT/etc/$WORKGROUP/scripts/after.rpms.sh ] ; then ($RUN /etc/$WORKGROUP/scripts/after.rpms.sh >> $CHROOT/etc/$WORKGROUP/after.rpms.log 2>&1 ) else echo "No after.rpms.sh found" >> $CHROOT/tmp/after.rpms.log 2>&1 fi cd $CHROOT if [ -x $CHROOT/etc/$WORKGROUP/scripts/final.after.rpms.sh ] ; then ($RUN /etc/$WORKGROUP/scripts/final.after.rpms.sh > $CHROOT/etc/$WORKGROUP/final.after.rpms.log 2>&1 ) fi $CHROOT/bin/cp /tmp/anaconda.log $CHROOT/root/anaconda.log cd from installclass import BaseInstallClass import os from rhpl.log import log import iutil from autopart import autoCreatePartitionRequests from kickstart import Script class FermiInstallClass(BaseInstallClass): pixmap = "workstation.png" def setGroupSelection(self, grpset, intf): BaseInstallClass.__init__(self, grpset) grpset.unselectAll() def postAction(self, rootPath, serial): log("Running post.sh script for workgroup now") for script in self.postScripts: script.run(rootPath, serial) def getFermiTextFile(self, filename): file = "" # fullfilename = todo.method.getFilename(filename,None) # fullfilename = "/mnt/source/" + filename if (os.access('/mnt/source/RHupdates/fermi',os.R_OK)): fullfilename = "/mnt/source/RHupdates/fermi/" + filename else: if (os.access('/tmp/updates/fermi',os.R_OK)): fullfilename = "/tmp/updates/fermi/" + filename else: fullfilename = "/usr/lib/anaconda/fermi/" + filename log("Set fullfilename to: %s", fullfilename) for n in open(fullfilename).readlines(): file = file + n # log("Just read %s into file", fullfilename) return file def setInstallData(self, id): id.instClass = self id.partitions.useAutopartitioning = 0 autorequests = [ ("/", None, 3100, None, 0, 1) ] (minswap, maxswap) = iutil.swapSuggestion() autorequests.append((None, "swap", minswap, maxswap, 0, 1)) id.partitions.autoPartitionRequests = autoCreatePartitionRequests(autorequests) self.setTimezoneInfo( id, "America/Chicago") self.setFirewall( id, enable = 0, trusts = [], ports = "", ssh = 0, telnet = 0, smtp = 0, http = 0, ftp = 0) self.setAuthentication( id , 1 , # useShadow 1 , # useMD5 useKrb5 = 1 , krb5Realm = "FNAL.GOV", krb5Kdc = "krb-fnal-1.fnal.gov:88,krb-fnal-2.fnal.gov:88,krb-fnal-3.fnal.gov:88,krb-fnal-4.fnal.gov:88,krb-fnal-5.fnal.gov:88", krb5Admin = "krb-fnal-admin.fnal.gov", ) #CJS added auththentication back in def setSteps(self, dispatch): dispatch.setStepList( "language", "keyboard", "mouse", "welcome", "findrootparts", "betanag", "installtype", "partitionmethod", "partitionobjinit", "partitionmethodsetup", "autopartition", "autopartitionexecute", "fdisk", "partition", "partitiondone", "bootloadersetup", "bootloader", "networkdevicecheck", "network", "firewall", "languagesupport", "timezone", "accounts", "authentication", "readcomps", "selectlangpackages", "package-selection", "handleX11pkgs", "checkdeps", "dependencies", "videocard", "monitor", "xcustom", "confirminstall", "install", "enablefilesystems", "migratefilesystems", "setuptime", "preinstallconfig", "installpackages", "postinstallconfig", "writeconfig", "firstboot", "instbootloader", "dopostaction", "writexconfig", "writeksconfig", "methodcomplete", "complete" ) if iutil.getArch() != "i386": dispatch.skipStep("bootdisk") if iutil.getArch() != "i386" and iutil.getArch() != "x86_64": dispatch.skipStep("bootloader") # 'noupgrade' can be used on the command line to force not looking # for partitions to upgrade. useful in some cases... cmdline = open("/proc/cmdline", "r").read() if cmdline.find("upgrade") == -1: dispatch.skipStep("findrootparts") def __init__(self, expert): BaseInstallClass.__init__(self, expert) self.postScripts = [] scriptInterp="/bin/sh" scriptChroot=0 ferminame = "post.sh" script = self.getFermiTextFile(ferminame) log("Just called Script ") s = Script(script,scriptInterp,scriptChroot) self.postScripts.append(s) # # fstab.py: filesystem management # # Matt Wilson # # Copyright 2001-2002 Red Hat, Inc. # # This software may be freely redistributed under the terms of the GNU # library public license. # # You should have received a copy of the GNU Library Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # import string import isys import iutil import os import errno import parted import sys import struct import partitioning import partedUtils import raid import lvm import types from rhpl.log import log from rhpl.translate import _, N_ class BadBlocksError(Exception): pass defaultMountPoints = ['/', '/home', '/tmp', '/usr', '/var', '/usr/local', '/opt'] if iutil.getArch() == "s390": # Many s390 have 2G DASDs, we recomment putting /usr/share on its own DASD defaultMountPoints.insert(4, '/usr/share') if iutil.getArch() == "ia64": defaultMountPoints.insert(1, '/boot/efi') else: defaultMountPoints.insert(1, '/boot') fileSystemTypes = {} # XXX define availraidlevels and defaultmntpts as arch characteristics # FIXME: this should be done dynamically by reading /proc/mdstat availRaidLevels = ['RAID0', 'RAID1', 'RAID5'] def fileSystemTypeGetDefault(): if fileSystemTypeGet('ext3').isSupported(): return fileSystemTypeGet('ext3') elif fileSystemTypeGet('ext2').isSupported(): return fileSystemTypeGet('ext2') else: raise ValueError, "You have neither ext3 or ext2 support in your kernel!" def fileSystemTypeGet(key): return fileSystemTypes[key] def fileSystemTypeRegister(klass): fileSystemTypes[klass.getName()] = klass def fileSystemTypeGetTypes(): return fileSystemTypes.copy() def getUsableLinuxFs(): rc = [] for fsType in fileSystemTypes.keys(): if fileSystemTypes[fsType].isMountable() and \ fileSystemTypes[fsType].isLinuxNativeFS(): rc.append(fsType) # make sure the default is first in the list, kind of ugly default = fileSystemTypeGetDefault() defaultName = default.getName() if defaultName in rc: del rc[rc.index(defaultName)] rc.insert(0, defaultName) return rc def mountCompare(a, b): one = a.mountpoint two = b.mountpoint if one < two: return -1 elif two > one: return 1 return 0 def devify(device): if device != "none" and device[0] != '/': return "/dev/" + device return device class LabelFactory: def __init__(self): self.labels = None def createLabel(self, mountpoint, maxLabelChars): if self.labels == None: self.labels = {} diskset = partedUtils.DiskSet() diskset.openDevices() diskset.stopAllRaid() diskset.startAllRaid() labels = diskset.getLabels() del diskset self.reserveLabels(labels) if len(mountpoint) > maxLabelChars: mountpoint = mountpoint[0:maxLabelChars] count = 0 while self.labels.has_key(mountpoint): count = count + 1 s = "%s" % count if (len(mountpoint) + len(s)) <= maxLabelChars: mountpoint = mountpoint + s else: strip = len(mountpoint) + len(s) - maxLabelChars mountpoint = mountpoint[0:len(mountpoint) - strip] + s self.labels[mountpoint] = 1 return mountpoint def reserveLabels(self, labels): if self.labels == None: self.labels = {} for device, label in labels.items(): self.labels[label] = 1 labelFactory = LabelFactory() class FileSystemType: kernelFilesystems = {} def __init__(self): self.deviceArguments = {} self.formattable = 0 self.checked = 0 self.name = "" self.linuxnativefs = 0 self.partedFileSystemType = None self.partedPartitionFlags = [] self.maxSizeMB = 1024 * 1024 self.supported = -1 self.defaultOptions = "defaults" self.migratetofs = None self.extraFormatArgs = [] self.maxLabelChars = 16 def mount(self, device, mountpoint, readOnly=0, bindMount=0): if not self.isMountable(): return iutil.mkdirChain(mountpoint) isys.mount(device, mountpoint, fstype = self.getName(), readOnly = readOnly, bindMount = bindMount) def umount(self, device, path): isys.umount(path, removeDir = 0) def getName(self): return self.name def registerDeviceArgumentFunction(self, klass, function): self.deviceArguments[klass] = function def badblocksDevice(self, entry, windowCreator, chroot='/'): if windowCreator: w = windowCreator(_("Checking for Bad Blocks"), _("Checking for bad blocks on /dev/%s...") % (entry.device.getDevice(),), 100) else: w = None devicePath = entry.device.setupDevice(chroot) args = [ "/usr/sbin/badblocks", "-vv", devicePath ] # entirely too much cutting and pasting from ext2FormatFileSystem fd = os.open("/dev/tty5", os.O_RDWR | os.O_CREAT | os.O_APPEND) p = os.pipe() childpid = os.fork() if not childpid: os.close(p[0]) os.dup2(p[1], 1) os.dup2(p[1], 2) os.close(p[1]) os.close(fd) os.execv(args[0], args) log("failed to exec %s", args) sys.exit(1) os.close(p[1]) s = 'a' while s and s != ':': try: s = os.read(p[0], 1) except OSError, args: (num, str) = args if (num != 4): raise IOError, args os.write(fd, s) num = '' numbad = 0 while s: try: s = os.read(p[0], 1) os.write(fd, s) if s not in ['\b', '\n']: try: num = num + s except: pass else: if s == '\b': if num: l = string.split(num, '/') val = (long(l[0]) * 100) / long(l[1]) w and w.set(val) else: try: blocknum = long(num) numbad = numbad + 1 except: pass if numbad > 0: raise BadBlocksError num = '' except OSError, args: (num, str) = args if (num != 4): raise IOError, args try: (pid, status) = os.waitpid(childpid, 0) except OSError, (num, msg): log("exception from waitpid in badblocks: %s %s" % (num, msg)) status = None os.close(fd) w and w.pop() if numbad > 0: raise BadBlocksError # have no clue how this would happen, but hope we're okay if status is None: return if os.WIFEXITED(status) and (os.WEXITSTATUS(status) == 0): return raise SystemError def formatDevice(self, entry, progress, chroot='/'): if self.isFormattable(): raise RuntimeError, "formatDevice method not defined" def migrateFileSystem(self, device, message, chroot='/'): if self.isMigratable(): raise RuntimeError, "migrateFileSystem method not defined" def labelDevice(self, entry, chroot): pass def isFormattable(self): return self.formattable def isLinuxNativeFS(self): return self.linuxnativefs def readProcFilesystems(self): f = open("/proc/filesystems", 'r') if not f: pass lines = f.readlines() for line in lines: fields = string.split(line) if fields[0] == "nodev": fsystem = fields[1] else: fsystem = fields[0] FileSystemType.kernelFilesystems[fsystem] = None def isMountable(self): if not FileSystemType.kernelFilesystems: self.readProcFilesystems() return FileSystemType.kernelFilesystems.has_key(self.getName()) def isSupported(self): if self.supported == -1: return self.isMountable() return self.supported def isChecked(self): return self.checked def getDeviceArgs(self, device): deviceArgsFunction = self.deviceArguments.get(device.__class__) if not deviceArgsFunction: return [] return deviceArgsFunction(device) def getPartedFileSystemType(self): return self.partedFileSystemType def getPartedPartitionFlags(self): return self.partedPartitionFlags # note that this returns the maximum size of a filesystem in megabytes def getMaxSizeMB(self): return self.maxSizeMB def getDefaultOptions(self, mountpoint): return self.defaultOptions def getMigratableFSTargets(self): retval = [] if not self.migratetofs: return retval for fs in self.migratetofs: if fileSystemTypeGet(fs).isSupported(): retval.append(fs) return retval def isMigratable(self): if len(self.getMigratableFSTargets()) > 0: return 1 else: return 0 class reiserfsFileSystem(FileSystemType): def __init__(self): FileSystemType.__init__(self) self.partedFileSystemType = parted.file_system_type_get("reiserfs") self.formattable = 1 self.checked = 1 self.linuxnativefs = 1 # this is totally, 100% unsupported. Boot with "linux reiserfs" # at the boot: prompt will let you make new reiserfs filesystems # in the installer. Bugs filed when you use this will be closed # WONTFIX. try: f = open("/proc/cmdline") line = f.readline() if string.find(line, " reiserfs") != -1: self.supported = -1 else: self.supported = 0 del f except: self.supported = 0 self.name = "reiserfs" self.maxSizeMB = 1024 * 1024 def formatDevice(self, entry, progress, chroot='/'): devicePath = entry.device.setupDevice(chroot) p = os.pipe() os.write(p[1], "y\n") os.close(p[1]) rc = iutil.execWithRedirect("/usr/sbin/mkreiserfs", ["mkreiserfs", devicePath ], stdin = p[0], stdout = "/dev/tty5", stderr = "/dev/tty5") if rc: raise SystemError fileSystemTypeRegister(reiserfsFileSystem()) class xfsFileSystem(FileSystemType): def __init__(self): FileSystemType.__init__(self) self.partedFileSystemType = parted.file_system_type_get("xfs") self.formattable = 1 self.checked = 1 self.linuxnativefs = 1 self.name = "xfs" self.maxSizeMB = 1024 * 1024 self.maxLabelChars = 12 # we don't even have the module, so it won't be mountable... but # just in case self.supported = 0 def formatDevice(self, entry, progress, chroot='/'): devicePath = entry.device.setupDevice(chroot) rc = iutil.execWithRedirect("/usr/sbin/mkfs.xfs", ["mkfs.xfs", "-f", "-l", "internal", devicePath ], stdout = "/dev/tty5", stderr = "/dev/tty5") if rc: raise SystemError def labelDevice(self, entry, chroot): devicePath = entry.device.setupDevice(chroot) label = labelFactory.createLabel(entry.mountpoint, self.maxLabelChars) db_cmd = "label " + label rc = iutil.execWithRedirect("/usr/sbin/xfs_db", ["xfs_db", "KLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~-x", "-c", db_cmd, devicePath], stdout = "/dev/tty5", stderr = "/dev/tty5") if rc: raise SystemError entry.setLabel(label) fileSystemTypeRegister(xfsFileSystem()) class jfsFileSystem(FileSystemType): def __init__(self): FileSystemType.__init__(self) self.partedFileSystemType = parted.file_system_type_get("jfs") self.formattable = 1 self.checked = 1 self.linuxnativefs = 1 # this is totally, 100% unsupported. Boot with "linux jfs" # at the boot: prompt will let you make new reiserfs filesystems # in the installer. Bugs filed when you use this will be closed # WONTFIX. try: f = open("/proc/cmdline") line = f.readline() if string.find(line, " jfs") != -1: self.supported = -1 else: self.supported = 0 del f except: self.supported = 0 if not os.access("/usr/sbin/mkfs.jfs", os.X_OK): self.supported = 0 self.name = "jfs" self.maxSizeMB = 1024 * 1024 def formatDevice(self, entry, progress, chroot='/'): devicePath = entry.device.setupDevice(chroot) rc = iutil.execWithRedirect("/usr/sbin/mkfs.jfs", ["mkfs.jfs", "-q", devicePath ], stdout = "/dev/tty5", stderr = "/dev/tty5") if rc: raise SystemError fileSystemTypeRegister(jfsFileSystem()) class extFileSystem(FileSystemType): def __init__(self): FileSystemType.__init__(self) self.partedFileSystemType = None self.formattable = 1 self.checked = 1 self.linuxnativefs = 1 #CJS Not sure why this is only 1TB so making it 2TB # self.maxSizeMB = 1024 * 1024 self.maxSizeMB = 1024 * 1024 * 2 def labelDevice(self, entry, chroot): devicePath = entry.device.setupDevice(chroot) label = labelFactory.createLabel(entry.mountpoint, self.maxLabelChars) rc = iutil.execWithRedirect("/usr/sbin/e2label", ["e2label", devicePath, label], stdout = "/dev/tty5", stderr = "/dev/tty5") if rc: raise SystemError entry.setLabel(label) def formatDevice(self, entry, progress, chroot='/'): devicePath = entry.device.setupDevice(chroot) devArgs = self.getDeviceArgs(entry.device) args = [ "/usr/sbin/mke2fs", devicePath] args.extend(devArgs) args.extend(self.extraFormatArgs) rc = ext2FormatFilesystem(args, "/dev/tty5", progress, entry.mountpoint) if rc: raise SystemError # this is only for ext3 filesystems, but migration is a method # of the ext2 fstype, so it needs to be here. FIXME should be moved def setExt3Options(self, entry, message, chroot='/'): devicePath = entry.device.setupDevice(chroot) # if no journal, don't turn off the fsck if not isys.ext2HasJournal(devicePath, makeDevNode = 0): return # add back -Odir_index when htree is safe rc = iutil.execWithRedirect("/usr/sbin/tune2fs", ["tunefs", "-c0", "-i0", devicePath], stdout = "/dev/tty5", stderr = "/dev/tty5") class ext2FileSystem(extFileSystem): def __init__(self): extFileSystem.__init__(self) self.name = "ext2" self.partedFileSystemType = parted.file_system_type_get("ext2") self.migratetofs = ['ext3'] def migrateFileSystem(self, entry, message, chroot='/'): devicePath = entry.device.setupDevice(chroot) if not entry.fsystem or not entry.origfsystem: raise RuntimeError, ("Trying to migrate fs w/o fsystem or " "origfsystem set") if entry.fsystem.getName() != "ext3": raise RuntimeError, ("Trying to migrate ext2 to something other " "than ext3") # if journal already exists skip if isys.ext2HasJournal(devicePath, makeDevNode = 0): log("Skipping migration of %s, has a journal already.\n" % devicePath) return rc = iutil.execWithRedirect("/usr/sbin/tune2fs", ["tune2fs", "-j", devicePath ], stdout = "/dev/tty5", stderr = "/dev/tty5") if rc: raise SystemError # XXX this should never happen, but appears to have done # so several times based on reports in bugzilla. # At least we can avoid leaving them with a system which won't boot if not isys.ext2HasJournal(devicePath, makeDevNode = 0): log("Migration of %s attempted but no journal exists after " "running tune2fs.\n" % (devicePath)) if message: rc = message(_("Error"), _("An error occurred migrating %s to ext3. It is " "possible to continue without migrating this " "file system if desired.\n\n" "Would you like to continue without migrating %s?") % (devicePath, devicePath), type = "yesno") if rc == 0: sys.exit(0) entry.fsystem = entry.origfsystem else: extFileSystem.setExt3Options(self, entry, message, chroot) fileSystemTypeRegister(ext2FileSystem()) class ext3FileSystem(extFileSystem): def __init__(self): extFileSystem.__init__(self) self.name = "ext3" self.extraFormatArgs = [ "-j" ] self.partedFileSystemType = parted.file_system_type_get("ext3") def formatDevice(self, entry, progress, chroot='/'): extFileSystem.formatDevice(self, entry, progress, chroot) extFileSystem.setExt3Options(self, entry, progress, chroot) fileSystemTypeRegister(ext3FileSystem()) class raidMemberDummyFileSystem(FileSystemType): def __init__(self): FileSystemType.__init__(self) self.partedFileSystemType = parted.file_system_type_get("ext2") self.partedPartitionFlags = [ parted.PARTITION_RAID ] self.formattable = 1 self.checked = 0 self.linuxnativefs = 1 self.name = "software RAID" self.maxSizeMB = 1024 * 1024 self.supported = 1 def formatDevice(self, entry, progress, chroot='/'): # mkraid did all we need to format this partition... pass fileSystemTypeRegister(raidMemberDummyFileSystem()) class lvmPhysicalVolumeDummyFileSystem(FileSystemType): def __init__(self): FileSystemType.__init__(self) self.partedFileSystemType = parted.file_system_type_get("ext2") self.partedPartitionFlags = [ parted.PARTITION_LVM ] self.formattable = 1 self.checked = 0 self.linuxnativefs = 1 self.name = "physical volume (LVM)" self.maxSizeMB = 1024 * 1024 self.supported = 1 def isMountable(self): return 0 def formatDevice(self, entry, progress, chroot='/'): # already done by the pvcreate during volume creation pass fileSystemTypeRegister(lvmPhysicalVolumeDummyFileSystem()) class lvmVolumeGroupDummyFileSystem(FileSystemType): def __init__(self): FileSystemType.__init__(self) self.partedFileSystemType = parted.file_system_type_get("ext2") self.formattable = 1 self.checked = 0 self.linuxnativefs = 0 self.name = "volume group (LVM)" self.supported = 0 self.maxSizeMB = 1024 * 1024 def isMountable(self): return 0 def formatDevice(self, entry, progress, chroot='/'): # the vgcreate already did this pass fileSystemTypeRegister(lvmVolumeGroupDummyFileSystem()) class swapFileSystem(FileSystemType): enabledSwaps = {} def __init__(self): FileSystemType.__init__(self) self.partedFileSystemType = parted.file_system_type_get("linux-swap") self.formattable = 1 self.name = "swap" self.maxSizeMB = 1024 * 1024 self.linuxnativefs = 1 self.supported = 1 def mount(self, device, mountpoint, readOnly=0, bindMount=0): isys.swapon (device) def umount(self, device, path): # unfortunately, turning off swap is bad. raise RuntimeError, "unable to turn off swap" def formatDevice(self, entry, progress, chroot='/'): file = entry.device.setupDevice(chroot) rc = iutil.execWithRedirect ("/usr/sbin/mkswap", [ "mkswap", '-v1', file ], stdout = "/dev/tty5", stderr = "/dev/tty5", searchPath = 1) if rc: raise SystemError fileSystemTypeRegister(swapFileSystem()) class FATFileSystem(FileSystemType): def __init__(self): FileSystemType.__init__(self) self.partedFileSystemType = parted.file_system_type_get("fat32") self.formattable = 1 self.checked = 0 self.maxSizeMB = 1024 * 1024 self.name = "vfat" def formatDevice(self, entry, progress, chroot='/'): devicePath = entry.device.setupDevice(chroot) devArgs = self.getDeviceArgs(entry.device) args = [ "mkdosfs", devicePath ] args.extend(devArgs) rc = iutil.execWithRedirect("/usr/sbin/mkdosfs", args, stdout = "/dev/tty5", stderr = "/dev/tty5") if rc: raise SystemError fileSystemTypeRegister(FATFileSystem()) class NTFSFileSystem(FileSystemType): def __init__(self): FileSystemType.__init__(self) self.partedFileSystemType = parted.file_system_type_get("ntfs") self.formattable = 0 self.checked = 0 self.name = "ntfs" fileSystemTypeRegister(NTFSFileSystem()) class hfsFileSystem(FileSystemType): def __init__(self): FileSystemType.__init__(self) self.partedFileSystemType = parted.file_system_type_get("hfs") self.formattable = 1 self.checked = 0 self.name = "hfs" self.supported = 0 def isMountable(self): return 0 def formatDevice(self, entry, progress, chroot='/'): devicePath = entry.device.setupDevice(chroot) devArgs = self.getDeviceArgs(entry.device) args = [ "hformat", devicePath ] args.extend(devArgs) rc = iutil.execWithRedirect("/usr/bin/hformat", args, stdout = "/dev/tty5", stderr = "/dev/tty5") if rc: raise SystemError fileSystemTypeRegister(hfsFileSystem()) class applebootstrapFileSystem(hfsFileSystem): def __init__(self): hfsFileSystem.__init__(self) self.partedPartitionFlags = [ parted.PARTITION_BOOT ] self.maxSizeMB = 1 self.name = "Apple Bootstrap" if iutil.getPPCMacGen() == "NewWorld": self.supported = 1 else: self.supported = 0 fileSystemTypeRegister(applebootstrapFileSystem()) class prepbootFileSystem(FileSystemType): def __init__(self): FileSystemType.__init__(self) self.partedFileSystemType = None self.checked = 0 self.name = "PPC PReP Boot" self.maxSizeMB = 10 if iutil.getPPCMachine() == "iSeries": self.maxSizeMB = 64 # supported for use on the pseries if (iutil.getPPCMachine() == "pSeries" or iutil.getPPCMachine() == "iSeries"): self.supported = 1 self.formattable = 1 else: self.supported = 0 self.formattable = 0 def formatDevice(self, entry, progress, chroot='/'): # copy and paste job from booty/bootloaderInfo.py... def getDiskPart(dev): cut = len(dev) if (dev.startswith('rd/') or dev.startswith('ida/') or dev.startswith('cciss/')): if dev[-2] == 'p': cut = -1 elif dev[-3] == 'p': cut = -2 else: if dev[-2] in string.digits: cut = -2 elif dev[-1] in string.digits: cut = -1 name = dev[:cut] # hack off the trailing 'p' from /dev/cciss/*, for example if name[-1] == 'p': for letter in name: if letter not in string.letters and letter != "/": name = name[:-1] break if cut < 0: partNum = int(dev[cut:]) else: partNum = None return (name, partNum) # FIXME: oh dear is this a hack beyond my wildest imagination. # parted doesn't really know how to do these, so we're going to # exec sfdisk and make it set the partition type. this is bloody # ugly devicePath = entry.device.setupDevice(chroot) (disk, part) = getDiskPart(devicePath) if disk is None or part is None: log("oops, somehow got a bogus device for the PrEP partition " "(%s)" %(devicePath,)) return args = [ "sfdisk", "--change-id", disk, "%d" %(part,), "41" ] if disk.startswith("/tmp/") and not os.access(disk, os.R_OK): isys.makeDevInode(disk[5:], disk) log("going to run %s" %(args,)) rc = iutil.execWithRedirect("/usr/sbin/sfdisk", args, stdout = "/dev/tty5", stderr = "/dev/tty5") if rc: raise SystemError fileSystemTypeRegister(prepbootFileSystem()) class ForeignFileSystem(FileSystemType): def __init__(self): FileSystemType.__init__(self) self.formattable = 0 self.checked = 0 self.name = "foreign" def formatDevice(self, entry, progress, chroot='/'): return fileSystemTypeRegister(ForeignFileSystem()) class PsudoFileSystem(FileSystemType): def __init__(self, name): FileSystemType.__init__(self) self.formattable = 0 self.checked = 0 self.name = name self.supported = 0 class ProcFileSystem(PsudoFileSystem): def __init__(self): PsudoFileSystem.__init__(self, "proc") fileSystemTypeRegister(ProcFileSystem()) class DevptsFileSystem(PsudoFileSystem): def __init__(self): PsudoFileSystem.__init__(self, "devpts") self.defaultOptions = "gid=5,mode=620" fileSystemTypeRegister(DevptsFileSystem()) class DevshmFileSystem(PsudoFileSystem): def __init__(self): PsudoFileSystem.__init__(self, "tmpfs") def isMountable(self): return 0 fileSystemTypeRegister(DevshmFileSystem()) class AutoFileSystem(PsudoFileSystem): def __init__(self): PsudoFileSystem.__init__(self, "auto") fileSystemTypeRegister(AutoFileSystem()) class BindFileSystem(AutoFileSystem): def __init__(self): PsudoFileSystem.__init__(self, "bind") def isMountable(self): return 1 fileSystemTypeRegister(BindFileSystem()) class FileSystemSet: def __init__(self): self.messageWindow = None self.progressWindow = None self.waitWindow = None self.mountcount = 0 self.migratedfs = 0 self.reset() def isActive(self): return self.mountcount != 0 def registerMessageWindow(self, method): self.messageWindow = method def registerProgressWindow(self, method): self.progressWindow = method def registerWaitWindow(self, method): self.waitWindow = method def reset (self): self.entries = [] proc = FileSystemSetEntry(Device(), '/proc', fileSystemTypeGet("proc")) self.add(proc) pts = FileSystemSetEntry(Device(), '/dev/pts', fileSystemTypeGet("devpts"), "gid=5,mode=620") self.add(pts) shm = FileSystemSetEntry(Device(), '/dev/shm', fileSystemTypeGet("tmpfs")) def verify (self): for entry in self.entries: if type(entry.__dict__) != type({}): raise RuntimeError, "fsset internals inconsistent" def add (self, entry): # remove any existing duplicate entries for existing in self.entries: if (existing.device.getDevice() == entry.device.getDevice() and existing.mountpoint == entry.mountpoint): self.remove(existing) # XXX debuggin' ## log ("fsset at %s\n" ## "adding entry for %s\n" ## "entry object %s, class __dict__ is %s", ## self, entry.mountpoint, entry, ## isys.printObject(entry.__dict__)) self.entries.append(entry) self.entries.sort (mountCompare) def remove (self, entry): self.entries.remove(entry) def getEntryByMountPoint(self, mount): for entry in self.entries: if entry.mountpoint == mount: return entry return None def getEntryByDeviceName(self, dev): for entry in self.entries: if entry.device.getDevice() == dev: return entry return None def copy (self): new = FileSystemSet() for entry in self.entries: new.add (entry) return new def fstab (self): format = "%-23s %-23s %-7s %-15s %d %d\n" fstab = "" for entry in self.entries: if entry.mountpoint: if entry.getLabel(): device = "LABEL=%s" % (entry.getLabel(),) else: device = devify(entry.device.getDevice()) fstab = fstab + entry.device.getComment() fstab = fstab + format % (device, entry.mountpoint, entry.fsystem.getName(), entry.options, entry.fsck, entry.order) return fstab def mtab (self): format = "%s %s %s %s 0 0\n" mtab = "" for entry in self.entries: if entry.mountpoint: # swap doesn't end up in the mtab if entry.fsystem.getName() == "swap": continue if entry.options: options = "rw," + entry.options else: options = "rw" mtab = mtab + format % (devify(entry.device.getDevice()), entry.mountpoint, entry.fsystem.getName(), options) return mtab def raidtab(self): # set up raidtab... raidtab = "" for entry in self.entries: if entry.device.getName() == "RAIDDevice": raidtab = raidtab + entry.device.raidTab() return raidtab def write (self, prefix): f = open (prefix + "/etc/fstab", "w") f.write (self.fstab()) f.close () raidtab = self.raidtab() if raidtab: f = open (prefix + "/etc/raidtab", "w") f.write (raidtab) f.close () # touch mtab open (prefix + "/etc/mtab", "w+") f.close () def restoreMigratedFstab(self, prefix): if not self.migratedfs: return fname = prefix + "/etc/fstab" if os.access(fname + ".rpmsave", os.R_OK): os.rename(fname + ".rpmsave", fname) def migratewrite(self, prefix): if not self.migratedfs: return fname = prefix + "/etc/fstab" f = open (fname, "r") lines = f.readlines() f.close() perms = os.stat(fname)[0] & 0777 os.rename(fname, fname + ".rpmsave") f = open (fname, "w+") os.chmod(fname, perms) for line in lines: fields = string.split(line) # try to be smart like in fsset.py::readFstab() if not fields or line[0] == "#": f.write(line) continue if len (fields) < 4 or len (fields) > 6: f.write(line) continue if string.find(fields[3], "noauto") != -1: f.write(line) continue mntpt = fields[1] entry = self.getEntryByMountPoint(mntpt) if not entry or not entry.getMigrate(): f.write(line) elif entry.origfsystem.getName() != fields[2]: f.write(line) else: fields[2] = entry.fsystem.getName() newline = "%-23s %-23s %-7s %-15s %s %s\n" % (fields[0], fields[1], fields[2], fields[3], fields[4], fields[5]) f.write(newline) f.close() # return the "boot" devicce def getBootDev(self): mntDict = {} bootDev = None for entry in self.entries: mntDict[entry.mountpoint] = entry.device # FIXME: this ppc stuff feels kind of crufty -- the abstraction # here needs a little bit of work if iutil.getPPCMacGen() == "NewWorld": for entry in self.entries: if entry.fsystem.getName() == "Apple Bootstrap": bootDev = entry.device elif (iutil.getPPCMachine() == "pSeries" or iutil.getPPCMachine() == "iSeries"): # we want the first prep partition or the first newly formatted one bestprep = None for entry in self.entries: if ((entry.fsystem.getName() == "PPC PReP Boot") and ((bestprep is None) or ((bestprep.format == 0) and (entry.format == 1)))): bestprep = entry if bestprep: bootDev = bestprep.device elif iutil.getArch() == "ia64": if mntDict.has_key("/boot/efi"): bootDev = mntDict['/boot/efi'] elif mntDict.has_key("/boot"): bootDev = mntDict['/boot'] else: bootDev = mntDict['/'] return bootDev def bootloaderChoices(self, diskSet, bl): ret = {} bootDev = self.getBootDev() if bootDev is None: log("no boot device set") return ret if bootDev.getName() == "RAIDDevice": ret['boot'] = (bootDev.device, N_("RAID Device")) return ret if iutil.getPPCMacGen() == "NewWorld": ret['boot'] = (bootDev.device, N_("Apple Bootstrap")) n = 1 for entry in self.entries: if ((entry.fsystem.getName() == "Apple Bootstrap") and ( entry.device.getDevice() != bootDev.device)): ret['boot%d' %(n,)] = (entry.device.getDevice(), N_("Apple Bootstrap")) n = n + 1 return ret elif (iutil.getPPCMachine() == "pSeries" or iutil.getPPCMachine() == "iSeries"): ret['boot'] = (bootDev.device, N_("PPC PReP Boot")) return ret ret['boot'] = (bootDev.device, N_("First sector of boot partition")) ret['mbr'] = (bl.drivelist[0], N_("Master Boot Record (MBR)")) return ret # set active partition on disks # if an active partition is set, leave it alone; if none set # set either our boot partition or the first partition on the drive active def setActive(self, diskset): dev = self.getBootDev() if dev is None: return bootDev = dev.device # on ia64, *only* /boot/efi should be marked bootable # similarly, on pseries, we really only want the PReP partition active if (iutil.getArch() == "ia64" or iutil.getPPCMachine() == "pSeries" or iutil.getPPCMachine() == "iSeries"): part = partedUtils.get_partition_by_name(diskset.disks, bootDev) if part and part.is_flag_available(parted.PARTITION_BOOT): part.set_flag(parted.PARTITION_BOOT, 1) return for drive in diskset.disks.keys(): foundActive = 0 bootPart = None disk = diskset.disks[drive] part = disk.next_partition() while part: if not part.is_active(): part = disk.next_partition(part) continue if not part.is_flag_available(parted.PARTITION_BOOT): foundActive = 1 part = None continue if part.get_flag(parted.PARTITION_BOOT): foundActive = 1 part = None continue if not bootPart: bootPart = part if partedUtils.get_partition_name(part) == bootDev: bootPart = part part = disk.next_partition(part) if bootPart and not foundActive: bootPart.set_flag(parted.PARTITION_BOOT, 1) if bootPart: del bootPart def formatSwap (self, chroot): for entry in self.entries: if (not entry.fsystem or not entry.fsystem.getName() == "swap" or not entry.getFormat() or entry.isMounted()): continue try: self.formatEntry(entry, chroot) except SystemError: if self.messageWindow: self.messageWindow(_("Error"), _("An error occurred trying to " "initialize swap on device %s. This " "problem is serious, and the install " "cannot continue.\n\n" "Press to reboot your system.") % (entry.device.getDevice(),)) sys.exit(0) def turnOnSwap (self, chroot): for entry in self.entries: if (entry.fsystem and entry.fsystem.getName() == "swap" and not entry.isMounted()): try: entry.mount(chroot) self.mountcount = self.mountcount + 1 except SystemError, (num, msg): if self.messageWindow: self.messageWindow(_("Error"), _("Error enabling swap device %s: " "%s\n\n" "This most likely means this " "swap partition has not been " "initialized." "\n\n" "Press OK to reboot your " "system.") % (entry.device.getDevice(), msg)) sys.exit(0) def labelEntry(self, entry, chroot): if entry.device.doLabel is not None: entry.fsystem.labelDevice(entry, chroot) def formatEntry(self, entry, chroot): entry.fsystem.formatDevice(entry, self.progressWindow, chroot) def badblocksEntry(self, entry, chroot): entry.fsystem.badblocksDevice(entry, self.progressWindow, chroot) def getMigratableEntries(self): retval = [] for entry in self.entries: if entry.origfsystem and entry.origfsystem.isMigratable(): retval.append(entry) return retval def formattablePartitions(self): list = [] for entry in self.entries: if entry.fsystem.isFormattable(): list.append (entry) return list def checkBadblocks(self, chroot='/'): for entry in self.entries: if (not entry.fsystem.isFormattable() or not entry.getBadblocks() or entry.isMounted()): continue try: self.badblocksEntry(entry, chroot) except BadBlocksError: log("Bad blocks detected on device %s",entry.device.getDevice()) if self.messageWindow: self.messageWindow(_("Error"), _("Bad blocks have been detected on " "device /dev/%s. We do " "not recommend you use this device." "\n\n" "Press to reboot your system") % (entry.device.getDevice(),)) sys.exit(0) except SystemError: if self.messageWindow: self.messageWindow(_("Error"), _("An error occurred searching for " "bad blocks on %s. This problem is " "serious, and the install cannot " "continue.\n\n" "Press to reboot your system.") % (entry.device.getDevice(),)) sys.exit(0) def createLogicalVolumes (self, chroot='/'): # first set up the volume groups for entry in self.entries: if entry.fsystem.name == "volume group (LVM)": entry.device.setupDevice(chroot) # then set up the logical volumes for entry in self.entries: if isinstance(entry.device, LogicalVolumeDevice): entry.device.setupDevice(chroot) def makeFilesystems (self, chroot='/'): formatted = [] notformatted = [] for entry in self.entries: if (not entry.fsystem.isFormattable() or not entry.getFormat() or entry.isMounted()): notformatted.append(entry) continue try: self.formatEntry(entry, chroot) formatted.append(entry) except SystemError: if self.messageWindow: self.messageWindow(_("Error"), _("An error occurred trying to " "format %s. This problem is " "serious, and the install cannot " "continue.\n\n" "Press to reboot your system.") % (entry.device.getDevice(),)) sys.exit(0) for entry in formatted: try: self.labelEntry(entry, chroot) except SystemError: # should be OK, we'll still use the device name to mount. pass # go through and have labels for the ones we don't format for entry in notformatted: dev = entry.device.getDevice() if not dev or dev == "none": continue if not entry.mountpoint or entry.mountpoint == "swap": continue try: label = isys.readExt2Label(dev) except: continue if label: entry.setLabel(label) else: self.labelEntry(entry, chroot) def haveMigratedFilesystems(self): return self.migratedfs def migrateFilesystems (self, chroot='/'): if self.migratedfs: return for entry in self.entries: if not entry.origfsystem: continue if not entry.origfsystem.isMigratable() or not entry.getMigrate(): continue try: entry.origfsystem.migrateFileSystem(entry, self.messageWindow, chroot) except SystemError: if self.messageWindow: self.messageWindow(_("Error"), _("An error occurred trying to " "migrate %s. This problem is " "serious, and the install cannot " "continue.\n\n" "Press to reboot your system.") % (entry.device.getDevice(),)) sys.exit(0) self.migratedfs = 1 def mountFilesystems(self, instPath = '/', raiseErrors = 0, readOnly = 0): for entry in self.entries: if not entry.fsystem.isMountable(): continue try: entry.mount(instPath, readOnly = readOnly) self.mountcount = self.mountcount + 1 except OSError, (num, msg): if self.messageWindow: if num == errno.EEXIST: self.messageWindow(_("Invalid mount point"), _("An error occurred when trying " "to create %s. Some element of " "this path is not a directory. " "This is a fatal error and the " "install cannot continue.\n\n" "Press to reboot your " "system.") % (entry.mountpoint,)) else: self.messageWindow(_("Invalid mount point"), _("An error occurred when trying " "to create %s: %s. This is " "a fatal error and the install " "cannot continue.\n\n" "Press to reboot your " "system.") % (entry.mountpoint, msg)) sys.exit(0) except SystemError, (num, msg): if raiseErrors: raise SystemError, (num, msg) if self.messageWindow: self.messageWindow(_("Error"), _("Error mounting device %s as %s: " "%s\n\n" "This most likely means this " "partition has not been formatted." "\n\n" "Press OK to reboot your system.") % (entry.device.getDevice(), entry.mountpoint, msg)) sys.exit(0) # XXX hack to make the device node exist for the root fs if # it's a logical volume so that mkinitrd can create the initrd. root = self.getEntryByMountPoint("/") if isinstance(root.device, LogicalVolumeDevice): # now make sure all of the device nodes exist. *sigh* rc = iutil.execWithRedirect("/usr/sbin/vgmknodes", ["vgmknodes", "-v"], stdout = "/tmp/lvmout", stderr = "/tmp/lvmout", searchPath = 1) rootDev = "/dev/%s" % (root.device.getDevice(),) os.makedirs(instPath + rootDev[:string.rfind(rootDev, "/")]) iutil.copyDeviceNode(rootDev, instPath + rootDev) # raise RuntimeError def filesystemSpace(self, chroot='/'): space = [] for entry in self.entries: if not entry.isMounted(): continue # we can't put swap files on swap partitions; that's nonsense if entry.mountpoint == "swap": continue path = "%s/%s" % (chroot, entry.mountpoint) try: space.append((entry.mountpoint, isys.fsSpaceAvailable(path))) except SystemError: log("failed to get space available in filesystemSpace() for %s" %(entry.mountpoint,)) def spaceSort(a, b): (m1, s1) = a (m2, s2) = b if (s1 > s2): return -1 elif s1 < s2: return 1 return 0 space.sort(spaceSort) return space def hasDirtyFilesystems(self, mountpoint): ret = [] for entry in self.entries: # XXX - multifsify, virtualize isdirty per fstype if entry.fsystem.getName() != "ext2": continue if entry.getFormat(): continue if isys.ext2IsDirty(entry.device.getDevice()): log("%s is a dirty ext2 partition" % entry.device.getDevice()) ret.append(entry.device.getDevice()) return ret def umountFilesystems(self, instPath, ignoreErrors = 0): # XXX remove special case try: isys.umount(instPath + '/proc/bus/usb', removeDir = 0) log("Umount USB OK") except: # log("Umount USB Fail") pass # take a slice so we don't modify self.entries reverse = self.entries[:] reverse.reverse() for entry in reverse: entry.umount(instPath) class FileSystemSetEntry: def __init__ (self, device, mountpoint, fsystem=None, options=None, origfsystem=None, migrate=0, order=-1, fsck=-1, format=0, badblocks = 0): if not fsystem: fsystem = fileSystemTypeGet("ext2") self.device = device self.mountpoint = mountpoint self.fsystem = fsystem self.origfsystem = origfsystem self.migrate = migrate if options: self.options = options else: self.options = fsystem.getDefaultOptions(mountpoint) self.mountcount = 0 self.label = None if fsck == -1: self.fsck = fsystem.isChecked() else: self.fsck = fsck if order == -1: if mountpoint == '/': self.order = 1 elif self.fsck: self.order = 2 else: self.order = 0 else: self.order = order if format and not fsystem.isFormattable(): raise RuntimeError, ("file system type %s is not formattable, " "but has been added to fsset with format " "flag on" % fsystem.getName()) self.format = format self.badblocks = badblocks def mount(self, chroot='/', devPrefix='/tmp', readOnly = 0): device = self.device.setupDevice(chroot, devPrefix=devPrefix) # FIXME: we really should migrate before turnOnFilesystems. # but it's too late now if (self.migrate == 1) and (self.origfsystem is not None): self.origfsystem.mount(device, "%s/%s" % (chroot, self.mountpoint), readOnly = readOnly, bindMount = isinstance(self.device, BindMountDevice)) else: self.fsystem.mount(device, "%s/%s" % (chroot, self.mountpoint), readOnly = readOnly, bindMount = isinstance(self.device, BindMountDevice)) self.mountcount = self.mountcount + 1 def umount(self, chroot='/'): if self.mountcount > 0: try: self.fsystem.umount(self.device, "%s/%s" % (chroot, self.mountpoint)) self.mountcount = self.mountcount - 1 except RuntimeError: pass def setFileSystemType(self, fstype): self.fsystem = fstype def setBadblocks(self, state): self.badblocks = state def getBadblocks(self): return self.badblocks def getMountPoint(self): return self.mountpoint def setFormat (self, state): if self.migrate and state: raise ValueError, "Trying to set format bit on when migrate is set!" self.format = state def getFormat (self): return self.format def setMigrate (self, state): if self.format and state: raise ValueError, "Trying to set migrate bit on when format is set!" self.migrate = state def getMigrate (self): return self.migrate def isMounted (self): return self.mountcount > 0 def getLabel (self): return self.label def setLabel (self, label): self.label = label def __str__(self): if not self.mountpoint: mntpt = "None" else: mntpt = self.mountpoint str = ("fsentry -- device: %(device)s mountpoint: %(mountpoint)s\n" " fsystem: %(fsystem)s format: %(format)s\n" " ismounted: %(mounted)s \n"% {"device": self.device.getDevice(), "mountpoint": mntpt, "fsystem": self.fsystem.getName(), "format": self.format, "mounted": self.mountcount}) return str class Device: def __init__(self): self.device = "none" self.fsoptions = {} self.label = None self.isSetup = 0 self.doLabel = 1 def getComment (self): return "" def getDevice (self, asBoot = 0): return self.device def setupDevice (self, chroot='/', devPrefix='/tmp'): return self.device def cleanupDevice (self, chroot, devPrefix='/tmp'): pass def solidify (self): pass def getName(self): return self.__class__.__name__ class DevDevice(Device): """Device with a device node rooted in /dev that we just always use the pre-created device node for.""" def __init__(self, dev): Device.__init__(self) self.device = dev def getDevice(self, asBoot = 0): return self.device def setupDevice(self, chroot='/', devPrefix='/dev'): return "/dev/%s" %(self.getDevice(),) class RAIDDevice(Device): # XXX usedMajors does not take in account any EXISTING md device # on the system for installs. We need to examine all partitions # to investigate which minors are really available. usedMajors = {} # members is a list of Device based instances that will be # a part of this raid device def __init__(self, level, members, minor=-1, spares=0, existing=0): Device.__init__(self) self.level = level self.members = members self.spares = spares self.numDisks = len(members) - spares self.isSetup = existing self.doLabel = None if len(members) < spares: raise RuntimeError, ("you requiested more spare devices " "than online devices!") if level == 5: if self.numDisks < 3: raise RuntimeError, "RAID 5 requires at least 3 online members" # there are 32 major md devices, 0...31 if minor == -1 or minor is None: for I in range(32): if not RAIDDevice.usedMajors.has_key(I): minor = I break if minor == -1: raise RuntimeError, ("Unable to allocate minor number for " "raid device") RAIDDevice.usedMajors[minor] = None self.device = "md" + str(minor) self.minor = minor # make sure the list of raid members is sorted self.members.sort() def __del__ (self): del RAIDDevice.usedMajors[self.minor] def ext2Args (self): if self.level == 5: return [ '-R', 'stride=%d' % ((self.numDisks - 1) * 16) ] elif self.level == 0: return [ '-R', 'stride=%d' % (self.numDisks * 16) ] return [] def raidTab (self, devPrefix='/dev'): entry = "" entry = entry + "raiddev %s/%s\n" % (devPrefix, self.device,) entry = entry + "raid-level %d\n" % (self.level,) entry = entry + "nr-raid-disks %d\n" % (self.numDisks,) entry = entry + "chunk-size 64k\n" entry = entry + "persistent-superblock 1\n" entry = entry + "nr-spare-disks %d\n" % (self.spares,) i = 0 for device in self.members[:self.numDisks]: entry = entry + " device %s/%s\n" % (devPrefix, device) entry = entry + " raid-disk %d\n" % (i,) i = i + 1 i = 0 for device in self.members[self.numDisks:]: entry = entry + " device %s/%s\n" % (devPrefix, device) entry = entry + " spare-disk %d\n" % (i,) i = i + 1 return entry def setupDevice (self, chroot, devPrefix='/tmp'): node = "%s/%s" % (devPrefix, self.device) isys.makeDevInode(self.device, node) if not self.isSetup: raidtab = '/tmp/raidtab.%s' % (self.device,) f = open(raidtab, 'w') f.write(self.raidTab(devPrefix=devPrefix)) f.close() for device in self.members: PartitionDevice(device).setupDevice(chroot, devPrefix=devPrefix) iutil.execWithRedirect ("/usr/sbin/mkraid", ('mkraid', '--really-force', '--dangerous-no-resync', '--configfile', raidtab, node), stderr="/dev/tty5", stdout="/dev/tty5") raid.register_raid_device(self.device, self.members[:], self.level, self.numDisks) self.isSetup = 1 else: isys.raidstart(self.device, self.members[0]) return node def getDevice (self, asBoot = 0): if not asBoot: return self.device else: return self.members[0] def solidify(self): return ext2 = fileSystemTypeGet("ext2") ext2.registerDeviceArgumentFunction(RAIDDevice, RAIDDevice.ext2Args) class VolumeGroupDevice(Device): def __init__(self, name, physvols, pesize = 4096, existing = 0): """Creates a VolumeGroupDevice. name is the name of the volume group physvols is a list of Device objects which are the physical volumes pesize is the size of physical extents in kilobytes existing is whether this vg previously existed. """ Device.__init__(self) self.physicalVolumes = physvols self.isSetup = existing self.name = name self.device = name self.isSetup = existing self.physicalextentsize = pesize def setupDevice (self, chroot, devPrefix='/tmp'): nodes = [] for volume in self.physicalVolumes: # XXX the lvm tools are broken and will only work for /dev node = volume.setupDevice(chroot, devPrefix="/dev") # XXX I should check if the pv is set up somehow so that we # can have preexisting vgs and add new pvs to them. if not self.isSetup: # now make the device into a real physical volume # XXX I don't really belong here. should # there be a PhysicalVolumeDevice(PartitionDevice) ? rc = iutil.execWithRedirect("/usr/sbin/pvcreate", ["pvcreate", "-ff", "-y", "-v", node], stdout = "/tmp/lvmout", stderr = "/tmp/lvmout", searchPath = 1) if rc: raise SystemError, "pvcreate failed for %s" % (volume,) nodes.append(node) if not self.isSetup: # rescan now that we've recreated pvs. ugh. lvm.vgscan() args = [ "/usr/sbin/vgcreate", "-v", "-An", "-s", "%sk" %(self.physicalextentsize,), self.name ] args.extend(nodes) rc = iutil.execWithRedirect(args[0], args, stdout = "/tmp/lvmout", stderr = "/tmp/lvmout", searchPath = 1) if rc: raise SystemError, "vgcreate failed for %s" %(self.name,) self.isSetup = 1 else: lvm.vgscan() lvm.vgactivate() return "/dev/%s" % (self.name,) def solidify(self): return class LogicalVolumeDevice(Device): # note that size is in megabytes! def __init__(self, volumegroup, size, vgname, existing = 0): Device.__init__(self) self.volumeGroup = volumegroup self.size = size self.name = vgname self.isSetup = 0 self.isSetup = existing self.doLabel = None # these are attributes we might want to expose. or maybe not. # self.chunksize # self.stripes # self.stripesize # self.extents # self.readaheadsectors def setupDevice(self, chroot, devPrefix='/tmp'): if not self.isSetup: rc = iutil.execWithRedirect("/usr/sbin/lvcreate", ["lvcreate", "-L", "%dM" % (self.size,), "-n", self.name, "-An", self.volumeGroup], stdout = "/tmp/lvmout", stderr = "/tmp/lvmout", searchPath = 1) if rc: raise SystemError, "lvcreate failed for %s" %(self.name,) self.isSetup = 1 return "/dev/%s" % (self.getDevice(),) def getDevice(self, asBoot = 0): return "%s/%s" % (self.volumeGroup, self.name) def solidify(self): return class PartitionDevice(Device): def __init__(self, partition): Device.__init__(self) if type(partition) != types.StringType: raise ValueError, "partition must be a string" self.device = partition def setupDevice(self, chroot, devPrefix='/tmp'): path = '%s/%s' % (devPrefix, self.getDevice(),) isys.makeDevInode(self.getDevice(), path) return path class PartedPartitionDevice(PartitionDevice): def __init__(self, partition): Device.__init__(self) self.device = None self.partition = partition def getDevice(self, asBoot = 0): if not self.partition: return self.device return partedUtils.get_partition_name(self.partition) def solidify(self): # drop reference on the parted partition object and note # the current minor number allocation self.device = self.getDevice() self.partition = None class BindMountDevice(Device): def __init__(self, directory): Device.__init__(self) self.device = directory def setupDevice(self, chroot, devPrefix="/tmp"): return chroot + self.device class SwapFileDevice(Device): def __init__(self, file): Device.__init__(self) self.device = file self.size = 0 def setSize (self, size): self.size = size def setupDevice (self, chroot, devPrefix='/tmp'): file = os.path.normpath(chroot + self.getDevice()) if not os.access(file, os.R_OK): if self.size: # make sure the permissions are set properly fd = os.open(file, os.O_CREAT, 0600) os.close(fd) isys.ddfile(file, self.size, None) else: raise SystemError, (0, "swap file creation necessary, but " "required size is unknown.") return file # This is a device that describes a swap file that is sitting on # the loopback filesystem host for partitionless installs. # The piggypath is the place where the loopback file host filesystem # will be mounted class PiggybackSwapFileDevice(SwapFileDevice): def __init__(self, piggypath, file): SwapFileDevice.__init__(self, file) self.piggypath = piggypath def setupDevice(self, chroot, devPrefix='/tmp'): return SwapFileDevice.setupDevice(self, self.piggypath, devPrefix) class LoopbackDevice(Device): def __init__(self, hostPartition, hostFs): Device.__init__(self) self.host = "/dev/" + hostPartition self.hostfs = hostFs self.device = "loop1" def setupDevice(self, chroot, devPrefix='/tmp/'): if not self.isSetup: isys.mount(self.host[5:], "/mnt/loophost", fstype = "vfat") self.device = allocateLoopback("/mnt/loophost/redhat.img") if not self.device: raise SystemError, "Unable to allocate loopback device" self.isSetup = 1 path = '%s/%s' % (devPrefix, self.getDevice()) else: path = '%s/%s' % (devPrefix, self.getDevice()) isys.makeDevInode(self.getDevice(), path) path = os.path.normpath(path) return path def getComment (self): return "# LOOP1: %s %s /redhat.img\n" % (self.host, self.hostfs) def makeDevice(dev): if dev.startswith('md'): try: (mdname, devices, level, numActive) = raid.lookup_raid_device(dev) device = RAIDDevice(level, devices, minor=int(mdname[2:]), spares=len(devices) - numActive, existing=1) except KeyError: device = DevDevice(dev) else: device = DevDevice(dev) return device # XXX fix RAID def readFstab (path): fsset = FileSystemSet() # first, we look at all the disks on the systems and get any ext2/3 # labels off of the filesystem. # temporary, to get the labels diskset = partedUtils.DiskSet() diskset.openDevices() labels = diskset.getLabels() labelToDevice = {} for device, label in labels.items(): labelToDevice[label] = device # mark these labels found on the system as used so the factory # doesn't give them to another device labelFactory.reserveLabels(labels) loopIndex = {} f = open (path, "r") lines = f.readlines () f.close() for line in lines: fields = string.split (line) if not fields: continue if line[0] == "#": # skip all comments continue # all valid fstab entries have 6 fields; if the last two are missing # they are assumed to be zero per fstab(5) if len(fields) < 4: continue elif len(fields) == 4: fields.append(0) fields.append(0) elif len(fields) == 5: fields.append(0) elif len(fields) > 6: continue # if we don't support mounting the filesystem, continue if not fileSystemTypes.has_key(fields[2]): continue if string.find(fields[3], "noauto") != -1: continue fsystem = fileSystemTypeGet(fields[2]) label = None if fields[0] == "none": device = Device() elif ((string.find(fields[3], "bind") != -1) and fields[0].startswith("/")): # it's a bind mount, they're Weird (tm) device = BindMountDevice(fields[0]) fsystem = fileSystemTypeGet("bind") elif len(fields) >= 6 and fields[0].startswith('LABEL='): label = fields[0][6:] if labelToDevice.has_key(label): device = makeDevice(labelToDevice[label]) else: log ("Warning: fstab file has LABEL=%s, but this label " "could not be found on any file system", label) # bad luck, skip this entry. continue elif fields[2] == "swap" and not fields[0].startswith('/dev/'): # swap files file = fields[0] if file.startswith('/initrd/loopfs/'): file = file[14:] device = PiggybackSwapFileDevice("/mnt/loophost", file) else: device = SwapFileDevice(file) elif fields[0].startswith('/dev/loop'): # look up this loop device in the index to find the # partition that houses the filesystem image # XXX currently we assume /dev/loop1 if loopIndex.has_key(device): (dev, fs) = loopIndex[device] device = LoopbackDevice(dev, fs) elif fields[0].startswith('/dev/'): device = makeDevice(fields[0][5:]) else: continue # if they have a filesystem being mounted as auto, we need # to sniff around a bit to figure out what it might be # if we fail at all, though, just ignore it if fsystem == "auto" and device.getDevice() != "none": try: tmp = partedUtils.sniffFilesystemType("/dev/%s" %(device.setupDevice(),)) if tmp is not None: fsystem = tmp except: pass entry = FileSystemSetEntry(device, fields[1], fsystem, fields[3], origfsystem=fsystem) if label: entry.setLabel(label) fsset.add(entry) return fsset def getDevFD(device): try: fd = os.open(device, os.O_RDONLY) except: file = '/tmp/' + device isys.makeDevInode(device, file) try: fd = os.open(file, os.O_RDONLY) except: return -1 return fd def isValidExt2(device): fd = getDevFD(device) if fd == -1: return 0 buf = os.read(fd, 2048) os.close(fd) if len(buf) != 2048: return 0 if struct.unpack(" to reboot your system." "\n\n") #CJS Changed out the RedHat stuff and put in Fermi stuff label = gui.WrappingLabel( _("Congratulations, the installation is complete.\n\n" "%s" "Thanks for installing %s \n" ) %(floppystr, bootstr)) # label = gui.WrappingLabel( # _("Congratulations, the installation is complete.\n\n" # "%s" # "%s" # "For information on Errata (updates and bug fixes), visit:\n" # "\thttp://www.redhat.com/errata/\n\n" # "For information on automatic updates through Red Hat " # "Network, visit:\n" # "\thttp://rhn.redhat.com/\n\n" # "For information on using and configuring the system, visit:\n" # "\thttp://www.redhat.com/docs/\n" # "\thttp://www.redhat.com/apps/support/\n\n" # "To register the product for support, visit:\n" # "\thttp://www.redhat.com/apps/activate/\n\n") % (floppystr, # bootstr,)) hbox.pack_start (label, gtk.TRUE, gtk.TRUE) return hbox # # package_gui.py: package group and individual package selection screens # # Brent Fox # Matt Wilson # Jeremy Katz # Michael Fulbright # # Copyright 2000-2003 Red Hat, Inc. # # This software may be freely redistributed under the terms of the GNU # library public license. # # You should have received a copy of the GNU Library Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # import rpm import gui import string import sys import gtk import gobject import checklist from iw_gui import * from string import * from thread import * from examine_gui import * from rhpl.translate import _, N_ from hdrlist import orderPackageGroups, getGroupDescription from hdrlist import PKGTYPE_MANDATORY, PKGTYPE_DEFAULT, PKGTYPE_OPTIONAL from hdrlist import ON, MANUAL_ON, OFF, MANUAL_OFF, MANUAL_NONE from hdrlist import ON_STATES, OFF_STATES from hdrlist import Package, Group from rhpl.log import log import packages def queryUpgradeContinue(intf): rc = intf.messageWindow(_("Proceed with upgrade?"), _("The file systems of the Linux installation " "you have chosen to upgrade have already been " "mounted. You cannot go back past this point. " "\n\n") + _( "Would you like to continue with the upgrade?"), type = "yesno") return rc class IndividualPackageSelectionWindow (InstallWindow): windowTitle = N_("Individual Package Selection") htmlTag = "sel-indiv" def build_packagelists(self, groups): toplevels = {} self.packageGroupStore = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING) keys = groups.keys() keys.sort() # allpkgs is the special toplevel group keys.remove("allpkgs") allpkg = self.packageGroupStore.append(None) self.packageGroupStore.set_value(allpkg, 0, _("All Packages")) self.packageGroupStore.set_value(allpkg, 1, "allpkgs") # go through and make parent nodes for all of the groups for key in keys: fields = string.split(key, '/') main = fields[0] if len(fields) > 1: subgroup = fields[1] if toplevels.has_key(main): continue iter = self.packageGroupStore.append(allpkg) self.packageGroupStore.set_value(iter, 0, main) self.packageGroupStore.set_value(iter, 1, main) toplevels[main] = iter # now make the children for key in keys: fields = string.split(key, '/') main = fields[0] if len(fields) > 1: subgroup = fields[1] else: continue if not toplevels.has_key(main): raise RuntimeError, "Got unexpected key building tree" parent = toplevels[main] iter = self.packageGroupStore.append(parent) self.packageGroupStore.set_value(iter, 0, subgroup) self.packageGroupStore.set_value(iter, 1, "%s/%s" % (main, subgroup)) def add_packages(self, packages): """Adds the packages provided (list of headers) to the package list""" SHOW_WATCH_MIN = 200 if len(packages) > SHOW_WATCH_MIN: cw = self.ics.getICW() cw.busyCursorPush() for header in packages: name = header[rpm.RPMTAG_NAME] size = header[rpm.RPMTAG_SIZE] # get size in MB size = size / (1024 * 1024) # don't show as < 1 MB if size < 1: size = 1 self.packageList.append_row((name, size), header.isSelected()) ### XXX Hack to get around fact treeview doesn't seem to resort ### when data is store is changed. By jostling it we can make it self.packageList.store.set_sort_column_id(self.sort_id, not self.sort_order) self.packageList.store.set_sort_column_id(self.sort_id, self.sort_order) if len(packages) > SHOW_WATCH_MIN: cw.busyCursorPop() def select_group(self, selection): (model, iter) = selection.get_selected() if iter: currentGroup = model.get_value(iter, 1) self.packageList.clear() if not self.flat_groups.has_key(currentGroup): self.selectAllButton.set_sensitive(gtk.FALSE) self.unselectAllButton.set_sensitive(gtk.FALSE) return self.selectAllButton.set_sensitive(gtk.TRUE) self.unselectAllButton.set_sensitive(gtk.TRUE) packages = self.flat_groups[currentGroup] self.add_packages(packages) def toggled_package(self, data, row): row = int(row) package = self.packageList.get_text(row, 1) if not self.pkgs.has_key(package): raise RuntimeError, "Toggled a non-existent package %s" % (package) val = self.packageList.get_active(row) if val: self.pkgs[package].select() else: self.pkgs[package].unselect() self.updateSize() # if they hit space bar stop that event from happening self.ignoreKeypress = (package, val) def select_package(self, selection): (model, iter) = selection.get_selected() if iter: package = model.get_value(iter, 1) if not self.pkgs.has_key(package): raise RuntimeError, "Selected a non-existent package %s" % (package) buffer = self.packageDesc.get_buffer() description = self.get_rpm_desc(self.pkgs[package]) try: version = self.pkgs[package][rpm.RPMTAG_VERSION] except: version = None if version: outtext = _("Package: %s\nVersion: %s\n") % (package, version ) + description else: outtext =description buffer.set_text(outtext) else: buffer = self.packageDesc.get_buffer() buffer.set_text("") def get_rpm_desc (self, header): desc = replace (header[rpm.RPMTAG_DESCRIPTION], "\n\n", "\x00") desc = replace (desc, "\n", " ") desc = replace (desc, "\x00", "\n\n") return desc def make_group_list(self, grpset, displayBase = 0): """Go through all of the headers and get group names, placing packages in the dictionary. Also have in the upper level group""" groups = {} # special group for listing all of the packages (aka old flat view) groups["allpkgs"] = [] for key in grpset.hdrlist.pkgs.keys(): header = grpset.hdrlist.pkgs[key] group = header[rpm.RPMTAG_GROUP] hier = string.split(group, '/') toplevel = hier[0] # make sure the dictionary item exists for group and toplevel # note that if group already exists, toplevel must also exist if not groups.has_key (group): groups[group] = [] if not groups.has_key(toplevel): groups[toplevel] = [] # don't display package if it is in the Base group if not grpset.groups["core"].includesPackage(header) or displayBase: groups[group].append(header) if len(hier) > 1: groups[toplevel].append(header) groups["allpkgs"].append(header) return groups def select_all (self, rownum, select_all): for row in range(self.packageList.num_rows): package = self.packageList.get_text(row, 1) if not self.pkgs.has_key(package): raise RuntimeError, "Attempt to toggle non-existent package" if select_all: self.pkgs[package].select() else: self.pkgs[package].unselect() self.packageList.set_active(row, select_all) self.updateSize() def updateSize(self): text = _("Total install size: %s") % (self.grpset.sizeStr(),) self.totalSizeLabel.set_text(text) # FIXME -- if this is kept instead of the All Packages in the tree # it needs to properly handle keeping the tree expanded to the same # state as opposed to having it default back to collapsed and no # selection; I personally like the All Packages in the tree better # but that seems to look weird with gtk 1.3.11 def changePkgView(self, widget): if self.treeRadio.get_active(): packages = [] self.packageTreeView.set_model(self.packageGroupStore) self.packageTreeView.expand_all() else: # cache the full package list if not self.allPkgs: self.allPkgs = [] for pkg in self.pkgs.values(): if not self.grpset.groups["core"].includesPackage(pkg): self.allPkgs.append(pkg) packages = self.allPkgs self.packageTreeView.set_model(gtk.ListStore(gobject.TYPE_STRING)) self.packageList.clear() self.add_packages(packages) ### XXX Hack to get around fact treeview doesn't seem to resort ### Have to keep up with sort state when user changes it def colClickedCB(self, widget, val): self.sort_id = widget.get_sort_column_id() self.sort_order = widget.get_sort_order() def keypressCB(self, widget, val): if val.keyval == gtk.keysyms.space: selection = self.packageList.get_selection() (model, iter) = selection.get_selected() if iter: self.select_package(selection) package = self.packageList.store.get_value(iter, 1) val = self.packageList.store.get_value(iter, 0) # see if we just got this because of focus being on # checkbox toggle and they hit space bar if self.ignoreKeypress: if (package, val) == self.ignoreKeypress: self.ignoreKeypress = None return gtk.TRUE else: # didnt match for some reason, lets plow ahead self.ignoreKeypress = None self.packageList.store.set_value(iter, 0, not val) if not val: self.pkgs[package].select() else: self.pkgs[package].unselect() self.updateSize() return gtk.TRUE return gtk.FALSE # IndividualPackageSelectionWindow tag="sel-indiv" def getScreen (self, grpset): self.grpset = grpset self.pkgs = self.grpset.hdrlist self.allPkgs = None self.packageTreeView = gtk.TreeView() renderer = gtk.CellRendererText() column = gtk.TreeViewColumn('Groups', renderer, text=0) column.set_clickable(gtk.TRUE) self.packageTreeView.append_column(column) self.packageTreeView.set_headers_visible(gtk.FALSE) self.packageTreeView.set_rules_hint(gtk.FALSE) self.packageTreeView.set_enable_search(gtk.FALSE) self.flat_groups = self.make_group_list(grpset) self.build_packagelists(self.flat_groups) selection = self.packageTreeView.get_selection() selection.connect("changed", self.select_group) self.packageTreeView.set_model(self.packageGroupStore) self.packageTreeView.expand_all() self.sw = gtk.ScrolledWindow () self.sw.set_policy (gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) self.sw.set_shadow_type(gtk.SHADOW_IN) self.sw.add(self.packageTreeView) packageHBox = gtk.HBox() self.leftVBox = gtk.VBox(gtk.FALSE) # FIXME should these stay or go? # tree/flat radio buttons... optionHBox = gtk.HBox() self.treeRadio = gtk.RadioButton(None, (_("_Tree View"))) self.treeRadio.connect("clicked", self.changePkgView) self.flatRadio = gtk.RadioButton(self.treeRadio, (_("_Flat View"))) optionHBox.pack_start(self.treeRadio) optionHBox.pack_start(self.flatRadio) self.leftVBox.pack_start(optionHBox, gtk.FALSE) self.leftVBox.pack_start(self.sw, gtk.TRUE) packageHBox.pack_start(self.leftVBox, gtk.FALSE) self.packageList = PackageCheckList(2) self.packageList.checkboxrenderer.connect("toggled", self.toggled_package) self.packageList.set_enable_search(gtk.TRUE) self.sortType = "Package" self.packageList.set_column_title (1, (_("_Package"))) self.packageList.set_column_sizing (1, gtk.TREE_VIEW_COLUMN_GROW_ONLY) self.packageList.set_column_title (2, (_("_Size (MB)"))) self.packageList.set_column_sizing (2, gtk.TREE_VIEW_COLUMN_GROW_ONLY) self.packageList.set_headers_visible(gtk.TRUE) self.packageList.set_column_min_width(0, 16) self.packageList.set_column_clickable(0, gtk.FALSE) self.packageList.set_column_clickable(1, gtk.TRUE) self.packageList.set_column_sort_id(1, 1) self.packageList.set_column_clickable(2, gtk.TRUE) self.packageList.set_column_sort_id(2, 2) sort_id = 1 sort_order = 0 self.packageList.store.set_sort_column_id(sort_id, sort_order) ### XXX Hack to keep up with state of sorting ### Remove when treeview is fixed self.sort_id = sort_id self.sort_order = sort_order col = self.packageList.get_column(1) col.connect("clicked", self.colClickedCB, None) col = self.packageList.get_column(2) col.connect("clicked", self.colClickedCB, None) selection = self.packageList.get_selection() selection.connect("changed", self.select_package) self.packageList.connect("key-release-event", self.keypressCB) self.ignoreKeypress = None self.packageListSW = gtk.ScrolledWindow () self.packageListSW.set_border_width (5) self.packageListSW.set_policy (gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.packageListSW.set_shadow_type(gtk.SHADOW_IN) self.packageListSW.add(self.packageList) self.packageListVAdj = self.packageListSW.get_vadjustment () self.packageListSW.set_vadjustment(self.packageListVAdj) self.packageListHAdj = self.packageListSW.get_hadjustment () self.packageListSW.set_hadjustment(self.packageListHAdj) packageHBox.pack_start (self.packageListSW) descVBox = gtk.VBox () descVBox.pack_start (gtk.HSeparator (), gtk.FALSE, padding=2) hbox = gtk.HBox () bb = gtk.HButtonBox () bb.set_layout (gtk.BUTTONBOX_END) self.totalSizeLabel = gtk.Label (_("Total size: ")) hbox.pack_start (self.totalSizeLabel, gtk.FALSE, gtk.FALSE, 0) self.selectAllButton = gtk.Button (_("Select _all in group")) bb.pack_start (self.selectAllButton, gtk.FALSE) self.selectAllButton.connect ('clicked', self.select_all, 1) self.unselectAllButton = gtk.Button(_("_Unselect all in group")) bb.pack_start(self.unselectAllButton, gtk.FALSE) self.unselectAllButton.connect ('clicked', self.select_all, 0) hbox.pack_start (bb) self.selectAllButton.set_sensitive (gtk.FALSE) self.unselectAllButton.set_sensitive (gtk.FALSE) descVBox.pack_start (hbox, gtk.FALSE) descSW = gtk.ScrolledWindow () descSW.set_border_width (5) descSW.set_policy (gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) descSW.set_shadow_type(gtk.SHADOW_IN) self.packageDesc = gtk.TextView() buffer = gtk.TextBuffer(None) self.packageDesc.set_buffer(buffer) self.packageDesc.set_editable(gtk.FALSE) self.packageDesc.set_cursor_visible(gtk.FALSE) self.packageDesc.set_wrap_mode(gtk.WRAP_WORD) descSW.add (self.packageDesc) descSW.set_size_request (-1, 100) descVBox.pack_start (descSW) vbox = gtk.VBox () vbox.pack_start (packageHBox) vbox.pack_start (descVBox, gtk.FALSE) self.updateSize() return vbox class PackageSelectionWindow (InstallWindow): windowTitle = N_("Package Group Selection") htmlTag = "sel-group" def __init__ (self, ics): InstallWindow.__init__ (self, ics) self.ics = ics self.ics.setNextEnabled (1) self.files_found = "gtk.FALSE" def getPrev (self): self.grpset.setSelectionState(self.origSelection) def getNext (self): if self.individualPackages.get_active(): self.dispatch.skipStep("indivpackage", skip = 0) else: self.dispatch.skipStep("indivpackage") # jsut to be sure if we come back self.savedStateDict = {} self.savedStateFlag = 0 return None def setSize(self): self.sizelabel.set_text (_("Total install size: %s") % self.grpset.sizeStr()) # given a value, set all components except Everything and Base to # that value. Handles restoring state if it exists def setComponentsSensitive(self, comp, value): tmpval = self.ignoreComponentToggleEvents self.ignoreComponentToggleEvents = 1 for (cb, lbl, al, ebutton, cbox, cbox2, cbcomp) in self.checkButtons: if cbcomp.id == comp.id: continue if value: if cbcomp.id not in ["everything", "base"]: # print "restoring checkbutton for ",cbcomp.name," at state ",self.savedStateDict[cbcomp.name] if self.savedStateFlag and self.savedStateDict[cbcomp.id]: cb.set_active(1) else: cb.set_active(0) else: cb.set_active(0) else: cb.set_active(0) if cb.get_active(): if ebutton: al.add(ebutton) al.show_all() else: if ebutton: if ebutton in al.get_children(): al.remove(ebutton) if lbl: self.setCompCountLabel(cbcomp, lbl) if cbox: cbox.set_sensitive(value) if cbox2: cbox2.set_sensitive(value) self.ignoreComponentToggleEvents = tmpval def componentToggled(self, widget, data): cw = self.ics.getICW() (comp, lbl, count, al, ebutton) = data newstate = widget.get_active() if self.ignoreComponentToggleEvents: return cw.busyCursorPush() # turn on all the comps we selected if newstate: if ebutton: al.add(ebutton) al.show_all() comp.select () else: if ebutton in al.get_children(): al.remove(ebutton) # dont turn off Base, and if we're turning off everything # we need to be sure language support stuff is on if comp.id != "base": comp.unselect () if comp.id == "everything": packages.selectLanguageSupportGroups(self.grpset, self.langSupport) if count: self.setCompCountLabel(comp, count) if comp.id == "everything" or comp.id == "base": self.ignoreComponentToggleEvents = 1 # save state of buttons if they hit everything or minimal # print "entered, savedstateflag = ",self.savedStateFlag if not self.savedStateFlag and newstate: self.savedStateDict = {} self.savedStateFlag = 1 savestate = 1 else: savestate = 0 for c in self.grpset.groups.values(): if c.id in ["everything", "base"]: continue if newstate: sel = c.isSelected(justManual = 1) # print "saving ",c.name," at state ",sel if savestate: self.savedStateDict[c.id] = sel if sel: c.unselect() else: # print "restoring ",c.name," at state ",self.savedStateDict[c.name] if self.savedStateFlag and self.savedStateDict[c.id]: c.select() # turn on lang support if we're minimal and enabling if comp.id == "base" and newstate: packages.selectLanguageSupportGroups(self.grpset, self.langSupport) self.setComponentsSensitive(comp, not newstate) self.ignoreComponentToggleEvents = 0 else: self.savedStateDict = {} self.savedStateFlag = 0 # after all this we need to recompute total size self.setSize() cw.busyCursorPop() def pkgGroupMemberToggled(self, widget, data): (comp, sizeLabel, pkg) = data (ptype, sel) = comp.packageInfo()[pkg] # dont select or unselect if its already in that state if widget.get_active(): if sel not in ON_STATES: comp.selectPackage(pkg) else: log("%s already selected, not selecting!" %(pkg,)) else: if sel in ON_STATES: comp.unselectPackage(pkg) else: log("%s already unselected, not unselecting!" %(pkg,)) if sizeLabel: self.setDetailSizeLabel(comp, sizeLabel) # have to do magic to handle 'Minimal' def setCheckButtonState(self, cb, comp): state = 0 if comp.id != "base": state = comp.isSelected(justManual = 1) cb.set_active (state) else: state = 1 for c in self.grpset.groups.values(): # ignore base and langsupport files pulled in by 'minimal' if c.id == "base" or self.grpset.groups[c.id].langonly is not None: continue if c.isSelected(justManual = 1): state = 0 break cb.set_active (state) return state def getStats(self, comp): # FIXME: metapkgs # allpkgs = comp.packageInfo().keys() + comp.metapackagesFullInfo().keys() allpkgs = comp.packageInfo() if comp.id == "everything": total = len(allpkgs.keys()) if comp.isSelected(justManual = 1): selected = total else: selected = 0 return (selected, total) total = 0 selected = 0 for pkg in allpkgs.values(): total = total + 1 (ptype, sel) = pkg if sel in ON_STATES: selected = selected + 1 return (selected, total) def setDetailSizeLabel(self, comp, sizeLabel): text = _("Total install size: %s") % (self.grpset.sizeStr(),) sizeLabel.set_text(text) def setCompLabel(self, comp, label): if comp.id == "base": nm = _("Minimal") else: nm = comp.name label.set_markup("%s" % (nm,)) def setCompCountLabel(self, comp, label): (selpkg, totpkg) = self.getStats(comp) if not comp.isSelected(justManual = 1): selpkg = 0 if comp.id == "everything" or comp.id == "base": txt = "" else: txt = "[%d/%d]" % (selpkg, totpkg) label.set_markup(txt) def editDetails(self, button, data): # do all magic for packages and metapackages def getDescription(obj, comp): if self.grpset.hdrlist.pkgs.has_key(obj): obj = self.grpset.hdrlist.pkgs[obj] elif self.grpset.groups.has_key(obj): obj = self.grpset.groups[obj] basedesc = obj.getDescription() if basedesc is not None: desc = replace (basedesc, "\n\n", "\x00") desc = replace (desc, "\n", " ") desc = replace (desc, "\x00", "\n\n") else: desc = "" return "%s - %s" % (obj.name, desc) # pull out member sorted by name def getNextMember(goodpkgs, comp, domandatory = 0): curpkg = None for pkg in goodpkgs: if domandatory: (ptype, sel) = comp.packageInfo()[pkg] if ptype != PKGTYPE_MANDATORY: continue foundone = 1 if curpkg is not None: if pkg < curpkg: curpkg = pkg else: curpkg = pkg return curpkg # # START OF editDetails # # backup state (comp, hdrlbl, countlbl, compcb) = data origpkgselection = {} for (pkg, val) in comp.packageInfo().items(): origpkgselection[pkg] = val self.dialog = gtk.Dialog(_("Details for '%s'") % (comp.name,)) gui.addFrame(self.dialog) self.dialog.add_button('gtk-cancel', 2) self.dialog.add_button('gtk-ok', 1) self.dialog.set_position(gtk.WIN_POS_CENTER) mainvbox = self.dialog.vbox lblhbox = gtk.HBox(gtk.FALSE) lbl = gtk.Label(_("A package group can have both Base and " "Optional package members. Base packages " "are always selected as long as the package group " "is selected.\n\nSelect the optional packages " "to be installed:")) lbl.set_line_wrap(gtk.TRUE) lbl.set_size_request(475, -1) lbl.set_alignment(0.0, 0.5) lblhbox.pack_start(lbl, gtk.TRUE, gtk.TRUE) fn = self.ics.findPixmap("package-selection.png") if not fn: pix = None else: rawpix = gtk.gdk.pixbuf_new_from_file(fn) pix = gtk.Image() pix.set_from_pixbuf(rawpix) if pix is not None: al = gtk.Alignment(0.0, 0.0) al.add(pix) lblhbox.pack_start(al, gtk.FALSE, gtk.FALSE) mainvbox.pack_start(lblhbox, gtk.FALSE, gtk.FALSE) cbvbox = gtk.VBox(gtk.FALSE) cbvbox.set_border_width(5) # will pack this last, need to create it for toggle callback below sizeLabel = gtk.Label("") self.setDetailSizeLabel(comp, sizeLabel) goodpkgs = comp.packageInfo().keys() # FIXME # goodpkgs = comp.packagesFullInfo().keys() + comp.metapackagesFullInfo().keys() # first show default members, if any haveBase = 0 next = getNextMember(goodpkgs, comp, domandatory = 1) if next is not None: haveBase = 1 lbl = gtk.Label("") lbl.set_markup("%s" % (_("Base Packages"),)) lbl.set_alignment(0.0, 0.0) cbvbox.pack_start(lbl, gtk.FALSE, gtk.FALSE); while 1: next = getNextMember(goodpkgs, comp, domandatory = 1) if next is None: break goodpkgs.remove(next) desc = getDescription(next, comp) lbl = gtk.Label(desc) lbl.set_alignment(0.0, 0.0) lbl.set_property("use-underline", gtk.FALSE) thbox = gtk.HBox(gtk.FALSE) chbox = gtk.HBox(gtk.FALSE) chbox.set_size_request(10,-1) thbox.pack_start(chbox, gtk.FALSE, gtk.FALSE) thbox.pack_start(lbl, gtk.TRUE, gtk.TRUE) cbvbox.pack_start(thbox, gtk.FALSE, gtk.FALSE) # now the optional parts, if any next = getNextMember(goodpkgs, comp, domandatory = 0) if next is not None: spacer = gtk.Fixed() spacer.set_size_request(-1, 10) cbvbox.pack_start(spacer, gtk.FALSE, gtk.FALSE) lbl = gtk.Label("") lbl.set_markup("%s" % (_("Optional Packages"),)) lbl.set_alignment(0.0, 0.0) cbvbox.pack_start(lbl, gtk.FALSE, gtk.FALSE) while 1: next = getNextMember(goodpkgs, comp, domandatory = 0) if next is None: break goodpkgs.remove(next) desc = getDescription(next, comp) lbl = gtk.Label(desc) lbl.set_alignment(0.0, 0.0) lbl.set_property("use-underline", gtk.FALSE) cb = gtk.CheckButton() cb.add(lbl) (ptype, sel) = comp.packageInfo()[next] cb.set_active((sel in ON_STATES)) cb.connect("toggled", self.pkgGroupMemberToggled, (comp, sizeLabel, next)) thbox = gtk.HBox(gtk.FALSE) chbox = gtk.HBox(gtk.FALSE) chbox.set_size_request(10,-1) thbox.pack_start(chbox, gtk.FALSE, gtk.FALSE) thbox.pack_start(cb, gtk.TRUE, gtk.TRUE) cbvbox.pack_start(thbox, gtk.FALSE, gtk.FALSE) sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) wrapper = gtk.VBox (gtk.FALSE, 0) wrapper.pack_start (cbvbox, gtk.FALSE) sw.add_with_viewport (wrapper) viewport = sw.get_children()[0] viewport.set_shadow_type (gtk.SHADOW_IN) viewport.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse ("white")) cbvbox.set_focus_hadjustment(sw.get_hadjustment ()) cbvbox.set_focus_vadjustment(sw.get_vadjustment ()) mainvbox.pack_start(sw, gtk.TRUE, gtk.TRUE, 10) mainvbox.pack_start(sizeLabel, gtk.FALSE, gtk.FALSE) self.dialog.set_size_request(550, 420) self.dialog.show_all() while 1: rc = self.dialog.run() # they hit cancel, restore original state and quit if rc == 2: allpkgs = comp.packageInfo().keys() for pkg in allpkgs: (ptype, sel) = comp.packageInfo()[pkg] (optype, osel) = origpkgselection[pkg] if (osel == sel): pass elif (osel not in OFF_STATES) and (sel not in ON_STATES): comp.selectPackage(pkg) elif (osel not in ON_STATES) and (sel not in OFF_STATES): comp.unselectPackage(pkg) break self.dialog.destroy() self.setSize() if countlbl: self.setCompCountLabel(comp, countlbl) (selpkg, totpkg) = self.getStats(comp) if selpkg < 1: if compcb: compcb.set_active(0) return def focusIdleHandler(self, data): if not self.needToFocus: return if self.scrolledWindow is None: return vadj = self.scrolledWindow.get_vadjustment() swmin = vadj.lower swmax = vadj.upper pagesize = vadj.page_size curval = vadj.get_value() self.scrolledWindow.get_vadjustment().set_value(swmax-pagesize) if self.idleid is not None: gtk.idle_remove(self.idleid) self.idleid = None self.needToFocus = 0 def getScreen(self, grpset, langSupport, instClass, dispatch): # PackageSelectionWindow tag="sel-group" ICON_SIZE = 32 self.grpset = grpset self.langSupport = langSupport self.dispatch = dispatch self.origSelection = self.grpset.getSelectionState() self.checkButtons = [] # used to save buttons state if they hit everything or minimal self.savedStateDict = {} self.savedStateFlag = 0 self.ignoreComponentToggleEvents = 0 (parlist, pardict) = orderPackageGroups(self.grpset) topbox = gtk.VBox(gtk.FALSE, 3) topbox.set_border_width(3) checkGroup = gtk.SizeGroup(gtk.SIZE_GROUP_BOTH) countGroup = gtk.SizeGroup(gtk.SIZE_GROUP_BOTH) detailGroup = gtk.SizeGroup(gtk.SIZE_GROUP_BOTH) minimalActive = 0 minimalComp = None minimalCB = None everythingActive = 0 everythingComp = None everythingCB = None for par in parlist: # don't show the top-level if there aren't any groups in it if len(pardict[par]) == 0: continue # set the background to our selection color eventBox = gtk.EventBox() eventBox.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("#727fb2")) lbl = gtk.Label("") lbl.set_markup("" "%s" % (par,)) lbl.set_alignment(0.0, 0.0) pad = gtk.Alignment(0.0, 0.0) pad.add(lbl) pad.set_border_width(3) eventBox.add(pad) topbox.pack_start(eventBox) for comp in pardict[par]: #CJS checked to see if it was the everything group and just skiped it if comp.id == "everything": continue #CJS end of add if comp.hidden: if comp.id != "base": continue else: if not instClass.showMinimal: continue pixname = string.lower(comp.id) + ".png" fn = self.ics.findPixmap("comps/"+pixname) if not fn: log("could not load pix: %s " %(pixname,)) pix = None else: rawpix = gtk.gdk.pixbuf_new_from_file(fn) sclpix = rawpix.scale_simple(ICON_SIZE, ICON_SIZE, gtk.gdk.INTERP_BILINEAR) pix = gtk.Image() pix.set_from_pixbuf(sclpix) compbox = gtk.HBox(gtk.FALSE, 5) spacer = gtk.Fixed() spacer.set_size_request(30, -1) compbox.pack_start(spacer, gtk.FALSE, gtk.FALSE) # create check button and edit button # make the comps title + edit button hdrlabel=gtk.Label("") hdrlabel.set_alignment (0.0, 0.5) self.setCompLabel(comp, hdrlabel) checkButton = gtk.CheckButton() checkButton.add(hdrlabel) checkGroup.add_widget(checkButton) compbox.pack_start(checkButton) count=gtk.Label("") count.set_alignment (1.0, 0.5) self.setCompCountLabel(comp, count) countGroup.add_widget(count) compbox.pack_start(count, gtk.FALSE, gtk.FALSE) spacer = gtk.Fixed() spacer.set_size_request(15, -1) compbox.pack_start(spacer, gtk.FALSE, gtk.FALSE) buttonal = gtk.Alignment(0.5, 0.5) detailGroup.add_widget(buttonal) compbox.pack_start(buttonal, gtk.FALSE, gtk.FALSE) # now make the url looking button for details if comp.id != "everything" and comp.id != "base": nlbl = gtk.Label("") selected = comp.isSelected(justManual = 1) nlbl.set_markup('' '%s' % (_('Details'),)) editbutton = gtk.Button() editbutton.add(nlbl) editbutton.set_relief(gtk.RELIEF_NONE) editbutton.connect("clicked", self.editDetails, (comp, hdrlabel, count, checkButton)) if comp.isSelected(justManual = 1): buttonal.add(editbutton) else: editbutton = None topbox.pack_start(compbox) detailbox = gtk.HBox(gtk.FALSE) spacer = gtk.Fixed() spacer.set_size_request(45, -1) detailbox.pack_start(spacer, gtk.FALSE, gtk.FALSE) # icon if pix is not None: al = gtk.Alignment(0.5, 0.5) al.add(pix) detailbox.pack_start(al, gtk.FALSE, gtk.FALSE, 10) # add description if it exists descr = getGroupDescription(comp) if descr is not None: label=gtk.Label("") label.set_alignment (0.0, 0.0) label.set_line_wrap(gtk.TRUE) if gtk.gdk.screen_width() > 640: wraplen = 350 else: wraplen = 250 label.set_size_request(wraplen, -1) label.set_markup("%s" % (_(descr),)) detailbox.pack_start(label, gtk.TRUE) topbox.pack_start(detailbox) state = self.setCheckButtonState(checkButton, comp) if comp.id == "base": minimalActive = state minimalComp = comp minimalCB = checkButton elif comp.id == "everything": everythingActive = state everythingComp = comp everythingCB = checkButton checkButton.connect('toggled', self.componentToggled, (comp, hdrlabel, count, buttonal, editbutton)) self.checkButtons.append ((checkButton, count, buttonal, editbutton, compbox, detailbox, comp)) # add some extra space to the end of each group spacer = gtk.Fixed() spacer.set_size_request(-1, 3) topbox.pack_start(spacer, gtk.FALSE, gtk.FALSE) # hack to make everything and minimal act right sw = gtk.ScrolledWindow() sw.set_policy (gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) viewport = gtk.Viewport(sw.get_hadjustment(), sw.get_vadjustment()) sw.add(viewport) viewport.add(topbox) viewport.set_property('shadow-type', gtk.SHADOW_IN) viewport.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("white")) topbox.set_focus_hadjustment(sw.get_hadjustment()) topbox.set_focus_vadjustment(sw.get_vadjustment()) # save so we can scrfoll if needed self.scrolledWindow = sw self.needToFocus = 0 # if special case we do things a little differently if minimalActive: self.setComponentsSensitive(minimalComp, 0) sw.set_focus_child(minimalCB) self.needToFocus = 1 elif everythingActive: self.setComponentsSensitive(everythingComp, 0) sw.set_focus_child(everythingCB) self.needToFocus = 1 if self.needToFocus: self.idleid = gtk.idle_add(self.focusIdleHandler, None) # pack rest of screen hbox = gtk.HBox (gtk.FALSE, 5) self.individualPackages = gtk.CheckButton ( _("_Select individual packages")) self.individualPackages.set_active ( not dispatch.stepInSkipList("indivpackage")) # hbox.pack_start (self.individualPackages, gtk.FALSE) self.sizelabel = gtk.Label ("") self.setSize() hbox.pack_start (self.sizelabel, gtk.TRUE) vbox = gtk.VBox (gtk.FALSE, 5) vbox.pack_start (sw, gtk.TRUE) vbox.pack_start (hbox, gtk.FALSE) vbox.set_border_width (5) return vbox class PackageCheckList(checklist.CheckList): def create_columns(self, columns): renderer = gtk.CellRendererText() column = gtk.TreeViewColumn('Text', renderer, text = 1) column.set_clickable(gtk.FALSE) self.append_column(column) renderer = gtk.CellRendererText() column = gtk.TreeViewColumn('Size', renderer, text = 2) column.set_clickable(gtk.FALSE) self.append_column(column) def __init__(self, columns = 2): store = gtk.TreeStore(gobject.TYPE_BOOLEAN, gobject.TYPE_STRING, gobject.TYPE_INT) checklist.CheckList.__init__(self, columns=columns, custom_store = store)  . ..compseraJ fermigenericdesktop.pngdKbtevsimulation.pngn Lbtevworker.pngs,M custom.pnggeN$genericdesktopoffsite.png0fO splash.pngf Pfermitux-farms.pngthQfarms-console.png yoRanaconda_header.png07 for more information. 02[F1-Main] [F2-Options] [F3-General] [F4-Kernel] [F5-Rescue]07 . ..boonedataserver.png astro.png boone.pngbtevtrigger.pngbtev.png clued0workstation.png N btevsimulation.png btevworker.png( cdf.png cdfcafworker.png  cdflevel3.png!cdfoffsite.png" cdfonline.pngc#cmsdesktop.png$cms.png% cmsserver.png& cmsfarm.png' fermigenericdesktop.png(consoleserver.png?)cpd.png* cpdserver.png+css.png, custom.png-farms-console.png. farms.png/fermi-kerberos.png0genericfarm.png1 focus.png2(fermigenericdesktopoffsite.png3fermiverygeneric.png4fnalubatch.png5fnaluinteractive.png6local-printer.png7 kerberos.png8mysql-tools.png9 minos.png: openafs.png;oaa.png<openafs-client.png=openssh-server.png>postgresql-tools.png@rip.pngPNG  IHDR00WgAMA abKGD pHYs  d_tIME DKIDATx͙yT՝ǿᆬ^ګffSę`p3J(ehD='88EL`20M@i\h^k{!n\띪{xƭs`SL؄%&EO ܦf_zKKg@gHϯ/| xBŶ}o<*n"ҐtL4Hp(#vQ_uEF3oܑ){9 :ƀwkU~P#kNɻRJ]@рN/:bJ3ꂨ4U+1#x`m9U(w&fmo П5TAzj>A6AH%d06/P VB1+UҘ!H*D\( @2$B䝋F:Xp㿟d]ƕKtQ%U*;uQ/=1U}F [/w]0H):6PNOMo:,87YGM᭦i~\tZʕBtcΜn:>ۧe2kr W]WJWw߿*tKr 7ϲS)n/z$5|l{{k\|~q*Qso^gg^xH>&J)֎769?ݶv(H=4zϯ.] zbÆusf)i{Ӧͫ{޼^_;pN\G?R+uuݭiVwuZxY V=Ԛ§'0aE@QUb8UQ|̝;g0LݻK%OAwkkӏy3>-r5wZvmbEJyN{;J%%owahgK) )njj"k4EQ<| W(BD"z*JfRHŧכ^z-x0R..5k<3; >yrYCUI< FL&xޡ#`pp۶+ ']*p%)=\SSl˖-w}=<x2ھ[nnrA0qbs&DnnV7kQ'e@LqΊGi2.4e2B :r]vr3c\>CoM9!Z s" `μ!Qs'rA%1wz [7cWB?`ݍwmg:, Ẑ/ QQI9lEWles'OFO_/wD>_@a`.EUU3{?/Dq" ܣyK*C}DNРAEP15`K FL!8,E&ۭ&aq6!DkM#ru:(Lq ( #yd̃9-z!$\X1Tl.a{&U 8R3T?GUUDbqpA!@~(4j\e! AQU! 9 '9 H(3cGp_?CjcxŃQ"([R 0BjIH)!% p(. @Hh- )\("$PK>ņ4( qK4<7)(<(8gBu<`SrM3IWXTHXDʔBi0" .PG7wס>V0x?\PP):P.(G N%BQȏU~t?Tţ`Rr%"`+< O  (@0M Xu$77"s )(e\@H>gq?=-*\$ʒ "Xt||O{- RB`(G0A]]=e´LE&@=8B| @:ǿ^;._/B'UPiYա OZ,.$T!)6tq (8B@(t_Z{38qgѢCpu[S&tT9p[~h8*e";4xPU8Lzey`׉oaxW@H%86dr uh*4MC(Pi(sdDb+R(KA)wg}v~)M֥@2x2}H$ P, sPsB`/~B!f l BPn0/;f>aʴ Qa:a)EBut#!ڹh=mid9Eȡ?h1^UIENDB`PNG  IHDR((bKGD pHYs  ~tIME ]IDATx|yy篖^"i{zZBam`Y|dMbc<vb|cs 8^b6I` ғ~k?n09S:t_]p]An%)G! O'o& S߁8{Y|_߇67+ŖEC x+tן5owoe;MZ<>Ojԏݫ^C 7Ng xg8Y@_N{"`f. 8x>OϞ k4QY|*-SuhãEb$fs97)𽸙Y'|>9g&rh/4 8hh<{Gci@CAGy69D5iEQ&掜x!O(6LzV|o"mѰd39h]th?nIFc Ie MB4I)`DMM#Ε(fDD-@Cᬛ.~rEY sQd95m5H@#jeэIqiP_"pt& rtGI+@cD}VBH?]l$B?@:t)B܄FDS!D=ёOO#ڕf tSis_4w1C]5k 0m,n|ZCM.b4/riIxfD' >ӔT>kQųR'W4 } ΋~83U@`t $ȐC9L"D?/EM ,^uΑT;x"!Cd&BaD3HvRZu 6ЊhFHarD f=V܎ߖUUyDaRvX:/U>⼎1i; GS$VhܟVj_}daz9MbD0ꯀ{tW5nJ=\۪k /-^܎WK# L\7n yjy %`96Tf\{m6¶RQfk d0I (*ɐÃ-w_hIka0cRLaM^<RZYDk~╈tգU'K,[lrk:wdŵW̝$~=rB 3?|[$NFۣ@:ZNLԺ/)lq^ס: @ʥ/ r }^\4Rɕ]>EFnMC'?\?\462&]Jg6,[sBh=Ɖ+ 4M4 (W?'h4oR_ t|7W4:ĉ-:^h9= Ār\]SL3KCM?R=oY8熀'GtKۋ[F?mluخ SutZX/53%w = !jMYY\cq1N}OM[ @Q@`ÖT_?bP) 6;7r̓Hd]GbKI OtDKct>~,1S؎$ \kw]7c/.=S#.sUUQQSJ?Z z>Y>ӌt_R&>9yC誩j:8$9pֹ'$zXIgׂ @^'~gcDc08\閆]N&)C]"yh.[秐\n!Ѧρ+.^\Y)X+5\$+RW\$K'tmR#5 0K+#Q~ 4co@+jf%Jhoao.Dm>j{t'~pAK^츮rܡM w48:f~U`sK'Ř sZ:yzqs;) x42ShѭYq>=Kq`R8 gǐ({:nJa>d2bQ3PYwT.@PYʱhMg(DM.mZƮm-(e`MӠ6 drvJyYeY,轪g9D *nާN^_ ״~18adKHR 8a(q.GS>8UP40DDQAD"y>8{K|-Rs!NN0#څh0 g;~o>C* OZשvPOEw--պxP ; A)u*}mS3$dȍG?e ,FD$\~UOr>a$~XTa<~.`e5/KgĪmji^/ <2 {;sG;h\}s;A Gs/()y[seYF g\k5suHX,sT[ݝ<\.C~d]HuR*w !KF   㜇Mb/ZqSk01-FFpVr܀Ǐ3d!/ s0)fbssifsEN)1y]>H)F`'oS!| hRG}MӠkxΥcOprm˶mt:uuhO=?<w:(Ƣ6IZBrBrN3@V.'Z`'Dco/YG䋉9hRuM0;; 6;W~YW$y;k<'F]D.ɏLk+1=fnt48h<8W܁$r @k[wN@لaJc۵3;W78 x#¬q~e8+^_;7?z  ׼h?4)sNC۲R9@eX IuMB%e\:XdYjhOCpG^q%yyH98<ԷG8V\@@V12Mn{0wkb 3)c #&ow.AT%C&5eTUB@gxD/"@H%_es;YGNS?n8134 3!)1ri/T'Lh47-Q\uɉPǿ={K惵F cΏK}]?зHF7KZn#grj4zlv.Tʓl+٣S$cԏt2 $;oy1-VBzIsK$hT̸J)9TF1l]qӍtoSo|3蓭x> <2Lӄ^vڹ>ݲ-ӉsbH6~eL>Q\.b@TR7 }Oq~KkiC'3 nmZO9(KW<:qUUѸ;zT%De7]рenqݳBb}r4o[kJ'O}kJNWue, 0y pTϣԉ[7]d 8'N_"qEi$X^D$ w4z]߸m@AupkV?rqH'O:oU \i;mxc_\ZŸ^xeJ{ydgJ~Rѷ3Pb!fSYjRA0`n: "YEZLHx 37A3F6|xa y g URüCJo|:t]}`H@y\%ƽhjZwdFmMth R~pC$z<Gb< ]JZg;߮ir`y<9$!Eܻ"A$.ϕXO6\ދDX$Gvڣ7_j4-]Y -+)QcImV~ʟ'}"?_RPMSw57"-%RB,Fsu.uSH[wq O&ͮ|uh"px"Van]vh?G9%'X2)dŻ IZ.@hiO$cKLG)i)@ Ki%3y+89c:K0HSʥ Xb$Sc{t)^2=G6͕w'׶Cb:xxEC ` AjshBya(]Jߢy|ϲ<郓(Ye4seɩ3v] 8 |qe:/^:U^PgfOu^@v}W)0mEy0mƳˎ,{-Z ŝHp8:"ѮBҝpHLM KC1|HbmN*9xHiV腴hK(yȲ|>P N+oEc,T0n '\9Oɺw˲ KjߜIvI0ln$&]m=`lbbG<2. FR"gz0$upGqg5֋05ʣBnE6 ل}Ihޫm?v8ݲ~ۢ( K a]Է2O}rsj%۶aIvoU>ё! .Ie~(T A zˇav@+{o"e]d:ȧ:ȳ_(SL5F=eᴾʮ"/G!ŀD*ᱛO77qњF}H:,_<0ެOaԚGN~IeHߛZ|绣&J2Kl42=s0>O3/(+S}WRriy}vFԕ{ȯ-y^[/-=Ss+RSY&O}I I 5 C?t%'J/B l(mw˅(C s[aT? eNYQ~}-WKtad"`hxKj2\o=_8ǛAZ&ʍ[j}nPC dIH^؇p[W} c;BhH РYN {X)0KPhޣo쿮 = {pVeh|ZĘ%,/;{Og!D`T󖌫9GX˔KeF4$]a .:&EzZpk#&ND!Ai ܺhs1HE1d1򊀎/*51ӑ|/6|*[QHzBIz; ON~% r$}2;Q~IlbUqxvD> ws{cŅ<`WB|#26;麬?m& zĔS-fs.W;`0rH# r}*a,E.*&E./^̵%H߸31҉NN b>u'p wT].?"srHP tJZB;S<щo:TcƩ>YbZ]%$sdrOHyqGMfϊģyHdIќ/169rD/aU Y8id%zWG/z(K )IENDB`PNG  IHDR$AbKGD pHYs  ~tIME6xr< IDATx|yxdWuᆬ{$vԻ{vݶmLfIHjBB dbHBdXIW:qonw뽭JJۗ;!477)( 2xޓMH @ttG2W|ckֈjKKrHUmmvJ^'```dhjUFy\mDDk~󟧧Oq˖RIɷ,0 }Ͻ08$?I?/clw\b׮;} i0"NsJqW LFfٷWUgΜ\=sf,00 uB(4B!fOafJ%j+/ Re~ffGf_//|?_MﳪjMQ4x/PrY1-˴JW('˂ V$0󾙙: J)lDXpU%f ùoff~Q,Ra\T*!:JwhZ]/ ܹYu)@F,{;J޽{M|RuuB$8B> A8vL)'eYZ**~IA0j-h8#8mk?2Srォyfܧi:ض5>D!tuu]'859+waXp J ad_ey8J9\$lũ;Nj\.+;33c&XR``YBxH>߂$o 0CUk8}z)+˲Z~ib.˲ `Y~筃$X|~fuxZx,KcȲq/ar/> OOla皚J%|_F"qAx9occsFy4Z^,]U ma޲'4 zfq( 8zP h Lƶmz<k8(65FKKyߺU)9@&'9rPmu zB@)RIð[%IH;QYّNL1M< `a,Xi?_|4mvK( ڢ9R44wA(tmQ,S&VEYJ\1eY߻RO&a,) D896nq((+u?|>fA=em@R/Ciy3"[qøp]q_yU51&'7>맧+BwZOံiS\*R.Ƌbٳ  rVni@ pzrrl=+XŠBhm aY 4ME\[~kX1kڵ@?bڦJ2"4_a@QXB!BSE$D4 GHćY̥1sF]ӴX_)b٩P.i|(yv]<9@ggHDةYY=w}6`y: {}|> K,K]LF0M~G!>gdOR o߾g}>q}j4-gX,JWLǏ?FY`E ЄHA<.'`G5wzĹmmQJZ(z9⅂Y^RUoYp=(Mᅕ\39]_** axJ)XxSQt B(bxs^ZZ|Ê2!$fqH$@ڒgsR\W7X[E$B\Ao>_.ν5p]Z.j&h4a3Ϙ .\'{{8.X,CF<ނx\rvѪUgg@<07n\BKKZ[[k4~x! ko~ oǎ@f}>cT܅e9yӧNF2`zt`9BͨT$,[Ν;/6VwݵJE𡥅^-˓hiYjAWjNܹs,+x!nSJA`a4yg:ItIJeK/Mh\Wa$)+J/FB,Ot/cc纫nz]:2d2:5;1|>cil6?>>>} l+Vj1?"GAHRLqsUF5+'珝>{Wc1kCu&3htj T,DeY&ȷCyuD]ntYSlSS?Zy?bh4_t}2 ;?_h_Q(:aPpwBT@]5׬~aȏ_!~8 /N.WrWa1M:/ucl[ZzsOϝ;7se2|sHe'lDxp]V'FFr(]t> 0,lj&8~ض5bm@d&d$Ix[\izFg+kC|'./J~ NB|;^+RT*eUAWiR&DZ8b3.y.d2|~Riݻ[AJ92$Inj_g` Vw 3]ò% hxF[Ua 4-v͖-??JtPtC#qP,\s*>tv ;w1)T,X}4kY@(ءPey *(uaY˂U%(Db<'~l.ڒ?q-h4ӧs*7=Mz!C1`Z[3t:ݸtj7x3A4po,s!LQi reJ e?::|llm?| `֮m˭XRt:]~USOg|RizeQa$|]g2B7>~-q)aٳZ[2N:L&ܿk]͂a"S5Οm)rhųgTV͒Heof8C{X,^Р*\W:`,=try0sTDb(X,_::Е/e媘/eض#m Ab:"?Rܹ*>33]QP[|)f6i:` :/011}O.WXW,jMartw_N${R`Ǫ:]Y4e9Yxsב#C!M PlTDFoeyɓou_\V:)AME鑦ٗ^9>^bmj"zN;7^wo /G:'.<pk7R)ss>۶>_ib|!^f'k{m6 gbp9*" /!Nca7~՟i}GN={8fgiE~vLÇ|fluv1 , U|9IJj;55:66sd~cKA/|?ݻwfzzcc3R0ժw]7 SSS噙MŢ̲(pp]@#|zh|NQ2?oP2@ U՚Aߍ,˩TYȑ_\oV)@oTjFb>n觔[x}UKM?2h8p]V4BSX8T`'&Vϫ?|PRTF_Mobl;uheGp1IjaHߪ,KSSs{&*֬Yc MPHlE>y Ti^'ՃaBXMM~0 G׭_vlvt?w"Z hib* Z[c8}:+922|ՀlYgN̙󝁀@f 8oWah2pWz|r8\n#{do*&w6-q~wAY>zKp/=Ǯ>y:5B y>}$9z+ |q<:(B03cJaN-$4 hjֆBۉ$# |\\il;S#YkG\4ib"Ķ'&q$(skGG; xqۦy8 Af',eoBV{澇;b13MVk qt{g/'n͚u_ڿ(X04;wq|1ﺺ³dt0M IfSSD"0-I^P | E0=U*>Bmo F=yguy8Jt:޽)=0ʲim~?KyOVooX_HN{yPHjamSXpXB΀e Xֳ*IzuO$өWssM2VAXwQՎ3d2B={$}R(:,}9.:NtS0,KV4-ۖ@ JDA{Wp1d2faUee=g\,8bW3˒,<4$ٶq4}x2=]i,wYVJ*{T?KRdƍs-˙& Q{{͓nǛSFbzZ #q%^@;ϩ*D $I:qnVϟߐV}% @QX@(5M,ou<eI? # cgf&rq(e"bϷc _H\Q6P<8!mwFŃ]|[UU,]g0pe J?/U'`r:m{G(8z݋A'BK,eFCQU xh+Eo^Q,Mnk4F%I'uzP]wEQpDiA Þ+C>rCa<#I6 zxŊSW\sυ4PrIJzͻe\.s {g\C8aY@ '.[N6zSu3h D3$ܱck84Y\ 0fۮ\FssR IDAT$[}瑗'AӴ6XxGJ taEl&9q%8Ȳ̗Jlvbتi*Ŧqn~e;JCy Enn֊,#cf?!G*!~RɆi0 !T=;- n߳G"^?7<<'ΟRqQX4-$mQj00NE48 q;% l43~H*L&Ìhs5dY~1JiTڭy'XնwpJ8t̶m~,'ggg7;QV['e1Nt|-Գm[bvb{^i,e`Y:t]e!XoY6AaN<JYlY,~iQLV4~7˲p8@{l8sNs]:>%O|MIJqb eYv>R,/j(N8tMpaY.,{ś w On2#;/x󫪲P(53 y<< .y9utQ.d($eq|͚H$?rwB6ApQכlch^?2r:T~~%rd2}z϶kiRݩ6q Ð]tzRԙp0p쬷 .R{vvV923SN-x/9myKKW\Gڲe6JulY;hhkk1  Ui2hmmMGEZGG[^rx:JfA/^yz!+5<88bY:&ðC ;fg+4Z-swoV jPzeb T+>J2Le(]]wWJ^wq"a" B!,@JEhmoŬJSEBDVjX; mn-~򓟜ikK_M@ R:$y(Zy* ), Ûo^tGGwy\ց~aeY`YDXrgƾ^N?}ΗL&3z 8NJe4d8C,7o=3<|jj`)/DևBV;U-岎X,bq٬+R>3OtuN][,\.}ƃ 6F#Wbn[OMJ.*<',`2("ذ4_;Wy睫 }}-b (|)d2ْ͖gTm[t(jOr7s^o3m_;e2f=*sVk?lpo?|6Cjժuu7 Y={|<T^֬=A>099.:J%, RF8>tt vMOjiJN ?/nIENDB`PNG  IHDRY4ֿdlPLTE $$$***444;;;BBBLLLSSSccckkksss||| %%++33<Up֪1#`GI}\J !oɚ; ->,o&aB>;d do0pN~;1zg[$_ sJCrQ >Ib@j 6en,KwM );?Af˥ ) | :eVMgN:AiY]i^n] ABWe}ŰuZRvmuNe8Dr>m{rzI!8*_5-IE;C,rm/T#gZ<.w)kS]z ]TqDIN{IvxY..uû%Lpֳ @нgnk}yk^Yp\Ną8vݾJ5On O{$GNNЕG ;IENDB`PNG  IHDRY4ֿdlPLTE $$$***444;;;BBBLLLSSSccckkksss||| %%++33<W U"ezmJn &iBr$zRpnBrQ `R6 ]+ԗI.$gŪ.+cW_FOT}w3zf*z?pHZZ}@_#ĩ(zE,UG7R Yʹ'jWZfl'y:~nV!IENDB`PNG  IHDR00WgAMA abKGD pHYs  ~tIME #'_IDATxX[l\GrΞ؎cg')vZ5T%"E -V+DP/&/m_qQUU"%+R$@QCm|݋w9g.<86e3|?ְ5a kXr:英sqzmKߘ]U<;\TU1 '4>)-sF*cNyOJ>J ]'jyR=t_ ۢز)ԮɁ?S_[A-g9h=]-l&M-ҚۯOOB SHJ@ئnAORcÃvD\|>qWJDLm-?Xz>f3dvJ%PTMl6b*:544')MeSv2IB|\.,)JiB)X__DUB,۪ UcଲrEL&|>eYoW% L8pgˍH"vA}XB~]fcY F] `0X} .ءUu($ؚb<=S/ xgؽuq B],RreZq1'$JʇPs;ZcC҇3Ĭ‰3%'f 47lVB4{;hm\<&{i_*`'!fUŨ!&k F=>rOKطo&~# Ls["nBdklZi4j)P, F@9' NDcF;1>mx wu4~-6p/cG'EhbGI ϗ@k$ࡿ֏w0<<,o| ,h y 5>Á/8#~BOرcs]&04.#8[?~WX8FSSl}ȑ#|[nt]h:FXRjUhD"n5Bs7L.щRH$rɲg^0 N>S.u]C4 x<Bzzz~W ۷Z'alƳwwwp2J}P( q8΅ΚURR >+ dexgaqzq$o1ƇΞp6WM@l^$ o: l%VÃfZ2s:P  BABm"0 '| ڤ~sJcUgd5z??g R#(.q 2_y5LYk&&ܰ 0:b/B6ۄJ982K7_$1_ 4/RPR#=[@Y@H@x J@ )0-(i K(hFs79 l,cIꤛ*$OrnqZfv{Cz!v~7u]VA>5z$wZh^~X˲ 9FkP\TwȘBi"M" 0P\M"r-1Y8_ZНr299gIC*: xSDɏ=  I.`IENDB`PNG  IHDR00WgAMA abKGD pHYs  ~tIME 2IDATxitՕz׾}l$yb2L$3!9>̒ 'd2!8%$,Îc0lql[X%yӾY[Zݵ2^$#0eU]޽ B<&E,OHli蠻}.M&]+K/-:y8vǷyxߢ!BqcsCA)!$*ZP%oWOhm/{ys u)6ܲ\bb1,|B|zvUa73ڍ!ZtH-;bUOjm_ݷ6IIȠh~!]E7LaƄnYrvדQ-]( D%y22uiӠǿ~ʚ>  ~~@0tu^ʫ+: ;ia۱;팍L**SaعiT."ٺuk|w6^(&B> 7*1>鲛ۼ $!Pgih0B+ Pag7jt'm.}pJ_x|OnU,ݧ7#@ʳ &( (6,: !Rɖgp+P䆣](9 <|≷_]xb=l,L=PN߳BQ~nD.iuk*`cY|%%$wO_ ߧ:/O߼ՓYWv`.)i}YuznDQ,"((( A2Klnp?@Sv1<{ײ-)(Nxx^'whxH-6 ISHUH(M NcIiRePh΍&94Ɩ7+yM%Ekkk5Ī#6߷lJ>zgS)@Eqqtu1UQZlPp X;_#`=C/oĨ*C0!dӸ}k>"<Ɣf >s'!qF|ҵ `?ډhtKFLLKϏ5tm Kއx7#(h(*Hc?|=+{N\?prh~!M{a Қ(@l$H)a &]O zn]ȦޟvEŢm; BvYU>M` AdKXN{*Rd YI Ma oi#0:T(Ǫ MNƤuIw<:%!0Q G6b4)FtRԵĊ@}3/I?eB y'H~t4ybJh7\XҒh8ܶևJOn %IJck!n5٤&_#Ӛf2Nl{HSh2TQ!A A5|M[2L^T2*NLX~ӪdQ>#]:R/+F8lbXƄTTVY|h琈Y[.J k,ӉazSynmQ;D޶#"xw?$7j6ݡ0L޵yh_e Lq18r;@WP3M?n!0{p`Q$:FA /#89Έ2Dv>5c'}KMK>aܦb:8lVI B7MTM\3L E~e!̞V|)!,vZFbϝ[bAKb],nƱn:LDz T ArV.͘ɮabӓh6b(Rb(*L g0jBh fd W0s8k@Ip!>э.̐i)hаo3vE|j,`5 ,INf"㊂t!U !%yӕ vv9yq\,(iM*{zF2tTiRkrxٍUHIOdР)~NYe iބބ)nEA=&ͭzENid][^:2&ɣC8CAi:2ķvC>rOb%L$4)˔@p0H]ǻ<';{GIlB"ًH0,A^Ɔ'C'k`eddy|/ :/=s҅h@sH5%Pg)Qq( 4 K6?=#Cֺ[T~uu`g-)emWl̈&6CLarE1 3@=a8~W HL"zI>~ƒR"- ='pq.o5”&W.8V?/9Y -垕ko~opj U0q5>UDb(9ٹ|$$a)a|g؈S]4:^t%C9,Z-EJwY .3n0܌+~VP)U,l5K)"u8Iz~:+&Fł,گ^=I;[;1~EUã^x^IAMa$K}bZTM#v\iI(BAJ !j!Ky},\R4zwͅ .W5oGZ7'.`Wiw=Sz:D 0u閊eH)qxlx()yW#ͻfݗTZݷT>v!S:slzk닛z{N4s+([X?< L=D^E}}`A`ԏ"-+"Ҳǩ{w?)Ρo}{kkKʺӟ췿|=?~}xR7L=aTNvvɸo#F'N*{.Mksp_)?|?~㋇kFat=-@J n8mrRzS,.ٛkoL>w/h|2Xiƶ#'ArF"c ?|UEc^o؜~!_2uK3IENDB`PNG  IHDR00WbKGD pHYs  d_tIME6 RUIDATxipTו׋]HdXbCgLc{;T55x*g2v&ql' !8,cvYHHbѾ"Z~˝-@ a$09Us=s_? m)_VބV:ZQ사TJ0}FRg7~u|I?3 7?sNWv/hMn'n $ u5'p_/CqY*;9~߻8rWMUK&( 7rO|>_ُջ|P58!J}: ;ia۱; **c!;<߾8f-*%ص =-kr?͛"~~%U80~(!Pih0Ƅ(`09l/ԞH|덽{n-t㏾/㾬4 aWp%á@(\.c0K 8 ,!/ 2҉ 8GҴD/Yæ0w\F 0 !ñgS6óx}?ꂬltCgMŒD h}vTE݌4@QT5 ߉z/N?oB :ᮭ%'ItybJh1"ħ&RsyMO>K/ ǰo>[-IQcɴx0" GEGV1U%Pqb@MЌ\@k3@%y9L HEF{EeѤDSL'rt ѕĖ_د$-i<%v)0-m?v92yϖ6[&w#hB&Agͺ$g 0mىm"Ա Plj986!$MJZ 4r֕%BðLwa:XlVQ B7MTM[y9I&T{;Yy8dv6wcL KY;҅#.W|DpZYf“Lj_:"&Ekn8NZqv4U# iZ*.̸*sĤ'pfb(Rb(*]]5u `@5 GECh z`@!gԅ## >]*A2@SOwؓvE\J @*1>կ ,ɌFQ<BJ ++P1Ҙ?ls\_>6IH7p;0DU''de7a'47&#x"∎b9x4Uc Cq]@2:B@gzCAE 1sEf, ]ᴡHԸ(?3?৵VUc Cag j=!^cLdeeg ;ȀAsm R(i|L+nWHM?{9IQ(--\pt g EQP$ޡK'`X] уƅ92L2s3i;ƌ42]3{m3Mf-WS uZH 26 EaKDQ e4UfcMǦ?eI)Son.)\|6#6-M1 Wez F]G(g:f^zN@X&&(Σ'"9+oZ 9H)DZ BY(A@S_SbV^OS*S4om ^?{}-KLkme#XR-:_c(30gqTH-R9 &؈SXsSxEAw ATt$-ٰiCT8**nXз\Wzo? _猷EQ1MI9Ue\r>pȌIt[YdtIU`^Vm\șS,e"1H0g^/^@JIzN:#,m'}EUqc1p 7KqK?Woml8zJ6&/O,";@ŕT0-V5b(-Io|:[v¡5_䗍:%1k#xk"??iq[})aޞy_tgqtp+4 E,DJ?}AoSof7gOW] k&p^>D@_|JcG)$9+E7@w\nbm{ّQ gT9g& F{kqbLړJ?ͤĘcuG]=B̜BrKs,̦ЮZy73CjZ>>Z`˅FFt\+OfmgQ<24xم]Z!n-}6Ϊ>cN9ʋ[Xx`#7W& E_-u/j{Έڷ49g@%R#h_2+$ݚ+EOOJwсJwt=<]f]9Zheڦ'+EW _)A>Lx':+Erxjqu_2??_$ 3WvIENDB`PNG  IHDR00WgAMA abKGD pHYs  d_tIME RA7IDATxřytu}%=Œ-/`lma1$ IfmNۜ&mI&mzH8 a/62’wYӓޢeƄ=;o93;|Ŝl/ُvrmŗƒښD ө+$7/eU%eKc:w=ޚѩovw, 8[=˻deQRSfW ~ FPG7t5͇*J'83G1ǘa:6iPƍ}P_}Gp42 plOEQQ˴؝vfB t >7)D0?;|>*r|ɇwoIJH^yűc}!,C:.d7 MSP4F8o X:L Mq29MS?{`V~gџy_b]+ڍoԏX)/0MPPlXunEA(*@JAk?{uC;RQ!7|Dx=xu,LX|gr?ka3Y( # .oo|bثsU,\ͣ&ggqك%EDˊAZYug0ڏ(XEvIi<1!h ٗ;Όv$΁'^TV؎o]C7z6l$.-Q$\^Bp`Ji* U]XRcbaRePh\B,ln4 {ws͖ke%=\PPCX/kn;|u[s|Qs:>D#CGp 0WYڼ٠jߎh 2.rK!"G~{7-(4,̥uWּqXׇPxUdEEN25]3 Y)  #ä(-p|Cӏγ9zGwboC {+EQ>Cz~:6 >8qƱu%5ŴmF? zR7=i#TwBK?/&>DJ&<| EvwgUMBcW*LF{PUgIIfw p_ٶӳEw**ĤLZN%N|K<`}jY~ySaS$e9 sf|^KSi+EHAX?nIcweּ 4RqrZwLH xN0I|N&K{<1%.,4YDkk LOJ|uǬ VSxS'ۚ+ɭdu?A ھ&>wfHSiTڡ>S++O$dY|aܡc:GD<6+\([覉i.jqQXU"{ocJ_<1nn' C25.̈i)hĤRGSL4/p:ހT$/;YE?bzxYUCH BAa0mG WQ/^iDuǁ)%^d 30lWcɽ_A>X l ӂR2b6hpٜ3=X67viCU5) ^G3NkOUy[sljўQl!R5<,~!R@4Yjsٵf|=ȰAϩ^P T r3;/ x6FQ=H-18q Yhzӳ:)S8#a8i: Xl)ȭc M<I)cNXj:[fH+q2_KykGƾ.h6)*1jL?պLE?2ȝ@AԚӄZﺛn@w7_1)U*l!vP#)FcTex oKsOo=?hqS>RMZQln[$05l'Nr,o>D$AJH)$<}WB*iQER5fǦX(gxf)%Bbbb iiz&t۝WܾN6u%dp㜅 !mx܈D*ZXP4 EYE'nGY]_e 4LN5@8AiƛJŲ%߿ͷކar׮]j;S\Rigz  U@QL9,$25:@ҒсpL,BML?p& %ff21ajJ+Xzy-aUWr螜LEW ={ٮ._ĩcTüz>XSSǢ(`4,=zw8^2S9̳ۜν Y|oomHsy96w^7m$e#DIpX 0grM, ?@bKTiu;1QL3U*eÌ7<߿ܾsI yqzVߍ CX\ѡ12`r|>;m]^MxŲ,b5Op`xS rmruWo%d+Y/?}dyZcc_Lb+-c9[h 1ǨN|`b~0u&Yr(i,]_@eejd S力WѰg|3QtB0a`bIII!z%@kK ׬[  f&;]ceu#D0̽0|-eH!= lg3DB!TCv(XօjR $lēBb4%fv#dzzظ8+шBUGCPѨ~#!{OP;n^F.[cJXuAPRSY.3a3wB1>ƓQQWL~aP B|_ !"p(#GFp]\5ezJ/oqwW'+ʳ&^u=11lddpiyh$`&ϙeH)qxl-)%O1ٝAMtvuQU CQw+0_[n!??Md.А_tukܖ;܇=}OTN7t (Ӂriz"FFpźEzN*Eu%f0럥7I&o^ uD]]WŹjfcΝtvvDXBő_1?(/|鷭kMm 2R(\RHl=j"XWYB2_#2"'7eHsǙߥȉNQJf~lv{ >>!iy[\wCg{Х+f[O4v."jFuΙNT22Rߪ[^֐~kwնn3%6 _PW'Nm6RSS?Luu5>(۷oMRR@pz_k{%5r7[[ZF[KfC*M77)YI4 dtdd$h\b z>kk'ѓ lD23xgaǎ(wjlVʴKܑ}TTdo-h>Zf:r{CUy%3=]lBNm[$r·VQشPr'cc]oӚ YrRǬ_t3 Һ5LMt7tڝlPon׮c_{8Tt}sm IENDB`PNG  IHDR00WgAMA abKGD pHYs  ~tIME $"IDATxy\Gu444IŖd,6vplBB`!'bLX-8,[P,ےlF4fgzz}Ւ?Z`Wu{*;pT׿~鿸r N;4@5bu[=Gy^򭝯.&|╱?m'N]6< xxko\ϡҼLSdž,(sn 51 pdojIZ/gl{;֑ʲ&X-\҄\72c[6ipg7爐eW -wX(Z;:F`lJeBk}~BX%KB`SlmַaCy ^ڷwqvh3g4g~gIcYy/KH|?ӊ,˯ U5Ó,urǟ[ϩkY̎Du.ZFَmkd),BkMKg3Sī(sdy0`9-s@ ݄InVvlFSC+ ~O !{];}7Su,oo’Nr7c0Jm , h8!s188xI"=kns]!>x-=޵7]ˋO@1[l }yCCS2,p g}ÏQ tď6!W,D-PI*?'ƒ@|4ug/ʕ}=,D2 $3Ʀ8T)C0<ڔL(~*3O&K0=9 V,˦@? mH'o|=ʟwNgVw\H }B4U u&1 vBtBm_G;Ȓ˻JW7/}=D*qµK@Xb1*Q+h!(- -8=0m q~^Yk ,.Y cd3=s&ۖM݇N'E<ή/|ږȀek!mj'_d-q裏u( c&N"fw.] 3كGYrz&d>>LubtTDׄuΝ'4qHݷjUG5|eӅΚJv>sl :mrv={*Ztw 0Bj Uj98vAitu2(&Y{y4Exj&'l"(i4oOǥ.;ubdpX]aWHclI>$8 P}V?s#DY>MΩ׼z,_U˓UμsR4ub5&[pnakM."# e(thtdb&d &j#e1Wwh6lP-X+ r(#08cbkYt۷8lHxo0ϋ*Bڧ#Mbǣt,[H8e^FJ,#^#0\]ãReVpY#/QQ(v'(Wc1~Q` S? LM7_Pʦ:xcT&AZ6}b@̟ ~ *R3D ґs# 7HWa)i&0 PZcXDZ]JS=Bʬ@'-6Ԓ,yGdla  IW 2BcL(ƒoeaᮽMC{3 R ?PDa1ض+ƽ@[[3a?SDAEwDre%Smdwץ]fѲ e ݓYcGbC8¶0X?C.cˢ7]#=Ne#|koyg2`=E1wFX3t`;0oIkXskƽ|Hy ˲n+Um`Ԋư?]˩fBA@tW@`0 Fss0Lgǃ7헬bK:yJޮ%FAk1`ilee[[nSR*^-zaRh^j^܊2P>=rrȆ#lz_>H\>TRZHQ* < FK(z~ RS+R"Y~m45XIN!,0_c4ig0#| 7ٵY1PT |1|fƃBc1C)JIh1033C2 _ I.+{e53[|̭v}ӝy…bR0إIGhio`?h˯`]B+EkK#--RI4Y uA]TEs̤3isa!UV̶%FTUI)` >^HF!`֬^0TԒHr64p#c1c/I@*^˲ Ar٦oZ=Xv/եjJ & 'SUcYrt @ =&A)m88BYql'D4rlB 7k~Hk.st˼@REJ ՗q|1:2We ]v6R\r%%2QJr1ihlqd" Rv8؝ޛoڛm߻ ,^wۮl#)٠,dkr~/  1|% h HW,"OKW_Bss#(m)̹ {a߽xٚex⛏<gD*,Zŗ.L&HN7֤#/=<__ `Z z6}l{7|]l~q"YFj\Ω!vx˲QJQ?bEp|o,@(b]ࣤϒl2LRUy>5 $P>RޅWO}]ݧ͍w̶OKK'5 z6[f(I X九2xUW3PUU5{ h~0|x;=ӛ1o|knb1X}'ήc =|iǗlPNݽ,[M<`J2_Jo;n%oun[|Y7z;=82PH`)H`[6ʀ8ũ3;pvL@IMؽ?}ݽȽcuK._JSGP7o Y $!v RPMcB< m;N!ѺDk#Jh<1:Qguߑ~GFg$SzP^'-s%ZŶ "Cfj{S E)^<" l`4b!(&ELLGW,ͤ3dfX _A.F㺂H(L[qeƈƢkX!2p@J2' &3>2PBX|CؠT)"iG]Z>r/knqK2T)e4,ᠤGXĴP?B ,ۿr,mS娬f|b fff&^ZO._ٺ v}g_yK ]u ׭"c1  31> yBH&c iSrӓ3uc_q]x6,h9Dp$B1#0lt&LRV^Nb=IENDB`PNG  IHDR00WgAMA abKGD pHYs  d_tIME 2n\IDATxři\yu}zfzfhFF8B, ,Nlĉ񇼎q✘ % !2Bb m4B;={ЃyΩݷg_w!ssɊC'{-X92x+hoobpC'mǦuo_=='\o'Q>ybGZ?;SsLO(9_O(إ\{DGSCMr`} w٥]uN_w=K9rp NUMR2:u3r2?DM|:cc} .xG:q_O֏{/6Էгk⸒|d#m6rGLɑ) *B\7XL"c/5goZ:|#]wM> ^Tx^~/D0(l1۱Rr ޽Lo48ωoZooknٟAtavVglMۣL0 R\J^&΃2h]2gLܲv}`M]k}?C۶y3lۉ)4o{/H Z mhX,⠴EleZ =kA+Ċsc Bc2'WqO|Goottt|zllX/+?|뵼ċsE\Zw ^75Eq]Xe\U%yT8̐Z&}BJ.U~?#w?Ymvilsnt}' {*ikWx\L6)^˦@ RLc ѓI:ty{bc 1ڣ8xH"şpћzw3y6Zj:)uI*_au2X&["t(~W5c3Djw(5Mu96v<4Y~g+RB= 'pq;jZU2Z(&\Zat@M -&|nMѕO6s&Y[a+3jywU|Mt|響Hc2Rwۯ~?`/}<鉲}̋,'2T,Ҟٙdۙ15Wɥ38DHu9vQ)nwWX.'s@cf߬eQ>UկkSU; )‘Ӳy;SB:Μ&H W^ƥU*~#5p8g+XeZD=>W58TXS\#QUX0]xz۶) ;[ЛX2!P(Yn}z K$]46%\ , OL4VQ(4QPխd t*Oj6FִZ0мYH1?so{Y|тD"v$5iŸhoobv)8tq $;:A,&LIvS8}%\^[pf[H* Upٷcm1kc(O`hE}S䜿XU@Rܸ?rbI&LWE532K̏30>bo?ů%Ub+ߏm.G gڑ:aXׯ0CɊҵJFkJ"p0@=TJpJf ?>42?p-M-xT@S]B!6gp{op3hIPObIISbHlj%.*Js]CLOOտ* `v2?3Ɇs(GS<`,^tuНw! A4wnE+_) >? 9o׾SIڤߓ`J}T6|C/7u[Q."Vw[J%Aj!&T-$]j^xz/؋Q9Wa n_\)z Lp8L}C5Z60R(Da Dy{ϖ|l[|s̎>?9ώ5^j0L7R"j8MQKu2vd,Sxh`KAvkh R%RiJEUQBR9Ha9W072s>-wF{](?0P|LkQ{Ź$:I UR1O..TsLON2;31c H ѸRow<5iکx︎ د5i1-eu\4B a pei"]eq],?Huh*UHqHPZ9?g]wo@_K"\֏f| G@,בThF*E2"\ESS Џc!r5B]\W:ϓ3?wčc;Fǂ]܏#ϠtJtbnb@8 r4PU tm衩-J6/*檎 XG*IX"NI$Sd2Y Ee=yǶ);#x"[Ӳp8cK[Y%`\&L1qbR*ϪX@Ǐw<ū3]k G%%F2\.hjl m&)Kr9ҙ/9W>yM{l:SⳋqMk Z  l z57o,_446Z)tJd'J IumyPFJR.q'ӛ]|0mxxl0`Yc'H&Ҁ̑WٲuΞh4r&,˃.ZB~@{*$dcҕ}^*!橮que9wufWVRo|^R(Ri4Qx|>,"`~~& "JHW[|vM+ҕ %!QTa2;3Gk*)Ѫ+])aޭ􄂡(T 0]B+$1 bKK "Hw$c#]"OK),.Γ/YZXɩ|~?Ris7D)`+|J"JiC+L&[CJI<*"B5E)98,*nP>|ѩ)V]18MuM- K>fPJdqdB){J(& Bað‰X򕮵?.~m:arW_J aXwPkiy} "nIENDB`PNG  IHDR00WgAMA abKGD pHYs  ~tIME ,̧6BIDATx՚[ly]Ф4#c[vFc/: "6im&U.FH!m`"+"^$@[)(ӮQ]0­8lj"J4|s!;;;?_J^It0,%Fb u@PMi8`8 ˇ/ 2] rw#"ŦlCqbE1UkpG!~RS1PL ίf5 +~85J-uz*״p Kϣ.CPN&`Y (0k ־ V\<_(E밗% 엶B}$T_1<6"?Cmxe7&TzV\zisaqJ"[nP(( .½ȁ85Y<,āf&puTJpƂK+XsN4N{=w~QUdE, &:`VWZ8gV]*eLĤmwk{F&{*R?жdvMj@ؑx =)4O=K_^ 4yo?T&1w=`>xu`L@ATχz<<;h$ElkRoTK@S(eė*(J]_.SGXdPdB,{2q\y/$EUyd3IA'J 6+Y1o=Afh3Y2U3of%3inueW Sk=D~kꄦ/ҦyH/l%9س_Y ,T+|:U{9j%,UT2 8{/4*0z"ss,O:. % yeP]乕%fg.C֮_tnmr:я<)By:UfN-\'K-6WKy}2a؋C&kX4ĢiٸZ~v'kK(iZCl`J!Y2W+dqЁEGPLq]*8?V3/- ( $MH$IS% 4$~O b8!hrч ǎC +}cA'+RyiG"'K|KU$)QD1($qH%qH+l$!@Z-"hCwu y|Wp0_O_7¬RK)Sd5}\p~#ot0l I)Y_gWڄaNO>g^͞n>}]2"(%Ψ*<˲Q,\S3~I>wvE;ӴoXhn]oѱ(O."@t=0 su%9Ewig29u7$qB;j102:q8Ot@V_}7^{0V@.'S0$bFJH~$YF2YUU.UgtWDDP<ƄQ ȳI';VkdwLY!ʂ)aTT/\!mH-¸M?Έ#MR cNISHV V@:|,5MkqG!_Wx A/ FԔGaگ rl%![߹0aghSO=}cK#(L7dW(P4󘺁RykxbaN `;tH,׸h-uJE v(۴5m$IJ̓3*6Zj Sm :ȥ9(Z([55 粝qQp2>=?a}GHq24rCl!(""]@K -6w]~=v֍j \R(1)6FG]Ejײ=#3#a"40u9e77䏪9/c+&\0lt0DlE 9DB |u(fϐ)(>j JHe.vmY,˴NVp0%@Uf`vlr<=̟'Oi:iMZ.fw،( 3g-&/=!(>Hi:%E1<8:R OXϡO,,W" 3}!_zq4 Mc44"4 $!"4 A爓8 inM~ϓ<[a3mvv>KQpKsd`=ȤY0>Xۯ_";y&naDAlnla& ۏᰳ+;z_\fkJZ3 |S8b{p=P@W4LEdPP2K,T氍C6Z-C/+?]6&>M;~Ň柫/1(?XY% {% Ti k r鞞*ᯫ-~H㈂d!5Ȑbddabi4y ^ <~⃬f_|[5 OP] k < gD\T@!^Fy~ow >iśobQ(-IQQ+%!%Zi$77sc{{]#IC^y~r& qCǾ{sH5Ps3@q g i>~(#ق}>_Wр|!ϡi֝ v'# S$"G~#G;=ssǟ󄂫34'}b`P^wv33M S*|w  _yβM]Dz|/ #IENDB`PNG  IHDR0/\gAMA aPLTEﵭﭭcc{RJRJZRcRB19)9!{kcRRBB)9!cZsckRZBZBR9J1{csZB!B!ƽZBsRR9J!ƽJR)Z!kc!Z1k)ֽsZs9c)c!s!ν{!c1ƽ!ƽ)cc1!ƜcsΜB1!ޥ֌ck1ΥJRޭ9!ֽ{){1)޵9RƵRƵJ1Δ{k1ZB{J{ZsBZBsR{ƥJJBJRcs)R!J!RJsƵc1cR{1ccJƥJ1΄skB΄)!sJ99!ZJ1)ޥZ!R{{BkZR{B!sZc{9JsZRBs眽ޔ{k9s{s1kޔΌJc)c眭csk9s1kޜΔƌc{RkBZks{sk{Zk9JsBZޜ{ZkRccsJZ9J1B{BR!c)9{ZckZc9B{!)kR֜ss{BJsksck%"tRNS@fbKGDH pHYs  ~tIME &IDATxTsUzm&1&%u &)du(]l*xITPnU+)&H,M:q 3Œ/33ӎsP~gg{}Yӯ#߮[v>>vM#<Ádžh1ziO+YM JæVu*7n($ :;V˖ \AWYɛkwN6lZ%^Ω"C;w5)"ء[H!|jFxi!YQIݽTWO `bvLx 4#QwgQko'i !9<xI^7W%=mXʋHNʅx>a28L3=_o(M&?ȿb/Z/>^# gB @7Ndz<7?qKuVh.u88ɑU/Yx}Mb rbY,K/SppoxJϡez X+=3p=C {-%m4C4 郃/;- ] %R߰Ĩa޹Z0?V]d_,q4Xm/`֥ꭔ^{#p*G%= :=[̪pE D63/_j]`LaקNy_+>?r۶ U啕U >_K[kG0ԕd޹;Z4}mz[tXeBV_Ryӱ/ޠSIp s.xuh7 % 3߄_L("cpt>˴pTg=6888'ĭⴐ!]n @mIENDB`PNG  IHDR00WgAMA abKGD pHYs  ~tIME +M=IDATx՚]lu]JwD-9tS[0٢tKB }!h y Q4RhziަFFt7.4\7N4ekVp3$v?^읙9?ܻ#7>mic*( XE04г! BҰбA@EO(0a -| -*RKq~U1QkpIG0a-_b[1R6u.A,P{qPsJ={,裢KL}Cc7|;Uڂ"p30fz@`uS4z7 e|@g@zUk@e2/5Mv5zlKT ɲٳd1n?hw)F)[q`l,SoFL;R7}AO)-Yv%hJc{>i_|wѬD9[R6W/]ˡbWiܚa̵ƭnn^o|_{xF߮@3@4fM,\}F^pQڗbC#\7gΐ?T w޽æ|Oq ƹF@i}CвLasYo3ZfTy!u( 9m t(aC? \$x!:a@'i5qA|0A*|FkXwUДP5fL4w":TU~/cr)ܻsg!ɑqEq>aCGt:QĦ{k8T b㠊H:J1Eܥ+Wo+̊26 5Oٛ)c6SNJFspt?<achd8p"`:v8 7N'udpEQVi9MD9UI#b(.8U0Jm)WSBm g*5Μ5K˰AЁ0NgdDgh(G@}$|˜t늸)@ c6Md }!62x0Q6\F&.DPsbf/`j2bP"/T/ M,ڽ`qQpwyIB|~`}y0" C0"B[R D4R:bJk+bCi늠WkӪFW(yC0aD1"[)>ёaԐB$sÈ(   r,@)=M|xU23c-J=rnѸ,t~p8N:a'ɑ#: x8a"!uu., ZMlic`"P,A Egas3`x!{@vVJ^WA߿ :Dqȑ!uN:Aqin!D1q8J94<, txR %ŕZCW .Zü:ɶ0*5260h&ڸ`)/zFOS:OY8"C!#y'O$c($;t#~2]&\>x_S EdRP2J,La})z^GVHK|0~ODADQLŜ9u\>'ry67VZ: ^(8k(U(Fadmk-riOzlTegg㭛ƑIB0 ,Nx) /Nb໌c}3–6a ̮1ɾMy듟> cMXu]ex R4*G'y6 }lWi TY{^g޾J)ZL#Jq74)=I_E0IENDB`PNG  IHDR00WgAMA abKGD pHYs  ~tIME '2V^5.IDATx՚}py?;ٿ_+pka19S DdžN$j6L&&J;M4- d$q2VS(GS!.A- `kYhY']{kҗLgov} %l1oix4a*Xa04K7UE3IȜmH쳐<5X<446Bm!~2Yy6G >x9Ϻdv9Нq5ȥ_nPmB@6E8:o68.~Y,D[ 8ܓd%cAL]xGMYLT~ J]tjLeP4$>x?ӄtza_eQv[`ӚT:Gݎq wXNCPsyTo'9%D:R^zrm >*E`2Jx(%ss\&aAOvpJG*QS擷T /҉bU/t-Og>zb@}A&mY % _8ZTOp* Af xЕ,{$$UМvL)0BV7d dd0Y7QTKo97HgE"M /o:QKtw`N7)W a¦^]CPdĢOZ86@_AIFU) V;,;c33ispe+JL@/'qiRjlJfY餗^Y{a("NCɃDVs zKb@M ijϾJt vK \ɁRgfh(TCtto79Nư؝fl2K.ԋBt^,9~Hg-q z2{Τ a\87ўxWәe֮1رc`x;Sϯ}B%fX45c K= {tQǦu!%r~M(0[bQ{{Qk<dw~[rUP"Y%ّbW`h.)Bg8W/z/g0'_૏xd#P(H8E:;8`XA܊ˊj%e+/+@4Tl}o Ҟe'ƸoK lnF^5SԽRǟdjGv!LPyyV Pm@n@)! MT2d$:H؃첏b:_x7׿׏<0W>5XNfj!BSk\2\vARP)9a/0*ᵰINbnv㮡}7qdͼJygeM{{;y\}ܓ +ΏȦ-LPWB>κh+X'lt%:#J" JP {DSHJ Q VNI k*鷸$˜Q(( nݕ9}֬Y+W ;e팎RLC)$` KYs~K7i 3+3 DvLoewHhyLw@! ʄ˦PUYug7|K@qQ% R,x"+V7J=IR72{.as{\I5qS1uTg0 nFd-,1P-a!+.&>Fŵv$eΜyN*  yqIFG/ =a.ur@:=T̜Q %JG,ҹ4u܏tሉT63:)<;,ʧq^?[o?iLMMQSSCX$P, X亍M&xwդ`m-YWFP>PfQ|GXԡ^!: b =Trjw7ڨD)EXP(P,*Ja1\~%V5rG]Q١|bZ]*U,ڽikEz1;Q}L;4c U <q_=ȫ?~)ņ *EF/^d||r:@)^9^z$)J%֝w~fd&?;B[a`X7jvc$uvØ0?O20`9;-ׇaM< +#FxϑM6JN-[naڵ|ygEuZ Ll:vpJ)IJi ]8`N V})Lx*h:6~S.:ّ7XKXuɧ*>L0kȾ((dDa4(|*D+F!S4L8 PVhBZ*CO adV273li\G>w9~JQNyz./mwxqo8?|߸FDaͮ'낡0D0d?})T(+Wf{ $s'NQ_#ϸ5'Nbܾ>ŋ/ȵ?%E劫mwsMV5REd=yy.=c㸂oMC-}@R&|jk)0Ɏr0==MSYZg?X,rs_/qߥzfuC=M{+$ 5=9,y`늈*OZ dEPV S)?UQnz(bu\r}2[رJ)>jkk?D?'^>֭[yn_/${2Z-CUd$ bDw`)詔^ .(d|2z(G~|޼&.=BAr355E0dU(ʾ̶ϰ<~>͟޽̹q+)QX. P 5EV65۳ eby ῔JoϢN-wka_g7X{n}ɽ~_):r{.g^9s/|nOqE^i&򄬜9n1T`CYh@P[fPBx~E /v\r~zhlp:NNۿ䱣|KGǸFg|n!kʾ.aBf*=q&c|?&RM4???iIj^ǝ`wt:{/ݸ]巶oX̉yŖLj @D uF錰1Fuc!9`*Y 7R8 9⹀&|AaOfx| {h!~g\A錬#"nwD^8<^Q>xfjXdU Vp޲"׆ٻ?nO.dcAB6ʲL.Kq+$'ZRLo,KM({5|KVדdJ ;;z g`N+jjiiBF#:toN<=̪IdW Wqp㨵n7m۶ˉ/R__DYÒ9>X~=E˗=JzZTg`3+fwd#iXtH׮],J{`S{ɲL тR$Bߨڴ-ž={fD2L^Qܸd5MLQm`DxaYY|BtRNN<&{#tv=Ӿgڵk|NE?V+FFF``Z1::XWe^@ 8.&# 6-Fc) eKQi```J/ڴi-\Psssx2MuHl6FFCH=q}}t}Q}}=effjyQ}}=1m~?t ْ"p$KD7lؠHxOM 8*޽ 'bfIDiŋǏ+G]"\b8u)./G)SAȲ-[nƍq)Go2}g4ђF4``έ (Bt:yftvvO<:(* 972C#===qs=GJO>$Ҿ}l>G6E,iP"颹F*#1رc15aሙ%?xɆe]) 1ThYYY ttzu3uwRK>}^7$ͷb ZcpYhD/. s M T?*Ay;0]oD6pKkXt)Fϫ/_k 7;y F_ ,:`tj[5} o}x;p]e%϶x\Q+zc`zaxW-/>؏'`S-E?=:V/V,"MoXu py&'z!^߿S5x|à-Z ^oO^ V Kq`$hßCȿ>={=._>OEJ.7R=]#/[=!^⼌8 [?gnPt(TIENDB`PNG  IHDR00WbKGD pHYs  d_tIME IDATxY]lGgƻ;6 >A1^{:񂐈.'"P7$y"3I(JD !!Nl$ql6.3;30?W'JNv}U]U_B$B@)=YLOOöm@tz,C~ hgB)$I!1Bʼ~zE~T !f,YA@Auu5^z%TWW/8u];e' EV $Jo>ߤCCCx6 j-nE-\! y`u!{p]\VB9T l9':_nPJyE1O: fvQh-$YA)W^ݳwC]Pᚚ ?pK5i6-Zqлi,}wy1ItY&]y7>>СC2 %R Yގ[nƍ*7~;K*h4> n70Qsss&矉ibffW\eYB{g6^(!d1b1!a466qȲh4Ç'EG(0 l۶@Ws|'b2Z&I"!PeE ޽; C)߯]vضغuY^۳e˖dss3FFF`6݋'NWR "|B4B#PPX\m[xӨ-\G/yf2@%3Lbxv*{gCpQl$kG$饀 !\.(d7$!e\1 !` !`۶/|PX,Ƙ^ٶ ?+ ew` i omۖ4-0꺮weYR)\~.\ƍq}|2ݻM6̙3~ReBi2Դ2ǟ)lnݔMȲ;vU%!eVs;w&ѣGu۶!@ui>ChV` ,8N»"!D0}λY"8}vKIF<i{Gi>_$I^Җ'=Y8vxDžaAJ躎d,ɂyQU"0J|tnnf9n=c333 ŋ'UUԯH8pΝCgg?|~cʕhkk\\CCC%}n?#(>JIT lëvŋ3cgg'(bEs (w)vEuuuPUޫ{!2h>ttt<_j]8ߖ?k1P 㿗;qga||nQSdRoBe\r %7|#s5i(sضL&L&۶( 4Mifsβ+x뭷BH_iJ)"T?066K%_l+˥ LIENDB`PNG  IHDR|<FbKGD pHYs  ~tIME " ~ IDATxw%YzN_~'RrÐKrRV Z%X6  &d8 EAئi АHQw N0_tss9u.ޭ{oݪSp=]"PV#Wv! iUToH)UW̞gZ @943+cRJiԢ(T[[[jooOez""uttD9t:`flll`8𐫪(:u1EėeD>c<3s˲d{YDX)VJ /7, J)ZRJR @Y⽏{{@APosV^;~*q4Yk&3+"{eQZS̬RZ)h"I<53~sNu]2(Rd5Pu{f$I<qSIxV쫪p*`k-k"*MRFZk @RJ39NA"|oHL<8}$H`4Aj",%DDVZkW9bfU{eIh*ZMD6hf6XommtJk1Xc R{`0h1^knY;Ǿ*&"oc@>"DkZ+QJ֚(T7q=6Hr6Ƙ$IuUU{o44MMQtXPi|NH)c Ps{x$IsY`E3{_Uwyffj{u$X)%""c8-n6 h$@ +`9xox h"qmD?Ih$P۝*J3k"Z2F)eloolҝNc"ZZ+s;;;nXxkZWeY:c[[[wJ)W/9缈[koF9㪪8Z90I (q/Ҁ(pTsB-uu4h8AsoiZX1+RJ3nO1Ves9$Ib,eYf ijΝ>뎎UeeQ`0(<<˲*ue n4M9MSQJqUU<[Ŷ`[dy73^h>2os ;NGmFY{ FݪNDQ"x~oy'eY&J&eYD(^MӧS;bPJWJ$1U%ԩ(\9ϋ,meUUUz tQ2Ϲ( Dv6rގ,".pw (q@0KcG#WGW+ȹD0Y" \mtx \j@i=MX褣tϋRܸZ+f\[kښWf3ĶhۓE.F'c)nۿYq,*-FPWr$uNZ'}jnHd2Ii6{h/AD{_8AD‚* n۶{d걄RJc3kk|85ZeVJ9NDߙ"T]Xd$jmBv4J!:.F&TEQc=53,Z/0ƀ"µb3e1<{wὗMb>AiP%0fOL EΎ*|-a$#mo<j:% @''l5zTNͱL7/xڱ3 ,E$@҈x?0맿Aw R7{sIg|pWc[ ZTȲ , {YOw+<gw7a`q.O JN'MyttDADZ[x'{|>tl=~~*'=s:rpM̏ewwnqʧ8&֚^o7oĝϞo~LM67܍ENI8N-==6G8uvxO_BTH.1T'ΈH3,*M)C$IbkmJX+Wv#HdoIIa/nL&0@g{R`,|ìz "E7?G' ]H%"Xޕyt҇6מ> o_)޹cnM@A/_OG& K/(oW|/x2,_s@bjC (OKqލ>zUUI: Xk'=OlӴz~=#˸7v*t](<ws X,׾qxQUx={m=V`R rUUp#3'/M;\|=rx0L$I@-?8 [pa07\zݯv.t:b{{?{Sp6vAo\Һ}cUW:"<[uU.X 4Zt,z~|1&/"5wAhQ`?s{m\&ʤN)D@u$H>3n=Www|秵t@D$ u{=I^ 0Ɛ1<,p8Duæ 2EVon{po]EUnm?2۷h6uZ I,ʲDQ hˤ2jäAZ7?=[z_|3/"I=/' t۾ GQUs4nYPv)j,%tr5~ZL6/Sӭ)DCƥ3-EH*Q3ӧptM`CQ5(RŊQ2׊)pWk7.rNs6IF#i4|~; v2%Qr2ږ"N6[;ၻzt͆~O^Ό. a|7DdjU>rbϋo^ 'Pt2ƓZ=mlmmakk ndGo[@z X@5n]2 X__׾5it̕6b1 к'?Ie(1CQg7&C"ŌX4BYI3 1N/xwϔ{mtNʴM%ۡ&a :6xYdgA RvNMҦb 2p+LC8~ܘܩ->Vʲ̬ :7e, I@];-26Xz+ڶoqy$)t:, i7o4ʼnB)%EʮIS]'G9.\2c82}}βL3sttd'˲-wyn~kkkxmLgG 2C IUXĚҹIy9bwwo.]fFXk-b ʲpDC)O U,T pnbtُxv:L&{`G]-obGn^RDxp0̄G?dǎ@W[:JY$DwZ+NGݮdYV'W5x<6j`0pZgi>a<ckk EQ( t7vbb Fd2qnJ%"X[[|>ǵkr썩UNQ9H#ZIiZPB9*˒Z,34MMeNkʲty;"ʒ x뭷p/_JQQT%RX)}u=l9}||}TU7n *^ot|>kmgg[^Cr]U(˒,$IZ4DBc,\6ryt7qe C CX̏w?d{o'Oe`P Z[SCYuRKzNX[J?T!jZKZ oOD佧,)s繋]&^cW/k1~:.]~}6F~eQee:>g~foon ؃&ݜW<_4MITy(ycX se /^$J)z!>kW:VfBR !l^Ń5s J6U t0/O=z(Rp΅f1Á(C@;E9eI! ""c YkaiLXk$/Z4Mig{ ~#}=<Ӳw ٿyexk+kDe~0~\w ǁP), m0XXT(o KPBFVppp4M1F#ܺu zv}&'39<<Čv+&I)icqA9OHk@)nt59%G(eZ׫'Ν;oawoo{xx ->GQc=epek[̑Ͷ>;Y?ۺsnlmX>xUׁc{bm#NOIDATPTK8'ڂ!ŋxAU0%rY. lnnb=u8<1쏾ln>{W322mct EKsZk[ŁzU҂rbzWxHB@K)IJJiSW;"8"v"} 9ֶ^tz O=F9ЬjudUJ)6boϴmkhu?kp6[k?z$kNop옓n&Y?qX2& ʲ@j}|fCYY,3'p8]Eԃ„YVJSw<;/~+IJSZ0$"|Zc2`cc‚zT"R`x\*?갯'"!Q*f6HרX쬭\r+rNMP[k,K[TI*=,)as܌$TI~`X@DN)$%䋅r 4q =sJoq2!ҮE0F6YV}ӷ󓟾䳏?y#SAS:ӧqt:iDH]"\W0s!uuI1WR4_։p“e =h[â)=TX`DXEc s;Ob#J=+U^gOO+_/~_I +Xb+ Q 2D|CDI[f#c|I Hv`l!P/esZ;w`6tP/¡i {Kľp hp :u bV@X#/6WC7D0s'"sFqp){G#H* *y x#"<~u:g`_Dޔ]v?3?2yR9UdIv~&pe¾ z)" 3AZQ~0=5}^[ӥJW #ɬD@pu?xS̡qNLjLӹBƳW`{ׯtKNE6yM4mMt{ hH;JÙLScnb @B+R`綜LM\j Q:ɓ}^/ WI% <67h\"b{|bU^+- B(Ѳ^ʲdE1S"uHbXXج5~ YA&cQ+I`*U[Uv1DG'?Uqq Dzf<zZ$\D<%IU`Yjezҏ;7˿%{+X ~+Iu2Cf` .󣧼sT9gzѬv\+*Kb:"&3@-j>63T"Q4ꇨzktb:6mL'SHhM\>@f4:ٽ[˳Lݣ:v6/L2ZB+PygW/QhSsWt` 7g^No燮);K6Rie1!@ei\ue8XZnh 2lm7<ow6^O2z|Ջ.{G>W#O==|#(qm^e3gpox ]UU j3Ri^ @9uR>6M'991|1u׿қ~byε^;qæ!KϢ)?~^gFͅ%99<<=)y%VV[}<')f ,DD2b;{k~Oϒޣ{:]Hӹ.N%,&:Qb2]VDT<)ޅtΎ޼?ߛϯO1ǒtܹs.} ZTUQG76{)\l "Fg-wM2Fٳg{w5$$.tzxGR xx뱟M?:( 5 Ϊ59mb\!Vf(bPL@Ō¡,KiHI#UPv6^|vNlk;ɫ)]EPJf4u`V=_\Q"4QۆS,WhvI$ ryM3ѫO8KP +q5$\ϼ/MY@ZNYDH/g\5#:ZIA&R8?s)B` T^VM+(V}kOha"(^kndu! B::\;Ig#HBnMD eb6wG?/qj8@~ &pBDR~O){eHQ, "eE{d 6iDC+dր}aQZ2ڠeVAT޹RT6?OMKVT9.*Z 2f7]FH4j\%, 0YXDLLd}tEu_^E- X 4Wke6i #֧C/PCtbQՆl$RJͤ#k ra6V|XɶdG(Baa;֚U䈨rU̾FmEpFQ:tKQ`CK294!͐Yڴ[ ƖjXAQΔxR7q-ìagZۀ#uUm[ IZ,GTѤmUL<*E{3m ?Tպ*7kʞ.o. Q&ǺbEmץKk#D-նj@o jQ}ˆ}rpl>cCd(1RSi6"A$僷o)|$,zpiu-;gODi_@S @^qn{k{£xCI.Luq8qzeZaNHcilvQQT Q ʊnJV?ECBZQ`Kb= 5_?h-p{"2BR=.NBRB;_WƉ(#ggXKc(T5"6 :h6Z&%2 G>)rV>yNO KLn4:~aQKr'@trĴq`'hlqں ,c[{=$:XnIתϫ=չh?r"}ܷ. `D.lZ4Jim\A{*<4/S["IENDB`PNG  IHDR|<FbKGD pHYs  ~tIME 3][& IDATxyyR޻h  nII(."MYIeGxrI'g|4l%I9L˶d--GlR$sAAhUս?nuus ~ :g(=oψ;301 J7$"B2 ,YPJ)0HPJ !dfOHe966&VVVDǢZ "dr FFFh4$(rR1jN)es8vguN)7f3 !,p9Z9鞡H7>k{;j3 0aז" `3E:y $`fQDDZk$!֒sN>J)ᜓB)$ `09Y4ƈJBJ)a!"R,D[kZ MDV"s&ISvRJGDhrΉ l6Rr4 !ZGƘ@{ИۍGzk )bX"3LD$9`RX'@5I)RXk1s"TXkRJA IVj%)R)%sjɱ1tVH)R*W+K#"[]ٴJ)+RJ"sg$l"ZkVJYf&9fvXJ,tRJ@Xk9Iˤ_CHQK i78]2Ξ1?Gp>W~v;QrI"7H: 9G"۴֤ ktRA$IViI"Ra(Z&[ 0^J) gur !A`Xc)BY-3[McsI)Z !VI'`K1l%: ͮz:2ha7' ʛTw|_%@x{ nnD'9'Za$9VBi֖SnW&I"hkR H)8R !qb1j[PŹ=vlc\$.SߩqfvD`q[|aK䀟)o3 ^ ._"$D @fKo{EOߕM&'$ \&PJ81Fc$ TTq+VJi: C5 j VI)wBZ"jլ s.adtt4Z((L$mYk;nZyKq18;fNKQțl'K6C za^{i+~p΂ك2!=fڞ{B{Iv{_@J)DR3岎X3sR77zbbD/* @:$RIswDI$ QJ%^/+J2 ($I Jl֘dDJ EQ{^m 2}ol Sv.vK?1tbع:2V lkIy j=>R)A TE8֥Rrq@)!tEAtRV9U.WdJ)T9g1{LVR*(zǃ  q'2)J& ll9. C! !\$@l/96 6Ӄl<(yjgfw{g^]YЙ47yK:R"Ko[EsޭRV$I`08AR 8(Bj5l{ Վn4  !L$ Ø"u455>"z(caa$Im;^:"z."!S^J. vk8f6t'2{d٤盆f9νpjjy% MD:73ttx6QDz!DW^8Ux3Ŵ9TT JR()EXV8C)e 1ajeJ2*x=\.G~ooN30ƨZj08k ЖJ% nDZs1Rϼh=x9Qs޹W&fN&EŶ%}7j@][x@SYQ@Dav B' AHJËR ZHk Ip0藄aV+q*ʥRjť{kZ&J*tI7>^3ͦ jcrxxHceSqArlv]v9I8˝ xrq'Dvafq6yUٳxT ga4)V̬H3s@D!3a~."EMS'(/M*P9m A\.jհAihQNSZWHp>Ik4G `x.U˺ʠVվR`0Zk944I4nfjAz-f&c[~"Heҏu)'Fv{cjx PPb0<,^ڷYZR "v8 fQWƎ3]!R9'Jk{ߘ6$RR"!0;]q>3 *ZqmrBƇEXj\BJQEؒ^O:̑_^ КzRp21=ollÇÇ"<6:Zkz]) 8=?PxdK7.\ticQ!=A6tqԾG.7y O?],]ێ IRw ]n]}` $I`!֧n;N7ݪmA ۖ{%fgp k+?RC7Ί`||fxh/yX?"T5ll=vibw3exQʋnK։IC0c` qpQQ40"lܼ\;vMz2rZmMS6v۝:>ɕ]E)fF2X/'Q[%q,@#JO HeҪR 80= 6lo={g>$I0>>f+d=ֱi((X*h0VZjD,t#ja@\<.VԌaw(&&*(PǸAe};6 튐%=<KT^SjulPH 64h׺[<3~~iGΏ B#vD@)pm5wgRR\QTV9)RB)EJ *Jh4ra ; b-c}e%$I֖eI xGnq\n:]ưQF(y B2?~B/-W<@8z&Չ?<>tÇLyx(pJ'rE82|[8sNvZXkc@!0DT!cl5,^Reꡫ2h3sn^@D4B!@)5{C *-WʦRRdv0]XXB8066^$E 6hlM[gall V v<0}H7_|@Z$Iq;>1t]h?3[k]$6ߍ697cyf2%&W^󓇿C2v82Lq>53!>qd y:QUA{n Ϲs:9=O dX(xaۘyԃs4%!c"(}"Irц+"R ]xv}F{ﴻIRJ[)7PT%@ | )OW\P 8ڐRbxxO?40Djj=!+<9FRx1̊ z}2Kψy*+R2nMMF0et:8]9t[Z=?ij/R9al)/5T "atyTtTQ""p!@w$9kk=1U T*𐙘1CC19-FAlpA\;V;0\X]u4 O$R459Dp$I9ǽS2J666R9! )='́sM_kAa(._7CaVղR?>~s'oˠC!E]_*yX7waC3/(Ps|rrkk, "+0B@sd*+ޜ4lVoâѨ Pj0ٳ׬_52VWW1VRhs=9,Ql+;='Ob||qpwژT*Ã?"2-rP dDŽxߎfAwq0[hӎ̨`_xWqA8Ur'.i69)%gyTq#AeXf|[h\F;K 'yg}nV /SRDVF$IjwJoxRMLхݤUy4#"֚2W*.JirVKZ-tu6]{ZE(bciȤ췤跀0ٱ5nJa\!">}CCCz| Hh 㜳J&'"  v)%.^"֊0t iEۥK>Z^QI,2aa.\Ilnn"c45)jpd;HbxXc-j7044Cg±vieý8mf, "4J)aR0dzn'JJ%Z-S]weLOOW^:JzQEIǖl5imr||p333hZ8}4~ -ԭ]$h X[[R [[[9jL!gΦIw6aO,n?2 !v_?;X|+D"/ ztB34ɛ2A cXk -k<$LI)ZkaBk11z= PJ%#4q`` M^̙3G>_X]0Vᐢ(FZQʜ$B |iȭ-!I]2 {JuRc]ckmbb?T  cJ.ΛDsβsYH\"3RDž h4h4믣۬V^EWgʾf&ԼG-/o򯳲$bf*Teexx r9KsP7f֤DD9R4 `00ˤZZ`(QY)UJY"sss8x ^,,,k:ȕJ%)?;&; 6* N8gᤪ*%V \o0jʃ("k(`0@`0rǰE,pwY2~״XƑ[[|-alll@J8n52\p{'&#yI^c~~>o;Pkxk4aFfhI)Ev~Y5s6-`=qcOfM n6.$jv(oPPNR Jܫf0R!cμ\㘍18Zqֻ :#"gɺ:n1 022It#mֺ^,7;Z)CDԏh4p)jvf߽Pz,MD#((IR)dc t\^UZi3!AL O4%U CJ~cL&R_E <@.`nfTE^5 !`ʛ JgR=(QdAǔAD"5R ff5g`gGNk0 ib| yw jWkg%x,RW_;Nr!ъ0]}2{qb ȸl2#Vv 8v,qz| d2!,#X__GhZX]]Eĵk ,V{}⎓zT* AJi Ðk_Jecc j<DQ S .\.]LoM]D9 IDATW{xC <:4OW]fK_`4in2²ˠRl=&14=6k-6|C,^j"3%ϼKX%ߨ ph!Ԥ6Au:0m("fN={BH2C:g12 @ZMDlevqyTUO4~w~k&(OJN) w:\5v.C@$R$Rypᱱ=Y|6yq޶}l,DRN8s:W/)AX IuySh͗W6¹iDQ  Xⶣ69LIk-&*Jʬ`+2*B(lPcX󘙙q _[MtcY+&")j A֚6Rᣛ0_0 9'$qDǸtXXXH#lk B#uZ&.4pVvM&˼zlllXñ(,HwE+*RQRnql5Z yld! UTt*cBdqfV8m9-*MO)眤f]fId1(B$ؿ?qyh.E{67I70L8cavYX㜳HI)Er0IFޕ%"j5z%8#w`;Y?;e9",8 $`jzz2~)D"lܡ tlߋEё[ #!*]R8[{v EAsVRJ-9¤dBDٔ:qj5p$*bPb"a R%䒄""gNA1FH)u4I.|E_9of^@h&IB9Z !6e=O?vn[Y>Kn "uT7]kv`H)SIan}N'J}ov8j[Yգ#tc]X;lL[x;-T1 06['+F9v(Xӝ/~-0}Ybv +X|+4<%\g+" 2]S#%7NAwD^յX&e0$|iTiOݩ#E 6y_;Fӎ#Focډvk_)\~333x⣏ן;7ުPC&[*Wߙ:vu;HL5g a:!윴p۫DdVMe1V NӲ 9X9""J\*@V)gN F %ưp Sv{y133xDG_w0oB5># Uy-=GJ !N)|,7NA9pE,*{}p7XJIZ ;JϼLdד?2Gj'"H2'\8*:*/ycz2OZa߾}N=sA-9יq曷K > Y2c '&tje҉Gۋl=p֊-{(g:n[M'K5k  ځvpMp%+kc89t(mϛΊ1xv6@9g`G_|}{*brr?W/*co*}cJAedKjگ-?xV5cPzxQ-- 4TfFF|EYSuҥ QΡ{۫t`G!/c8sH*("ӿuq "j9j+O.K]o>Қxyj7pjr{%a$))yRWou'޿?N8~o_~=}TzGۉ|w_>td_fk EQ^GJR#_`{$YkN̈Jպsftۇ|n$-w_66^BEg8Zq63|t~DwXeR5⋩:3`Hh$@Bjo+++ڂ\8rյ2]>z++;g ,;k7fzm | I$5H~g˓$T@B`ktLJ)cnAX_, lβs6*N޿6n갋lVNE[[/M5`BJwN=s2h4y@DoK䤚] Y&=l/S*6F^uXRsɛ{_Gjm_CCC{_?7-66wBrsoZݫ3.?7| qĉ`;+OupK ;'lҪGg.m-aii gΜH)9G.~xX/ AЕ|; /ot#i}3͘s7FT^P2;zk.Sr+"wѤ02s]vܽ:ݜӶ ;ɷ̹oFOc70 G7IZ}_m j ymvCn ȃ婻|$ YJty6n^lw'k zE=Soߌx84t<}1 L ǽrx^ʪ{.^kNMp_|#??=gg^}!w-م Aa ݱ\o.pCs4և=pߝlڃrj_S>Ι&΄7, eD6_[tzfs}G@)clxX_zm3x]II722"^|E54[8'pfCp87k/_O][FFFpƙr?>pCs~pqsk3l]%&'(߫( {}reÇرc9[<|[:2<::={EJA\9vVX:R& >3s·' h~SY҃ړ/DaxvC{б6_}3<:Q{ "#8"H3ν4ˣ14$HPBW}8Y<;#V_QoL7MoJ÷ їzGK%zd c쬞c>4qW(|htvy-[k|<qrn醟bfSXh7Wgc8nύp}= 6{Kb wk8 ߉{_t&>OĦ>5c^_>>[X\g?C2c{8 -["NfXf~y]?R` D9<$*Ķ~!MIP"ɅȑCڙ[=zCO|K,;v_z{L8|+|KkSMa'Jsмpfggl0lw?ߏ74::jatDB;_Fo]6ͳ$T2:_#&[+BW{גdnueHWt(rxgfvfNCyܹ-% 2v04}Z#fIo4|twNNԶ;{'DyYZo]|muu6m]N]V;+m|' \x6o6GJT…D{N['?x+kWN}_r? xjr~fj /p6ø嶻֖6^9o7nm,:u _{=K/ ,w׾,t㵻=\΀p=x֯~W3W̅{ELXO;Ii[=X; wqm&l/KSE3d5ŊkDD>(Ŀhёڑ?vssssssʏ:1ȍrƒNOCD=7n?ohhw'+Ņ7 $gJ_",}ݺq BVn:PJBLcn޽һb!L~ 7܀oyˡ'_ϣl[>U"Vp,cw`3Oxe Ŷ΋狳PC](tx4@J wk{k@s7O~xOGFi}wsK?O:{?'3KU=xgߛu\z }gRd9tQ]f{[4փWިҳ= v_Anyfk-st;:GFbqʖ;,_h]vNG; f~4<wn3nvڙ6i{"p栃RQOX+W.L,f'D ypu}dg~xQ"dm˧kG%UVK teLے.n};oqNɱ_i^CgdDM&z{d8g_~`tW zǏG7b>6O=n]4S:R1?O>$o};b&=0ov4O<ϞtIb_Ї>/W6O?oɻ`vıZ4 l5+i_S¿ U':J/}񡻟//?:77fj/ث//W>Zyjvv?yO+~G{{?.?PqmC?H}Z<|S9;/}wf[[wSE^<_Js9gJ~/S9 U d_o=r{>+ܬ>}c˟v7y_o%|w@l/mտczal_O}Eh J/?CnsKnN ?/h;zgSɩ$3GyɏA^oJPepG=яW>~+=p}s{~"BbO<H㧮N/f b݅@m7杤)ʌ<ȥ́=,C-ώo,\Yz)*@x콿coyga|l_~%Co=qΗ/q13zqxpn>lv|7[/^cMuh;Q*0 )Xy~_ 5#/bIENDB`PNG  IHDRY(uVbKGD pHYs  d_tIMEaIDATxyEu?{[^{gaAȸ hDL4bhrb≉%'q9!:*r-*Tf{o~^={noݺ[U+:@SOˀux1|鯀W%(z[ljTAֺ]/-x)k,K0lo\0e@ֽXOu8bYض-R `]ՄcV\,T*u @RV% ö@9wfa(Am۝nB`sl+*.m۽ӱ@D/`Q$?Fv=V,f``@|y0u^~-qm+?̵)㺮J%)˝`#x&Ň ,PJ$ Ity cqƐ)eq4޾'/p,HY2GAD?n1Z뮵iR3',#cR,,,t[[+=-@{-Yh4,kq:v6 C,K(N`x9tthZaz滜\C DZ-@O=:lm䶜_<)O𱀍k|IIzCVk csn/X-V'Xvfnc:AN>mM뺝a6k8PpΣ~Z=O֗JyYGE=0Wstv?<)𴖪mZMJZ-K°S\a-@tc7J}@'uǑ@kPHhY2\**s󧘬QL>XL8^.KMkqAGkXTD%Eۑ- eɦ0x*%˒s] `4/Y7ԴdY)%!H_(#0o^M{ HJl}v9ђc+vVZl/eSzϓ+Ȁ/g1!;Y[s٪[l"Yﺲ- eXkgi-O+o`ˁ>ˢdېeiM&8>g1SsCt`Ps]$IEO)Z"႙^ 7-Ԧ"xi:ql$IWmSm=Pk*jQ 1%-0N>M4p F *6@kʶ$acrz:AM/J1:H!Tfej6+r'0v#*똰n2 OeeT&NVVLΒ& q08hRF(Iy&|.\P,6>&N)3Y<zjů51mӯ59aZcg[-Z-ET8h6RL% JQ<,EZ40y&E!%iZV~/y<'=eL(+J8XIBui8? 'pbrN a XD2f 2l)XRdiJI5oCPIS&8$<gk.:&'30#Aㄲ>@)3IBͶt]*BI,BIO$]9 |S< L~T RiZ k%BŲPw5cv>JF&4_+1 2l$OS&y&DxH5Ef@B"aH;#ex |?̉0 5>0e.%b}e8hYt8e cjE*eJ=bKizZ' 0#"" (EPR+'.7b6/`F' <ff3V`k`+El2<8ED),&z*1;q=IM5wbnG140[0d <&0(¨SoO)<a˿^{E scq̄vaDkeDi#RU,o#YH)_]ž ];o`->=w0p`@}G?!Ꭹ))e󅂮lJ4 rY0,)q̰VȂ(4 oVhY (2ƣ'vy*fg >L=N41ՕJm!,b?88+Ewt sFs٦W5|N7Mk4خ5Irq|W fMTscq)u e$&i=N~p&KH<z;&S/+bۢ$γ5Wgq%ǘS0Ͽ 8DNϣ*KLITlj)@&\V{)KI};=,,FgnYvl8^hk?I䣏r:~ ͘ cp xi„$_&}'0rMy 6 34p L+,e}sI1o=Czs sRz{`O}K 'OLwHˁٔf0+]Jկ+5&X}wvGL/?s]hNO R;zx<{A>y6RV80/<3m_n\Ʃ#6vOWs1wh ey>nQ o&v#Ô]5:wLz'xË}^{qd9\i- qIB0ar1geXҊ?Ctuږf> A'ؓ(8pU5ߞZ1Qz2qA>&t[+ diNGVccUCYIENDB`PNG  IHDR00WbKGDC pHYs  tIME,t IDATx՚lgz?3;Ǝ 6N &4 K+AO)DR*Ҟt4UI{6Ji \QИ`% vk5`oNlp^լ;}ww _Ӽ~K,WUUHUUXz`7;6|_t),cِ$d2I8+ne3_UUU@uuL&fp|ɥFihhp`ZJ /vaaN0u3WVz^֮]Kuu5NDAA$ &'' b1dYFe1LD)/F&'4455|x^t]GE$Il6Ÿ|2b1V+,t:x<3*#cυ ņ,ΊŘf-O=ǎX,$Id2jp8p\3*(#/\B ˩VFVɨyY]@Eph4jl8iB/12%r "fiUCD]GDMPU`0,vY(Kƣ2^5S6jn͛eTUE4DQT?MNRX嫮9x$RⲢdUDcuBۼ,oª5k`QessTu, ^y_v";{Kj@ MO6AV ,:: ::΅J/tTMG@dzOй# L ̧ylf jd5 UP5cN1L8>x?͛ }>m "iB*咕?MEzQ?~Mww7x<ΓO>{vhnn^x-4Sk&Q$ (YU9Q'?L&balٹs'nUUbLNN299IyyFcjhNk`||d2nG$TU7rE"tuuq{1mvc g# JF3瀜v;VH$®]8z(ìZ Gmm-@In4tlQftvvDr/RQQ (ɓ'w$=XF͇P6UUq8:tÿދ;j_@ϯ4:jg]בDqMyy9H4K(+ /?-eee-/k֬ȑ#v!]q-eJ2,$ vE<9ٹc[u=W>I`]wOuWnabL]B˦d2zX8A:6% @~UU%H`Z$ipEoh6ٲW$w,+++kFAAtR]2),4XA4v:::سg,$.ݘ.jfitt:ͥKf(DQi𣣣8qݻw >EϷ|2ldeeqYnd? g-bii9TQun7,Q]x<9s2Eall`0H(… ȲL*"J144DUu-{}յXl @{{“$EAef3Hdؗbiɉqm~FG- L׭[G:L&ٳg ŖL&C4l6/W,21Y*2D7b1vҏ0 =ˇfF|g#t'p8$h4Jkk+\wXÝWr^HgÌ9qߧt}***ܹjE!G45vg3{65X'?Ƃdl9m9p#"9rm]c=5Tޱ*DL&OO\ʴf<ӞJ*"366F2$LI ǭϟa1a1<ǃa޻V_8Nqy q.BD~opwP^} xhM\.TU%JZDQ@("?^O?X,f}ӧhzaXy5X%t{p=BE /v΋L"KRRRbWbMNgWdwaS|[0/(ιDfW:Onq#m[H`ah?rk-}?8J"9uSYl-=][wέy  p@O8ު:xS_ۊ8M Sz9;/~:^5s9lmkWsCh2!v&pnNYߴr7]:=vq'b:ܳ w)nX/-IENDB`PNG  IHDR00WbKGD pHYs  #utIME2]IDATxytTU?oE FQMAE28?6:tF=8LPqYuDdOa !!;N:#tFf'u;nUݪU[WڻwHIIAQ$IBedYFW>N`0(сf.zɤ A~7v<~}0ٲ'NY9z(f9DeEa7Ij׹{JRt_ 6\SL&BgPg˦Rx^qYOz8 \.Xbyyl(>lMz=^ii&U]ף9?#ф`h02vK"Bf͚e&|̺\YV%YVwb{^ rdp%%R0FRSЅ@\~t]G,Xu={BlڴA[[띍&NZV5MN Bu$'#ɈVʓu& jꠜ2cF\u8<@Q(id2=Vc @5!t F!VkR-d%mGJOY} B'(<ܼ)IŞF.$&B`yN'<9@R^F$i3gܹs1ɶmعs:Ԡr[-vO0lu ERx^4]C`&M 3z}oʉ'hii!==.]EG/~qk~,X&;%oRRUkBeaK`0ӊ{c~\.TUU Koր,%]h!HC2!vrrraTUE4!IB(sۤ22~ UBAlv3*QetM_9KD $a2\ dggGi?-Yu]' 2dQ^pP(f`0EDx`P($yaYTEQfJYmQQ}Yo~lW$d¡0)ӛG*xFuꨬĉb00&dH KC KN>R^^~PдGynˍ/69fΜ-sII6,V҅E?d:]EPC]].1b8fGKK 6%.ڼ]H0`xZq66ijrn޼YHӧ`[vfe/:z8@yy9MI0n8~dY6$IDuj/_kFJJ.Yh2މn%1)}jbM~8;Wz=cǎiv9'gMK/}_Qal,amA߾Q_eʐB9~86TD,~t/G9[ Y]᥶o>=W}0uZZZ'Z9KNxWxBRwg~>cƌaȑL6sń`Ŋ:t$l6,st^5ofs$Il6#114bmk!w<'kkl {׾9gΝ\UUYxq#*lsd[.pkni9p/vǠ d'Jrr2ʇ~HMM ,r[0L> +|>BN>M}}D,d- rSҫ@ PEh… HɺE+>bś`a7ϗ` 5-%Kt:ɡ?]ө(Ann6$q8u#Fk֬AUU r9 W/DKm\nnUԫcMI8t̙3Y3?5FLb?OL$RQEIVW8|!fvI'8r8<:[ 1fwyz2 yKrߧWȑ5m4ƍǘ1ch~3M;Ψ( `0HQQ1}RY+ShR46$$ deg=3]9rF%~OVݞ>p@  yUUs l?b^zGӤd"' s8 C$ց(Jvލϩ9+FT/ѣ\i7qϢ5|3gE3d{n̝;G<w[sl)//ejNQٳyl̯6nܤ~M~=@@_f\ -o.$I)))b֭ᒨ>O~KЧM}]޺[[[ 耾tRF|>}z\\>$%e_}hjjBx6sƘ;-7[ EV:;4i))Qc2gʔ)t̢Jrr2B'O`0駟)333gy駩nG%Z6׵ci\j³9DoE͎j6l#<,-21{IJJKKKc)++-*bJKo`0PV.{BohFRǢ!Kb lځbLNNN BpZ[[v;3fLG4غeKA*lݺuŻn*qjJ00N@XB%!H,!z6l`Ŭ{qF#G1O%t^}UfChJH]zH-[^j' EiC{hn> :#g1Ϯ}Liz?<,"1:y,$%%!I-lc~٘؍e [,fxlw,yy3>TK$^hR\~sK̇TEy&w¬4͐zܻӽiB%;x`\>h] ` _Uϙ3֭; U[87V&SUk:zqdt3X*7\wxa}}l)ڻCFPy{ SxKa_#/YKRmN2Q Ǝ݈s Ic<4QoƄ# ֮]u/\#j.zpUAط=ljտ=.lݻwۆMKJҌF}ҿJϪ;(?~62]!ر=kLYB2T6p³+gi8N[z:{OjIMMEUۣϹ3!%|<[{ޓ)yv*rQC/ 45Ě5e|i>z*5=( 3{)C|\iӼ 5?p:HۣQ vOզ`+|Gfb0ihhMMM -aO5k5:}owϺI$e$k[Ý- Iydr–ܗFN1W׿vi'{7ouFJƍǎ#9IENDB`PNG  IHDR00WbKGD pHYs  tIME .IDATx͚{xT?9gf2L23\'! 1`&BADVZZXS.ږgu-E4ADIL[.C.3I~#ҵg}{~> { Ă @@R:?#!{.D`^}N,bWhmmL'x s].ߌ.$9N?HNN=$ q(;T[~ss;]NȺ, ̋$_~@sOkh+BXrHJJꓸbA..]*PĔN1Xt'|r---_K.-WǏa6b#a3[U >|D"bQ__/\.@{5 c%I۷ohgO&O0'I/ER\{3y!D""77W C3 |~˕)/ef~Zp<6;'g'yy.:eEEEt]hTb? ղ%ԑf/rCw\A'zuT]$}pF[yyܯȴ~>=ceQFYa̘1\Yft:{XYW\qExx@4;I f׳\{͵mjE 0L׋s|)//J<ع 8q"-'_~;Cbb"6lSRRumA=Xli(,zԄ٬֡ǚq-i&q^L4~^2h Q[[+bժUBUvG_usRQLg,gSIyK}r^)Q_ܮCUZZS@iiOƍǚ5kn &`4Xx1ӧOgǎb14MCCIx<466\m1{ߌ"QWC r^o#61ˊt7 3?]iŝwɒ%Ke/^ۻl6n.B2l6 N'(Z;Ў9! %\6h0)ރ﷝X '&o0SzdͽwQHߕ{(… DBB}FYYVy{4`0uY8'ӾDB{:o ~ߔZ"uM=HJk;P.B& dێvMVAab?Og'tH!L#7HLf8jHn&e& 42!p_y l>AdO`\x~$ifslz6u?&5G$r%^xv''))Lc#LqgB+5_ (Lkf%dT x{nSƻ~ ,nY.>#`WsPSc;d6)N',3\6~#`lݺd>@L[_+p ]ǔY??'Lnn.&LogA{bl( &h·00e܈)ƞ&mttttfuvseh\lrss1|.Aç^0bv;֭#5- gZ.C%;;Gyp8$I9U.˸er]yZA; ыk/+\+&9E(gޭ|?l6t] Ӯֵd̙TVVŁ@羣 Z[[D"Hz7-aw@~,Ԁ:e^H@c\2󩏜;х f y߳._Kw uzm!1ъ?( A B|i$!b8Q+ KBL&ƫ|6j>mo:ƌ 0a#pƶyM3g/`m̽:us(![8,!pۘpg3;ZXmM&U4}A74).y ;Nt M1&$XhA kL44uyi:)58O14?0!16ğ|ȼJ˰E ;x5}XGIݰ6󣆎r % 8ouGηDN W뢧Jgt'4#4ʉ&3d#!/?S~%bGH?_}cc`_[cA bdF~Z=KcȨq:&wX>᷿/|ɱ&ofRZSҮ_%/sY^IENDB`PNG  IHDRF0U XbKGD pHYs  #utIME+6IDATxZNJNJ T hpx&b>YMVYOp".D!T$x.vv\t?ɊssbaaA8sWbLlF|&Cbgm9 qY2#h7c"PVt]E5C4<}4}ii ~lL@Uا(Jdz~:3xq]w*dg|c QEI ˗AL*0zjH?sAzMZwww#nn0<;Z  kRv1s(0BMei_TDyeYR䥲R)jT?"W "!fHzc+B =F(Jooo#NWZۨtMD Y@(MU*.nK+sye)xqm& *{z,œd{:j;Ӂh4 MP\]R ]h6a( e *!L ֻwM  Xƽ$MS ף(e_ۖV '0¤Z ۓ$`Xzh_X0ϫUz=P*7KN #&*A .:|ټѐ#()k"B@1pO_;18-meq]`0Ơzi>i`%Q?> djT7w3y1E 97=K6 ll:?E=َ#`sLlnJ9gg}q1#? -7*aླ=?=:L2!+!,&%@BP֊*VXOl]*^ X,*F )K&ɬw{9d. >AO>s?==vNjrsA0W1^|6[иZc:àg竆,ǯwļ/^ !=H|19˗)y1?/o2)q f2{}}QZ:Łv}4'; :#;/Q/Im:6\H[GCz'S:kA>3|uS 񇫮(p\UUH%H$OG(pFy[mI@8F]E0ƐqƁ4Ο?1Du{f%1vfĺub Bs/`ʕX%:kio7 ЃLxJe{t/}rPJSdUW׳4DUfPe bЁ'|8y, l1Φ8L0G2Qk^ 1`캲8>9>|804}FeP Šw`Z!"͛gώo2/+(rV7%!jE"*>=v4^p(<.|q@0!b)><& .[oGbF?ȳx h@E PUBYZM&WEqx%[,X,zBcn--$<U 222Y%Bp- ++ 10ƠvqI)E<ǨhcɊYV:\͓+S 2.խS`[32BQgֵ9FEÑL"XT@0(ati.a|m+܉xiuH\ }=KJvHQ}2,0_T9`X-Xt^P~|WxGnn.E$IE 31Xj,m4wSZ#@)Cyy9qS]Ҧw$} F%ڪj@9B b s:QnqD(;햸јzr=N`0 Ǝ.IBAo[gE9uY[`ЧO$''w Vx Pbhj$vjRzAv@O9j4L&s7W `@8xPǎ-f~ hlRJH嗰X,>|x-5Gw~#Dؔ;٫+QQ y,X@ٳ+`YP_}kׯ,ÇcA:u*&M9sŋBڊkرp8`jl6id%)O0d|dg)3fh8{h4/= WWS3ύ?v{Mfg$%lBv]wh4ʂ 1b`0TZVVF8bv| zZPOf3yfddЕ/@+fN/lAyoz) C4EMǼ9}ň,evL sGUnաAIA]HIIArr2DQ-[P__ގ'@ӡr.\4߿H1;wn/rZC!\js4s>q(>9>~ ."//_ԡC֘SF+ RI&C7ܽ|"NsLף<,YnǦѫiwFQY4Xb1i&E/P(Ģ(|l߾}œN'۵ks[X}}=D"4|s̡-JM( Btٲe]j$I/ 6PHK/kCmnn 0jJx!NDΥLEm!L t^ǠAPQQL 6 fɝV̚UQGOzzZ4… +VOvGpF1 Ɗ.?ǭ{OUw/~ p~`؈%終"vmظqv*AlFQQƎ(b?w4ⳈD"6L p1tM婷Ӆ#4BƑ=fnwOg ''Gk+u B!,^XSk`qPUnv:D^OIf۶m|>Oç<țdBU#c#~۷'OaEz_GYDXVBI@wT0xSq:ueKdm}/\GArOaw}矪IJ_B/NS%Vl6`0 #tSa#`XPέđ/!AӦMRpO 䪘>0ؼy3g 9PXfTjպw4aW9{`\_OgVSCU&jw~BXbO_Hc8pSA &pƝ{h.]*xs8&JۦnWOO\[FkA> Cn2C RaEIm*ꔅ{o "viFl}wJe8@d1}J3˲.ZL=X;@BkD{0baL6Cvŭ{HHx{gVZ`om?:6>46R|R Pm9,{4:xo㈆U})gnw IENDB`PNG  IHDRM<#w7bKGD pHYs  ~tIME%%.IDATx{pWye7vu_Y(%c'5@@! $mVB3t:L败LkAa-D $Ip &1ŒoJZVwIıcYtx4{yϣ=ss?qHچWC\k.B!6oyҒJ.v锦&oV.Nv:@lêwyΦ&: p饧EG:vHgkwO!ѱa +R; lKi*X ,h*:g ea"].pP`{0@N77tnikz2pͦFW,W4l:)B-֫ո tzz*"$1ӛ ÈgbnV櫓P,ǦNݗhB>חo90vb o ?Y>2y22.Qf\o6㢶^m6~X kX=1KD/Q+nGrzE߾}(?;1kllݮi^֞U'Vg5àP)c'wk~?3m?7}=#KHD\ܦ_caS3㥚4#PR7,::qzhH.3h#Ј`K)RƞH]&voJSc&z%m+sHr2 TȝEʪhz~dY|[sl65P7xZO/~~c Vr6 sO= I~{f/~;m(HyϺeiOd'&'Fx^4MGխ'NMm5$QjvнI0TذВg>6aM02bm `N}QH{'.9H*Kw"I"(!疶keWn:174ўJhՀ=LDu #:H9jZe۳lR1O&Jn;-Q43P.Ləɟs.dFZ4ۊ$]lX>OJ౧aP%ڔ@(zv Mn/Ge -KaP, X& Dj]k 2ER l&7Qq&VJN'Wt%C=&*ܾ{׾[}-rWiz*f Bp=p{|xE oニOMsp|L΅grҘFD^/֌Wt`87)"-먙n 'c4榦DчX3He7-Fm\L>]{hi[G[ėg7~]+4FtYa T͠*LE:/(M-<F@PyfR.l0Xv@)pvXy}؂j>шlyY `+ӗSViѶ:7oj%w#{8"EaGV5XP)V6 +(2%a1XOm[ij`D 9R~7OPP51gD[#X,XiZ 'j ް<-m'-!NY9hSsLX4syC=D7n]Kǎs̚B{(B/#Wpd Ej*!W)bnR>Ag{ۚZP%L^!,7w;߳}uXi@z2S!J%ZȒKb<}$ OqMMс o!gX5SlYjQYQja#jnV{a `y|#㔒|ï+$Xir- Y6t$8g zumyw]=Ƣ৿gR2S[AMxs̿C?A^3i`cQe6QR[^L>1IW[/~##(<D}!HG[3Z/ճ1Ո%h$kr|ɤ_OD]gBlf6#ބ[躀h؉JqmȖubMUx`NrCp迟ub=Bؔt!/|t!&ʡxћJo?59Hd:|df~.ULNbZn.&gB !KqPx O&3ĩA??zau|g9ImCNO(-p~xt0hv,Ԓvy#[ۇzmIs5RNQ('3L$A5muRā_i$AfOlU{oVx.Mg(ii~UB+& m#A jMR*T([ZtuۈcZX@`myT+ST!O/cc^7ȭ;"Cwoy㔦]xi:(8T+IPvqۦjCZ%v`rlX-%@tMt-wóc>-Aࣝ[;/u-z~U\;($$͢Ӱۦ/f98 TU`r-n~_UҲz ziQJ,;>)W=^U‚3Q8% $Ӕkӟml|8swH{RH844o;ki|cU"WcIM̶q\t8+#R`٫G][Co?x- A1mlgKW5yRj8e^jZʛ5!m0Mïk64MD~˽Fi6m}n_ ք4 Dӹ]z82|׌BU*%&XQ'֏2?Z/uŰg-f@mߧ,*4\Ɗ'+6H%,Vu2`>PX/T+7xLZoߎb +9603o 4ai]KtD+"*؎D?X5gu K6/XRӰb*`W6B J#,2#d(wOW5 >Ux;|NLp˲F3w>%*洶+>t@Xǁ %.MKq}>{xw@wkY*ʟѹyR?O}ʽd5>OS{mq3v5 DcjZVh)P@x"bT,HucWnzA@ ]sT0[ݻwOl [4zmXC<"jDl˂eL0L , L@iBMk7)!֊\F-#|zP](Um_.s\`鷟7Da>B2eF0D4 R A*0f՘ }gR#P<9W++vHCoۍ/z 9G T@Jh)]̊yUA2] Z8V4& "rʦw4NHޑ0#l?BW i6dRR($ *#8B`%>~P1PB@ hs̞L`cm҈g"6S5Mn!xi7%LBpE܌Zv/>|+a͊I}~~6Zr3h D%wK PB.)s:C}a[|(/y?e\,Cu$Dcq$$$ ~~yU2^+[Rig>QKQ92EDUx"4RT WAwPTU G@ivЖO+mh%K-hD"FGG]}SO=ŪUXz5n>l&barG9b)-6bQ(#Q*2'AȺFJBD)B>*-EL&tpvmؼ@g. _ࡇRUk===H)innfѢEBi>ϰ-=8U5 Bf#E*%5l4Kuɕ̋Hɱ9P(<^P>^>G_C >j%`(ޅ,8sޓ~ׄBT%Z_yHH rr r.Z,*3:8';ue 7'+m6V\!8hmXeUDd_:@jY/OҎ9p!/8=d#< @+#ABIr&rD.otM.3r=z982-(PZa&~v|Te bbhAiR?jZXjqqF>tMV`ݺu*?22B*4MHXE8[͛7hկ~rZnvnai&x_-R}6q [?DK p:PN~g qjp$V)$nI[{R_~D[uVjjj{Y7>c0tc*/u˼W}+HOї=hmcG$j#ض#m J{ScJ)ٷoߔg^{5|M˦Mp]wJi}v9"_hPWWǖ-[x9z(7|37xvgرc!R]:c $[Re + ڈ GׇGi@y9 .T؛2hv'4:;;Hy䑉O}S;߲.AߎV"nGR8ሴ2f ˎ7XT5Ê9s|n.N ̸!)tPޔ\Ɏ;YP(Lt%\DEńx0+jPDۃ^?l̡KEc䚑Ja(#47T77M6$=&% ]/=3q$>o<}r8p(yϰJͥg}2v%S]S3<<̜.EU5,Fжt]ܮvCBvXuxƅ# L]WtW^BFry=C \T ;v}u?YV[Y\/~v/՗=i0`^S eJtAy eZ Ղ#"6f*dFΐ-,Ae{W!cb];h];SUpn#@wcS}YZ);ibG_\cOƪ[I‹H4`TG)R#%DI.Df|/?:bXT~zJ}?in sdj0~?!a^kTyxp%qJhQJFJ 1 dd-88&SO1Ih0z7A};p?0c.x6шI}L-p'K3xlj}جS}q̭ªwU HR!BXdksW)7?˷9GGsٝ=~R.uy"Dy)$xZ"VH(ePRR,9J N,g=t<i-oO`wgE00zy7 ν MLvAK?Ґhm ,'% _ iGQEyp=WL'M RW/ŷ 1W8gb#+j9 @ǡQ2hJ!=d,)<{<'([~sgǯ`A{v\h9<~*GaaDXM3򓀅 $xq'!߃ ח>wv>XfBbH&i-aq젹?M&nqx)O` =eVД^?е1wlu9']#3|`ጜ7WF([}Y?eIENDB`PNG  IHDR00WbKGD pHYs  ~tIME- UIDATx]O0i  K,!υH`hYd Phi{^zfRflvЇ4Y߳(Pd}O@Y{Ä#לNd2`<~PCRbXX5rZBtf4ɹJl@k=Q,^("^>;x\ϖr  据Zy~y1{4M[ooo~IjՌlr c|ֺ8̒BQ詔2vAD,!GH)eYZz(Q1fo!cGnTJ )=˲õ2q9Tu/|N`6ȹ}݅f_V0H!^__1.O?1fRX.{yy2Gh$ }=h4EH97 /2 "2dY6֭*v5777&BjQ/YFjVJ)Q=tu@W]ROF;dYJ^ ;c ZƘ El^:}o%6I*|1+te-Ȫ zNy=3I0ֽ]}C9 "h:VZ.ʲ 8ө ù~e$.é/\N')7oPB \__[<8C$nd^yeM撝,z(CF{IENDB`PNG  IHDRw4HbKGDC pHYs  ~tIME.jÞ4IDATx[{\y}]0 QU$d*j--سIJ𚇌0BDPؖ!&M*ZA"{&0ڞ8̽w̆lIG3sw tE]tE]t@Z4.a ` <@a;1.< 5i Wrq7 )g\򥭄nM%탻'h^~V(9 W>n~fC. o]3 y+|LJ2G-la~9Zj#ʗNq0 I2&Ra%](p47D"*PDDNC ): W@4(^6VxΠ$ N^{:ݎf~" +%"kO4(\54jPx PPnʩdL+>,JR d҅M4//T&m7,AmL7u`M HK vk:Q43tX0(R3'04T!~|.3],-HX :#YMh `y۩8ehSM,S qK259Ӊ`;ZAWf`AEx1/G05CPy/ A/ C;[mICL$̦H?lot$L&ß4p;=6cuܡ}ӒtoDoO?/SC,y/HgH9Ip jo?0΋M]dƑ xti@e4Ƈt|ha=4FG:@RДIUz-#,a2kc\CC.LWWH"9Apu >ZY8QO9jZFa@| meC3ifL]X-Xglq% bvAl_ ICqCk\73o `h54S,c/>/)XF6H+|H',d ݋ej5_bo\VF$24!v:/pB,] iHFhxK0;,g)A+<;= I˄T+- kktԡmpzy___8?:owYKiTƅׇ /<o಺78jR_&!{(d0 (dZ2AFQY+ y Dth֣K8LxzWg̿?]ܰAF2 s@ب>>xK`Va^zͧz5} ~m 0(YKىgFLBT/Q$V.c~^rikrO\;ո?ώd/%$#:1?6$ʤEh;U5/uxBbv %R26B6 yab.5:Ǟ;?}ȇ}*_B>_ƱsQڐ;LJ #wJ1w,]Rz83.o` w7j%fݱ岝:y 4VosySە**4&J<{ww Odޟɓ_}ݟbth|[R>p.Qxp޺+%{Ml+јaG>[ޞbrXTQ*وȮsE8Ugzc' UBg`N/TRCc_ܙJ͞(fQ_/W(T~^X+?Շ$QS;67w4reX=ۀ }(ӭU-< H| Ax@l3:Khp]G(%3e۝yp[_fYR?h@MP;^,a|3zjܡ1 r_YӍUl\TdZlQ V;_JJ ,#{@kAʗjdȗ\6@?`ത~ hsr`#@ 1Fwbgof?d; X^q] +=^ _U 袋.袋6X"7GƶH0 |2r"75BQsS3*#||' ` ?L f C  ţ߯ sB&GsSgw5< ~zj]nj IoK P d8E5) od ,.0#cSw<759+e[yӹӒݒ bўER֡4/IqڦwZ&.K=C;Ġ$gsS4ߛ!d5Y@m^Xey&FQ=#cқX&z)X¥{@NfM=9tA(hGkI"fDnj24C5V !\4hfK=@rekplWjoMGεDiJ{AO~/,՛ޕ͙MMNFC&&wE j׵o]tE]tE]tE]tEmIENDB`PNG  IHDRw4HbKGDC pHYs  ~tIME-'L#IDATx[yy@"v9pS)8xWWR&!ApڎsP1HV` s¥,]W@#0v VAؕ 8夒蜝tϼ9V̯{} 1C 1C qR i E(@H (! Bև6OB9@yCԡ|wB/}zl)Cw~߸ JN)/M[nhا$PȰ7woF(R0͓ lo@ߢk?O<P'GR@π!iLq0#N)C `U@塂IB|B" z:P@ jPA} JAB/Bx(d>xehZlJ) 啡|wAYPsc6W BI9@AU( p P~a8I(Ɂ H ~ Kf}FIT~-TLv'~M9`!T@g7p0P~} 7@pƦ['A肬W|a?eo`BoPS\zӋ*QH:}@6~M9@풵ec G ʤ8K0>m[3ic.0%lSl?z|EmZ8à"gp!`r6F~D)ئLZ0S8p, �Y `0yߥ8cK!:`R 4lJskEldi rFXn dG꜋\QWbO>&mX❛|`:`rLX9nZbv$cw#@wV9Hƪlf=>؏m,8wSwO:\M)l~Uƪ,(++*ۀc 8fIȚ580Hp-eI4[;lG9bpd#/}m峦*ƈmLdo= @:@VМMUV9V~uc#c '\)L**S c 8+(5);dpCSG6mƒ)8,˂)xlceC0,ڦdiaG `sA>3a 3}%t a ϙss\wWY2ڕ >m)8E+-XjM7?ʇh6_)F6 o`|gJxFn5e$bLцeL{׮|By3YBƶr\T;=`9s "}Bˏddsi6thdzߗƈō`  7Y|tt4^l۝rIaqBǓp2S`UƆc~n"-hLO: 90~A'XKiL~U#DAT^isաW*$kPHAu||M51Y0E j}b֍H n"RNm>+M^a̢ Ƨi A![hrѥ2AAQ Y+y5h:Htцnm: A ~|q-~eI@{b(AzK. =TxsY"G.tӹ?-Rݰ_W0}|/f7|l. Sp'"^*-OKC:Ĵm:\ $@*' #ܶ/ܵL5D Q(F&^;~RzeRY[>O@uFH4P/G4ci4B ii2=u#-ZD0pihfxM>2SyҠǯ/eCiyXc&3a\,S }\sCV!DOl\P`עZڂK%1#&P?-wu) ws\_#d4EfyT:]w}sƵnԺugB &p:DL sR-a,!]WciÏ5HjD|HdcxA2HPe Ds҂KOeԐD/"TAr,$5oNXsGc:#4GF|Zbb8/OǙ 眦޷ƪbDTfKf.-pc#cP -txi8Hz4hԴus/+*hF;{̸@ hm6N>Frn3Y;0H]ޛƐY5S i=z2+I_Xڿ0X#~lKYu> 0H|_U=+uu nS!AirV jB)C׾WځxRCG|xhmfˊKx^Q|?EJsku~w})~uk VܱUr;7:[ ?DIfގKocgk?OOІn R)r.QdpMfoZuh^}ڧ{U.מ.WjT\T*.42%&zz݃0$Z_.UpTLا-t_6o^\ek.(:DP_3K^?Xěחݬ?mCjCpU?)t[', XR/.MqF'C]3dگ]lhׯΚ\?|©RC vX/OLw'M^QuH.RJLls#T1BQ! @0B*R0 ܸ{Oɚȸ!8'7W8 ܱ\pd]:޷ٯ~&H}qxyS*R.U]J)0JQ/&HۄT*eMA(a $ ή`p )n|]7 @TwfmYB J`]|߈jvAE)?^иE*&`)%P!jZ ~B)@0 W4U JHB:dr9l| j1c$2?GQü;gHS0>rx'宄ۥG+!b![.F|m8?6F`1wHA'uh綧}0;UxOkVۨ"8eS!ɡC)1غ2uJ$:rdRk xBV H|fHMзݍՀE%3g;- ,l/,84VD6wprADLhREIC:?@%X뉉 #+WK$@'EN[@ +ѣ^bc$brR{މ'ɓS'A0`c2+&ӄ"Ad^ܹsu0"KYײDd$\"IɆ"N?H. :2H-@*kkIaF%5&F41^Ѩ&D$I!wlzR&yꕭ#b.K R5/{$iXHGyfor)FJI0ٱQM:ٿޏ@ʎC5-IENDB`PNG  IHDRF /bKGD pHYs  ~tIME9IDATxq _a=  ǰQuI{n.K؜Ehwyc8/Y{) c<#g(Gέ֘gN҅R4 0GJњ-k1>yZck!gu^{o]?9֨ f#Fmh`2bf+zLc̭gѷɆT>>d\4:-zfLS?h;{f9Oij2庀?L3$мͤtApәp)-r>t߃ ? ]dЧb4w|2?榔^gIYZ'9 3!L"fD4vTWEI'$ b VDLFkPJHcSO-X^^yIrIIDH"=̡ыAh݊ E!qHm9y؀`LQW{OĤiR \I^%c΍??Cʼvo7_.5 @ҪYp?|MvoVr1x)Lhb?Incv^IX~eO 3OOLa!@[dF :M2'}"шq0Fa~>ˏ߿[ |x.ҁԒc<}yg_|'y3ECX_@_n"G= ^_>]BT+HHBˑǹX"3}I0зTzQH6rL?kX|ǟ( %8 N49zO߿{,1G0%!rX0|.sw#P!=+8xf$ !FƖ``qF@ k^*J]nk7wN1NpcL%H"Q^_ ,//GMqSgwD9TTuDAfڭڗ[R $Yʲ В$W q à0a a  IS &-5Bj n ̦`]qLfk#җ5d!EJrB&F~xR k8" D;W FkڌP6%D(BJ C9 pq  ̰Fv,cjR` E$F:`Գ|ev/݈W= wﺍ7\!pM c]BZcO T HCDB85%^#snpq#)$XK!FK͊ؐ "8)\pj%̿ʏCC&cYL'@:AQd2VҺ5Վ^/|J@L')Jc{TqJ(֨* ^%`s(B %~';)9'3X d\}ՐVN^cF9Xj<*مJ:/I|`K)*ϘëuzUG7x[ Y>M X ,% us@J~p%ßj1+Y_^.S6Mtn\Z"Xg52vf+%J¤TTASD:…9k]Lx]ڞcvPsA6sz3RbǪYg@UcķġK/@QVR"vz`zanO<DZ%@zL y ܹp;Xq'(x;Z9y|/yZ5 &Z1crN|dA:sT(#uء+"pEJEx1V"jn>pB$TFEβ6t4LЎn^{S{rujy  >!!xSYEMf v.>4bx!L)B q7.pH+Ȝ%cޘeLQy|ݜdʘ# O4B DZ p$B 8RxrFa@QZ511M;|SsV<笋t#o}92Q 0rΝBεE E)' +,@uhAַt8BN+8CU5.i*fXu(XMY_cc}HgX5aG|w%2G5lR5= އapnq9b:S JFl !S6@0,"X!6ƷFKMO;r%nϳώS1y}g^?ҥST]Bdub,ʭ_%Y *;aX^B;y.UͶ[MJ>G\O+I^9#\Y"1ġ(=N(\ѐqލK;az.%{Ei$ .>M1ʩU+H};1]: *`vXN+Qv+f Ƴ{fހ{ozIsC0ksW8~nG rD 81e^vCx8Gs G:xEa{i;nuMz#$6]DZMVѶ`&q4H@! + bO]Btik20K dw~p3 #?@!!P!+ :n'sd!넡es߾5?n6&ZeH*e]TS8OT&,LϾp j:A%F0&/tD9VjhJM9y󗘮 tCݤfÝ=Փt1H-h` eH*&@˜-ja5%ÄSQ|$[wo}F#|Hau JWBҥQ )~x7޻nDz0zK U$ n. Qx%.!kL2VA$P@h -(Y2U( ȗ%T䉯Gv-4ڷal?P %ZX_UbIxoˇ)DXhHg|ž4kx6I^u 7˷,%k! –%G*Z{Y?u'y@ށTJd+/aNRQ~^wuCCݾVa.A)DG!N8L390t! !%6+/=Ƕ~߆Z~uH&Ƥ*&h27Y;ƅNO,U0N(F@IX0 "G;?O|O瑏~=~8mPW)O plHSxQcDIYJ#O2=x ,:rMoO'3sS5RM 34ӌչ;!"Ux7ƕ8PR8!/|gãFJi.1.JJ*.عmMoxNy0&-AY^<α q0z-*<'K>U~4Bbm ]iUx{׮6_p2#JAH9p$OQdRQ cs8M/~א y¹Ms۷KyP!w7^MmzW;>)*VI'1 Hopu=IDATQuZD$ ,^d0̑lCMVimCO,};;#JG,)<2Д’{lF%HhU[\iI[ϖeE-ߝ28{ϞsJ 笚!}:]1W.` R&9;D8^tƯi0giRYIH)68%&>e[>A{X |=Zx^>,+8GV|>6 :NWQ)Ak_; }uKAW_zKVN/~, IK)H?:IENDB`PNG  IHDRp(&=~bKGD pHYs  d_tIME1&{IDATx{yunަg  0,$aHd)"+)UJRɉrUEcKrJƠIDQffzw;zٻq*U:U^{;{wmK%#v&m!$e <a+,1 dNaʃuZwxlKөCOϾw)htl~^óϴw!Xt뇼_̭sk9:K2p kV+=\` (`B0E"`3Ȉ{ߵ~z(3s]л%DtW:jήޞkKF>R l[v^G/s"önkAEn$ɘ{μ+x=;/9/,<]:"$ڮ)ݦFX |Rbp3X%8")P/2¿\qV3\ {6e bd 9.-C9NX/o)8t %#1;\9g:=?NL('+ QU|F*  BU\0t3T"" _*9B=p m*0)T(ATMfv28 *V)٢H[ŗԙ]!W3** A|E|͟XUkBqCu(D\Pm Ԇk*C{B֩P W` GP*tfQA$P^%9ή4'S0ЙΞt1T(UxTH:GU{cm*Wv?;X ԗōܽX9Bw~2BQ. wu漣VV GEܯC-;Agg?#'v4Lfk=+WP\N>_b~9!JHšB+֗cu2 wcaM~U(YS-WV oY)t,gT8z3[.Τ>x!Ů*02шJT#љ_]>❻˖kV;"h.ׅd䭹LCۮH̓A$%oqաPǢO,Flo({9Z T>eр|gePiHDUST1mgʗ ]:tgG޻=VTEUF>,=Ǣ_¹:o)9f~bKƫGc,) +Vg\ΊtZygЛIs tf@i*p(f2T3FԆigy?;m9 ?n}i o jy@},AD-=GonHћ-%[k5Y;:jLe L^L L#A`J Sח°yּL[0tn`K8K՟WN~q9? `oGMf ƨ@g:CTeJS.j:BZ5#o~B1 ;•g"0efbLau53?,Kdea{ R"O{vޜPOLw[ȻO8mՆ9[JnG1f5z3 8ǶdKBg*E\g+Un>y,z6Ⱥvs8$-"u j":6=[3O yWcs~l}MgkɜFmDt< `YP@L8rO}P$yQ$:0S_B/u:I֤)sLEQd@O(Rܗu̙݃<~Ӧ7t`K""`B\ `bm.%.977w3-*2j#*s@\%t侍Wq&qp&[RmLgp®SVݨ Bڟ8Dj{; B8!ƥV~k7ngy.ɞ13R;#X \ dL 8Bָ'1$P 3𞈈AkF@T^Yv٥Rpp!g O<<$ ԝEZ!3Gp(ĦU#JA@<{v)b.WS}@%d_e*8$g3˚' !`C"{p Nr:1%9¦<}<37VF`LǠ~Ǖp]o= 5ڜ8j%#@#~ @Y@T {[q-$HpHpBC 31xH*B?@.ثxV2ALq%\1Xt{SֺG# Zb7t @W=UIv#'*:Z\^!NzJΝZO)- OGGj;痄J"/ @Zn>=Du ;YCC+k AhT "u`@x  5N KU)5P^g(J'>"y#r΢,v O uKR pQkķcI-wYQ? GWWX&%#f1Ҍn<GUxU:xX] G ` h u<De$@Xn2iK^5}7˫&f ȵNǪԎvRe1>" UxDn8IUj/=k @uqLVlДhE 1/7v+)nۂT +PL$5e"==_&Z?eo eE<7u[eid,)r,2 \oFrlЎh$+"t;F&pGHty"z߱mfYqo-l~ץ66ؔMk*+"hk5ld*{h9pfoapAX  _՞J|f';+H3<|Zi_jeYVơv;3pr0'f&sg8?\B@+(568Gv؆h49a=\4q,oy޹ZSzG4%F?xrr"EԚmd* 0hMy뾧s{}y:9]zzfKTp Īs.8Br`nrǶ_{[9z1XF!A&wϾ\f&BlB?yk$ 4Z6}M6X2ULP'5{r3щ!$s%*XFq dǾήE*Z|7yȫƸ]Vʆ. n:g{!n--g; MκCI5jM06\vgmKB3o~7/ꂻun}@2T=zsG@ߺF?{?;C&w0A@[}x~~O |c:7le00 f$ERi0;/qY\ILYkcRrQxy x>4*I=:=TkZy˟&"䃈:НK#Rĵ՛Y~|kהv1ƲuWgRwf՘5kݙA"*^`0}ֶK韑aBj)EZfG9'E5w)+Dס9!DqL!at:C`XID?(2wrc.o5W+<˕Z{S$fSDN6@dOk@g򱡡fg{gPRAJYI\uT#%1R08 =rpmsT !EW2Fu޽[:۔iE0;z5zOQ cKjts@Lrα CZJ 1<Ɓ}x(k_˙X JUιmyYiYeMPzFQ%<q] +,Z\0}`S{g0<6 )0:ɞ~ w<^>\5V:A\"2%5FM!# lR]U_6ysX|^,Qaģˇ7jwF貨J.q8oAySXiinB,KYc9nQkPoomog MZ n1TvZ^'> BGꖋ7'?ꌿ ~<1(K\YV{RR HX=C;1\PD#飻]nW>uYxa%^;:+rgқn͗/EYhv`˵FOkr.BR\ Oyw ڧL [a½pME'|588<1۵H)8gP}z:8h[}yRk,2H:)q%lpG{=1"C 0ƈ !9?.KVvE Zm~ۏ1}pBJB)Tyg.̒ٚt,fmf`D{58Kq{|3i٤qSeN[8Β qfEiJ ws @Z~ !1@ bGPAB㊴xNgt)-?R! %Q Hq_'(FP6IENDB`PNG  IHDRF0U XbKGD pHYs  ~tIME/mIDATxMlE3aWm vU!\z&6\$.UzRmqE-X7,Fzm !. P a(R3ֻci6vK#gy<¿-ɤR*;w\0 PJZ1j#@vd29"MF˲z )eGmHWEtZommuFmXʼn'Blie0y@yաǘfyM3p< )3XGq2B3gA3l!lv:uTtHcׇIgsse iQ - máǘe %!pڌ:pݪ/f{3S]uw} cjȪTBw !Zk2ݲq<٪C \3TBjL]8u._,//;++^E#D6ۥqc^G., oڨV"&cT ..B&m^zeNܼI¶Ic}1_~!H`}9 qwP11:ֈW^Aݻ!X'g-? zP@F+"lֱ0R13s!Yի%"G&( kע Qݹ\X@LLo&;;;]j){b-b-b-^V[o` @i0X<+#ns<& 28p"8ٺ~#'1-C`maLJuS]w;DO^wy.e$' +!` (+ F <'h"( brŀBt͗~1C=Op$3  c ZNs.%Gz齗NemTД]9cf7^ 4 Nd|]7aNE*q;(l@(J@ܡDR@a":\ lj#IGm[!uM0ʿQʲڑ dxtCƈ{ YŌWGXPcު.n+{$kNyyvC].@h6$ܬD2:`oy 1Qp뙜~yV2KnxƅƩܐ .٦D_5!k@T@`=GIX߈1uJ@lWZ"Hs6HJۯ8aI $pfrYR2T*CT >* drDnyXh\%*ʌe@#$"NڇV@yݘ㖎RY"HQGc @/v66̌eFQC#?7l$ Y^ٞϦfҾ3q ~;-2 7wYuCpެӵc.@z§&/^-c].lŭU* *Թ* KDҘ.y|mOň/DE $ց[~!wOIJ.Y=)˭ԬK: |&w ̌?oŘUO:,+D>Y*A{6v|K SB#D;үd)f%zj_%((nzP=8GT%ZxuMrՒ,V)K.TQ01aOAܺKyշe`5'`6yGՄl=*糉$ 5%5Ë l5o ?H8wSJQsgо+j1wf(w_J.1;+Lh.!Nx&fYiw=\w,yH쀧O9^42FpC)[\w٧QR> n7Eo,+㒼[%~$E3 89LbA/{z|@El}Fxc;gzn]pϞF-ESоo0/EED֦}/ H96-O;<2\Ff0]rYi 0V=Q((P ;qζP}U[q!*C"o'Fn=qp*eR4vqrX3%Gc,bUYXS{R$4fs{P5׮nfz죩O$)#:S|U-,pG&7_,l}N  0 \J$/럸slElCRFs}yP}O4tl8PΦmKC;PeAD\wᓦ!i>Rx@2 ^)inMIrk;0CY&5g\ +e>uΗb{b3(UXC_e~Iu!tf6/졗mJtS o~~f x!e2OlGw5&~oJlAoNٸny=L>qrg"+ )3cjhL1I Ɣ k$Vǎ K]^4+M;mgNȡ{W _ȮGkꮑeCLg4/IA'_HW2N9W3ˬT69X17trO_ݯE&WʺZGZonkn?ؼ!`xyc`t݀RHKC"54THLZ";r B`DBjpkr@tiM $o@Ʉ`)j.\\)?0„$5e^/e57!- =ދ9X<=]>';yfԡccsݱ[V!)P(\/P>6~9#OiGŪdn?~|)&:7ǪV6Q^+*I-i Wʘ䬖Ξ'=dLT(:gk?"e#38sc^_=cT!9Թ*tjYRÌ@ _!y ɾ GLPuK$w)kY+o<h ËURY!Dg qweJY$V˻6׸WAm}ӷ@ Iv"gLV7&/NmIG=^{(r_@In5g' ؊ )mcޘx:sl޸la&DXܛtw87cgv"('ܮѓQ"x3Ocunx r^txLjZ`,Lj LwmDfJE#B,byg۝B%^NۅVSJMȺhsO"NBў@wY[P$x;$~afIq@~Vr#'qG6|VSIWo<92+ԃ?)I hȊ@d<3%Y>>%ƹA) ڇzvB>7CmJ[we3O}hpo{ӱߪö5+9;_D28Dz1!Hf<² -kuJRʷ =Y6\Z$8IFqэ )yBB (STH@EWVXg3J &Hvo}N՛-Pk~) 7Wňܺixe*l}LJڴ?08e+$ D}I9ne /d]`\#y\{!Z1x.ܴak( 19遧dã _J"sGNbQK$Bũ5+#i"/3J+55>>2xiꋵ|! ymT?ms pֽ,JC %"ʉewWmϞ>|`783d}gʢ^ۮ0yr@N9.㾗H,;䧈I+Ug9y&أ!ʞtn_zW@f֔!ޱ?gagwvA}P[͙Ҷs R&)m+,DmɹOe-"=p .ccAHwT+kO:Rר/ neLS~8eF% mǺ2V^j7G~49qB)`%C#Ul'5kOF9QX:7 "-YZB9Qc,99}:go 6í@::NjCh1sI61)sLsxp@j1]fé1UنnTsݵm= J Nꡑ\`Lvd2eE~{HҎ=`4%*¾y*%3yomc( 9tO]r(9yrk>pD\KT&+SwpK[_ LܙGc, ""I)]@$d%t ֞VbYp3f8nýj"(]Gs&sQs\9}KN~t61-e/޵C{Ś=C#1EZ,0PƹC+ReGϓ!%I)ry窒y=O5{&\nIKw|{ ٤$O33>YyICLMIw=ĥЋB BIi!_ɬ8V'˂,"_zYIKxPž:.ݭW#1_~]YѺs 8ތj\p!o{Q3,=[ޮ~t@ 0ë?~I5{ODޱ @Z|EIENDB`PNG  IHDR/~!bKGD pHYs  ~tIME @XS$ ]IDATxAH[%"@LC!lt-bt!؋poE{)o)ҾE[/}b]ڊ ݈}H!^ 5pgKj#Ms\6*T[M0 idn+WFFGW;lQVފ{`[ [9~oi'1cKBeKV* V2F&,̭P* [Lf{E{Ma˻Ko(iDZCVWSH3u4kk_0e سIO/>ǹ1m\[E|Xb!X=eLO!gaM:nbf6TyaFXZ*0D~yxnɺc/u=EjmH32rX[)a\ ׭`it% [YU[|<6kݾQBM#=9u㾬;C|ywȖ;B+! lғgST7TiޗVEJaVn_SkhW02b"I[٬lȊ>Z.+7=9`Iigf3¡}6}@3%߷Q/3\|_= 7|X#~:O<Жb (Ӭ̱ YXBsȹKhGa!ɘSzؙ #ZН <{ɒ\6K ^ X"lz?=J{fH[tYhy8# r;/_2`"#`LɌV`_{I]'g% ĭՐR\T?eTU `*;Fk?ahthM;64Wƕf0^CFVU&@ijɝ>fG)ZPZ fȬVpDaEv"Ų@[sF˵]`šA* LiEYF!Zev"iV`bqaX#é%"[/iF(2L+,T=Dϓ[5Gb+fX2eݕSAdI /GG,>BF7̌/%fImÖ?5Zq%䚏jc ʌj>ck=.# |4jfkw"l^UجR`4,-tiȂc6\hN}.*6jX".fJ`ǥGCVA67!_ҖVFRB"FyP6tc Ze&rj}ظ GHM_}Mpn v)en>6 6WUKHeF!?B+ o:hb8ѳːaLr? 'NzzJusCr?'2x\ C+DG2D2K{%t,D1yuSq+MS=d@+! H1$VV,M,,lGW;adX7*V/_61:7`t`6ɵS wn|FW( e>S-,E"HL.(E dUq; )ʶ[G<a2FޛD?nY8H&[4Wk Ȧdf;uz(5 q%d9(F2]+* ʻ:88+_dL4# nϭb<~۞d6,\t6D[ aY¿7ҥιWo&HYgf36sq%SqNʆv $΋mm<>DyP(}5Ÿ2s^oD/Rr0s<~l;j9^*CeH=f Z^݌5 KvF`<`p@ᣪ=pe@wUc]*J׈0 :/5z$sQ׶|G׶7K ϢRB .K VK#V 9xf .R\_FosTJ} %8.Nv}|"?O~^<싒H{B%Y д-' d2N/t`a9^ڜudž1X)WlR?B k#ʴYWf/H<}0 -x^V*c iStLPM1sH(.ne&u MJq(Yˋq<vXPAl+2їroiAv)B%2rN %jٓt&oC+/15X_yٯ0H7* "Qh)RȁуL3"}qEf8dcP(QH=_`$~H`NX %c?sQr?}V?LocyP(#lEXAw䁲m/Hq/ګqu M& 02ەkXT@YraIͤ 5uN(J` l ZlUm ݜÏV-/Q)L``uJNʠ5++O5t|^nBS(asp (H?h(Hr`@*t(06'I2'hbPmĉJECIMbCRF*\} 7MJAIA؉hXAhgO'J=fHKu@vﲢ}agZzC{oN?eT;+m'{G? I=Pv" >)H8iR;2Hw l`b Ш iLS4jd?V7bӁ"'¯8FE͌>ŗDjJey { [szF9ck,,/~5 p49?S+="*@W+<$/fɐ2H#?))K={>{|-n7H"W ET!BG5B, }vkZp}GQjG7Ny("kSd2ydee$%%02݀s܋~iQbҕ?B?GJ،Nh0oLOы':),,$r.cǒGIvM233])cHs ^`;J(D=,=fw9'6l Fl2jC.2~xr!:Ot zV ^J(YqPዋ;/nV+ټy3Ke=Cדl6oOAr$L[zG,؎BIf%1v!|:]ؓb 4tdŤ8Nŏ?H:23S! g_WF/`,@<:xy^/ERY /V*dns dժUdذaG.]JJJJf#|IDDąr*Ĝg(1p[{- n*g=zFx =]/^sG|yr _[_)\3 C.\HV+os:lٲ{$///Afdffyw}444Z~֬YӵSw(ܡT\"~8_q͊s=5lz}cNIf%6?Ə т.t:aX؈b8qը0 #//h4b஻{wEh:\E_c\s;۰ބnWxxkSy:Ys2b} Ϲ5&7ѳW|?GJ@7{Ztő-P>CCC4hPSV a ?0lirfz[c`[n R\Xݮky=dGק&m yBA֮]4a 5| QT]SY9RDa/[<yل]l"66tl.>Nq Di!q \!i\" 6MD^C6fO1n5K;.ks!eς#:::hn)#999]貟C޷v`=r E;\M%D6\#nfyZ4mٮEpC\F~U`0 yǤˠ{{]Ϸ0Ŋy-ĈK۵m~\r}13x|Aõ*^aRdٶmQW$W/<>6GPB\$sYQHS -@?W 1rȐ!bHA̝;W/Gn[ u8/&AO~+>"5}e.s|̧:{ *J !j?4M7ppo.C wcNyJp![ {;x_p}5{W;)-|[ fTBa1d֬Y#fćT-gB& 9Io5!].ܫ3bk!/AgN!їay Y%7.IP=#G /Xeee=^ᅬt شi>( T5`\yxw{i;7՗&$""sGLY|/E i׊UuͽU.4O92 N4) @V^ý^?&&>}Bw:`VtOw5,6I/b^b+0Ҏoq|xoNؼi3 CT*nEr Ma*MckA/_Qnxޭ`"|/~(BO+x7 a8ndޑ?1X&N^Z@II :FIƐ.cYŘ$ >p}鋼Ý~k`uEA˻}}ju`rΜ9X,^߯lwL67nէ cƌ-"xqap s~ u3Pu #H]˙-*č2DY`$4lprrr}L :UU 4 (wË7#s4fmVϒMXVP7J皃 .C k.6:۾ Z ʘ I&AI='a\.ԩSm6'MW(YSm<<Ÿ)2Up=;Jܸw } I"׭[0 ;vx3>zV "[B.cرRG1p@`>s/k}R()2 a ĺ"*|p aHL!^,WIIP\DQM/6)??QQQ!C7߄nd<EEE>Czz:.]^{kgܹ#tRacΝO4i k- Qd/Ңi2d'11sO?ZGR!>>* C aEEEܖj(((>wizP(xkV wn.fI^/\ٌ>6l`Ĉ*&Fii)jw}erL<ٯDFF2N ~q\S (0x{dE05a8-N_lܸ>`@&D&[o=\zStRkZ֞y>#>>]kаCaDLB]WFZ 3bDs%''Za\k֬ Xh٠AHkkk׮]Kd24yZFb#Gr=^Z?[\%F+6̶>[,OwK"##ZV김FUzߎk ( JsZ' b@ px^*5TYDKXRgy'O$#F&xoU z9jHs{p[e7o}{Qf֯.eۤFPʼnKBIQQq:9v2e { pQQwQC1~.+bƍXr% *tj38a.s FIo} C5<]˭=]5E5E}M- 33^{-SSS1n8L0~cbcc?wuvލ}ѣ5VCCV_>/PxZE^pz9|f|S{KW/GVr?#Q]TNQ}|nׁg{/FPx 6 bbbD7!b:111ht:ƫL&Cff&d2<`&`ࢹwQ]-1"* v٠ N;2dQׯ_'x׹ 666mfϞA0a;""";6[TB.foҷrG;Wz_7,<_[Es\"L]a&MQZZ*Q]LtDJJ y'yE,8wߑ)SRz2`Sg\ _J7툂t]R߀XSOncv;~m|رcoC徰B: K˴݋pEuc}!MTJ_`޼yhiiy`ӦMx nrP—`"ALdv :jJq:.%,}v$HnZVֺ|]Xbxw XܵU[[]g:",4M@vܠ`&iC06ǧjs A}!E###4ijL%Gh4bcܹ>}:sxb9s|xꩧ0 8`T*pB{hu}a)z(!?/Pɒ\? {:ѺAT*߿KELL }ǩ "NW9N^LokCee%G3uTXj%\BewRwW J!V‚ |&?Q{qPZZ{f\״l8|0RRRp󪪪h"M4 sO\---tY@ki'%"#| b  6tsO.gJ 5!۷oJݻws[`9!$::ZpRƼyؽ?~$''dq1'ia<|P5#BݻwGkM@6looz@Akk+L&Sȓ2hmme$ &/sx$_M&/4M85j*M!fڵkC2NI 1??\aaZ[x%tge_D!x\ֆ~{f[dfY[`0:qM22H`P[WK"Hr\~GꫯzN%]7 \͛7w4"\PP@ZZZֻtn{ ^o݅ .E5l޲ƲG($==ݬ/@k4RYY) 6}A 4_o _"R{5Bߏ5557oBA"r pT ci;bG[Gܵ*`}K~uO=v7ػ׿ʳgA3M `k~Km)B +V/6[ФRl߾MMMxeܵ^-Kg@mmmhf;ˮ}Wǿ^ SQF8aԷKP3(~ť1$N2B+%FAqOxX9Zf^ڎIO݀=SBf3^],n*ğFv:A{Zje ŵ͛7:ȁomsX~Z9{#ZP(`Zd@ֵ\ 掼?CvE8y$oXwp'0gB-cz |Cb=~ЯRZZYI-7<}C0p5Mekk+N}X5isGt"suMP( AN\buadݮQg"܌_SSyrk 0Fw\yb,t: xst1 L.?ީ'P6=$L&Ss9½ϟ.k,-d2ꖠPzDiqE"XZ-11@A}G]jBw׎GpB\>Š|_~ƹͽ]X4}Gih5IFѣ]ߴiZ[[7V%Եbi娦BLD3X=B -[/Y,sxadLh/(6hii ߿+c_qШ1A9k| |;hhh-[6B}[r51[dajn[BwS0]#ڪU0gr1|9v1Icr|zt2U 6\Y,οQs)NG\`ۊ#P܉rWDQCG?<[_Ev"np %h/ŀT941",Yri"?@z= dpj%o4oH+W +6`J qޗʰm5cYflDȹwvVt< #|BpF;"uh-]++^Bqmx"jG;w,ƭ## -ڏힹ' 2g_~6B`ub<*J8!_-sFX &P%| n*8 ⷹ|iooNjšMqcL @Tx֓פ}輟e:S)JX!B@v}ݲm@tBoJ.^l\f"hIf dQl~*Rkӧ1$bV S.>y~hRW#)J7YJBZ]u'Nz9>X`#S &j`]7{N ^ZvM qxg5!ZٴN7pX6-S B AY5RT=|@7w%h9qVCk1 pE>> oi <ͭO&LĚWpdjˀE4]wͼNNGТS0N-_M q^=B<' 4CX6P+ 8_ax⦟02[uhn/5#3~WzZ--ZM1boBx}簹ʆY }@7IhwjTa=\hioa2K=(٢>1ow~[ : ˦L!~Gh DG0e'i @淾E{y aEORO?_]\~h7POpWrm傂*(JK#TEMW?M#$ʪSQ<5M2 3鮀5s ZΞ^Z[-m4\aTbF/+iU5 %Kj^mo"fN|ϕ).|<ӮR Wބ/ƛvRH{ڴh7ԠT|)ބ>a#l mne|V-4ikF^mvv ]qc^ÕR Gca8gzK;A{\@T: "~YAz '05(LLToBS 4h68:,-L5 mjhR;0qIlYk8JYahPE„j#>KS(ފ_"| Nmlgh8 TUMvs@ 04 t-m~?Pu)tPN6]^;*TK;8\ewwJP$(;^_sj hGQ]Cv6j%hd*XkC]^8]kЌ01⬑Azk&f&@l,mPI%$Ce lB\bҝ+TrC;jwGj:eZCn  7a2j&JaxVhn;Ya ņXf;f$֣):3-y="3v4 NV'eɢ B 6aP%b+^u6+sZ}n5`q}<gM7C{U *Fu婯)ھG,[ӯyHɞҪЍb?+ P(V"$!C8qvFqf3!5p~`~y&t3BPBIxpɲ)[ChiR̆Bo}2'g:$gY_(0G~"$2')3x/pwX1$qύR)J%65q/ϻ!F!>l98p ㊂hiiASSΜ9Ǐرc0!Lp1, A %X,Maz0w`6]fڵ vvݑNNg7\/3媎Dζ=+P(p `nv(;]>8Nvv8"@̈FhRW"ֿ#ց﨨l`瑈Jg':  Er$*eʷMr].T!ZeX@ۿ#eka32ŕrƾ#X'$v&KFP( BP( BP( BP( BP( BP( BP( B]xIENDB`PNG  IHDRR* ] 7bKGD pHYs  ~tIME'.?IDATxZ{lמ3~8n$$ /RHQhTzҊVuUԊۭ.+{ ڪnhՂ*CۖBx$MpCP<N]i?i$xw~ !u1j4)J<׮]-e˨`5Ǚ̾H,C>q(J,Z eټ!d2 ۍ~ T*ֆw}J&zD"jh48 xnܸDee% <ݱc/_"(J0 #4b npG @{{;Ffc T /^˲PT8.C|<+**022k׮cccGYYٔ$ W^Acc#8C(V 8 z!d2 Qq]B!h4yfΝ;tn޼ ٌ_~ Պ~n, Qq aj#`,! fΝ;F2˲\èÍ7Ve[ҕ. X ۯ] (**0LOkZ,]0 1T*TWWeY֒6f9r^mVB߿Wŋ駟͛(d5H$XnTT===yZGJYQQTTA>;wVtuuȖt\.zIzQt:N'&&&ܟ/ N:88Hzߏ z0 ?ܾ}LF\!Bf}v@SSUTh4FEq! B(H&Bќ|x"xǦM AiII I֬YC֬Y3#[, ٴi֮]+0p8=l"3g+WÊ+CRAնtT '555x<^ tvtvv"JMR QgJ+W$[lG}N,Y7oޔz<$Z-meǹsr/hSO} ܹ!x3R9bYvsd6<ϔs=$W\H$ze!JTHJ)BФv@MM j5X=˲lF@θUœ@w.i֭[iSS(Xbx``@vPoΛK%5xqq1z{{P(RlޑSPS(bݻu .Ȅ9Áى2l6XVP(0 (HRLHJ) e%p_ĎPJ~z\t z>c/qͅ9RP`ժUbE(’%KPSS V hmm%z^v0RDY~=‰'@)Ŷm000 g39REB!'fZ Vp'i؃x###hmm~-( F @"@}}=jjjPVVFy|:tx[%QB Dp˖-`… a4ڒ -R ZZZ "/_ p~F ,@KK ϟOy ʅZ(*&0Ph4p\p a*"L&XRDnvCbll x<à @E)E!BzKt `$@$0/)&2#bF{G;N85cB^{ MMMذa"|>a-j56l>Čt^Ğ}wfFڍa0vcx^5Y , ӃX,χh4!b!JRsbAncS+n7vfFllj'>Dl呑رNHd2c㸣*l,+N~}ϝwUb!>:WqAʸ,ϱ%aeI|Pf3͛݃𖔔ECC011X,6(`. G;v=>!|cpMhlɟRW.\fHbnͲHU.Z$pNs!H  !`||pX9(ZH&hoo  [v$G8~wRn$b2Ν;!tݲ"S7iinn31x'ݍqd,ˆ^xLee%011,ZhZ[`gϞEX\.V޶m z= x!("GE$AUU~?JKKB FP/@__N'~?,X 8^}UiV?[BbppzZ,0 &&&pfz)yO{:[ 7qxP(p8PTTŋ:N~ b֭X~=a>O8yjxX,6_ҥKq!t:_dYCBzJ,J!LSt!zUڵ ---?k  AsMdK:C)e*ʫP(,{V:QE T*7ҩDkxgvػw&B6]_yfP(rzwe? *˾X67EcG)T_ J>XfE3lDZ%'.z'd_e;n k! #t"3""b>g 3X,\3E3^gDs"dŞEB8 Ip/5[:ӝbmkϗ.pw_]8=Dsn971\ ŭsݱgnh"޾&v~IENDB`PNG  IHDR00WbKGD pHYs  tIME #(8 IDATx}l]}?C;1QZYJ01"5RЬgHSV*H`[ U(&hZD*E(JDӎ84/ /羟s9Ng'=ys~}ax8p\.W/ ^>bϞ=P(B FGGڵk B΂s@/po.ݲeKn̏3mM G2˗/?qվ>ض=oB^s< x\H7ig2^}ڵk$&pz/;7B^FB,X0ib IR. B^H.JeA ?Qծ" .(lχ4 $IS2λV}:t˲$i&-Idr{gi\ @.ПY7wtɭvrc=G&$aRY!EAQ `lljy w%9,WPCPtCyg?e``{R)0DQ$I",Bu4M4Ml۾vIOCX#K`yD4DC Wz6|;w"l}4ab6e.z1&&&Xfͼ2]}[}_bìkUuLϢ%+pqMXn nK4o@ /ڵkN X,R*O?E4EFu__gDH)B⇂eGOЖ^+-88j,N!Kj?$#GxgR),---d2X57񶶕]p7BUI,BTQ "@V5b1%ADru?sԩK(Bss3+V 6I>q>y3$_J-ڶM}$#!ZSdΘIVRQd"!+* ) nBj~~NիWىeYITɓz!ܺ[oA_% !!!ٸ䏱7Yjcs4) )޴J7O<j5ߏiE\%J NeнBwt69L[w5v *VmP@(." H6o߾K'0 VZYf mmmDy@lF6(B /($nT;$q}AO @d2].xB@Q %Zu}H&tttJccLޤiA f ʆGdER@:WC/pOp2^hDzmqs M`:( ض$I"e͚F =_l'kDd$ Dzvdd뜮4A.$9gx8 DDA6ϚN[\fʕ3eY>':IBv K2=BZ痸}M$61^ )邲a:nk R7 ;JBqX6 ;%r}%Z >;lU tSP7\l]4l]éF4șxQz=GPͩJusd\ƬVjUlv* 7' oiӦω+0`_`VUC*% >I 8S~wO#}˓OU`U+X*VwlM1d_ !Jouω_)9;SU>vr>1ZK,jol{+Oo[Lc8uI,FS-=y?V(-ZeA ?j<bվəS6_VŨ|@K'Bvo}]O\!b2E(E^+!@)+Qz2˵#z=t>ߊoxE`E)ov3p $@ȭ%>XL" -_ O4Mn۶swfabMMMHqGN9<|\n?poP<4hIurdi*)(  ܀d6,k|apG ᗀʀ$IwJyÅa^e6t]' +h@( :$9?>v-$C"JXlYȘ&ZxA/;_F^xK]KZi,*299y)4S~3( "GP9x¢MHLZT*pv $^ ?\p_ ?uuݙ)T,jT*0$DMxꩧ8vN9R$.E(d!& i$R*F2ؾ}@j5Q*¶mQVG{"N0uL{gd=SE+dn>Wfttwy7xW7vuuuצF|a7ڤUIENDB`PNG  IHDR(ԭ1bKGD pHYs  d_tIME(# IDATxgWY.|y4 h * 9D= Q M5DPLH !!;;2crkߚ==yg?C/}CMxŇc`8GdDõ 67.gkz J)yl99ׅc[RɨZ 0A)#+F@0Њnf Ic {A sBC^qyĽz$]a)pTA F@eC] ;-<*(-1r|وjk_- s\G'WWE<9{JG;1Y_S0$}l f~YFQ.V+;vHs-[hu)4,];(jA50f6.*΀;\'O/V˲_2)[>v/Ccg= B>#Zu6vNU)A!=hD Z+.E`ۖ!@-ll&">u^Q9fcS ,~t5j%%tWkn(J RWY4CN,ǚ]g`~yN]QaFie 0dP`4PBvu=+NRZn3z!ZFCA%4vj KTDvQz6RV(>-DZgX#_U]02TWu; J ʢx$@ `&0@f` ;vR Wn~\N]uqbFˇ0TU %5mM5Y^B B0Dn*d2 .//rSu-ިex6Q l6 Ak~E ;,)dc2~[76vGP- tK m fqR%B֠Vp(kM UY0p\N$+.vBk,-0ElE4خn;<9(@6 BV_ux?IvX >[Z_DV(mh.!4Pd%Ph4}1RR ah5gar @]i9e#rPLYu208x@9*(5س{nEj didkhc tz( Y+Y\4D#֯?קh{kWi"@1_F ΣGE)eh: aO\04 #ُ4C2Zga+ a o߽Hf[>ڽEx~UQ@Qw)5\m[va}sQ @eSܳwc:pǷ2-]wUE9-VJ\9{BMգQJss?{o\h3|惹Rup,*U:Jae:Cg!v-/ yزY*s^(#{v?vrl`IoI{?h;ܽ{'>T qlQW5ww\,N[jccHR/1+㋳88]9LFųcdV;mRXQÍ*P @+-H8S ,42 G9<㌻w/Q4; F')ɰ Oڪf{0AF0!PJ?$DX n:^pl-4Ѫ._X_z(CLWNwnxq{)f֩vσX ^B9X)ګ* huVwW+) aCwرkOk\;h:ǪH"W5:kmq^QU@Z}cnǨUm~PZ=,{e7ۙCӟ:&;"h휻9yd4W_[^kGx>@$s㸄(kTlzynDB5e^6/Hs)yQE!DM8E,2I;ڽȓfWynV'$lQۛ+W 󝹳⻍%dAY`R(+aY-jd줳"1r;ӇC ֎Wa#,JD1ko:,a?'lڮXj Q&|Z__74v:M/VZcX nQwzM|ongw݈ڂTeI}񦳌Ia"֧W~(=(.M+ z_Cl2GY$1i6E ^x>zo|-TlR"5=- a2@2 l;o=/%n3zֻFh4l:Z()- (D%WqƩ)nAXvˤYZhc4QndKPj>9{)p]mӸrl0Zв @Qa\~gSE QףvQ,zn3\?yŽw~%_Z?+Y|2nz =׵ BC>DaccD\̢`܂7Loniݍn)q,hQIc[hoii_Ux~q9#ţ_mN7 <)u.lZɓBoy~r]ZX¦ ˲eh6}C4V ㇵ?\UUR5(je1tZ t<4><:~L؋s Km  QJ-'>U8 q[Ӌ.V=.j)4\FU iNp|O967G`k̠rBzPveȹϳ>۽廪Ja4<T@k4)EiwsҘL2$Y A A] T:m]^p\~N)Z ]88QY.Bn5wɔPI÷7W`@jw[-u(WVFO\Gܗ2$B@j Dll.n(SE?u?ݘ'?p8qx߱"K1Nȓp\R@:Y!Ph! Pe ׸AGt:[֎1F29(#wzh)hw-c'Z|-BQdtx븙l l2͓?1FI)5muOU#BK(ǃs=7{/ O&at5Ŷv,֐ Vq9關(WEv7|XVYNQ4SR m>$5G R%0F#K䙀C84~/Jf)bwN<ouډ8x܉&ño۽͛O9! m`AH^-s m2N橿*FR)Ov$2YMYBlZXu,<,25t:v+|̻>3߽ UV6WN޻"~V/`ľ}up0p} u40Fa;8H E!P5D}[ЇpUUJ0A9@h8W9\YaݖY[umSZ#hxR)g#Mr$I4PW q4*3tϿdf4sh@hqg>y>ֺ'e)i\w7>%Lq-Tpe#3i"!F]cyl~OMN F!o]XޝHeTuY$_ (2_/NzU.mbaQ^釮[(5Nx2Y|&֨C(qZ)pNW].TIZ!KKq`}s 4e`Na$'DiJXaH. e)4,Fz*lǵ BTR[^k~ד/Į=♶)FQ~EgX`c} M2TݵlJ/]}*c((\E 6 ѿNV׾Zg-TeBXP/w\5y\5?z1VopkN彝 :7~Έ[]cDc}c$VS8<!J۩p]Av(!7) ! Iuh/&A0fۯpZN;{ޥO@M-y&ovfh> SU1l 1CWZJ эR[<lAI(\c=WHŇy2˲oQJ0E| >'hv(l{ 修O7[/1A kPJ`@P5\Ty ,kJ!I3UF?64sB*eݷ=(lsxuD]TL2Ȳ@^"'7?XCQddK^+bY Kaw񱫭~k^NZ:u^[Qhɕz_\{ϕ>CgF(JF1`$l#1J;A)CU TU $b2 8E>tplO*a-X.T]K$4B[$EP@k(Tqjc@PC@R ;p4qͰ"Q%GN q"-AA%Z@W?4}roALNBV9$mo.O,^pUUVVr.oQ0*~?sl5W6@(J[ E]# 0i|btڰ8VJSmDžҏΆڎvF@j p Ȳ U(+!\{0 %?"a[@ULadhrk'as8x lJbp䅀RR*07mesU 6G6CGEaU鎛~`cg/ɫ;΅.[tya}WO{m-Z\)Kd xGU`@I4.j7,0(:=tFcV% wKQ9xmsTUe'kwQVRTݺ0L%6WbkeF1#T0Pn<;9PJAk p QXѽѰ=HR(Tg A"LghG& <.cO{)=.O V}tfw@Y7:='nkc4ם߉Bp5|sj Zi.c,Zqlpơp۽u_5K7rƠ|p/,* Z,0={|ouui@TedgIZBJ Y%NcidYL'`6:ShZaA>ʅ' M hPjȃvsc EYjxJ)Za2} pxgD64pUʣꛮ8_?>s~ͦ+y^M~N^ڵ;m,<c6e{A(c:e 0? o>!~( !vS ~PoE)YeqJR]Ul 9ǹ e<0.-Q-E!Q*f=@x؞Lb0ʶ@hv~4^xљ2scp%XXRZ=6IpGQHa@ AӀǕ5/Ao61hYX8ڸ߼>{{+ᯬ#w#H <{ l!g$8@@eu9@Ƶ0n.ؽVBVͶNfYePf;+E97׿xO-G4d?}VYhXi !VF1p@)41cn?6'MQN; A}WLa%|%Ď `DUSCeܧ'̣N-5<.S1v?T,,.}\g[n N4*Kijwvۭ3vΓH7² Z- Qz_"ٻw>wRIDATE8pm#dzZ9'?yc.s9UTe nv/x198+q"o6WJuUC+v ~:6h 6h4;?um7Fed~Ocxds}|#FKX]b( \`:-10@) )t2 GVpw j496(̄.s/Y݈/'P"G:[ܽ{sscH6qO1)$(!\F룪$j'}oɸ8<f2x[WryHgh!1H3$t)l>"3 g)R(`NPt (1TkrU/~M~`;$W 5ZU6EMڑu<({ )(PB]a-hqdiCAq<0;}}#h-]{sl]Ԏk$+Qk8`R@ OsZiju:>vEQו=yV;@#a;&- Y+ZhN_ɿ~$35y8Fkܪy<ɃZ;zy(ȳ I#KrU8QVuQUk8_UC1y\׭\Oaksm7N\[ܢȲYZ*HVfxEJ㨫 q`:+Hy5[biAgw|n&aӅ:p& gOʳy6C2?p .`%Q6暘o# (G>m04 hۘ9X]#rBa >ykf}3\M'`!)YO"]ݎ߮tfcw@w4.4׳`;ҴD @Ic1I"'0Gl ϣ KgZ_=6&t!q~ o4PK/h6:Gzpɢ(e / o_ {I),w|q:H@׍ ϵ- Wcmh2 N81-K6\НCp{)%<ƑcZVPZum8Х3^t# pxf2óQu(frS'f)'+ȁ8y66P{otIؖm$)c3F)3NݖMYgz;._SSNGa8G`ykg~=g& rTY-;<{ئ5W05M T*b4iT+Ym;wѰvw|CR9B(b+UYba,5Qa̦ .} BMrT=G#LGhVh:@4:B|`nw^[ڐg°q٘) 4#\W <8K MSV$"t]r'$C96 mسp^7-"'V:b ?$H ݭfؘD`k ~PW8x{ ~ŰZ`1Ynk6U 2U;a4K&"88#+$Xs+bE)vl*)EF3 7۰vlVWD[ktjU;]Y()d 3ǚSf˶%0B4 ,NyP5&I4EPh=0%D,XVf] JbY :v|ߤf}~؞u_]QK~c*V5Fj}Vl e¶\ P= ۶ڞeJawH xv[$%D^e:`m B`]ݹ+-ˆSXHl^4:}7wM8WWSí9wa3 NJlb;mxA'$uVQ0Jv%RX=d:al'&!\1Z?82% Z ס ~hu -+M,MQ5[F]2$1Z^hTX4860JQ~by^=j\dT.gtp,ǟok:{BeIh[U40F P\d% F- Gl"J3P`Yi& ZOk J-=gh'߃4>_q^:v7g#f$hE6Ei*c$q -1X=\[4_~>t<'ڵ $P8 ݠ ZhZla-/01"x;uqOؗӞ #{(ōFZoOD`#-bpۑAD8u (lȳ8_Z)4vVʲF]i6p]úFH*3_gԗE6g[X BU = m[i !` (ȳU6`h #{ kYir]:]+F[O+xU)asגhli''7Xdy)XV[fovJ+"!atuvU jRSƶ1 +ldZ)dc| 첸Ŕ2ܘTv8^J+ w``F兀Bi-+%ŊJTR8e~ra/kVx֩"O.ͲRJ)eow\  lvv>hm{lGY?PIf7 QRz~R:rz@$& +E{N2:>*fq}~.R$3B|tZTqKz`!S\pKJ6j~:`JvQk f*nN:fŊSr*.$-Ӆ> 0)RnhNNrNbJx"O0ZdZBJV2#VJf^^ʂF6f(4D6~nb>_8R~^& /Be\B>rv6F %ƒ%N2%BRFTP,Hna HnPMrjn+6t>(ht&j+**j766,fB FNp:r|tRNS@fbKGDH pHYs  ~tIME =5IDATxHWw aZj2+@[wв%kT,QZ -lx\+M{k\H=VX }ݻxI~{}ߋ~gm3߬my.Ȳ,p.ȎԀFPݔ"IuD/O6 EENbA$$>U.m%4Oڨ3g#87ٕm$#Sr%[%5?~khU7A[^^Bl+ryٺ)v׮U5X[l (u˪ ^Nig{@[!hppfvNzcheB藘8D7ɒ$1n033cꜘp Rj43"YӒ{47Xsp*+8Ή*4&Xce#~\~7kb9:œ9s_#8qa!vqa\ΤZtl8rhs){4: l_եx |HK<@m2Ymܾ]O}A삈'ɯBWZFa[Qr 3gOC;}d *…+ß֍ٿfggCQMLOO/, ߹lڳ=)f4:l`Q(JS}L!/4\]]"v .45YwV M~x2>5DOϷ!.<>t߻W;"!G .߽L^xN-D51!~L%܄- S[q{$A#777гl&D9u}KA L4 acctg8XI+)Xu2i3?l}^|iV޻l`Ζ*:bcS"ki"_wdA 72:\C_qTLMMJGcQqm_ hR:jD,6hC7׉* 5O*. _UT`+uݴTx\IH&cN7)>|_ܴT@mj 5ݓQxF"ӆ@@襚>K5D$+mFqshD;Ҹۯj]V:%ϓ͖ŷ4\.B-q޽T+Rͮj%S"e"ֈ.B5e>NۆAK5VBtd/щFT-,T2jVao,Rn^6-"-KiKRMK5.`<*.HfuG *yEA#v I$\yh]I٪oio' pY}KKdIubtKX7DA;HKd pU,z4|rD2B8"=K. hawJ ?2T?IENDB`PNG  IHDR)/}+bKGD pHYs  d_tIME 0#}m ١ Hut]Gai2\]t:jv:\URN91޿Qmuݑ 4MOxA=EvFR-PMuJ\.r9GќnWl^hLO+fXiXE&!ٳZ-]Qj4fdYr<ץjs4!B\%9?[L&) pUznXfi3}_xﵳܡBsuSL&f$Ie^ضM.#DFA{}X)399ٗ#X{w;xG &}8⺮Tt:lDr_* 1%-o 5b< 4iǎ=,,-nZb&i"P5M﹇_zGWXx[j=ǎ {}:p}nT0 ,"H`Y\ "eq}>KKd24ԒiI~=_I& X_8RiLIJ,t]Dz, @?y3!gVy~l6l_A_2 76rKppEGj?x?87~5K':5=ϣnJv9wS(m[6Ѳˤ.^diiIsF2|'z+zD.T*H$( y'NcFէWv~=yc3ӧqRި8Vw*333A8aB$9ɲ5Ϸ۸ +5.]-; Fo*|JqkҹJҐ#tqhZtʊ^O{'p]w 6^kkk^'PQh<#\"ݮH&" ˲K#~@d^yE,=(}ҥKbmmMt:]u bssS8B8 ibuuU^[w$w_ZGbt qDYfv{;~^,._ +$Lru*?}^t ^Oxdng2ɠ A2 c <@^Z/Ҝ`bb˲ 䓴ϟp_2 b67ɟ=˥|'H(6}|on4鹽A& p=VV&C{+a yʕ2.s oNߤ.Q,=^o/)0-2I),C4=#K{v먴'$SbDrQn8[9]WY\ƨT0msu̙2gi9=>SO RYJFAgbgEי{ z.n\.\9}`H}reHׇ,>tG}MRaœ엩Q͛@92o%&|>3'O|EPP(JS(xb̛n:X LH$V2 :j3[mh48x z]Akgt^zIɮ[^fcc"]}).=TZym;rgnS|֨8lxQϾet1@LNNR*KMS ,l?-f.ԥ뻛W5癛cjjJ-?dj<𶛾00~-_`uurL:V~hnI7.eFXu2 Rz4jt:MJ$t:"١I^{ZRTd2r.+帲6۶M6Uk֑cq&BP(jLMMQhFPhZt]$Α|>iʹyWo;^l677*kn?UWZFA^RJ<oFU6dS~%r=F41>)Edfk_':Ewy ՘mIJ)9?lq0 (j5:zZF. \ !u77+ FMz<{ee,l$X{=/TyV3 DD"-Fx^~Y՟zЭ:hAOc&sssr9hضMXX,*]4Hy!8Guc1z&'xLt7g@ҍI NRx遲l6i.Ii\$l6j˲Ԭ\xY dK$G Cq''qiX|xѿ? D`=˿ʪV >Dޤ B{J;$ȑ#LMMJ u]#b{O34FD"jP'A ~_x{7&Z漮JŁ+Mi7}[@q '# PNA\ףjl6qGm uzcs6_ I2J,LMu.2x&,W<3d!nAoK·-O͑L&G;IZS>w~R4q?1~cNp]wƘRyް{l=%~eƞz=VVVT F'R t.^$?ƜCg\ˌx?Ň2M&a~~%QBUvD T*+be`p`~OЩP8Mxy{51.r(yZevvCq!fffh4*.qKrǻ~t:-9z4Cq!666Apy2˗9p gd(&`ܣ?NpP2ϳHRQd_(:7RVԪߨ4tq'n&]H&C-<Ǩ-xl'*2L*; , uVQ7JL V{ǩTY^OeX9xg?pYΜ92Qbkk k{[3KHy2 U!5h*C37Z2==4=$2D^Md\!뺎 Po=hh*_n`R< )]GHCY\*S8tX*T\6U4҉{C }X;:bmmh,n3g/rE $HS.͹\Bxi?YTdzUfr9 NRy&&&TS8U.6,ˢLUZ8\'|G4Kn̐d)|חH$?7tXzg7D d8pHѠP(݋xTi<կO=fd2><K#ild2pG4X+ =SS:>QO~/e$N2w Ee_ETQZm)i}[<ٓ`1'He34O+&w*q->9hjaXOV\.&JܸQJ{1KdWpO.t5N\urkIQ}sw??;y rٽ$NZ#ga!\!WU%qGn:9Dq33qFIwSy-ždnnFeYzg$ںC1I6vV}iM+_j2;;KT"J 61.h@ ;ͷnhb.8_|Xy]eggu7Ls{mzCўdF^0*E7u_U\n.#||&g. :Q*2y8x- Y\\d~~B@j lO>W'ƥUqGQvZ;6BC">YP~ 星l6t~ ~ ɈJBtr ͦB0LR,QYTiZ}4/vI211">GBSRI'.QR@FYd(jPTq^K;O}'wy6SFRD2T.Og+r t]g~~z>t]3 Fe-BAxXƝ( #w:!v97kݮNADvHH/EA#? In]CIENDB`PNG  IHDR00WbKGDo pHYs  tIME ;99I IDATx]Օj{fx|Gd${m۶1<U;:L&'@X-ːEDk7C1 pMS&/{YY̐H$pW\mZZZ%Nzr-:U;X>Vz4 ;+B+ ZJ}+i.0\FR i `xx8 4bnaY]nUX23Z1ެ*2+cYR 'YVV2*R;?|8s mUmb$Zzr ۶mb8CUUq:XQb/^k!JP[ڋgyFܹ)%΋r\k5҂ MӤ\.8J6@e$'OdttQ֭[&FB\c۶_kE&bhpJ%ǩ^:tqϋ/GX,L&@`۶m{AQLy#o˺ՊRwݍuޯ{]OOOs322@ 9vMss3fRLÑ&ljaE=ʖ-[( 󂮽pgϞɓ >>lM Ν;1 BP$all;vԁ]'&&Rr1R]]]\.o }K$+2o_9KNOݬ 6PU] |~u]ĉ011A,#L&/q`uM"@Q2 333*gΜ]ģ>*_yBCM MZ% u`ll#dժUgo9u---B!SQ(:| e*TGzH8p`4mLdXՐʽދL&AJI?Q*, ۶ B!p]4 qp]nC.#Lޗ_~Y!J%rnEB!C믿.9yH)rL>\.B5MU۪~7MK#`N0 ӧOswǏ ?ǫ~E!H,`͚5رc,bH" NcY֢QqL&lrM7p8i[K;ׯ{exx0iyL4MJRt:ӧ9qۢF孷-ڵk7fR|>OP`rrD"s瘘\8Sߴ_. ]KIENDB`PNG  IHDRo2ݷbKGD pHYs  d_tIME #IDATx{Y%Ǖ^ܭnڻHK4 ?~2a ^0 !ERNvޫ[Uw͛ I0p_sN\O1 "@?`Jg.Zj 瑂؁Lg9do_o<|ᕫ~q=Bt180N_Xw)t65v Os8wl۪ȳ c ;S^on]mC>q*ZxhV]u7o^ tp|vaM<2뫷?|PEV=7Yʳ44#856e%L$B@s(./MFV *(8z޹{gWnl. !/'twz5:}"\)]\˒Lk]bڗk_;pWJΕҹ毯csJ~BUXq:@^fg={|8םֺ7LeJq!Hןa'N_{5Sp&874=oH &Ln>?}VH?Py1UŁZRdZ*[l"^mM"˲s~265{iݍ:|ւ"7Wo?.NML7 Rn^ڜ0 ҼץE;8ssni{91E'ߝ4Ȳ !:#k׶uM:v{QKm"MV&0CTg+yR b+-aL67;]Vyh{s%JDΰ&,w@16W`_~/v^=Z쯮Oc^J':8hri^ioth3t盹6JksN8GuHhSR;nWEn2]$rYzAı~z9q#$l]x({)8>^otxvF;k) iQ_1+3BZk&a~o^f\7>&qRƤ2K+6$)ZkyﶷJ{li{ V 4W5إZVdȑXYѤoKMVoIQEjfV_~omVyߗ^VAow+eά鴯:Ip|56KNl X/I.\yR_atGze(1?Ϋ!Fs1iϼd)5.H :&3E1ys>^K!euEQJRJ ljT;ý+I^$\R{+yιwN/jfgv[j2:-2!c-gtJ3? K핵v?Ky^O':?KkItv?׻Q:kͳT4|,T*Q%JmY_ݾF: -RJ)B\Hu@eq K) }?:.~ suehWY\~Ht~ݕr94"nvm-%ڔ؆<91&(Gk9 c?L7Kί^ڊ8N,\ i_?{O9]\v}Vj'*Sc,zFPgl8nzj!p=+B\hS/otWѪlšRVz="cB?Ń-hUBFZjDLPx3$aXT0BPJG%%!?,<0" X=:ǂN5لF5`:D5āEtyo{>Yte UDyQR06!Ҁ'>싾)) <p")?PWG `+ɭp{W6m[ dn“OEmxA裹ʽC?Kg:rR*WyB|}2-!JFN#g0|IF$rT IH<<6GfƮygʏv"8ﳀл /TRQ og8Y- !agEj-r9<"|JFKPIq#&,y(dا %!#aAӭ*" %9<;_l'0nC&C(`$䷱Ky}gB8859Y)'g1[9Ƹ}GnN<e]7! AA)j)[ɉ`XPbԔo{IFp5!~v+`yFRM3'1BN#Gߠʆ B1!L IJ8kF0ńJ0%1!= Ig6y,cRZFcL…gf/;n%B{dR~N6e4@1v B0ŘL"`cexB0F~R AOSFX?O@X+D0! TfNNcD7w6!A IƼ9իQ[ EQw)mfr!bD1a2bJsbHFRRoZw2h ,!r~a#1e` >AO^'UdkmY& ss8t_n_H7b;Xd90A>B2Rkzj2Fd}΁󫟽s=Sac)^ ! BLiRO:r-5!z8G{DKtc)Ɣ`,X_ޭBP}`LI^o( й9Fȵ=;~qW-dDž/=&eգS$B m"~阐( "|3Rth)RJ8)Ł,QQ2qL(&pC<<6'׏ >pj뫛3ΜYZZY}yJ3 jnK{3.g7ZJ3@c &U2L j̺&r۰G'֮ll:~Ad|=5'mFxȐb`&J1N 3ElMa3k齽5V5WaRb{n٭nXY(s_c ?ڕ+K!82?7FI2L:Bqq`Y\0xw}^'ZB hk ;Y«zwf [̙9Y?;|=YW'{F(Qa$emd`o'gۿ2>\Y9TT(%aMZXXsshe٭w<K_- 򩗎 k)N?-u?jb'@@g)nkDx9lٽOfMJiXf|(/;;٪TkG--Ia6&8xEI9 R^9Zmm-՚SOBr E2JAuJ"~z9]iƣ{f8LTAQ@`t1 zq )9z} *"I~v=? +5(,+=wH ('9 6a(}͖I2)g !CQAJ)s9E2)U g <BPRk]")A(4$F#9NJ)9gBq7TI+>)͖l %.c+OK$[spp GgP-gs?Q潦Ɲoy2^3n.߼]v;{tX )܋GoVciy)O|)|~IENDB`H . ..Iprogress_first.pngR}L@RRڀ$@RRtڀ$@R]-q B&Xaz'">E "IgQ`ׄr\D7b".HJH8/1sܤZ$&{ ZX׶C.0{"Iv\߻XCCċK :\4=4dOn{}vpߐAR Fǻ)4ST1y]I;N$juQtwh;&΂0,()D7 o:)d SQ[q0f&^1DssAàFR -"yWK@ZZp@s*ctRBjOǔdϞm/Ļwz\ c׹j̱t"[L "``* gqv"gcI?·ҭKSg,sg ߻w6q6u_/i҄q 4fdgYbCU\j8vPV>p*)r٢"޼clisM!K:}m1y3Ks,Nhd`Em [-日 R9EBh)BΟo ;)jE𝞸53E H 5E 󇆙nҧ49p.jn҃w`8 Ιk{(І"ֹ &ê_O:ljc& ᒃG7;lf!h_H /KQEHELD$btքg2^@$sCdHԊ\-6zJR÷0H*r :n W!<$I.t/+Kbgc[o\"WP7 332 #IMGINFO:353x170 RGB (13963 bytes) #END_OF_COMMENTS 80 38 255 I$II۶IH%I۶%HI%۶ڶ%HIIڶ%HII۶I$II۶$I$I۶ڷ$IH%۶ڷ$IHIڶH%HI۶I$II۶IH%I۶%HI%۶ڶ%HIIڶ%HII۶I$II۶$I$I۶ڷ$IH%۶ڷ$IHIڶH%HI۶I$II۶IH%I۶%HI%۶ڶ%HIIڶ%HII۶I$II۶$I$I۶ڷ$IH%۶ڷ$IHIڶH%HI۶I$II۶IH%IIֶ)DII۶E(EI۶%H)E۶ڷ$IH)׺ַ(EHIֶH)DI۶DI(E۶۶I$II۶I)Eڷ$II)ֶֻ(EII׶(IEI۶E(IE۶%H)I׺ֶ%HI)۶ֶ)DII۶I$II۶I$I۶$IH%۶ڷ$IHIڶ$IHI۶H%HI۶$I$I۶۶$II$۶۶$IIH۶I$II۶$HI۶H)EH۶$I)D۶۶$II(׺׶$IIHַH%IH۶DI%H۷$II%۶I$II۶%۶ڷ$IHEֺ$IHI׺H%HI۶(I%H۶׺$II$ֻ$IIHڷD)IHI$II۶IH%H(IEIHַ$IEH۶D)IEڻ$I)Iֶֻ(EI)׶$IEI׶D)IEڷI$II۶IH%$EI׶(IEIڷE(IE۶%H)I׺ֶ%HI)ֶ)DIIֶE(EI۶I$II۶IH%H԰԰$I۶$II$۶۶$IIHڷ(EIH۶H)EH۶$I)D۶۶$II)ֶֻI$II۶IH%۶%H%I۶ڶ%HI%ڶ۶I$II۶%H%I۶ڶ%IڷHI%Iڷ۶I$II$I%ڷڶ$III۶$II۶I$II۶IH%I۶%HI%۶ڶ%HIIڶ%HII۶I$III$nIڷH%IIڷ$I۶%$۶$II$ڷ$IIH۶I$Im۶E(IE۶%HI۶$I$II۶$II$۶۶$IIH۶I$%۶)DIIֶE(۶$I۶$I$۶I$II۶%H%II$II۶IHIڶ$IHI۶I$I۶$I۶II۶I$۶۶$IIHڷI$II۶$I$۶۶$IIHI$IIIII$I$۶۶$IIHڷI$III$۶۶$IIHI$I۶$I$I$I$۶۶$IIHڷI$IIII$IIIHڷH%IH۶HI%Iڷ$II%ڷڶ$III۶$IIIڷH%IIڷ۶Im۶$II$۶۶$I۶$IIڷ$I%Iڷڶ$II%ڷڶ$III۶I$II۶I$$%۶$II$۶۶$II۶$II۶%H%I۶ڶ%HI%ڶ%HIIڶI$II۶n$I)D۶$II۶$III$I۶I$I۶$IH%۶ڷ$IHI۶$$$$I$I۶$IH۶$IH۷I$II$۶$I$I۶ڷ$IH%I$IIIڷD)EI۶I$I۶I۶I۶I$II۶IH%I۶I$III۶)DII۶I$II۶I۶I$۶I$II۶IH%I۶I$III$۶۶$IIHI$IIII$۶I$I۶$IH%۶ڷI$II%MIDֻD)IH׺I۶I$۶II$۶۶$IIHڷ۶I۶$IIHڷ$IIH۶H%IH۶$I%H۶۶$II$ڷ$III۶H%II۶ID۶$IIEڻD)IIֻ)D)I۶ֺ%HE)ֶ%HIEֺE(II׺%H)I۶I$D۶D)EIڷDI)Eڻ$II)ֶֻ(EII׶$IEI۶E(IE۶%H)I۶I$жH۶D)IEڻ$I)Iֻ(EI)ڷֺ%HEIֶ%HIE۶E(II׺%H)II$II۶Ԉ)I׺ID)I۶%HE)۶ֺ%HIEֺ$IHI׺D)HI۶(E(I۶ֻI$II۶)DIֺD)HE۶$I(I׺(EI(۶׶(IEHַ$IID۶D)IH׺I$II۶hII)ֶֻ)DIIֶ%HEI۶E(IE۶%H)I׺ֶ%HI)۶ֶ)DIIֶI$II۶hԱ)HEI۶E(IE۶%H)I׺ֶ%HI)۶ֶ)DIIֶE(EI۶DI(E۶۶I$III$l׶$I)H׺׶(EI(ַ(IEI׶D)IEڷ$I)Iֻ$II)ڷֶ(EII۶I$II۶%H%I۶ڶ%HI%۶ڶ%HIIڶI$II۶HI$I۶$IH%۶ڷ$IHIڶ$IHI۶H%HIPNG  IHDR:2 EbKGD pHYs  ~tIME Kim wIDATxZ{lS{}c'v&&  ˚fJDKtmDWi]ZڭvE 4tmV1@*k(D}@ ZhpIo;~\vB)'9~;<n_P,AeBEPBJK%䭜{PYJ; Ce EM`K)'^ʒ)ELhHee4 Il.)e˖JR"1rDNT216Mis,`[IhVVy^zK?&7ix?MJE(yH7:)Ti1E[bZ@^hoT-)S#$I"Bz7Uڳ(0e6? H.4>c|?&RM4???iIj^ǝ`wt:{/ݸ]巶oX̉yŖLj @D uF錰1Fuc!9`*Y 7R8 9⹀&|AaOfx| {h!~g\A錬#"nwD^8<^Q>xfjXdU Vp޲"׆ٻ?nO.dcAB6ʲL.Kq+$'ZRLo,KM({5|KVדdJ ;;z g`N+jjiiBF#:toN<=̪IdW Wqp㨵n7m۶ˉ/R__DYÒ9>X~=E˗=JzZTg`3+fwd#iXtH׮],J{`S{ɲL тR$Bߨڴ-ž={fD2L^Qܸd5MLQm`DxaYY|BtRNN<&{#tv=Ӿgڵk|NE?V+FFF``Z1::XWe^@ 8.&# 6-Fc) eKQi```J/ڴi-\Psssx2MuHl6FFCH=q}}t}Q}}=effjyQ}}=1m~?t ْ"p$KD7lؠHxOM 8*޽ 'bfIDiŋǏ+G]"\b8u)./G)SAȲ-[nƍq)Go2}g4ђF4``έ (Bt:yftvvO<:(* 972C#===qs=GJO>$Ҿ}l>G6E,iP"颹F*#1رc15aሙ%?xɆe]) 1ThYYY ttzu3uwRK>}^7$ͷb ZcpYhD/. s M T?*Ay;0]oD6pKkXt)Fϫ/_k 7;y F_ ,:`tj[5} o}x;p]e%϶x\Q+zc`zaxW-/>؏'`S-E?=:V/V,"MoXu py&'z!^߿S5x|à-Z ^oO^ V Kq`$hßCȿ>={=._>OEJ.7R=]#/[=!^⼌8 [?gnPt(TIENDB`PNG  IHDRd4KmlPLTE $$$***444;;;BBBLLLSSSccckkksss||| %%++33<l,cIꤛ*$OrnqZfv{Cz!v~7u]VA>5z$wZh^~X˲ 9FkP\TwȘBi"M" 0P\M"r-1Y8_ZНr299gIC*: xSDɏ=  I.`IENDB`PNG  IHDR00WbKGDo pHYs  @AtIME ; IDATxl\Uv?͛1dHJ;,ja+T  vQ[-UCŮv4DHtI&,!8Cbb{``;{"L^====n RJEAUUv;d3==M2ti^@ @.od?Pb3{VHen_2%رCڵ Ál6bSSSeNǃ#7L'k@]~z/"Wy R)_z]T-[PJD"a\.^ox*Nÿk}+D˝ ʬPUU%5`&a4 M`01qbkgl"^i΢䜮W)a`&PPFL|XYԏ0}(BAK]*\+F&M:)W vX\d+B]n)]UBsFSj3{Ŕaq+aPYv!y?{/Bs Ei榐eB,\Yv(Yϴmoլrv$ծ ۷o޸r(Х"RYRMGa$|g#m~?>|={JxG~syJ"lp0ibX/x - Ld2I,dΝݻߏa$I(hKW$p(ҬuJ!w,^"vQU0hjjb֭;wx>P(Vv95!VRJ~86lO܌]9q0G ~[^V sQxS#`W_}}Y]% [\h a>%QxUQ`~?MMMjYM躎V_{ykbCC|ggc!\dm|:7PGJ,t:m (ͺaiN'x򡭷pioqLaݻwSUUE.@|$(F^gffA>Y8i|u~?c#^gx~ :t ڵk BH)imm4Mffff*S}G}ğ~Nn'?c|ӡ7:w[kȤ@Uv!Tz <4ub g`\ee)U$ɍ72338ykl6kܖL&'CS}Va /3x#8UUI$r!xe6ZWo}f׬xO¼/ |.k477˽{ڊb1I$ln+ d2/զ^: p)x>^uyl=6ƪk'I0fV-DQt]D"Bp2xC)/_`oZCC\r%uuu73g8{,[n9h*\7"T @:P Ћ !'t$H Z T㯮 g!EE|W-!+/ '|hhQBYtˍm^^˱^Yަ]BC# 3<=Z1UR=OʮM?*B((6f`xqB{qM~+q{onj'xgk2 l%22KIENDB`PNG  IHDR:2 EbKGD pHYs lAtIMER0IDATxyt]y{Y`[-ccdb^BB$%(Ci/&t }94I $%MB!! `; dAeMuu{=?,$K }o~ku=g~|;>2|׌BU*%&XQ'֏2?Z/uŰg-f@mߧ,*4\Ɗ'+6H%,Vu2`>PX/T+7xLZoߎb +9603o 4ai]KtD+"*؎D?X5gu K6/XRӰb*`W6B J#,2#d(wOW5 >Ux;|NLp˲F3w>%*洶+>t@Xǁ %.MKq}>{xw@wkY*ʟѹyR?O}ʽd5>OS{mq3v5 DcjZVh)P@x"bT,HucWnzA@ ]sT0[ݻwOl [4zmXC<"jDl˂eL0L , L@iBMk7)!֊\F-#|zP](Um_.s\`鷟7Da>B2eF0D4 R A*0f՘ }gR#P<9W++vHCoۍ/z 9G T@Jh)]̊yUA2] Z8V4& "rʦw4NHޑ0#l?BW i6dRR($ *#8B`%>~P1PB@ hs̞L`cm҈g"6S5Mn!xi7%LBpE܌Zv/>|+a͊I}~~6Zr3h D%wK PB.)s:C}a[|(/y?e\,Cu$Dcq$$$ ~~yU2^+[Rig>QKQ92EDUx"4RT WAwPTU G@ivЖO+mh%K-hD"FGG]}SO=ŪUXz5n>l&barG9b)-6bQ(#Q*2'AȺFJBD)B>*-EL&tpvmؼ@g. _ࡇRUk===H)innfѢEBi>ϰ-=8U5 Bf#E*%5l4Kuɕ̋Hɱ9P(<^P>^>G_C >j%`(ޅ,8sޓ~ׄBT%Z_yHH rr r.Z,*3:8';ue 7'+m6V\!8hmXeUDd_:@jY/OҎ9p!/8=d#< @+#ABIr&rD.otM.3r=z982-(PZa&~v|Te bbhAiR?jZXjqqF>tMV`ݺu*?22B*4MHXE8[͛7hկ~rZnvnai&x_-R}6q [?DK p:PN~g qjp$V)$nI[{R_~D[uVjjj{Y7>c0tc*/u˼W}+HOї=hmcG$j#ض#m J{ScJ)ٷoߔg^{5|M˦Mp]wJi}v9"_hPWWǖ-[x9z(7|37xvgرc!R]:c $[Re + ڈ GׇGi@y9 .T؛2hv'4:;;Hy䑉O}S;߲.AߎV"nGR8ሴ2f ˎ7XT5Ê9s|n.N ̸!)tPޔ\Ɏ;YP(Lt%\DEńx0+jPDۃ^?l̡KEc䚑Ja(#47T77M6$=&% ]/=3q$>o<}r8p(yϰJͥg}2v%S]S3<<̜.EU5,Fжt]ܮvCBvXuxƅ# L]WtW^BFry=C \T ;v}u?YV[Y\/~v/՗=i0`^S eJtAy eZ Ղ#"6f*dFΐ-,Ae{W!cb];h];SUpn#@wcS}YZ);ibG_\cOƪ[I‹H4`TG)R#%DI.Df|/?:bXT~zJ}?in sdj0~?!a^kTyxp%qJhQJFJ 1 dd-88&SO1Ih0z7A};p?0c.x6шI}L-p'K3xlj}جS}q̭ªwU HR!BXdksW)7?˷9GGsٝ=~R.uy"Dy)$xZ"VH(ePRR,9J N,g=t<i-oO`wgE00zy7 ν MLvAK?Ґhm ,'% _ iGQEyp=WL'M RW/ŷ 1W8gb#+j9 @ǡQ2hJ!=d,)<{<'([~sgǯ`A{v\h9<~*GaaDXM3򓀅 $xq'!߃ ח>wv>XfBbH&i-aq젹?M&nqx)O` =eVД^?е1wlu9']#3|`ጜ7WF([}Y?eIENDB`PNG  IHDR6 bKGD pHYs  ~tIME . ݗ9 IDATx]w|w{Mw'h*E1Sl -ʼn{\[pI{8Nƽ 6;6cLDGt*'۽&U~w{  d7@ BX (H(@BX (H(@BX (H(@BX (H(@BX (H(@BX (H(@BX (H(@BX (H(@BX (H(@BX (H(@: v;Zɣw{#ckv@H(Zʡy%R!K&@T49{j7v9q!eT$,޾hIvC(8A 1nRMy{*Pd_r U l?~& Q@E9`eJ ,?nRt@ Rtn$!@X;>oxAQsNv+JXn艎 Dܚ ܃Ԛ$!i85P4[0d5 9j;VeWGyRuT{\ dĈ^W&DVڏVJtǁ۝z*a9jeQª 7[[:zӂn^4@ Y wґnN& ,Fܺ<&9+mm Pv+S$Mލw$@Gjq#s{t*%PbAT%s%<=9?IW;McTW(SRtMICղO#J BbH[ ,aꞻ<) ,FK5ZDUBt }rypBZ AT_HM$%EO[ ?ڱx6˛>yX\eWNu(# J.}B /CEwqMŵ IZ;G#۪bE Fޒ(H_^Pθ$O*6={y'\爵*eQ*Kc)NDW fCCaIϭW#\Q{#NR@z668cq } JQAc]#IwoP9#eDOJD BڊJt}x.)aթTϩ&+a5*P⨼?t Qb0 O'Kj8~ -}\.}7tׇ4{(PpJ!FB! aoO }Y5;vmB/_`3fl۶-kYwNqEqӇ(0oq4՗&F<8Jֶ>Ə?k,0_}j:>6PT9Pc"4K e"r˲c%;.bkOد5jH>O2ERy+̪~1H+ (DYCNjya c,'r* F_j㲗ot{|yaa!d2XK8r螓CA() ?(hgl'!ƐDs/uJcY29$'w@At4˃OJ"Y2KX'׽*oѠy?%ݻǏwww/;)lJ }W^˛ǏaWZՇ(kV)S*Pн{Oo:+d 䔰v}%ci1~:t'_4@½s&F(#{V^[9`XRK) cJ"d# 'X&uµQ!ɕ #v;3 UhK DXVԒnOoWr$,xc37 h5g;_]_^28g^}Pp utX/Y唰-d)O@@~UpX\Gؼl{}ZWm,4xG(J*p:o׊4Dy{Ԑ\hoʯ(W숢yYSQK)[LX!ՙ,,;(.Q>yMh5}뱫,=}w{$KYd aa<)Bi0^wipA NMq3~K;Wi2)$zJ+ydp({ߘ3*~,&V]{w}IU>wZ1l)Tx]j@XLWk+(4kƔ }X5xKx/̀4}:>W63pZ 2LWk5{f=vhj P= O*.moɧ9c/RVM+X:%/Gr[5Etji:X4zsʨ_1 x4l{O)xK{]=L8B#MmiXc'IW ɇa0 B @EuIH%vx9ӿ*%LP, #I/mr{:+"BhឞnOÑ86EAI>ڲ`ʜ2i݇Ndeu:GUL E I)HyPK[LO,֐}%,j[m_{gkb$[!XNteQ7Z{/ $Py C diJxt}m&\QFX|o-[ȲMW\q#<2d>a/NZgqW+KemγL6dl-36VN1 )g^I?KH}]:]l߻pW,d+8묳>7|gijꗯmMMͲe̙Q4Mz====N6'^ro4GzЫg}qחsBCT@stCI%`jou4զ<"_ 8c,_Aωk68;;wn+>^WWS*ꬳo~3k,I:+M{ѣG_{5U;T0c(zk %Dh+islۗAstCIe!6}go\_LEH ;`bb-썵-k_@IG؟ ?c|Hu A=t:K/4m4ɳ;noWZ_:trnֲj Cuu9sϟ_^^jl4MKuuu6l`<>`-[Ы>Q_S7WM/H{x#?Oe釖,nlYu䳹j>mw-k_O7mz3+I6|NCIe)@X8g X !_bgwHi B78(/<E}g}6455cƌ&zhB%z36臰@d1{c]JX}[/*}7MzdW[VٲG>;y ۾ -k_)XTB! V$ECL] å*}"欩yȑ#yE9NVd)Vg[ PnOwpJEfst{cuBJQ!|+¶Z$#")!+(+->$SԒq4.~{1j0жoCpNڜ/"Mu҇6ElpBSlQvOJ; 6%0WM##Y=/ˏ,@RJ1>mXG:؇>ɺ"/>|"`7p'{kWVd9zѯ`F--D+&]Zx"jmS-߰)GSjZkaom\jCd|ˤ*q=, 174xoeb[V:e{2 "ڷAkءW=%MRkr,~{hL@b/˩S_wmL#m-)̼6K5꧈t<:\KJECIm㙈TkV.> ŕjSe|s4"YC-Czg+ _SW?Y0Z%`9>Y%V0Χy巾V0>T$` K:9~qf9 !8pt4Μ9s2?j?0Mj ~>/O_Kl4M0^Hx~nGs= U/'o|@>zᣧt{S{9Oxd}ݤ6&7&g͕Bz (E-a06a#X>~\@y漂x4!?YyJ0"4vM?`6*uiҤIgy-/h^dJSܮgbؾwme[Xv2WNt6./Tw{BDz[F=T{a(hY UNnn >?t4 L8m uj7:" a4_g$Q>`a / |c\QNI ZkSz%ɓkjj6o̧;Y}tv)2DOR\kuEz񤾸 fV,:r8F<|ID%D!Ӕ.U-^'>a"Z{ãP!k{bGSߟVJ{Z-Q%%%Ϗ/5`2ʀ\N^'"Txyb!w>9tVvg},!*hlub}MИrזur!}X_%΄\&^kor:nupj:77mz??\kᆍG[!K$pRH\u_yB,ҍ1Ѽ_ Ci֭\ L_T?kR[x:G! ιZ_\A]!&"Tì[WgZ2$s Hn Zk))kc8ZgUqOB_]jjjb֭#Νk}iEYY~$ڷ\#RXb2<waZ~|ɴ"dMa Cm b?Wz(X~?D|$g!t-. 4P|NJC! AAnk~ DtLV/~dg2q㪫SZDa-9''?fBe]:;xjBs ax}Q(%֭ez)QUr6 s S|T *nwO 2W!hAbcd)x _lp!A5ri.:&tn,>FzȹhqIADcV(,a\DK/_cӧ$ao_ >YE$ڜgjpدnj *|Q h^SHIeR:rJg%5 + yA)Qd)8jRIT9 'X *L3ckɞi (j Nc,¹#eTB(m%نjOq2jzVɓ'yM~yW֮]+#x<---Bt:݀nX,3fx7h= ڍE"PTaVOS~9W+zrlnjX 0`m #Rs4|N`CBƈ:'b0\1dhEDyj%ڔG\9e鱖/ $/*/;Rc~{YD;cZNBQ 2@_-L$~ zQF V .ho0\.!;Hx ݩ_%~ܹo7Yӟ&FIjxa9WI\H$=O]ZzߗP({s}êp"eL6 s]Hfs ߣgn+oS B1WNILT:w|{0ChXPP)9Q\I IDAT S ~0Vɺm5C7t$uu^_{,nɓEF]G]  IJ~4< h c &,B @6KXwXLbp_L|Pdޱc{'/z@\ BtHs%C,E ߂ȰYBG/ dYo iZM9Cg'R0Ų{;͛2dôqƽGt˲:YA8 <>"Gūg}qɑVC9#/////*_a۷ڤy摽髪ƍ?}wBy,^gZ7pZݮ^O{EĔޜw/J(-{OgCL &؁F~_rrrnX7#Gyxz8V=u0xP҅]hezÅ1IBF!Q9r픆F{**)Flthlm#(&!_Ae_Y-呄YBb#jT;㦛nWVV /poVz}AAAKK6@%֥ E!pH"a@]FP?DSx QXh26`6(ꂂ9hrss׿>^za}NVסvGK#$Q` $|txpwE(I^"i+ 1YܵZmL>Z6`(,,\`u]fl6\s?[V/i4EbR ȼ s(b&$g|J+W`( ueYeI8Z{"6nN/r7VРA Y!4eʔ)S߿;L(BT0 )H}؉ TRB"3ĢF͍23EQᝡqRX`.r#,4i!ޞ%,A["A2,bI %/gp9  LUEHE,Z& :5#Uc޽RhRYce< lrߌ_$knZ[ZZb;'o{yU&h͛ij:~rE!eLT Q7㓥x9+!٣ MIPg;::oZtNG]54}pɼIYO^2%Yg1 #Jyz/a?RXb9 fQ0 ~;D\{X26Rax]zup:xC_1֥R`3>}i{o#g|g|vMpun> * >rDw!֤D‚-0I !w Qmqg!Fŧ7dK(8;?8_WϺ}Ώ"q瞸[ŸOhkk}}W8%+P5y} jE,IIr?svn$|X03pK^\o˿azH.}M~0pCCcr`ےElZZe.ꞔ3sG?E-7s݉`48O߿p]|+wqI@2NH-๻Y|k9 x1 FRq遢̽_$B9E'.e׮+yUf*k~v՗ ocY$ϊ1C_?xXO?TIm\e~0aM>,Vj4?F`e"S 2or]ԧǚF„ k׮}r:rxiJ^6-{MO:09{zzꁺOο}c+(q_Y6{RbSR`@`I߃gp-N~2{_$߆z3&A<[5_lZ:wp=sdx=Qh5f  מSٯ8HjB%b{oC xG-I^> }POߐLOw!_7G< E0H|_PS ËWX=BJEYu_) 9)m? ?/2(L@AjC?HtV A~,:$ c [f<8ESBl=ZrDN|ʨX"'[ 08z>VGns~9rx&aOd9 &:d >?,Az衇YDs> +.= * ʋZM;0rUCVӆz lx\SIqN|JA>ub; %H#7Em@Sd4w_wK'=@B$’c79`jS5L#[@{w/_2!tӆ_yp5C%$7o6 HH0D7D,o!_/[" x,! /tk^nQC!-ȋ,.Dҳ'1NJHV`K_#ʷf[{(vvb1vH?;ak',tS:?u4z k2+N 1}& qey|n!_<1[ReJw+q;[kahk0BV;~e7"p1zT:h8Nj4S8FL|[(OYʌICݵNJ۲u|["hHGwzr詧:ÒW ^%5m>Yp9jnxג$++|/XaE^B2 9 /;Vpj$I(I9jy ?iqfS. Mӆyyy6zz\+ޒ!yx[Ǿܑj,? 悑~zg€H$j3i(a*ɹx%jj-q ?D7iz kXݞ#^^sD*I F=u"'a,ޒ}a@wQJX<\j/Y65'Oooq%FT0,q Z>r.P!aFd,sN$OL9oG -rV1Ce,0/3l=x^Dc5f5s2dl"M4Mz^OtYYYEEk uӆyK~0LG1s8+XYHF! [c65Tjsm/ܨ{Z]9F:Ę1c>ze$nTth/J]Ƿ#hq4uyYD{u9555?@I(jz^4R<nkkkmmeƔݫLc`!wߙ)gax',,1!@=@ꃨZeCfsBH VjiqICQTVV`@! ^P,s/#`O7 mۯG >Zi0KcRovF9"zlE]&RGːA!$<|hjZA?0bĈ 3ϭYV\,zOT*N0EQ˲===---Vph4Vk0, /|Wax,eSw[K# & HqIf(jJK|:YJ;1p?kمu,)QA0bX;ǒ7 xҤI,h4Fh@V4 Sv:::N4MLT*m}[gDH>&:W=I<~ WzYa abSqVxo^CQ'@JC +,!ܑu%1*n7Gn}„ yyyľ0LOOϑ#G?q;t `T& XNv^ko7M]θ$|mT*ZM FV{^al6x{I"ʯ.1T(Q+f BP6hKX1+W@eMՓ(RjI{~MEXgV 0>x~%%:eEEEFJpPTC;NJ&%C{2̞5l>v>Ĭ쎓]6 6XYKMe gjq!cDE^̠3rHYIXlaղU ᪹{ N4v2@ks N ו~!ޥiZͲ,0]]]NvUNN#];mlw+]{ŠQg@B8c*޷ۿq2]} 9+JA d<"@r m`޿)A4M4uǎ3fh'n5>ZVB,˶uvvvwwn L&"mMmc|~k0c+Wc Mp3>?nʌ; Q(p&rxbD Q8 H$k-~VWm]wmeW4#E r:;;;::;::zy6CuW>Q::UKUFIV{'@' p7ݲo9"2#fg3#::sT d$4*rzH3VC*zGB79sѢE !DlR/4Nj:njZ ۰.oWl.`Ǫ^ jcVa(fV(ۙn>#3[!N@v;1N,,w-WYbH=T$BQ[GST*eYcو3nw\Et:HB~f{=мmy\@0VW%p}Fdi4 toK)%%j 'ifYb e^MP~bw~'qZIaXYIV1e*J? Qbv)+hƐ(N\ςbofbCw /$r˭AO-YjJm{= 5`GaA읨SVI'7TN4\AMJ|n7Ř bN U;0l8 Z aeY函0X5k9MsMYZu8* ?T|9YrI[e+<Ĝ4iw`¸ØRTTBY[3IX"mFgVcԩ~" < &1E 1_Tԁ`%P7ĘQ *|%;0%,F!d4L:5E ;1+PKRp%%!%8 (qQ2F+\m7(VJA'ԽĀ%iE\ޠ;.D&ē9!)er`|0;F2@8qJx7ۇq[ +Po-lltZe1hU5@q,# S ~ 28 r;* q6^Z҆}F "R@PK<kfM8U;#d(Έ# {`P G_Yg] + 0!K{^K W،VmƋ_6}̼lQyy9wRrU\S^^M6>C`<.+;a~|g(U)tC 9?'B +#dzbb=C\T^β p=kmuQl(,@_ȊϕE_1#y-gIG`d;>U0:]hc\v=MiT8 訾$r2y^鴵#viڹsڵl̙)p,Y 4/*9,zfgФ.VRSa(jD BG J_|]Z^i~?4  5].)˻{ɩat7'G3+,•Q',+e+ d(~GO^xycǎ-..KNRN'B  Xt飏w\gϞ3<_K 5K ^:aQsŅF5;aLQjdaNgnxSZ`v'[x1JVgee^tlU|^o[[[CCO?T[[;t 1cFj C---X,"| x%q p4ay!Tke$|n/kZprhZq^^7p8x|A屫a1uBS6ŃNry\!DӴ^ycI8v+eٚ7nܸk׮'NtuuϜ9sРAC9rdOOOgg'Ƹ5;;[XXX[[{ĉCЙG>g)H%]d, gu\$9Y|WYY)oۜ_|qΜ9ZJ'e0c근{^a\.{Qp+a%IqWWz&O|L2㸼czގI&v1%2a,Kh|wGPZL|"ާ8L )U"Mj\R% Q,H_%8XMEӴb)..6 d 688wު*xE[겳t~0a!1awW' H@)!Zlٌ3222HVe{Sr r\Ǐ.π|uD&x>ExvHX1WI*3q88# ey'zD*((4M{^PP̖-[>|WTTt:eA1,sqi6+ǰ^ Mڅ2 25@3!ՒK/]lQ$5MOOdfpp //=:yd" X@K&%KKRװ))UEZY\\<}tYr8@4Djt 2(v߿ .0n Ɇ@cƬ8˷|G Pv~U R|OFJoԩSvwtt }}}mmm.--]p!BhڴiHU#@k]z0n.kͳgϦirG9?'o~/O>eYGӴF1 ĕ`0h4BX lڴdʔ)r9x; b8W@v8 IpY~#$4,̌^{!uD0 c6i>q!,U1y> [Ǐy~ڴi (B r]]]8tmol޼۸ql&9CSE4EQPݹs'֮][\\Lf$A i!щ螅eQ n(1#9KEÊ~ėTURe&~# f:t_|yyy9Bjvwwk4AV <Tyyp8::::ݻw/]t„ v֬YH8'q/}ÁS=Dr:AMΪEMr.~ȑ#v^~Z{&Y E^ߨ-s sZȗN[Hm KW*&<,aZZZ>\WWGFa4,AY><w7W՜9s,b|< IDAT Ǐή0al0LdB C__^Ο5~t^G&9BȁΨjzg%"Lr͹ ^Q,qPTĉZZZHe˖UUUuWq455uuuv˥hzh]i&Fa0{lA233fN]$3.u \ \ٜwu7~{ُ?Gj7olV^}{^2{HQPNlEbtnݺumYd5V#)N;i8 K "yAJ.F/3!԰d̼ꪫn ZF/z|l-((whZbbYV @Q˲d3FC|@i c<00pmW(s<,w*7x衇ffpw~233YݹsdO4Maaaiii^^0L}[I+4~qlqqH&\&.ZCzJPXpN'E(H6Q[!``````eXrssKKK4,V;w\.Ojd:!$n70ZVledKKzy~%K|'O?]Z~vMӧ9;vXSSSQQ￯j)Z`W_Mv|Ŋԯ4*^=Ѭdb%䬄 QA2-W$d!!kOĉ,!,++[x1!%'BdGeJ&"h4 [ O/1B lk8tׯ׸r溺+bΜ9d%EQ^;v7\~}KKˣw[&cSB <$PġA*i $+7NHO kX.?2d<8##cz~ɒ%br2aP!Cy﹑bU?Ҧ̺sX@覭{4knEiȣ'N<#k[;hЍ8P1s{9K 墕~pS 0>,77w֬Y 577F(PYYَ;.\HT;8`e#RR=9_2z?\k}K}}/W5LQ 0+B@nDu YX96_DzS ¯iV*R7b!z0  Owb8u 0LNNNUU BQToooWW@*EQ%%%$$~D 6,yc;vX5\y\^4t5 ߺZ~͏S>N/hmW[8[ `6[ZZZ(x&KKKK Z]PP@>ˎ{ o!\g2w9&$5_RuP9SڊDhpYB~"^'k:l6fNlGWЊsgV)6e,'B2 YY O9!ajmY'W?qs=w7dܤaY,ӧOWUUnZ?C-[__|Olzرcrrr322zǑpv}``eۮXQ2b9:߼-N_VTUuW_3nAL,˒1dqBg_j cw/cӢy2 F(5" Cq O8 %gE+.$ d' Iҥ/W^y+7ܹ*??`0pwFŲf͚ɓ'cǎy79ں^KC3exaƤWDU??O<1nbW/;NT]\\L;F%#D;yw84[[[ czNn;."K"@h|P1f#I,":⣚̖mÚ7m |% ÃcZ='_yN/blQu.%B}̿?a{kMƌD'H$&z i^![ZMQyO8 Y.%[rqXz*L$7 ,b%DKoZQW\QPPr,ˁBWW0fh4fqvɐJV{uYei)~VX1n8Kv^OwWG4ctA("r:.kh:dhmɦh\#l6k昳DKaXg]ˮ_C7]T=IXW`@qƓm-mP^WVW3f\Eiana 3.lwq٣[9\cߑJC"`ĿfIg\.f;p/h?}JV/3j4ZgwbX$~Wx) 9؅ഹc!;vƜٳG*)6Sp;%Au~FmLG"JH!݀pqׅpyP_dBn#+(@T1PDq A H#@Aߔ>8y삪Ls ` a$QhYŜc.Kߣ5a*? XbpxE|EJpcXĐ TM®$+ <DjS9z\TM!mOpZq#'p<7V!   >wȒlplN[=:-ڛ_n]\/:Y]+oݺue4hCjGI 2d:ttT1@Z2Ǹ=F+x,M!":-`ci(zV,+ck8I>v{+W8c%+↭Zb2c m*Td3gAUՁh9jc^ ;|iqyyVaνKwpqׁMv`{ˢ@MI囟 =ÈP(0 ϙCCC^P\Rn ɮ:FՐG8 r&A%Ő!)5+ d+Uȃuŀ]EC `s<|xBX;%XV`^f؉.lsa;]y<^^cJQ\"|Pز'v{ׁƆM4=]UQQaZyd8F*@A,]F)i v8!du\YP'p4ΐe<ǧgI8[,r//$*qV5+GGL !cNM,H! @|@_|xbxaanam^, I$ IDAT^o!`ELSK+(ǃaFıAARHU__%WrwmEߘb7gH 1&٧)+Sr<4j)(Fyz,9fBo>"EayyśDNa3+U}P,?N@ȑ{Āwǜiļ\Xcnax(!Gg3c/_|#{W4lg-IJbjDrV s_ g4O?2zhɶs y?=o|㺱j)`YrM~/v @KYόJ!:8pJ)Q͏M̧nÁ y/8u\VUh:Σ;{zz& Xqؽifnq u:}4b^Soo ܑ&)J*6z7ukox}{[lyV\cY_h.gbʈ (xGu˜7{}~[" ? R*yndJ vSد j׼sܙk0 {#M7 Oq|}E3ƟzfiÐd].dNPY<1;p^o7&MJq*Y/1kKK@mmR{"hv|~+*J=t1sbQz왇O,J߫GXSR7fWÁ&,_v]=cӋ/?SO?op].h=8eDxjc?=Ӛ&fb$A!?MzPf51c(A`P_֜cylS}wu銊{CeN;z6Uw^{u:.'f0tCIX>X^N-"C p\.+q6Mx/zʋJ=@=R|ϣږ,Sgdƭ]w˲amڻ{[l+//^{Og̘<04Mp)L,d`5E<> M"vo-%rr@[D~ϫ/_99 dNnv/bYt qU{?j9cyvqڽb02htR9e8P>vw$h[@7ozͧ~*s֤I-[0$ Ib*DSK۔Ys [1 3fLɖ# :zlOڝnFCbfʔ)|Ķ žz"ړ<T'KP*Ȅ0>5 Py b4R]ƺfجՊ=c;79q7`n0g+8`ۼ He*dgjuq _onM#Json̓>w^U^YJ6JZQ Ĵ|Oe_{^a˕fk1=SZZXV^cpc~eb!g]%$1舧V$F`nL)9KUJ +UG6 NY0czt5e_QMQ^/_XXh2zzzt=vN`A2:G7xC8), YĨ<˟`JY7 &Dr FNJf=V+c%5X]+v16z?koQcNbhʛ&-OgYJB%vzZ=,޾DnDF.:+hX`kcg7<㴥Gbܚ)XdbMBv7Q\.yZՐfXhx8 =qe4{q" &d!›KԞ g#gOKu#_S=۾>a 3Ǧaʎ]NrNuQdo;,yF3آq%Bo]U=2V(1ZbY7=^^=jZVrpL@Hbh̢`_P<v:gW]Z?ft:B0do9_yzg˄e0?D|F؟zgIA, 1Vwn:3kG }n(nv?%j5DI1Q3Kz's'd-yұm\#wl~%@a E4޵ȮM'+*-[^~Y[Bn`ǧsZ s%'?SFrJXŷ^&)A^i#$ @zW/?9vx`Бe3LϷo\kz˜׬|<9yda^Z>b [82()I&&gi=GXr"g v%QhuK?_ǩw"Ѓ `/^NA '6fd(ل.{'[L[b.zQ OHn۾**h8иk'ϟ___:u*vDR%)%;Ϗ333Ngl `zDȱ@8rm۶my_$t 򡿼=ho;~pח-N^?_?x`IIML[K~.>rx4gSPPT2n cy75TbAA ` :i2zt>4<?G&:G(rSn?[wrD30f0؆q6ͽ+KFu-~J/ <;;v嫭V+!)՚?a2nܸ+WpLbR%̛1eތ)7 MRIȁB+E&=}_zSy%_,i'i*+AVqWaŁ@,SA,MF:".-*x@Á#ilX󳳳A0>_feen_hϚ|uW&BSLr%ؘMcpӆGxᵟSgO)+s@jOJw5,0nw9* XҊ,$= Z+M9ʮ4cE!@QCq}}}<>~xl6" cǎEY+V|c/#",>dӋ $GpV(5ڊȆlB!At'#|{\,*)cƆQaV-,,;vYrrrv;r*++oy)9!m۶e˖pD(r|͘BH4++ȯ#ӱKBK!K+~XBD][#w!W<(LYƗpߺ=k׮mkkkoo?xN/((X,,۷!TXXXXXdxxeYyG9x>`ҥsFĒ!_ۑOhS (J9 @ a=JVi/6(l+~*|fצdƒ- ӵ὏fUD\owCp/|ҤIz*Wպdɒo\84^A,chdfdy`nX>k$PcÐWIϖMHє# [}lTH=V ^{iglgVŌ~9 c7?'S[?!;;[9 ;;>Zd 4^3f|vy`nV ӳpX=7Lܳ%:JTAiq8tB>ͽ~M'Na7'NgN@N`f# fكYn˲z^i: &/Pr,𑕑I]2gՈ96fߝ8XZP_KRvD*"wJN(8ZOBƟa˞aᰀhۗmnVzyUݳe˖u-\d2?U)ي_ZkT YdluH{[&@Tֳ| Y0I^ԇGaj!@ P..޽[ryՋ1c?Ml}Ig}c|l޼+WZ¦(DggыgdZ,h=KL GH;.Y$I%(0}b["1Vr ?p8ZB1}`揵7sǠd!0a{#溟O[鱜[W-egJ +fΜ&i$Ϗ@;L( q3V,cJ|Q,g)VN$*Ksb&@UrEXA%v |J֖YVPƱπ>g*^@r@c 0ѶQm.[Y0,Zl%/wϛY;oԟ\:2\Ɋ*k CqN"g% qM"iX(I=HcV|+AԪ8@c(qṳ7x{<N|CuO>DV2I?8ey3kreܞ⫔'la,Y'HPSo1ȳgY)^Y CI5q%O~BB' Hr>0eXx,ٰDU 'X<ڼjX=<92!"UVV~ǟ| !y3ΛYQVTpJ4Oa^K KqhO3Rna%&^&GΕdPIٺKt1Ɇ5o1/߱dE4+1L[T{˜R eMP84"'UcCPP|te%E{o.)u 8DURpGMQ4MѴ@ш)vLn=+e%! tέ٪BzqRZ<`?)M:aE9<< W{sDAodz }oX~޹ǭOF0u3٢y<MS4MQ4E9i ;;575T;-T544Ӄ ~`&܁`ki!Hxyc' tNcΟ_c 4))(%\g1銕QC4(Ou_6#FM ?GITp؆5#4!)ڷdD#He Afn_㟕%݅95yD4ӈD=xtSUĎs[w6(w T|O"44f)))=lē{#@0aliQq"UW{kVqػfMKVVd|AalVuxxpxx9<4} {8Bc@ɢU=gw YJ3g,0KCjX\A,Xrɔyt:˲rsdL~AF^ '\fkJ @F$K f򬳇5ܕgCoGIX :anQ`ƴ!,8Ĕ\0=~rރ6לYS5}2L+YLŚ pPѤq DW~6NjT(]kUOBXG@_<+)%RDŠk1)hNpr<!]TD3bj't?XW@s@z$(ZGoh[iVΠ:ҩY-'4?J<=ɤ[C{uI!+-͖YC9$; 2PU@_egh&Mɞ EEeJ91oXѴVȷb hO!y(Gȟ}rL4_qT԰ۉ0.2`GorQ{(!, -q0f eD{)8wjR>E eE Bʼg)&8K>/sʐ0Id{zY@qAZ$,NeAd~ IDAT)Kwb4M6RY,Py %<+Єl؅n*vqg8k-+N'z`d5`i!XU(GU,!4aE%LB7t(ńΡk\P =K}0U ܂8R|>0Tq \߳:q"D:6(*G&`l}Z,P:jF(ٷ#g??3Vb餮BFjFta$|}ŌA_e=KTn.ۏR!r$F'B3C=+8i,Fy]?<[i?֩ա{F'B FH% Y$K[a(giq<$M$-?INz"b/Eƫ#c|A92?ZmCMS'UX̦ #U a UBŲ WP a7VF#*TLtPQrb6]l 9;[RnC6)WT%1eEY邺s n$vIhP#LՓH:7_/\H9U)S|>=I:"2$< J1ԚI 5Z(+%g_'ɧUo E%h*J*J:6h QқFw\Oʪ֮DN^i쑌R )%.q#FMJY"ngG!M(,͢b^0oN /J~5 ,f˿yqB}Hja>Vc{Ԛk֮4u }E@Bޯt@,FE$gAlXdScjͤ|v.q ._r% yI4/K,,]Sy Y~'N)䂬gVktΦF2Ȝ i:YM( _eekה)ϜyUʤ75[@h~a yERjHF=5m]OS/|9W_ DQ5 u%g, X(th"MXm]nxW#ynݲ<(+UÍG1+*J}ÍGk[ft]c\^ZȓO7t:R v]uZ#*z mԚa*@AC0YžAc4d\M& EKjjJf9]&5&0edb2VVH"-<ԯpҩ5wA56aS (X$shcGXrpQݕ8A`4uϊlۭv;~MJߚ"h\ Z _T-_ewڽ}Lpn] 5%K3^O-Hv!C Aɞm?d7f {TSIR^%c3 Hx!RkI[ W,:^i4H&8"-9 ,fNt4=&C j+H`1o(+]0onWjA:k$2ujJL D@Xל1V2} OST;v[> ^oR"dRX"_I) 3dqH)::Q[{怓e㲲h68\YFCQ$IETb@XO5Β8FgJ%b;v}A& ;:_ƣf)L1Ea^Eloj#ǻ[>X.N|N29Z t>[)XVbBjygKJK$ V{ ;SB:9#wg:l͡Ɣ]se&!9MXm6rwM2"fsŔeٕ'I1y. LpA26Cƣc::j-"j?K %$}d8 m}c3vE?>^pFyQ1s '$7#BVoc ~!ڐ~d Cޭzs/Nwtm1TϛK6KpA onl/ k~㸭~I+;e;wq[*kmAy P7hԋ^},Ї ܿN/׍W}5h +5c;ΐ3fggCs9~]}xwǃw~!R>uŦ܏Hm:5$g]xGq qfTEaA=a7CƗC/#/n\H7?*kH勁!p֙[QêML&O$0C 7J3|决gd(DpPKbXkub2ۦ']ީ,qY2 cVQ)"xs6K")`w{*DT-+W R%O$ 2?3QhezEYOt)Wv*P0KXTdm< K[2B>蜋^92]fHI_TBduBaDMÏb9Q#qu LyqV:jMGsN*I'.j^>.uėa8Mo&: FS./]OI RG!?Y~Z=ZN7.HU7q(%{R!8Mǐ.lb:jO[p祐gMf7`*B^ Ճߏ_[e@XY!335J!`^WT[oWRRcUb S+@V&;ҕ V_1,>(U6# 1f,"X*FR-=nl{3RA(LCZ#?):RXB܉4qn}"e%_&;qqw9дB EJ( f:fP~EIS)3ժ55OcoFks{iaմ[͕ 3ܹ{O Y0Am|=8bϽݴ[c]DqmTBq,3=Hsss n' |-/Q}, p tRvTr}R1!!^t0|aEpgeiqdC q'!X]r]EfD1, pVE~]Klp)Y(濉TVk1YMH80 fɜB+uei1G vm=Оݽnyo9i:tYV LbwuUk>Un|6L4 ^eYE||pskqnݶKEy+eYB{~pcko#$pV>s_\a^y@Uշ*>%ଅQOv"hsk.tyB:{JI\Y^tR:'ו3LT%py,{ }!qPo^戨j f!/s./V}\]' ݽΞG2]n59v{ҙ^kcs{w~n- u[VߜoEصyϺд5oC'''R^]9=~p5BgL̜nJI|'0G^ɣ}Bk d$',$ޜƨU@>),DaXM֧Ξ+adY",ׯ^=|!2 hzEbq8A/DJ78h qZ6RVJܹ tJ@s,Q^|aӖ`&#Qʃqg yhH9KZF}\̞;?NU,,r/a~7' 2!^wfr!@U<%!*I"85/-7T&'3VEUҶ2g坰o?y<9H@7x/2lᬂAZ baSwֳ&20/_xiZ0`pP mZ 򰹈zc`ΊGXRtêקbh *ҝQy<0oVW߽ŷ/z^֢`2ldɐN)#88=GC6D \*>59YyW !L #,*'Q=89z^wq{>ֱ4ehJXi@Dh10JrRjZ!(BW'D`Te*a ޽{)GΡY,M/Xцc "fFl[bPZ7RP)jtrrBy%ە.vvu.ӟ8g TqVa)AR2yQ2otY)?~() "vx'%=BU:]\S;|JXYsTj7k?_3es€3aډB'L-v%<+]("fyGZ"Z B ʰҤ<=' D 騞YQ PYiuĨ }uد[IMf)Ƅ5ꋶ0 BF\!u S=Rg +A;]W+ "Y s )b;UtScsU%-,ILڞm_q#z0mgYg$aiH胤@3Kciaq!$d(r ?cV#YCyXG1Rn)g圪 [ ,Qk'J ,K{yc.ء w uI64D;kT@:x q g V }1aVt@thoR,DVO1 ![VNX + 6GRrHzcaBr{{ ,-@$<(Roɳy^[GBpV2ggoJ[XH +bk"]`Ց;*80=yMŰ !J -Lo0#H.LH?,KR@;;.Θ3co5c1a1`LXc11caPIDAT Y>8IENDB`PNG  IHDR00WbKGD pHYs  tIME .IDATx͚{xT?9gf2L23\'! 1`&BADVZZXS.ږgu-E4ADIL[.C.3I~#ҵg}{~> { Ă @@R:?#!{.D`^}N,bWhmmL'x s].ߌ.$9N?HNN=$ q(;T[~ss;]NȺ, ̋$_~@sOkh+BXrHJJꓸbA..]*PĔN1Xt'|r---_K.-WǏa6b#a3[U >|D"bQ__/\.@{5 c%I۷ohgO&O0'I/ER\{3y!D""77W C3 |~˕)/ef~Zp<6;'g'yy.:eEEEt]hTb? ղ%ԑf/rCw\A'zuT]$}pF[yyܯȴ~>=ceQFYa̘1\Yft:{XYW\qExx@4;I f׳\{͵mjE 0L׋s|)//J<ع 8q"-'_~;Cbb"6lSRRumA=Xli(,zԄ٬֡ǚq-i&q^L4~^2h Q[[+bժUBUvG_usRQLg,gSIyK}r^)Q_ܮCUZZS@iiOƍǚ5kn &`4Xx1ӧOgǎb14MCCIx<466\m1{ߌ"QWC r^o#61ˊt7 3?]iŝwɒ%Ke/^ۻl6n.B2l6 N'(Z;Ў9! %\6h0)ރ﷝X '&o0SzdͽwQHߕ{(… DBB}FYYVy{4`0uY8'ӾDB{:o ~ߔZ"uM=HJk;P.B& dێvMVAab?Og'tH!L#7HLf8jHn&e& 42!p_y l>AdO`\x~$ifslz6u?&5G$r%^xv''))Lc#LqgB+5_ (Lkf%dT x{nSƻ~ ,nY.>#`WsPSc;d6)N',3\6~#`lݺd>@L[_+p ]ǔY??'Lnn.&LogA{bl( &h·00e܈)ƞ&mttttfuvseh\lrss1|.Aç^0bv;֭#5- gZ.C%;;Gyp8$I9U.˸er]yZA; ыk/+\+&9E(gޭ|?l6t] Ӯֵd̙TVVŁ@羣 Z[[D"Hz7-aw@~,Ԁ:e^H@c\2󩏜;х f y߳._Kw uzm!1ъ?( A B|i$!b8Q+ KBL&ƫ|6j>mo:ƌ 0a#pƶyM3g/`m̽:us(![8,!pۘpg3;ZXmM&U4}A74).y ;Nt M1&$XhA kL44uyi:)58O14?0!16ğ|ȼJ˰E ;x5}XGIݰ6󣆎r % 8ouGηDN W뢧Jgt'4#4ʉ&3d#!/?S~%bGH?_}cc`_[cA bdF~Z=KcȨq:&wX>᷿/|ɱ&ofRZSҮ_%/sY^IENDB`PNG  IHDR00WbKGD pHYs  #utIME2]IDATxytTU?oE FQMAE28?6:tF=8LPqYuDdOa !!;N:#tFf'u;nUݪU[WڻwHIIAQ$IBedYFW>N`0(сf.zɤ A~7v<~}0ٲ'NY9z(f9DeEa7Ij׹{JRt_ 6\SL&BgPg˦Rx^qYOz8 \.Xbyyl(>lMz=^ii&U]ף9?#ф`h02vK"Bf͚e&|̺\YV%YVwb{^ rdp%%R0FRSЅ@\~t]G,Xu={BlڴA[[띍&NZV5MN Bu$'#ɈVʓu& jꠜ2cF\u8<@Q(id2=Vc @5!t F!VkR-d%mGJOY} B'(<ܼ)IŞF.$&B`yN'<9@R^F$i3gܹs1ɶmعs:Ԡr[-vO0lu ERx^4]C`&M 3z}oʉ'hii!==.]EG/~qk~,X&;%oRRUkBeaK`0ӊ{c~\.TUU Koր,%]h!HC2!vrrraTUE4!IB(sۤ22~ UBAlv3*QetM_9KD $a2\ dggGi?-Yu]' 2dQ^pP(f`0EDx`P($yaYTEQfJYmQQ}Yo~lW$d¡0)ӛG*xFuꨬĉb00&dH KC KN>R^^~PдGynˍ/69fΜ-sII6,V҅E?d:]EPC]].1b8fGKK 6%.ڼ]H0`xZq66ijrn޼YHӧ`[vfe/:z8@yy9MI0n8~dY6$IDuj/_kFJJ.Yh2މn%1)}jbM~8;Wz=cǎiv9'gMK/}_Qal,amA߾Q_eʐB9~86TD,~t/G9[ Y]᥶o>=W}0uZZZ'Z9KNxWxBRwg~>cƌaȑL6sń`Ŋ:t$l6,st^5ofs$Il6#114bmk!w<'kkl {׾9gΝ\UUYxq#*lsd[.pkni9p/vǠ d'Jrr2ʇ~HMM ,r[0L> +|>BN>M}}D,d- rSҫ@ PEh… HɺE+>bś`a7ϗ` 5-%Kt:ɡ?]ө(Ann6$q8u#Fk֬AUU r9 W/DKm\nnUԫcMI8t̙3Y3?5FLb?OL$RQEIVW8|!fvI'8r8<:[ 1fwyz2 yKrߧWȑ5m4ƍǘ1ch~3M;Ψ( `0HQQ1}RY+ShR46$$ deg=3]9rF%~OVݞ>p@  yUUs l?b^zGӤd"' s8 C$ց(Jvލϩ9+FT/ѣ\i7qϢ5|3gE3d{n̝;G<w[sl)//ejNQٳyl̯6nܤ~M~=@@_f\ -o.$I)))b֭ᒨ>O~KЧM}]޺[[[ 耾tRF|>}z\\>$%e_}hjjBx6sƘ;-7[ EV:;4i))Qc2gʔ)t̢Jrr2B'O`0駟)333gy駩nG%Z6׵ci\j³9DoE͎j6l#<,-21{IJJKKKc)++-*bJKo`0PV.{BohFRǢ!Kb lځbLNNN BpZ[[v;3fLG4غeKA*lݺuŻn*qjJ00N@XB%!H,!z6l`Ŭ{qF#G1O%t^}UfChJH]zH-[^j' EiC{hn> :#g1Ϯ}Liz?<,"1:y,$%%!I-lc~٘؍e [,fxlw,yy3>TK$^hR\~sK̇TEy&w¬4͐zܻӽiB%;x`\>h] ` _Uϙ3֭; U[87V&SUk:zqdt3X*7\wxa}}l)ڻCFPy{ SxKa_#/YKRmN2Q Ǝ݈s Ic<4QoƄ# ֮]u/\#j.zpUAط=ljտ=.lݻwۆMKJҌF}ҿJϪ;(?~62]!ر=kLYB2T6p³+gi8N[z:{OjIMMEUۣϹ3!%|<[{ޓ)yv*rQC/ 45Ě5e|i>z*5=( 3{)C|\iӼ 5?p:HۣQ vOզ`+|Gfb0ihhMMM -aO5k5:}owϺI$e$k[Ý- Iydr–ܗFN1W׿vi'{7ouFJƍǎ#9IENDB`PNG  IHDR ;bKGDC pHYs  d_tIME014"IDATx}pSeo&mI BBVb_X*bYEP_@Vَ.#w.]޵e;eԋʋҥB)7-)%/$眜Ж$t&f0e  M~/`W}b/ҖI+w"m >|_I4vQމiBfSqk(d& }-y( V/ cm4᫷Ir.K+@7pM)t⾝Ƨf"&&%%%9sfcǎ޽{ &\ u>Ρ\ݫ-B9i0k0Z*J滮Dn] ~#AȩVlCMc{j5}/**ªU`ZaXp8шhۭYFޮXq #H{\)Ojq˸g{˕WO}AO\]3MЛ%. L ?ԇf >222P\\WR!55T*;:SDDDD7w'[7ÐRrw%zuvuM&jbgv G!w%rW-+9;II-.CzwJ.] wm;NY7xqqqzcbݺu܊M/=RZNWqNOt=Pqg@| Y5:7#){O)} 4r; 6iB}sHOlhH%L(؈M0 #ՎG:u*/^<$QXXdygۄ(ȝ53!ܿd\ӳBlJWj\n-%8Sh9)]*Լ8U k#k? _Jׇ+6rM iK^ >$\) @~SaJ+**BTI---˗aZQ__vqqqXbz?MB=;&vz_}Z\/orS2ݻJ' vE8T%a['P?r ǬWVO?-[nҥHOOǎ;pB>̿l2T*nK.l3MTbu+q)~ !T):Ojs>|@Be6. doѐȟ>$&&>磺ꫯԩS-[`Xhiʔ)`wرP u'Tyuռ8]r].3lbS%' Z*J`Z t,U/P Muxaϩvŧ~ t.纺:aĈ~eU*͛Z7mPsW^"""ɹj&.D,Hٳ3KѠݲ?R|wpwպ{SOyեֆoy(:,_$;zS>&lDCz Ƚp.w8tڝ[| ]Z4jjj`;v _|>Ǐc޼y*-Fn.K2s=Csv϶} rW!)MNWyzSgi8z w$|[l[a͞)X 7Duu5݋?3<aoDDDDpxKg z ek!wz}BFM+^,bn)6sdV0|뗪7Θ(1GU~SpJH}Jo`@7cڿӦ:c򊼦5o]3nT=%yxjq6g9s̙3hooDze0w\L8~ڐ Á&b{~^Qx]ra|cNXVXbɓHLLĚ5kP^^ٌ'x=Ν;=-ghdYDӫ. =,Ya¼Iqz1~şkKꫯl6qӳkۗ_~ H)cޠœl=pgQS:+LGw'pIhv|gGnOF*Q """" O5g>>鱶Om(Z`yz~@>-/ƸbdhٿhՁ .Pٙ"Pq39Qr:t7nzGp`M|6Y4_ǡ F뮞>I_X)LOp\ADDDDDDCMv^}<^ *`coZKz 99 쵅H&Jʀ, <>YkhJ>-KDDDDD~ dO#x{ tʯG1o'S6Xꙁ1NE""""xadZG]H|RG0 """"A)@ :LTT@i<1B4kK0 """" 1*5{kBSk .I~ш:0{) Ĭ;#""""` fQ+eƠxbSF8}~$2F)QdcHc v0 0AQ (f|ҥE0A;n f8[=VdABYY$AD0dJ)zluGX4 Hd/LjxK!F˘,` "?0S{MGYAD c G9+-tz3'5LLA ^Y \d.4vo&$ADQ.)W.0JLLAa=QHs-9(\I 7/=f QV2&} 'K#gR7\WoyZ&YzUSOs& ,Ȅ,z׭7=! 0ƃ/?7|8/ mWfL#Ah {ll-_GaD~I]l<_~)=1I S# )ۦ` i=U_ynx@/y'/MG}8Ah?LXԗwMK{K  j }dVfTVӼ h25go ?oA$IdX>*FfPAyC Iokye{YG]iv>]waw߹7V; "kB*p~PH{, YmZа(?:r<@ssEvO=6Y~YM]2 d>{Qڸl3Xz"d[qo.GXtiss33k,}k E[c5Xat0K_z awH$N;4… Bέh)AD;R{a"n&}¯Y᜜|hG:)WӴVe}}q8>H^/,Lİad [Zé'MF; KwOR](YT;fdX?3G ݨࣩ9$Nر~y,zfoC B#H^wOd7իO=vHK_o &5~9yGꍦ~ 6ݻx2~B7A GLFG: <ࣆ/K/K.ygz뭰Ug/ cP~?3) (@+?Rp 5C+cZzl55+( }Ɋ?>o)9\|&Asaۻ7}IG U!EldK27LP)فV.赀 ̂] ǵ6ip7>M.j[S%B j1;h^d 5<̧x37<."jfۚWÕJ*pm{ziq6O A\đT#M+ #WfG`k9k(`(@)*B`\}]~ph{~q[^sJ&$fM/=l2 2mYasbQ,^wKpkazk6O __iZj4!AQr8rSG`~2LpHrq#3XA:o)Ys41k2sp?=E-]  Afܔ!:1$/F&d La/d[Gop7n ַ@i-~>)W5Λ|{[ V d. .mx#r/c_}U7U{/>s 4P\ 7ߝ)}-UCC$-2kkV/xGYG!vD~VW{Mc%:vB%ADn8ЁՀ鑦z(wi[ y4 GDu x5FxnsSN0AJRGg2 $Hva&ռ'O'oˡQ&@jOM2ę~7ꯊ v0di *ɛCtǨ*-=ov쉤#8:` hk`*Džڃ184Էj?gFa `M^H#S*Ad24HHj4zDeeC2\x0ay۫GRi;7@Chma 8$5E<>]] Is@8аwvMuԌ J 1Y6hM(J'(J+nT'G`cz`ިcb|SSv>Xi  l`(g F6f{o8oZbZQ(j0d+^i\a91xr]O.tW|7l?r(.>~rC؊> bE1, LaޕUD22LEl{]~e{iqjZj|c+]2 (5ʣdf[D|HuX +Ovdobﮩٲe c1 .|/䒳>Wݻu$Zly_(obQ7z[d ^]$755e/^Z2d4M[t?c D6m>?O}=qmns &FAs`ȭG㬈AXh_fƍ~W.d2O~k׮2e׿kֹ|ڵ\rΝ;?;z&FE2L0iip{-3e.lvG7,YMwww?5Qn8sL%Y .MAm Dy^-+wf1Dl߳e_}H̪VTTq7|K5k׮zشiSɓ?p |"W'Xd 5:`U)=C.D<ָ_ 7x=SQQcl``W^qF%6'w=ŋǏW 6 {җdlk릑DP2Fw4)\Sm_83444(0]vͻw޻w 'LvqǝtIӧOZ> p >ϯ% &Q@ղ80݅ [F7n_9svUK 3xg}֭[͗c5ꢏ&Ȉ",L6H IxanSQD"}/G=i/477{uξO?9XQ (~*u(0kqhiA=PZ'$T(<*ln}]wݕW6mZOOK/d8ri]D@8tGwO‘-S EHc|xИO?Wu` ?pkkkZirc=vAsIW_ָ߰ň&PPt/ '<;o jиL1Y`0-][U6F;cҿZ{m>/bx%StG2C )241R&ۦ`(U>}>XUUU`ƌk֬ٶmHn9zڦTN%UhOD3ְ]?I" `oPo3gL]]??:z|"r'@vD򲃙ίa(#ɴހb8ih?Z[{Pq;k)Yr9\z%uokKϋ({sR݋134S!HZ-k@2j~c~STUU}_Yd ?qD),4F"<dK Ρ_)%3{W_}u:rʢEKzXyWc gDzӄ"$(K8 A<^2c'*|3)!פnJU?NڃDF,De݅L⠶V Ъ%FcbG ?JX=/㯽Bu6s9zkQɣF Q)aƼMaC_ .kR|KuWR+O|Ǐ/gCccҥK:/׳@E$e?C&M韖,Y;s1wފ+ _ٳQ}.ElQɎesjnYPs.:u}r:^9μQF<ɕ뻓FqkhS:萤qc +0y>F->ok)ҮJt P|pXf[O]ï6mڴqF璱cǚ{op߿?wuz衇\wz職ߟo#V_^X]N.$88!uK@k^uڢ*7c>esjU뻓C}YmUy2ګXsʎu]ɕC}rFCP wo^.CYj,3Za\$ڣGo_YYɕ(EN?3f}M^foۂCsg(H ߷RAh?,jzrx{&5p{kՋcٜ_9Funm*7%,8-u 3cE 91#M % bG2ƍ,5PYavc x̒L l511,%,>+\J.$9GqjZ=gpª*~Z:yi=3g Z1MR1te'֊)T]ʎa[׻o _q^ocﵾ;99);by:bO;>.Q+K*S>]\u+l[t36Hhɣ,#c3|fW$O)srLؾ/R .,G̙3'N-_p8%݉.n[uhQ;Wl΍1EnuoǤ&- ,}0n[uym9 Sᆴ;y=O,q7) TVC#EϿ}u'o[unc3"ōwE~oRS`) ⳼ɼ.2R2 ֖suN:S'LM2]\i~pʎajOg,):d IklN͢*=Zmsa  o[uH Vt \U))3leŏyGw478|y,P<%8JW<ңt)Y]w ϟ?f̘_|y*C|CCC΅c\* IDAT0iҤ[og?w/gQϟj*~pcθfYP(Mt-g,:HpeVv m0\;)H6kpo7N[ߝ^; IZ<SB vSώYXH]ԗXc0n[7▅s+$-&^eG@dG` 3חv_{auFY@)4<˝ϣxV?7^|"g0a•W^-w#[ljj'v.>l]oYPZ;Wu' .`$ .2 i,z3² fkIV4ΆPGa ~MMMy83g^}MMM7tSOmmm2]/Nfٜ55;{rx[A6{pk[s۫N[qPl 9`8KX9E3wE>VJ@aVv}ۇsyutw,<Nܽ bFD4P 5@MC(kўuQv!Qln9:wڴia Kuu:k},[lѢEceeԩqup!VVmG[t/uͩrgEWr{n4!Z4o]P.DOr!\\PrqhiyOaAa"Zd-*+!2.cVQif0L.Mx8+/.fbWh)mkk_/r†s΍}g͚%_1q˼85W@E+_x{n^]/s:]{0yRhs bn%La"Z2)iΒ|H3uXc !PGB[5,Kd1]橥2^444?tco}ntQQӧO.ag! cj\(N뻓uwnKBZ dvjwd)ˢZ<1]+~?!QB`omѴcn֑X.(,P(O aY`X B0P1D V<1\&J!Rp;^ӦM|ƍwYg})5{G}c1s)&O,]v/15T6faʡysW,K޲1m7p-U0 ^T6S\*~-Bvz' ?^,d(1$JK:C0Xox*\4iRO|38}{h466~+_K7n[ ;RZnjjT:cSA/|3NNI,pv}ΰk>^dу, vRYYM=58 ipvn"{.2c ! j.D@"38ܚnYeS0Rҥ)å!p'Əj..ܛ.K.$cVTTH8ow96Y`Uɨˑb9U2vT5PEm9ͤnc6˛<d,1#4PH fI wG:c0Wȵ5kia2kXGfyq_*%",v;vl #Fĺcdž2 }}}ɤ_iӧOWIajllS:0Y|]*HM\Vw'7}% <>D|;Ѻ> }뺓6z?,)5~ebFV?`l'pP IźST4R^lZ-VF,#95M}&8/ԡ,vjii ?o|gDlll4i\p{챁^达GٰaCWW fݺu*ū+**t]_ syY+ +;9{q}K@ Lɕ"+cXL&>"z#.*뺓.qlMY#1g;2ip 2%-1F̍^2,78/Ρj\SSj#GtvZ۷og*s-@0T Keif+ַ֬Ys}SԊ+կvur~njc(ā°;)jL׀^$\)Jв95 @M\Zs0. koe:o7kH D~;y#N{bIpԛijl:y^s- kWHxىb$Nm{+m9`$8TV)~9]}zǹ^'<K?qt?jم\kFe"`vO*k 2kǐ1`iD 4e9j8z8vme# ¢P*\3yw%۶m 1'š mNryNOZEUګfyv霚:<[^"q4y(1 nsfm };Tz@95KԘ!Ρ1뢄K6p2(`(k` ˣ,aUǘG4bj `f0Sn~c/*;:jAsfpZZZ^uR~g ^0;/[o'?IQ_pU`.Aޝ#ѽ2Dzb g([0bI39S- AhVf 3$0][%J. HWmnzw TM&֭kmm]~v׮]gs΍7ޘÞ}e#p%x9?K]m8liW`+rHOqM/1 `/n 2lٜ ۗS`VbrJC/oaϞ=f`Ttuu=C]viu֭[[ԧT*RuтԣO;wC;bxWCf #(ZԐjXg0c _ #CPCp)7,4upU Y42"cLE߳a,:G9J?ÌI"1cP4<h(c bGf14t`n [:HKUJ< B<ұDpyd1l t*;r7P.& `p?^z#P_I ,ci0 UCDD%@O Wļ<4 /q)D1 ;.===G{ }vE/.5.kZ4 8x݀=g I?D% k!3cV !Kg8eOorȶp}M޽\D戯7Z;/j&FP4,4"dKff2l*Zz?F 4]V%͒. (k.H%%QQܹSn/Q@9Q0Y z4OdBYJ\dkf 3 ˠ\hTDK%T@jp.} }{I'>Ore 5M1cFEE޽{{{{o/[Ғu+iM[r(oCdw19XVIGJaY э %FGriD"mj'Hi dJ^^ct0Vbݻw" {嵵ܒ1cnY9眨 ضmۤ6>r;&qjp&̕6PSzRL(V Ì1`)t@1&5.iH"40b. maڳg@ NWWWOGsB]qу#)A* hz5"/ !H:BL)=fQ|;4& m&M6ܣg޽rF ex:brkjr͌&iPORr`H)bȴh:?(V&K 0eܘ,|7n #?n˹zرpq۟vi]ifɳFaGGC gYJ*, jFД=kVX,]KhP5ڇL%KvJhivSTydxqsw&pw֮]Nu=_V]ve]vY_o!xV[B]ޅꡧ#!5X1`3LDVYfekd*(-TD;LA(ZO G(^N ְ{mٲ%)clڵU3Əёn)+.=N̝0hK5AQy)n(kXGz_{nL&ۇڷo߷n~c/bݯdEuG'FB4{ܩ6 &9J2,1\gh|C92/ vt>XYPڷ!)\J nR]ym8~m?7|G߻y|4{|_C/>ϟ7oޱc ?{O!(X\` .xF|f_²bZְez++qQvg|~u#BcۏԽD"+݇co?}J8vR6Ɗ=W;Sf/vpuhUpʌ<ȟ:aAc޻j*~)W̟1j`?IAߛLq)+)b7Ry-y >[ΜipAb8s=H1ۂp+!].s\s]sWo77txxwqNZxmK?yS]]([nſzΔ?qBk^\ .X=1/w'X ԦHN0}wO,n̞fup/=rҼӫUڏ~Gr,:a p`LI DѼOESX42!b *ZeKO45/Q1vW6%co]1~\JjLoMO%>c,[޻]W9λDEfOf59\~=Hϕq6(kpҟ#({%>0e`jM8 +Y 5KbFCShi3!S?G\ЗL\!BԼSL6_mBu L?Z1⽿yaڬ-vxx'?}Y0簺j8<|r>G9Qݳ鸾257𷃙<3wAE$S0x#;ÌG ʴvp}\ SU{*4Zؼy@k3'aM~ '4O1:zϞ=?&늳T n'&;y?f5|V20r%G/tfOǎ;} !Q˕'# %\(TLa58w4!юZCqayRjzu#CC59i%6' @M߱cϭzwbgOֳ>F1li ־)ܭs* OF0dG/ڭcf-r F<({$5D+:+qX J|&z 0LgV @pʱNk̜R?cӦL/^]c#)-pޱ9Dox"1=L=(m2s.Z,dJ.,^Wd{ 2leah%"g IDAT)[\FEӋ;u n(E4! z8wvC= ۷kwӇ*)"3>vꆭ[w\ߠj`DE_4cы^3v>䷐Dan3;ڱ0D,U> k-z\}ﰼAs#-.x+u4 ܷGYaOYC[S_ 1h֩Cp(#8EI7rT5i A)aYU# I),uGK3\1 ȔYEi #`)FO<:]%ἓ)4|9avԭ߿D;Ԗ/l48Xc^{Vc'}x"1pB2$g;AcZ:i"_9!c0@h .,YEfqWY854%^y[;oLkbJIOT=o\Ƽ}<ڪGX2+7 1/5dC=S:;1{.TXPqG$ k?呼atuCrǶ9MATW\q WВH4PlhZgH&Պ Ʊn4i_#iߗj q%k^*}7uWO:3r]CTȕ%;&󃿲+[jujPp0hݎ#,5ؐi&5&j0x4ZW%L ;/ 0:#ψ,~^8hzrnx7|-N8 &$>p4jԛc 觞rvDe`1Qi . Jy0pk0`0SDc39|0`@#/!# 8ͼܟ[M3yʌ3അX3\ys4VNT`hվa4LQ6X#+ nLǬT%Ya%ipQ!`)l-a }T`h9I'`|8Khn>8s8/1Vmlh܏CXROY0J7}A{ 4z!2@R;LY c ;Ep]6FXp\ZV 5SWֵD^ϼY Ϳ+++O:yBΝU-nygGKU`"j0݂3<欰dwl),0C8[!qM;tfse͸6P#m@Wrٳ% r8on:'?;;IJ,#i)US}̉o=X>6gsɔ)SJ Ɔ[SOQ`ވ%i)y lfC;𵂳$Dl.jhi<#I<mO^z]\RUU5K+&K?qf^J-`!xG^<-z/#c(V3dS3%44H ) A`ծfDe+d&w}݆>[;&ߌa/ bd9ΗD4jɭraрI(N&`..Ma)lV@;h;:ʻza W˧N֫-<[, wX8RTfs/IcAE=o10^hs33mSXGZ;دiJP4Ew;HcDp ͵ϯ'soj^v/) jp6STPtO?&e ł/ailE, F4)aT&SHs}VL,UP:W .s>s|[Zq$g]cxӷwA1{19#Ay&y%H{y#i"$Hm|4+`Z-4_-uP!YH \a m壑3]<2`8&utWwW42MXJ<Tބ_KΝ`y@ip,jp3KmFje G] %|J(nnl}O]굶uM^KB`ߜJ `9?f3`fafwu]( XS`d¡C-_4>Ν;g3ι͎htn8??:̚ulwz~:`uUjxXro8k`TNo"wh$1P`sJ@Kxd()CawJ26R`B*SZjW86Ͼ~Bm4Mú:EGSnc}>D4J`oV!LJE n ̲L(ehu߷֍fQWB`|޼ygϞ9sǎ >p@GGǦMz뭞̜͛v#CTےnꉿhęeзED0q*:x1t3Cq%#L B΀!1ì 4lk.k\j  XrTpz&h}z^zima8cxА577wFК# f1MK/ sьc3i"{}ntI K >V-aq 4zZ8쎂2 #JR)*fJcNig㫱476N˅s+]"r$8Z9bbŝI@#&KI-95% ;跜w ]98+55sC IQ5r,h5յK'n43t־/zr:{œ9]>! 1̮)Of ܑS^~On2=h>E-`,m2WG_ #yF @Cpdb+gpÍ%_xgw$K}o0IXݹ=19jPP 6 `*  ݨ9E-7F StɄrąOJ_y'BԽ=193B=dޣ+mI=C.#z}`>` 'D.e9&{ o}h&|iX7H4P[靡D…+I^:)1$);QX]Ud8{|ˡH@r0W!/rh0V`tk@2 ,Bp1 )l6(>tAL@xEȐ?jЩk0j0cܡ5S(id[[JJz.Ҷ 3I`i$ U#Jq&Dw,U`NVv=phi%F'3<  P`4W~'3:* Dd0#>t,IMA;ZДpQ r',dt54(3/9 y36uF0|!L",510ei"5K ]B `,8 " H##8 T8/4H5248r#A;XAK,k (ԉ,\RXT6t^GႢ8672e;²)I)L\,Gf,T`in5{L_,x$[ADLFp(k0նnMbnSXl' kŃT]qX>Tyg{"GD2qɝseIےb>?bdM hݘ͂#׆Fp0#hKQRb1@-cEfXǭp@Ai1ݻ[LOd 7\4z q(*^ӄ/tp adk0C3OIB.bB H›is@%Gr^XpU  h׭AKhaoV8nH&F+ue ~J<RY vpn6J@:yp-3" [~I%) 'aF3vhp1Y6su+&QbFR6>ex$`/2 43Mg+JDK}&p@d 3pL#/ݒ4e#ayjNGNqh0M@w4ŅR/(,S#Q3j -&X%&v"(N!AjvVyF'@ݥaO]]+e&=$Ņw9*IqJ؍9}5X1:_@n1YY7MLl%)qU-"3$H~|"m~jedGbqJ148czm@DO1YFn,ż{JcBiJkKCɢ*j`hq|\}`4ֺBj`IwU8m! .Ϳ0EEA!a~(4TR/v`j(X@9ipv?dbt8s yh'!{Q[u*$߆wj&v0`ThB;k02mV(!9"?Q(&­Fy/fjAY<4qeLc Y`#Y^qXѿ ) ~qSCD%٧ Vxߎ}rLI tYd1HAWLkXK.Dfi9K^JtĎK,IBфTꙛdA\ɸmU( t{k0R|p*b_ejVwDfQ4_~ kjc1"3 VaQ @NN8֞(E$98`z8rX`0/!Y,-0<(&g r>H[qoČqm _ F[[ e a #壙+WX:%I.$(q.M]=dNJWx?@yC5B&I41j>clQ$KK0Qmm`WԌ`'c`Ǫ33U԰ic" /壵8q8"B*A9/4tQrżo;svuE`nhVBV9_t5:`ɍwG#_C)2( %Y65ն#r} @f5!7{V@A.vpT\ ҕtRd% ,} w5e] Ѐ;;@]L4"qf^D72e;HpVа4E\dٻW FИ ҂KI%J4 n.ȔBL%e-S"MIF*l6SصŁ3LU@ z0V$;م͉A IJv̐c,%bAK8jeVi%zY?`Xq

!D_nȩRNpܐZ vjໄ,cHa>`ISaS+>p/3BFS/a!D܎6c#BY/Zs)15 J+I ](2K[3Aʵ$93#k%moȀcM\܄*K,;8 $%v%gV_wdc]De}\L57`uC9 Ck07a58kn]Aaֳtn4+%U_y$t9h^q-4y sBh0?X8HciVX6?|ᛉXfcG-KHGr0ZDk;D3vG02Z`0=Һ+8˫fu08!ha!g@dc Df_`tL,,#5?\R8|Yv0d"\f)IIB ZuUƌ$[L'$"jN0F߮* }1'!,SB:4Xe: +1=+Nv(␕ Y,ND fY%XӅI&=3b d"W5?a5w`keгa2$ ,<%,S(U̬fb0&Ka:0`FٟĐt5Ply-T)%c zP4I/LC!"sf1l(: ehȔx _h ;o+Nfi6! A֭uS"afjr~`Հ!{ d|bgc*UiM*hAB0j&@: {?Y_e0ǿj+4`4¤g_6&dqۑߤ5N3ƘZ-\VJR*-[^+cWL"X}ЧS!ipA /HYaYQݻR M2B~eT;&S2ܱ60GƲNtDV@D܈l1˰WS2MR1; kfhF;9MQX&:C\g+@LwЭ#jp k!> zt#SeGC@f|܎elcʸ[CzFxPϾvp1Hp|4SiIrF Zx [/FW$-m"VO6 Hta8Gjt(jpZ$E[ѽЧNVakUr k"'UsK0sQ$R^Ä ǧ$|}# h0iD u`[J̪'F. ҮmJo G-/ ߕ{P21 {Zy hwrp K A]m||i4Pcn62 H@MVҽ[yDf9o)+X 217FXpK/))c YP{Ad IDAT$9HɎ;ZPK Sz_N`6|֢+_p;e^PsD3*`Y~/DlF``霮uдZHUZ<gJvp&@ZLI̋+YS8oRROz/Uu;N i<4s#+QE|E 5q^3j5-+wd#@Zrg(~&C]0}/@BI6@"oq>ru!d攭^h:c4شGCJ//taМ `{.:(*Ѽ yef_{/ tBpa@e]U 4zIʴ {dG%F?ޕ.Je{:+ϲLp|{|P/V պbP<L>tLIn~ڃPylr u&H9t Dt)I5ЛD[K?TW1܄C^:deQq XJ JYu 7_^VcG]T*VQm /S  D{0IPppm6Ru>\U+ܗ9@?}+|v4݆3;7PZ&x.-{PEO ?B80+,:jH0d re=6>W\(2܇RAI!){`h%]VR.iWCy,.~1Yo;UwL$'85\e%]Ku]5:E5ʍ]+>+t5@ OFç qX"6Sa +e0zڠdžӡQ|ɏ8I([,y\璄V|oprIiW6 'H|Ý(1x`uf!Z NIb 1; YK[o,:Js[>V/RSp?Yy`[TVK8ûhxEgD$\4ͣe/EwJ/n8M/a< K2S5gXnD8&f~!+4kpl hv\! c{DeoCG (Z_uѕܞِ_↿!,p,BD~,h2h8w@WfEkC ?#a߇ksA v|-#-kv+\ԆNxm2mf}~LΎǃVԝtYdPgF옥G>^`r+P`uXlu!Ùk .Za8~Ƣ6Cy]}q}HSiۉg \|+FYҺpC@^_x>%GR^6sEo~oa>.?d#Km[pmS rp'a]&P2PlNkcq>2/$>E RjL_}RE RQz׭3Y V ^_NDҚ'db7><"6=f n-[P @b|Jc7]k~"ʴAU A|e0J7p+C>V_Q ޱ";m8D YHh[Jg<~0GyZG!Q=c}Vsd;w1}0[!ƉNܰw~Hq=vߋW iH~0". SZ.9d@XD}W "k6+rqTCw&p:mC.8xj455ZD0Hnqօ՛Ə_(Л:Tv&؜kE3k^X5YQhx/r7{`__V}5X;L2\*#5#:PNIjdo2k&Ysbяq^_88 Y_"]Վt:laYpR\SpGp+l%֝97"s3o(#Fg5Yf- `ݷ$;YYa8CqS&u؆4îjv Ytëd4w9jWScaiQ]קt] ZК9z ]ힶ^j,;; 3 '8Pޥp.v]:$;NS%{ime  `rùPVrWHݝi?GO}r! F\ڳce\ӧ+\.yz~`Y0vE߱~`ubUt?$ܬKu7qpHr<~Ò)~ 7=C~}HwFDj, x\'VqBsm,}W`譆_P[~3`+40ng;0n nd`'`b&& u(A &DA)jHK!(z\] ~NAʎx WJj5|5H|_:bDO5V>-ni 9XYhdSLWPXǶc;k8KF[ޜONgѪêEMbve&83Ţ:FÈn؉Eq|*IhO"h@ l¾+iS?܎]*+QnQ9 7/h+iƘ)GJ*"uXa 7%b]?4wnnPyEn]uᇞaHGd?-|C&g?ϡnYƾߋ.ꇛ+s9љ0N0C Yv8VM;hbE[8R2YF7ɎO !bBGLU_r;s!q|xrRaKl^h̆4ƺC8صGO E+2 ,hgp$ePHe/.J:$/ <ۇaK  }WV$Ps0`Y#Í頜Cp Dô ## op_D" o"3!j:-;ޫ! ZT\f'<uY=dGO@&F]0 X6#opV[e zv"'-Ԗuߎ T>?B`09ߝ'ӸX4$-" cki}Ybdp nF⣶% '}!aHrb@j8|6gC!qk>~d*S&;RY>27,k{#dW\ι~~NB???! 9b?@t%f4o A8xhhJZij`oˏ^-~vmdt6#GW@4":Mmt9":G!%sz(^aUb+t)aIk E5pFVm<-`+A/aM[OCkw@[5o9XV5?ꕧ! oVzuSkg!&yl TXv?" hsFZ-|ts0|&ˬk tʲ^AQK=D_Pq̹>%] 7뚹@G}s\Dzf3X ǍBndqh(XW&wՍ0ت:E6s^òՐw"]O"l,)Mǁ l4eY揗e~Isv7aKy>4#bLGk- ,7 Dyz`r(X5F'!J |: 8BDv-yoHFPauPnFI}by=NَmKO>tdE1&[VZ˜<5$I XBzII %uz GF|shB7n+NCoKt=Qry,-qƃIkt  rbJWLQRRevb;"hT :/d'p3R1|Gn}A,IF? J{ԙXXd,pE ϋ]5S5KtJ nnՠFӈm./z`>OYz|K l63Բ6=XX/wBu=Uz4.!Bڢ# q\-u`0 7`":`Ll1kj~8]`ۛmTpU {>UAu].63i&w%Yi5(6ˏXںU ǠR4 wB˽O9Q#O;9"5YmJЭ8)e9.Vj7+CWN~7b~'xS8&97ڂp*8uMҕm<:8|WRk )aRH/f8.x?o1m&)?}aZD"}cԛq0K X4J\4 O&l׮jC*^̅O=N1[?=i5rι\W JsHFcتIBoRos>Le璄h,FQq* eNM|1}2Rb~q3Ш_y (#xqϿIvFdlj}~XŢ4: GGL&|Y&<% |lbomy2ʷ!-;Ъ&A>%C<$օ|4%'AgYHK  Mt޽.=%r@ƚmB]>wۣSދV/ rny`(~+r&.]HgœʲqK|sbF' ޟN4ǷU7`u5:*3|ǮO~gS+SH`ia~Hpp-+&dCCވ2W 1L13Rm|=)R'LzN} fs )MrUV8(X~$jea}=ʲxLvԈfi ,>}qk̬v{~?RAO o)ShspIuanodIv\tD3RClsސ搋bN@bIz{VUspR+V$F 1BZ zMܭu@ـvky5ά{HG-x*N9 =εFܴ$rpXtSiC a[,*PʭE ](l?,,\*'CJ,3yC*pnQ_0ZV9qp(\/E?SdUWQ4j]p8&k\r3dG'7خN.`K0fejYs M*0w[c1HZhHiŠ"h7nCnGы^2V& .b~pv*0 =u %Jky~gh8O CUy|`J[F)?'#pml&&E[-+"eaJrY,;7.@O:=^/ҍˉasGG 'f[MiŽ9X/uy{|d͈+J(ʲJZi4/8.%,<)̻QPc0p `^cOGlMBe8 S&:XLvF> ((S1W\[Z0#'EyL1gӫIbqgV$ ^!?`WjXe,,JhD`[0S1`CW$la~ XPCsS::w*`qq}4$*µUlzյ9+=Z .r_8sPÿp91#i^K{&n[,[Ʊdp;bAUߣD:;|J}gK.okHێS&+T ^ f75#ZJ׉Rnf$Cӕ&w= > ܮ @Kw{BSBp|riHۼ.YWEnРjhH=BC\ʜkޔ(׵`GeyhR4hYTh#RUJ Q IDATgPPoGb/AF~wqHýu :I]c(+  ֞4-hѿJaKu}:*,F DY ]?>QC`<8x"X_z6C.`۞d KXwn+<<{9n 0Աso/yŤB DO+)mK._yo]L'YCV˪ѥTVH1!ȶ QЖѫ@;λ*b:{VH?TTb|}H&{9lA G˲~I*+,pvLF:3I#Z2'x,`Y{vb NJ /\RY+LqqPLFj(+|LCy2w`Y=֎ 1<`xԾ$~TDqe;*KM%?<a_]Z[<ziۋѪUa! H$p)b6wy۸R A5ӱJ؆zhuw# \D X Fƚ _*i#NW;q nݚ\.yd īrJlJ@^ۨ 8 |xcg~ Bk@9D{`0( W q`!K֞d,POV`'7de#5߲w?3 <bG7r;ZvA&QYĴg&!7DK]z J19dTTG"vx?*b&$WW6)&7owQoD↩@HiؓqU0V=KT6 ws+ IslѨg[l'z\g 2n ء4TG7#ynm$ZԦupٞ7l@4JjTD  2sD^`3ِ SyAEk4갮C[28fmKv X5(( 6D'〱U4֞$/{žp|@! `] HAZp"`W :j{ۤN0? %SN-T3/B,E*=xu8њQ^U+= dN@4lZpF]IlZ_j33|msVDg_ꒀo?{V 7˻ O&%x$a 7 4]U* ^ĔЩc+NS…֕RD+ .l:4׵9C`fUէ^̃2pZgp:~NSL*[YVW0·BuU[ _@ky[&1(9*jN&Łps =ъK9MqĪ0~xanR_W-ClT?Bft VhiQ w8aJE4)_⨯(HW+a5\:k,iJSeg0k+iGm"ZأV  -uE~o܏tVز C`ذ4qf%TJ'FT"-FKQrlBhh;u8пsřK@WEm>ħqphOZ~3V`ӓ%\r+[x"nglW-~n> .J5#sAK{MܕI8 U{O|t1]4!MpnDٮ7a:>绒»co^28S/#_a@4{ePNRÙl _!!.e&Do(!5[#8 Ve{tC %Z~:*W8!=H b3R@.px4]݉@'WZ+ $:ڱ eYi@b8ZˍD(W{' 4jS0g[œfnWӠ SkFȨ'ke=%OS큡WĘZhZ)+]#AAGgpx⇳ L<(IEJ h`'PZefs">[̃Zg ׶GbC`7JCƩ2m;'BRh۰RS屆? y<1J9X3;|$`$]%)ZmKXcj`k9*6LІ3X+$#pN'|Bs +4BgW i~ہ#_Z%pXV_1E&ā8͒J({ uCkN0DI: d.8EIGzeFl"(l_02 +?7} h 9z?i9{>^:hi uN]a<%\Gp?9XkX(D_V*EqwwнrZOs6֬$O+ya+R/0QۓL5!VsD;.O'|>Q70t%%uW]8^h( w|{%}F)FpKMϋ0|a6}ςN+k4\9*58WjItܿ|Q6S͖.ġ@ m0[ Ow?zk]e%s?)PGʁ{Vg}<eVD[@h/<WOǭ-7^(8U[9X~6!,IgA0p):>tI,o Pԝ$u%.R/.ߖppjYXޏUia6Tfw#|1&r6_d iV~H:ݐ0܌=x-ֽ=UӱS؅r̖!p/Q@? 4p<=m 0d/%-QYC,401h&};&`"rМx02R>ZW?6 Rgԕw,6В-9#V໑feb2 \ at:OTe4d{d@4,D媔&tc@3N9Xz^Ĕ0fMͪ-%|2 Ba58f̰BoHsBVG:J*Ku}DBXQSˆʲ\TǛ+|fu|9mY-"Q X(( LC.ȲX&hݙ\2ptc<=5\'0-wJiʵ[FV݈Y z,:%~U'I RW[f`i&h,92,c/f8K<Ds`ofҖ M̤\ZqpPۛm+s}f%C1Igcs|@! KkF_p%.myP'BSҫC{gWcjU:9Բns(ȓs8!$aƊz5=5쌪Fá'8J}V=3 P*%X,uPuP`çx tžJTQt\jz`ѭQDVF=4<%,upMaJbcy, 1M\Zh 8_BO4=ud0SdGP`p.'w(Y,<0-\fapw;y J|;InI +Rʌ#+Y jY ?ix07_5X9yUԼ1[(ZN~h!8ƓPͩr핰Fa;cgh3cxC) עWw?|aѯ@|Ks0:Gc3KX[EJ{ohsI[8IGtb e ~'z:r HEx=w2+j3?jHsdy4v%||ip|Ǡ_ ð.I,bsқFl[<<,<C?N~~Q"CʺuΊ?BrξȚE5rfXD]V/PRyPHO|uVo~a,?v,,< {ZG* %! ʠta$S̺RQ('W_A ,1|&KBw$@:}[{?A\XX ?YtsY/j!Hʐt|W^y`WEWއ(ll19ސ#B 4u\*+i**dKPX|Q#.gT1[-%|^f9׳$uPuӆC|Ȑ/pK2#`*7`3Q8rSabpqpC^CDZӴN/GVyK#2J{ f-?2Df9MհJz_|l :|DRGKX4PE}}$HIR0"!+n'+Hd@ޜ#c~2O(?&Hd)a, 9\NcR<=JBsE Seۿmr.k.`8v9*R `P<~44%88Tcϣ(mkިcBszW-CѩA &VƯkilbO|`K gpa8E$L;Lr.4 9j&"s8fK lWFvCp^6v ]%Fbgpgvd%u(a%l%8QZ,:/Q=p-v'wK"y zs?g9xc%(蹢-첊m::D߂$y B <^p[=GڕFw8xFҫ*q!r(Ŋ~KXp:hݝ&E{*ve;z`8c3C NLada{GB Vy[.?p }7BtjF\a-R K&e KLf;4,)8~|i rKK~Nת@QV"u o3qf~TCJwsp١HuX֡T}˧0܀>\wAkIS$N?Y@s([oJ=.a}S/ Y|+DGK Y^3R!apq-*SB5 >Q&NSFY& [ y |R X2v(}ぁq0jNJÑKsဧd<<㫓nrB(ګTʣ@J T.DZ0 BERGD`:3@7Wj;nSV DOn6>4]K!PpPcQ }+v#륫B>|0Z: k^pu"V,[wEseDaGqp+p0|k }?x. iЅF\i/N)gCnrSX+ʤJu+ X GO8<-O]~YԪ*'++?+f)wl\ џj:,mpT(6%} [;掵~b#yt9d<5 (/*Od)XGV_Ş°f(e١u\t)0p;G[gS j鄂L4џ`(G8\Cm>:r6z6"ÑmchSQ C3K_W~!rp!+z~÷V~#Npp7ĆMom93mEOE!d%'& 35Y#`RYu,H{@C (PVTQR®SIo$`mᓾȻr I/H MaNhrp1?(.5Uc%TJ(Q 3|7τ߅G/Qi;RT&5Je Iw!u((%ӡ?;=(FT;V =pN<V CxA9o~;ofs|_җt|=_v)SVZt/0hf`(:|l܂<"v(RE3;(M'Ҷƙ_Bղa;&hURzl<\O *QnL`ϸ,' `OB#f%Q=;Na ?UXt^D:*S,=#S `ZW`e`m.-ٱ&֎^ EX!'fհUq4~dJNiT*Ȼo(S 8VQ#OЎ5Ysj?[c`,G!%5 KB J@-l\VG.1)&Bf}q*4ÖUY `X4i\4T*JRozas߀AkZj7=,̾Sb:ő_Dz0 3xz4J8X,btl%_N HnI :ݳc2X.BZ~Q!FX[-L6qp!X̷0Xs$8a_3}@_bv90:,._,,o=?X~ Yƃ2IDATCg*v9s2a[BD?KYӅtC$U˴}(QUo}3ps[J|c).:bX GϒvfAȳ%:T!8m_M؎0#HDo95\}}}, urW=K`ߋsd3I,0 UQog([ٺݒbNcCE>| ENi%gk-Uf[:km 8aApS$V*EZwˬ;eS>5 c %y,! oF\o?&ֱh4BOMh>0iJYJxO#]8!p9:" ;,fCS`-5TVdy9'sVܫ.u(T]i@J0FQT vRU:܅́L,KO<)W)^!=K߾RGeVz,w(-y8es?:v0i) IENDB`PNG  IHDR X'bKGD pHYs  ~tIME.t IDATxy|ՕnUkmY%k / 6H @ K2c$& x Yx I`X`H a0`H7cy%ydZ.UW[[W/j=ߏ[J]99 (6 B d B d B d B d B d B d B d B d B d B d B d B d B d B d B d B d B d B d B d B&,%ف!Pd0[X(X5[@q]]7 DPdŒiA``,чR;oPiA`a'rQj3X0ɘǤ!K}qiB`𡮬bZ:R" rA _oPێ:Gy;UL>gbߔ>'3+,riQ ic^GewoT|TI5nUs0@ R-:)@`e۵o;iCAvWω7H ,繇 B&ƕM]em<\`7~aA݅:d XfbZXa+Uo0}jV?O4W G | ,qEzqѼ)xi̻>$-lwD|̘ ,` c̃gF;םoccLX]e(-e#`س/G:WcDlT&ng妍8'sFUx6*KPW:wMZ@%cĐU01,=xlA`eƞ:{szXO^߻/W \1-E( 3;ñʾ|@A̰3\ze(-ܿ;_NEQ=ꫯVUƒ|@AE42/i9Dd@QKKr\4~,qh?n#/GVLM-Ϩ5"81fy'fWgItJ>:%*t}O$tM5>ܣ>ܸeGV#&2f_,pJ Ϡo\EΡ6KQɉo}ak텩ǕGkj# h:hOFr */uZ$Bϋx'_-.oR+kҐJ+]fA`'oZe]}.YWcg _ώk2ʣ7b!QBnDYfA`)h?yMm te>sɦ> ;6-5rYX~QH-a:~}*:uN]gL_^秜PU#´ܼY@Q fTfjb1@w@=:mnJGat7n/xF"+u"X(UjǍ/C7,4)/> >7l: ,oQʪ D;mLDetΉI=1t>_rLD#L!F}s`@1:#*-It9}b~qhxiK-sVr2 oJwZCzzw""bRKmIDTWyߛ}O~~Q= 7eJbIK!?ܿ@qnOdcji1x6MO5T?ژhsjȮqeeucB+ t}Hl(҄:9C ihB-HSgqzq&6T*`aב'EX]{0jih4:u.1IX(/=nۣ|W&ش؉."J4{r "X_?<V{0&4ͺNni_)U%G&,h~7<ԑ+5>dJ)Q;.sJ՟6l?r+t 0RK&V- =s_z&)JɖG*zou;9`e0 [ 1|ێ :6֮Zb˖-g1xG}tŊvϯnk񿭉#ƓQw - |EڼSڟW\qcN, dEYr}7m4ؔ)S8?݇7~pӼұ%(@0Ac <<5nܸ__|"b?߾vCY&M7yUWUVV׮]bŊ]vSU(T*j{L;;nբ}w饗Z :;;ᄏ{߾L^կ~u ~a~s9СCDlBmL 0+({L[iq;㦛nRUA@Mv?6m ͚8q>kv֬YO?}饗QSuˢX0a1gj ϔ @`q~N->c뮻{JJJV+O=/qFgW~˗/_lY]]ɉs=7|:e)X0J< k+":䓟}j{4mpppk׮ݼy={E755}sΝ:ujyy0*^C/~ FWE?捍^ոq׿Ι3'%I/x"<Ӷnݪl;Ω >#F qPZY.h5͋D"3TWDP%suED{oiiЗ^lF3IL`|hg_?w˖-뮻r)S:t__wokeɪz\/y}Ol81// "`hOs݋%[\]]ꫯfK,裏'8oHa9z7bN-rs5=[ӌa!jm^̯/:P綾3up}G|]pz~}ICTYR,%L6 =|#?loh2co,duEDwJMӈx7;}Զ@,}^}I G}-Z[y ;ά&6[9DU\Uխi\1²h}gO74>5=,m)cNr.u} @TJ!׿"&ff⚗LrmYbŲe˞}xuRm1+T ":}ioҖ2]+:k,W_2/b^&k~[ǓYYҖ>giuۚ}[UҖ[U7C`P{4s]ib-Qn[ )eeط1c݉FOTV6 >Jk,Y .0ӺW[rv},U*ܱ,/혣Mu uűɤ}!ypFQ( Í @J(%eW\?JSOuov)++WSO ҂S\=݊"s)0xFCTXR̥uF%&}s*&5r.2gL_p>apU p"3P9z*U\ԏD_SXH4Z܌V@隸 ͜9/Y_tyнK\x ,/Q4'y6ّE5F[SuT~'. *M%0:lt}nat*++zK>X{0qˈEUwY q6h3eymח52X2HO2,qްuam'78sꮺ|u,Yp7xcx(qPJ?~0:v%nc+/]tiʫ[ ѧ==ew՗wد1ܲʢH'Pn>N&߇2Uo9LclHu̠\\#UCK6ٰK`ŹrnO:}AYroM;=P>SIF5=aUoٵZ{7{,+=2YeQR?=߶li?5zҴc]$h/ȣ2w]g5=t +x<dbëK+dY0}N뫪ww $tzzztGnCCCknnNJ?11e{>uk2^mkztUS̒>k*KO=Q}p}gg =]{fqPemL6V؁=ه䂆sܿR5ʳFQ+Ε?,?} Ο??kmfzᇛ?_K/Guԙguh byi5RKli_:8DG`Ҽ1f'pzk2j ]]х:f%UX@ D|]#c+Wz0o~}-(iey*"'hYp /}ӦM7n4;VSܹ޳ e]'h٫IJ@=ڲt[q%E,m)mM>SOyPl*7N?=ZR4ՆO/ryt{wcp]g욧 (xlr*9 Y5(ҽ2kdQ$l:Αȉ'8mڴ[~WӢڞ{b4HM1eέV,ցؾ.tƨb6޴Җk9DD+5+:]}j`&6~[CTy:{Ko]f  b$q%pIi9Xj ~D*R+}øsꩧN4)gH$6lf1a„N;:{xkIwӱ(d{$]*d[;Yv8D+7OǷ;w81)944;/svˢg.>^1 (lUY%Y;l8%t$Y\ȌHסݾ6Y|wZ[[_Z]y啊9"0X-5[Dt '\~c\po~]=)e!g 9}]kHdQ(lC1'` N GE 4&K,F>pPUuٲe`[{zKË5le]|5D$3fHde*O̢ǥ°=(6/)?jC{6>C O8 +U+ƕ{{履~OxoeeeֲXCS<=x4]og 8$<-m\W xᷨKO}\ZRw۾;f 3`hya*oP02`n/'Lj_v8vR)urd8^kT͒+@~~®≈uv|9aG&z<ƧboQ]4@A?00'R'YѬL|}* tڤ)禪ܹs.\8~hҤI---w} hE<6{~X5=f{Ra:Rg eŸڣ6uMw 0HqL88;8I_'c.~cƌ |Yfx禛n03cssӟԲ.. [`5k ;wҕ?CD5wƄiR.,wݿe~8{tb`]qlWϳUo9TtKfCUaBY ˳ l&]k@Sjm jլY"/ڛ7֖Κ5˾ 6u]SN9(ʬ"Xi>,Zʻvڲe }}}][ITNeV ۦQu_0Ƅ$!opn|׿={ }: IDAT꼟ʶ6]9\[[kXZ7Θ]Ctim!,_ۣ7i}%g]ql3>^@`ʚ*J2 Ca. =,ʶ*r㯼kkkspnӧOkkko ;wZ vѵv Y;DU65V.*__rP@G'o^\^1,>D4#%N{dRQ<7GJ$qy'lӗ=_V]&vx*an]wefbX-}ϑ2eJee߭R^^f/+',t֭/Zp?duW=U~5Fs!=|\nWΩh*I黲˓)-8ըӾr̯Õr SrN>e|itW`݋H#f|2K%>EQ*< b^,G(qN~oէ??׿KKKo߰oݺL=oW7 پ=MT3fX ݄,Uq(j,iзMyCCwu;o/9s#VSeԩ[aOs.袙3gf _y{lÆ )ŠuyiXRR"+[NE;0p}-sP&xj3&3-C\ͷ*f>PN[cUTTZȑ#.s۷y^xaժU_җnVi귿o΁+J{KD4vXO(l<ЖnT⹏Ca.IZ/r6ҎV y;;Yr}Ko~/Gu 462S{rAAha!&6**rH p1hII}*<?aayyD(^dl")Ar)5-~2y~zѷ."]Fww'.l+$k~+_imm5sΟz|;~^^RRr_q?c* ^gXنej><0\S \:`:n$4<FhyI&)rZڵk7nolٲO =&[d^Y9cՔGtցv,DoH$2<֭[ w֭[ظ~zݻwp %K\wurJk Ch!289fh3,IJ Y`]Ը7ڻwY29nGG?,Hc /T߾tݺu֭ 2ƾ/Xx\P+(:C j,cg8c"rrY,J.N{aa;44$^V2hO PVqe@9,1i+hK,oX6 |vA__L`ͫ> 5[Ay_ɗ ߽[aN\)hݧVIM\ \:t9-g[ZboEd2kz6%\\IMچ3SSҧVdUe)eUZ`"`#Ib065G=c p7CtvpJӊFf8͋fc޽[^ Ƙ""_1صkxAY=*dkH2 ۸,Q+MF4 NӘՔhIZ$ ZE"Fe4/o߾LܧΝ/|aEQMVRReY[n袋, }-N'*`!+M`tFXjM$E+u%%"Oh9'6͡W }XͧڳgO&SNWVVZFƌsϵ|_կ~eY:cƌ%Kfpηm&\4vHgpHL= mR޲P62Q㒴na*rHe% 1FQBߑ貧(,'nw޾|{ttt:$* ru3)t c(.~o‰r9g0ٶmƽbҴ#w#oKV-%@;=v5Hfq9bY|15:Hm^z%#t\][0UIjZ{58 _|E92-`bG"789,YhȬ,,r9,1 ֣l,i '{wZ,S߯}~`pp0g?Ծ}s?|?k;7~gEDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghij lݣ͋C;V[%DD= _7yƧ>soo<̯~`͛wa?srA(j^aLH48RCJz7ƹX~1P8K,N·~N3\|‰__{RßsG8*+}c:E555­5kXGzi jU0۷LJq,K|̾Lh洌ň}u}\슧P‡eCMM͇~X_o}>~[6~lڢ^zco[ѐ֭[.+wW'{Yg]z9,(e˖떽Mr.G`l'$drFEJ!Yۀ ;cD+GEJ#su{},X0|޽{߿VϞ5ҺIy)͝zd53ֆrϓW)Q:n7_;IJ2c;sw Y6Yze͇@QcQ8ְ-,BUV#㜸#ƹ~G*SVןK.D q6&&Tl#"SMkWWתޮ˗.)_?ģ644s ?XZeCj,cc&ښ?WN;$EHDݱwї?4i/vɒhmWr2f̘`o>~ז) /]G=9gg/K.]~ez!0 ̠nji:L>,aţ%EDvx$755M0{߸z_.?%*͙:Tڽ{uʌE vppgt婛4pEr:uKm{{~GW{Ez`$EcQfp06H`*mn?7o g3Y;51j'h?w| }e‘.>ǩҌ8~n.k4GxIeI-1,2f[?BK`\TybUW7?ꩪࠥt/:錊 }q_ZW}/?#W$cT;/=!^ڏ(nJ~oI)W` RK.'32O.EDmeG;?~/tq눢Ĥ??O>a$q{7zٻRA:u?B]DDgΜx@ƢӅ+~#~ l[qN-EDԾ0qiGUO=_!*:}v?E- oۯ:y}O(vP Siw'mX6X( <:d A.(i8Mb/>,\_Cu 佞;B[}}o/:쪪*"4?=w_|'Θ裞;UtTG_31 ']>, sӍ1V?n*%Z1FgG}fc4qY08} ]1+ssd|Ds3#Cq U򲊋/\O_N"N0`nKnV~$v9Rǀ0,/YGtcEW`/IHp>\H3e`چ\/c|.77SMiCRwFY8Y)AȊ,a#aaUf^EDskrE-_nC{(ǜpc3;sd®)~xֆ w)O'N d,.e0ep;e֨XDiGғg^wˏ+򒒒Sμxݶ:V+C&B q,.--UbAB(@1ND|?h,:tls~SrSS{iӦ X_jJG`d d}sKI-5X8,P .Y]`Q}KuhNں'{M:fE{"'DX8 0cfM^Ac/%Y4P nϣ ;]B}rhS7n{vv)s.5L4Xϋ5ЈrcIP4Ѐ9:,82-\6EMϿ[,w#[a)++p;xЁ!:9 QF&L#6tܜHd29Uħ,U"|Ѭ<eҩnd0[眶u>-ǝxWg`UR]!0eN!.z@6BaV>Bʎl]|ɓt)6p͆%;p6=](XKCY$f0+@` 8^buY2MEk+s~yi:,YEDT,냬%亮׶4 ee筇Y|9~J#Y9iv?K~uJdp @cޤED,d s C;ÿi*mo9ڢx:x{;EM IHҌ0 Y9KL}MsP{mZ~ں;hic1_4]K%X?C2?V!\ ĜݰMuo9o|NZrUtkG~[IǴ4Vֱdyh\9i~s:, r%^68+juQ$qXgƌ N=fC> P,X tD,19XRo\n$~ֹ(¢s[vС8:v0@JYR7dnƳCҾ!FadBYY])4κqW.툨W_]4ٳgO>nرDtnڴ>8t฿uoT@.:݅)0ƓG0F)+<‰NdԒ n^J74f0N D Dw}GWGs]{QWo//(tBM4JdoP/yAc9ƈ81&Xx|f,JWTv9)JHc5inXZpZ:^eiZ<PŤ9Wx'%ueg{%MS!("d.҅BBK!޾>Ph~bpM W.)QFcI,\9a#1ƈT\ξ?>w({YUcfy{aҿ}HxJ*l!D,BI^Ymq#Br]~}.hw܀Dxe am< Di.aE{h@`MT ݕ@ca;BrWTsIc)bB%XRwi,e#X:5"MTg8ǘTb g*g 1EaRwQBiCPw `(WJ&#e\"ib*Sȹ~׈8g1R IDAT[Ɛ1DKW ¾: $+Y$iqR8c3)*g*Ѳ*I `- %YqaܝFL3ƙ* ͔dQya2N@`'D4V^V%I j92/ңYTb*zkC(2~b,9h|!+2ʛC)9 5YWTRTq{c4KD `D /D1* -qiq٥D)\QI\H=\2J `㨱|b=Y_ /eZ9;h,"ĸ,\B`r0kt0XPdCcԾdBP*$MX>HʢQu@`@qߦFM5 inHzОF4׮CR"T&Y"GXP4pcOi4_J<4.Ƹ,+w.d,%4%BL0D7,@`r `6 ]˳.P \HK0mXcq2βf )F g2&f(C_E}D}Q'!XM(S-M62bt.e"4bi kLzY'7I(FDB]׈_b¯T r'E-`r'zQSN$PVdOO ^4/"SEẛ{R(@68v*Hc7Eف  FfLkӴ>#:1ڊsaV6HMЄ!J%HvKw;K:ְ K h?y[sdK(i˹ Z4"'.Q?ɽ1?#[Ōʛ&)ZWN}& !Yy}BceEBqUbƝB5D3YSI&cJHVrGiםuZsF~љJm8S8S!ΔdҮ=DF{@fA`rȚK[=5cn TRI9wȉ3 eV2NfY$Tle C94SE6sERw j 眈d7MRy> j9",w,@d:祏I2OFd jFJw9't?eEĒ鄊(0ۘt<X쑙R6˾D-I4-+xsp%i 49Cf1ƕ,1*b÷)h gq1#gs,[:ݠRV G|LAMt ""E9)r{42k(LD~ #5j*KűF3r1.tKxOf=?)DcVEnL4-  4Vȟfv΅8LZP+(]H[!{"# RuXRfyl-US{A`G+ wã"3W7K4k_绖`Z鎢5Ku6f 6FFTaa Xu]588ȫjـ{Njt!O/גRY6;LﴣDHX,*i&ˑQX#W<ߋ1j,2$y B"٦8h IZT$+59pxF}9@` 40-] 5.t Y8SIQ ɡ0˻iVFC`rc؋SO_O.9*e:۸ .Yh,2Y 1T#ch2vMPNٸ 'I ׆0>vKDB1҅,4 3,{7c,%Bj$پмء!fB`rGQ$i# b(:҅Y!]|h%LO LgI&dB%ba{ C iԼ!4G׆t2DnqJdʲfӅZ"yÄtuI[$'UX6oXi2  K sgppAA#{ycݞ.L^s=]HaUT `\9(jEc Stu$[ 'w3ZxjQ8eKf  cZJO=}-RB&FXPV$k*z0aq )eنIT3gYHځDz&1/c CYAPkxPv?pIK(ZH *2kjﱛ(ix {ts ̑u3h.S /NAOhŶ6LTޤMp=,`` . MTAFXv wy:D\ FP'Y&}Җd% (c(  !* "L4e[ 0*XK"LT.;x#ı҂#. x=zM0O?Ҟ覫=2 6_8jz2qcg1մ_FJ3ᙒ|`[,#5 =MUvd L7O憳,[TyIǒ L 2d! Q4fUdÿsancF3e3t\.^}E8!]?&D.UYZZU.4c <J{"6>j% sj,JL2)%q0䒅rw,Ή48F_;\÷FM@dk€8cd2yn\h7Anȸ*qiq+p[D˰[Jt̔,ƈA`!5!Uj.SXxX q%™Jpv!4V!l_HP49iqfB*ct2  ^˫y6+zLOK…EfXsLK0ɮ9c)$] .Y2*-ʼng^u!& NGwrQ.^%(X;!gpɀ({6UP'R)6xO2  w0$Rrey Vxpt.sZB~ eS; owsXKKj\2ˈf?Ce6/HΩCc'Xc#$q,l4Y.(gO9tA-]h-M8ۻPw1IdX h& K=7Gl/MWsΉ ,HQHc4V"z37vf;[m VpǗAYi/ӗppX8]cYDb !c'ҘXLh,dCYd1HOd jITRKZ"f>dC8ӡKْ ^4QkƆwi.fT[InR$N,,2Yg)͞T!k@27&S5Lc~?,=k,{E.QV++FZ"\|qƸZꒅPV>ze98Rss"ZͲC`ԭϕX\>m)nl\i,:8I~tʼnqk5 M/^YKCھD2]X(2+ ,Bf{&J,g5[Dwh਱\2O3\5S 4q,tmiCH %YE) A0^(be  h,JX@"h.$QksqQ"Β`!)': Xܭp~AXͳK0:Ȗ'-wX,ƹ]Knjk1.Yq:.j,J98xĂC"LRr|Oe571Tbe.b,h,^ϸ!5S̛Je Z$0Pj)KW]K;k@``&qVQNs@&xdty U2X<ݕd_3EZQ#;\ؾ0S6h JS+?7dtB/] KTI4[Q)\0uWlpp(D̲{[. $DUpv^4u¬QKJ/†3)JKz,"9Kd9[D s' Q4_Go 2҈kTS/3LQUp;DzUVxB&KC` ÏiݳEAc28.j,F>mCFj5V{wp{,t!*K`ZPL[* F(Ld %_'_2MS1]l U$ӘW!UL绦CY3nI㮀q(Ȭ1;s,K1._Xe"iDE),2绫!-BYp;CYi rF|Xw3\m1~3&9?WM@68z2yYJHɄ7Jiw Ӆ KĈ'/q '81F z$@@! bYX4DZ@dkރ! ?R{ŌI7n 2/5bFkGL*xC{0UTyWkB&\c1Ήk=)QrPqﴃBAXn^.҆_&ɾCY !F o*V:(-Oh,4u22"65\Oi41]c:. QU8MxHǯP*`<֑xe~i勤k UpOA,axAɄ,s4m1>'Tn-idFD\Iy:BYe09(J - nSq$!9}/=%-jQ\gED}`]tnB>!L)PVǙXN([;8?q]2ģ&N'+5oŌ:*)RL “YkcvaA*Ks n%o* XAV`DuK<>sOųJ&7gI0#-45qAB-fReL!5,7"J"Uتez>DD%X2|G+YEM/OHڧx=5LI.g@! ;Ǹ}v=L$=G DprX "Ο9D)=p"=eݱs_׈PXم:Po_TGG XjAM#-5ao@! l0R ޭJg5s,\EFa.s'IhT dz[E^z6pe8.V<(qEBck9řBR,%B]qt98#&&}2X6#qgph$'9zq b v7/u]r818b0y cC$QJr9?")fKB,I, wF6~@+xIJ, ,3k,E)Hce%E-$|W\I ZBYŒaH`b*  e =lUWJf;}ɘlz+ RD"@~+j, cǾ"8JNMq,RJ 342{KOܑZ4F5u+$zd1g]4U~:c9z{": zC6]cIJFW^chu&kĂRED!V)p,pbeBXk p_4q82 SLő}`R ΗexJȒvX rk=YwTkr"Wxi ̼LbiOڥ|X,Bqu_ŽПx{pɩo?gr IMP I*'qwq,HZ똼N]ք1:5cmz,w,]~%qZVYI:8tցCcwŕx˲ɐhfa[ʱcu7*k+=QK3kk z߳x ڽZDa6b>7eLP.vƔǺˀ4#Ifi"~Czn #蹻m]r\jz 0nPspN۾Fp,9Jh(k9B* /RD;s.G;Qo]~tZZϻrEqJmC9"!UbՔc-C᫹$ԡl}hLk$Tffd)Qf٬+$@*Ww.BY.`= ^_W:¯ϲb3+""nFؾ*8!n4y[l`]CYqtQ\3"nZq9h+݊,Uy~s,8Vɴ=Dz ȎX^Y,Pt?=5}9 e*F_hLNB蹇aPʔqʀ>r3\!95.]*Krq5 uJOi$W]W vʽܰ۾Q+UY.ZWЫ`M\8p,'e@,JXk\!`EV~EkSP GV9աk>GhcY&Qcq7_hQ\75`8WeM0sUlH q46:~~Q`U9ED(ܚ[%g8(;>IBXk̨H8p C(nK]vlnV t#ߴSH/- =ܝӼ;lYg9V7Ի!"v0,H]t?TUEߥlKe8mYrc_fP7iC̲6}2%r.ސ?ű)U gp,pA*k(b;wLJѧv<dMvFASXN`*Kq N"O}VZ8!ݠׂN@Ұ;H*FBVbY7nQɤ)T*}F r7 bRq G?.tO+=)6K!z=WJ3XpIRMP5T҅%z!eh/Wemd4BYἑ;fu]#Yp]Vy|;`B4?f{'f_K1g9V<1vմmXwqQCjv+!`ղ(gt ݏ<ʃ 'Iu4 )LF:,B(RemGw E>l'f:~=< l&X]3{!:x~Bk9ז{y?/ ڗ-8DSھ v͞!W].be5nSn(wl%v,/nt4n(" ʱV@gi/8t!V^YhZ`3JPSPW97RyWW*_2KA"@+4~ʱiQp0'+KEaŅLtٳ CD虚"bt(>ј ',繆@V9XPӲ)=k쪬 ;{W_йy t.9C- m+y9T+>Bc1%`wt5S{B@0*|RwpԋЯh"o \T:5 T k7ͱ hk<jl7@sPG9d2w"}PΠ*ǯ8 .@Bu^Rm A3Ξ/6el"M3MRv(F"E~Zb~1q\ն/??\Ԭ =v\q=rIE\=?sq`znk@i'҈, t ,䩰ize)J-NEwATj(OxQc b@3o7!p,hqF-Pv 'mt|ZToewlE| @6|j$ބ]e91qT\ !eX@p,K98V+|K^DBCN\2H_Via:Tik8\JoŁ) ~fĚ; %%p8Լs C nQC~ͩpݒ4 kh]ЪcO@A=+kVv4]tσ5JlX6(/QH>uѬp,|BqN+=h](u=i,I](+}3yUJ|Pk0l,꠿/eG5?;Z) ]hbq>_xKt+q+I z6s`ne[Ԓ4)0dLկ:k@?M­ b5{ }Mm*hEGa_zI$ĨArwK "chY84ps4u3Sx= "NXҜE#eڻ]GlJ\\DȍJ8֩~dĢPB ]~p/݋9f@i5E(ƢGCy6-λp&S,_5"02FJ9:xcWHwd1707@iC#X#~x[jZIqVbiXi'S>@@7$ح-XE:bNqw:Q3,D 肋{ӂ_e2hACV3 psTulr"w+BS5h|dmrEPpzpvYש8i|s\(:Qe՝>#zo_?7+@ 0}s%*&ONusE;hp$1lHZ΍7Q[/3)Q`ď>:q(;"%@DoTd9A,h$ 9Q[^:[9%{(Z}>sD#8Vy;#amW[iqCo[V|5o\̅z}BDr+ [wA)mlۅ.ʱEcItv=zXcAڒavTYAVWNf¹ND].na^FXLUnc= 3,{g8V1zUY=30á;NưٗΟ虨c(LN0 w=n:Nd ;\ cFS98XY~ (^,Ξe*'-IoRt Dȿ]47 [QOX[RqRdXˠ?q,j9w5,W"fUY>*+Ǽ0xpk78B4v/'L_Da!m/t>wz4%j1WS,DA i&9u(w0D4kދ:\*0FUExQ]~s[<ߚQ{Sb1f3馯A= i.I]YayN9ĴMq83OBTemɀ briLJ~ΊT5%vW>[²ΘtRͱ8Bcu]q,c1V|dTYzNgg! < kZXPes 4(/N cr,.ѭ+?(~Zb?yUV []V=&QtEkcaչ߅S_/nJ {|λ$Wd09TXDGpLȯ8Ƚss@.0~~XAKuqs4=Sg.nXpvo` T0Ҳww=W5RIėLcOa%|E~̑zzU'Kx,m470{ܫBa.]().Dæw Kz{njsb o]B-_dʱB;,X䙿uYoŠo:>oG%QJ.^:{v5Ʊ- ݲgc;c9`>UiCICYKRivV܌J#;*-hɬކϝ\#qcWR4o amK۪8ТP Ci8Kp,9VOX Ow3{,ns,-N ,uc9+ Y,xJ(=Ioc8Q}؛@WNB_ڐٹ#qjgZyc$ݒ!]<ǁ#X(=^a3{%zu̩MĒtʼ}jwnn0N:`@{±^8P$.e]tXPh~Vx{z}p79n,1Dat*(z+TKR>^B|q`CBScc&câ`5'(Q/13#H.ڋ(hs{VwCE5& [rmq,jVQ:As 2[j ]-q0= VCwPVSHMS|8XTbEć@Ыv׀b>ʱ&*zqջXg5hs,]$t.J5gTPXo6? NZaas AlC]HVXfSrXRk^@U_I& M 9OQCVєpYw ܳ=?>|n7Y b,(chMszT֑叡{5x ;}xvx )ʱ#eQpȂ39HR"ı ])~Ok q$|ڡ,0tX(|x hBCI1yJ(wԁT|Gu ı&o 2ʱfφm"?UQm±f(̊˸j 6z1*y9 TvyZwl~s@`nFAw ͊LP[c69V|W>R)julM+4BYhȅ^[^[pm3"\J,xp-qU9AXQӍi=M:\2"k"B.CmUvY4ؙ!~o,ϩ Z'`/pU+\)&޳MŝRhÑH[039C}vCʱIE [dѼ\!D=cRe!mWeEaVz&UJB^`4~tpK%8+e${ M& :;}ͼ D!Deq-xwk wY){?ȱ%)xm0'uOP;#UJPQY8/nBNj!XN):`пhQ{(ǚ=e`CҲPN ?Ӿ!7.BS>8y ?o}gf'#a >]pyo*Ctep4cg `+c->I ž|7gi O*~Ϝ;3,j΢;5voE9k(_Gaː-XǹB +3zMyi>UY%ư1(ơUk+ҲY:[M(u"(]6$ñr8űbk;;q= k V׻aEgx+(}X{%F*VòA1MQ"ҽ&3U^Rš icxMh4PF4 0Fk ѿAA]A,} a-\Io !|x韓B5OE"+8V1趬6'Q(PVGưChYz2S8 ϛ5d $D,X #pz|j6NJ}q,"$ [XiVh#3wxK5׷z g7υ٠57pE aXn S?r,WgeC9T"X>hO>cH wzt}z! hj gGnBߜ+/&ף7ӏ T cE𬯚Sns3ǺJS0(Jՙ6{nI=/˒?LDqp9,'T^NtG*(DDSIP (,<Q_xЂ' -F9 ,u`+cN1RcRI傲Y:[xf զa>x_s>x'H!O_zyBUπȱ>QQ5룾4 gJ,f h#+-fSSp߷ѭT`-$igX;:AU +۪q5QBcBc!]|GyGӛ1t {-2 eɮ_IC=2vךٯ5Eo}C.;z`m#Aa@K֣s:(8jT2Dz?(!D=ΝZ9zoVW#,+:CUr(0z\^Pon;w#97/Wm ZEBnyqQd!}Z`;(%8*Ƃ`# Z}gI͠}!8 E+l  m1¬]IּphFsl隫#(.COlh Eed;rQ1/R'AtMi c!@鰒p ٻK-pJ :!2IϢW5$=)_q/*+}FĸBb)#vdU WB#(R  Pg5j q, 4BFL@3h6`b _Ea|Ya&\ @i@+nE^[q?o=!BY!:I̻8KK#me}%R-p"ᶗWc "$]B(HY&Yqav7#沢P QE!$K+Ʊ+抱IQ< cinQơ'R9}I6P5Qk6}uۊtQlݳ=uڛƔwW˄wj @08(z0d$+$cYoJӸǭñPV$2y@[}$ѹ: ֨ : Vx,/fenUf^9Z ^$^BWDZ|%Yr: HDt}K.a?!Cy:&q,HH+.ʱ Dj%I7us,H&89P$YaS w"mЛ]'_٘yw S?z:'AOrdXQٚWjk(x9(+|kZ8˱jNttEε!4,PH/fpKV.+:fUzqFآҵ#?!+Յ:&f婝V^?9;\!rzyX Oacidq,Er!!+\+}=iت.F)+'H4i׹Hˏ[¬=:HFc& ثsCoRc~Jg!crg0l N%"]21cXMӂQb2J+O 2vXB| sbN4HШBi,Bpo?? C&Pܪk}ȥY0p̘dYpw6H4%J Aި"-WE?}| y;I͐t6s@}tρ v03HP1xzх{BZʱVD&|$ɲ1# ҅9i=]_j7hLÜycU/Da"'B:r *>  Id8raG?Gck=8'ab~2, R \t{:FGEa0>rq,DZ6>~lB9d,X3-ȱ+ZdO589iojסaϝ2D]Mhta2\mi'(X݈/ı8@\y!;XK%a]Nkhк/\p.$lķu֜У0X}Y=_xxc,;8J,SDXWUwd+7%k? Zb&: v9o=ʋ/@dAXRNjߐ J ?Y O${7 r_>ލ+uw)zb-gq^7kr,C 1f+qqɱvY ]-yQ܃rT9ZQU6L^WI-8"{ك\ӊ Ac-E'^Q*5n]*YLGyuy|-4*IVyzO6ur"eVN0Ee 8ӸDesxZ~'JQ˘;0YYAEP=uO+ 7M.#1V*vXk-x?yce,[#PነӅKsp`dw뗁Xc)nBm_uE Г'fZ P筵QgWy2X;ENDZ &R,{@; /ȂoJ.:iӰ~.]'nʣ8,a\[CTjw 45Tr$XkzGۡwc7eycU]tNIp>T$Eٺ e"@)lk]# 66|vnn)knuaE CK+6t^a;Chcm{{AǴ9PV/UݣA<٨C-c!f:91V;lA]#mxıde?}aıBV8hpMGVz 5Yέ%)qmp dv=#ƺA?cm !L,%+,{U_.Yg{*r̨'wj4BQ ɬ!_,lz鬶$y+,L<@ oX WEƥND={d uqgq8;;-94֛:LF;fPt92rc7,F05rKӅ[!,LCkˡp%]ݏ^^Fs?wT[upıv{wjW"Yo Æ&=X8׾ANKtaug:< X:*Axua6P΄0W_ip!fry ]_Q^dyv-,޽ CZ NwF@/yZV}|e< DZ) ~1uջϱt\䆮SѢ0:/梣{Gp,p;\XOI ?fd:"EaӝbbfkMAq7 (p~&E.m#i 81j(\CV z<׸;SS*QbT")>~ŀ($qiܔg2%bYlڅ3]SiƅrKëC#Is]JKy;z&M5Q|Ӻaٜuײ^K!'$rXpڐq n ?׵0Htd>%cԌcJ M3||{B^W7w֑2Խr8q |Iv+5cw@69"]=s\˿c= +ݨ/Ж.{+c+6W )e-w-A]pn N:j$5 UD´i8*1Յ]iX s/".sxTċh-Lztߢ QO&VOJ IrW,)[rrYceP:nBg UѾDEB zt(Di? 4Wz"qVX,c|c5c -Csn5##qD%zsѺ `඿nuDaШ(d2+ fwVXBnQ *[Ib0Iq,>Eei7< QtIF1I>#E"\ |E%N@Yؕ(*fxwJ5;.SNXǯ8f˺mPű4Ժ.͝7q LUt\X={Q0ZŜ&Rzӹ{\ l-7"Y!6$}57wZf7)kX"AYޮp;$Y6EeتHIR/FC5B0 +X~=ҫ{F5"5{h-Y[m9߲cFAqpJ2BS>{vA(m7V DmB0Fx!BwWQ "YқjH%YW;ޛG&] 5Ŝ]1,܇敹f!6쯥B 23V͡9l޻,@z(B͠z@q7E/B3#7 iyԭ1aµa K7lUD4$.Y47.d(ıH6u% ӗUkMR݌f'$O`e0k> DHb=uo_^ 6$p$+z4T[_xid\Ri !qp(Rbg ] ƃ?2(2ٟqXK_|Fs FU2)FH+߈CX i%c&Bgͱ³)']1+[4Jh ]aOQF-е!-M8iJ*Xc,KU# sVQ# [<熥RP@sӊc\ZB :&/ݓ0^Q !XyU#xM$`]aXrE{;)bַ$y]37upTK-b~ڷݔ@XdccePG$k6ꃼrCFwY|$)|T.#ڻO?).C\k8ؖDHmsukWIEX6DW&10ǒr2T4K taݸ0qm\ݽ]S+܍ d4-8.@-T tma=j8ftYGn`P, wn+/A76zD\t)5Wk^` 4Hqtq 3{xGT`7A>U"^s,VK-c eQRF@rёY.p%O_]7*ٟIm<4G& '@u/eD!w 5j:BY1RIVaʱRk>mLns}`MHJ.s>f}fŝ螙PA0&oH"' cE0m| H¼uKR |gB@AdDZ<ȒtA2]Hn%X bkrjT9 $vj8M1 = ѡ)Bs,d \ܔd墭Bi!:)h ΏSS\l=p(|QRj龁v";Z. u[#YG3#zÀVRo;YfjA( b-RǥKv""~}B̕q{G0XJ7bl2: $KFoGϥ N S9Md8։†b0Zeˊ $ [CZZ^_JLC.aMEY;><"' uB˪lNaTԊ{ ,JN|~dIVRmw*ӤKL͎]mqP~UW/+ƂXsmDZFfЀٰ9NomwP,/V,\5iKPʰ{j6Ȭa_iͶ\]bhI#NJ-t0Ѵ|q _;XZqdat{GJGpϙ& -')/_up7k]>:h -7 cū0R РwbNEI"&wS>Vk( Pp,%Xy@U,|W&.ٌ;2= &^1 2ζ,:ӆ9!.t,,F ıeA̳nZUo tYohf -$* Q^M! VÀ! MC#3r:q!K3OE)'X/e )ʨ2 IDAT&^{O7fwS!+ȱ& ?'P SE<űT#}Ã{k$rGSlpS~:YXs\j~?xD m<4.{꒥~}PwOY;jb*@ 0tCr^1r+5\45\0]*|l Dhe5EX\CRBb7T.|rw4kS< xQ~b-5ʥ]=Uu'o 6uZM1VZzL/v\ lp,!^ we=!Ⱥjp)… K~F)b[gD~ʓs!4.XŸ6yUa*7,cWdܙ~ΐ>J˄#<hnKVN@}{Su?dH%^8h]ܿ#s+(pDa(\ Q`*D1 ޿c>(j 8bQhGʯ23 g%OVr,+vY1m1|3!0dOf- F|m^;r@6d#T. )@Hw㨖G{CъD:'}4!VE҂wlNI}4W=_x w1kt q4]gN2!;8~O?F`f(\|BZQ r,t\Ϥn[bF:Ʒ)VA-rWv&ЫzHѬ8]1YEGPx-!U %_\}4b|}՜~!b::'w(87wkdBK 6J"JHli^t_w"t=8#"&HЇG[/hԶ oMuIdwt/)\d s!kk?#V qXQ3ݘ94vV(e1 J}.,3 X'z J>.%zZח$/~hNYA~IB 7? b5e3Tyd iP|^%W dZJM= ֒;V՟>nȹ>($hv*W2˱ )El/y@/%XG6rZK8UPL@5t1Ԉc ȟˤ!w`ġĢBI\@n".<+Xݮd̓,Ż`JaT5 /Qk߶aDb鮘dO4Q&}1AoK#ͱ9q%2uE.YӠk V+Za4w~q}jC@dd':/wE+*}b}~D;)e|prX)$XOˣa5:AKW3N9 'YE>8Y8˼+ gA,xcrJ-Xmá;=?DXHF_EWG6 .$;m1VR0,_/c%}_krvD\U9z'd:G@ BxUƧU CH}m1&)umx."ٲ5X7|2ܥ ~Nqw$Yt@ED=w`s,  }CC5l0u]08Tp=,h\ i8Vu^+بC B&mkLv; wMPخ\ī=,ПH،#6433dm@6KKd-0urO ` /K%Y#`}s1 pPQ:!"#-ȁ JA[謅I[*њc9ߏCɨ.Y 25POg/d>i- :ս]oSn4ibQq,ű.fugdT[tp%e|_]Y4,PMq/$#+eP~V' [´_|}!XTQYTH\e2N#k<"#N1\DfrMC<#gFAt ȱ2d$+doPls?uɚY>7e+?nl$~8T c u,uCۓY1V& CrƊۓ59}Px#B0/4[钥f i:f K;XQ~MSͳD!][謆&Jlsñ|5;XY%e)y?Npŭ`,8#vnh2/璵ODpn5r{|7*Y0((Z6~NJY'nÃЂ!-;wE4KՅv/eR8aA`$xUaYРXI`jU[T{]G#+f@rx /}41l20.YEjjly GAzePLDmN\apnnM4Qb 4XNē9 \ c?d,tJ0M-YR4p+<1D!Z ' qy11 7cYpBY&ɘGܵ}Bqk\ɕʄ 7cPbjUkt)Bl`_q"`es8宔lXA#XK|x/膅a~/HOg$Hʯ6ʱbq>ea^ݿΈxK] JCPI{PĚ:Q ?1*h7ݵcHZZUX%b9V½P.ypaj NdTU%3RS_aP(iARQ÷޵8 䀘jwD0a ?Qu;|X 7t$5zHjf""((\ѻ/78V)Wq.YCoS-Bٕjx7aA} ⨇䮄O$W|0qRHG`Ni@D}wQa"nsB?:X.Phbp[i}/5ĊSQG_Bbi6b8jh>-]t؊ceen㧝F_ePIFLBM@m73ҫrRw{z DRVNGPm9V~:w u(TuR܆X 8fPsme L;dBuZʍ(opg[A=NUV6_pp0 [P}.Y^tZx'}`],ѵad sEU!}yvv9}C]pe-qqp8BZB8lXZK8 eƒű\Qӑ<Kc+aT4/οګkO:k0~r"X#4| ŖMX:lQQa,"灆̆ifup@I gupH iAs#YBip(HZn#`lS(㑊nvoq(JUg 3"`O1ՋAK1 eq;+_}x Ph\ޢ!u Zp`]ʱ™xw3)@~cU??F线v~քh"z A,>EW^Lz(ZuщJ@ѩ+\>1iFU88|:+~1NfP+ȵͿm%}CXcIKR,Uv*THI?cՅfeZ뒬{$}~bt_6S+B˯C_'&Q^LA,\4XïuʱM.į: +%ѻ a$BCհb0:{~q\@G0&2-ʱD B:8lOo  _>nNãg ouo%XA ޑ?X^WB+-5uV3U(CYqk/`M·SJZK '[9bpjঐC=X*9ַUR_C˫ WFb8pLv m)%~XB. 9QD({v]Urćq>Z\t+H`KL~$KSc[?0_ƠdXq 5؛bQa1(V1]e`7чal7t]g~=~Fu,HBMb*,k,3ЈN& &O*Zq:ۅG3 m(P8Qa'Gpp\Ɏ#R5v Xye(Rw܉OŬgrwc-*9ǾFMe]:qwptB_gOELQ@%j5 q;n{kF>;bf/*T6N5DzI@ / nV IMJm(&هl4Ģjvx208kPUlޫ}ݠ1# mƖk W]`j[DNP)䠶k_cIl15O}8x=:D q_TͱJU޻p:x}J+ +I=HI.T5h~PpQCQT;֥Q1[a63y ]G ;. zK;oYESy{?c)O }Xm܋CP+@CC#D8V6nД˶o/\gd&1u_^dLLQ~LJI`)fAA,@d%|⾖DҽI*AWc*+s,x+ )_]&#iϏ|NC]g/FsvKT@pMV~rIQgk8c-X2JB5clj~vTɥޅ)Ӿ:9Ӆ |}RLb,TIf5#Z"'7:[˪lR:.EU|Њ&%[BY&sG.`:&j!7Gw!Rދͨsu0X{VшcTXXDOwȱ s*IuaaFZ:XJNO?DIUӑM6e0⦙N+:G"dȱ1VMVEӸw%}JpG0uN3"UqRwkUJ`2+WA 4'if\'e:ruʱظr98rxcXOO~G! Vɱ °E$$K#XSqWǁ}ڐAK [^77R+wQѸ!iOq7hcot/&!utafʐAp%.֌tv(K'x0!bH|D$ -|!YQ0{nâhƱRUR(Jx"}%8:%V U{u'pLⶂcvϫK2ƮwlC~/n4Hc8wVYc{r,gU'<~f b.;%pZ . ?:JG,l#њYB9jkUGAX *kYW'xRxHaAXيc}yFĔ>;sdCnl- Uk `)րlG!_˭֊Z0Tw.6 b%&!EA:JEEۥ+c%ޫG :ʰ?E+_GrHE6ZDzʊ,IXE(TJ}uX]Z%Xy1aMRq9Ҕq⩕u8U 0t4\6)q+7X6ajjU;}ǰ3w, !(ߣ>7J b=ެ34ae h|RԊV+!z!;;N|pˊYZBUw ͱ[$J+z %UB-f o,b v/T>*8rmp{q}{jc\WX L$9[JyUcyLƟc Յ 1K/ɹӉY U!}!k/<.'( ."?AcK1EуVM<  r*=vg,+gWyM^|W!LVsIDATTԸAsˁθ_NqkpJ P*ߙke(+ L#]pU(K & eDBCp*UQHah͐g l¿(XSPTp탵ctr<% im5Y%x^`p`F6]ͱ^~X^W̞9uE"?ݭB=_kuLͱ.:ފ@͢°`UB>c[.Ud6L0 |1{u!Qoc7u!8aƉ0bԨPꮸ=XIՙ?e(3cM(em(ďK JsHq\VO|z\  S"RÍeWuߣXP!8V|D O=dGa*5)j1Ue켤|?"Z <<[sPO/Na?j)d-Vf 25Bs bա[_"ƽ+@ \¾U:* 5Eg#&UWf|tI')nQkBFNHtX=vEr, ytϸA15I+tI٬ ;8)|?*lUzeƘ]Re ` v)0L}N<}v= KU~ք_X>k̄%ǪGѻ|\ͱVC@X{fnE Bw֌>~":ۤ6v$óFhԺJ{EցSm[=-bz y?? @ ]MxxՈ՜@A@kմA `~Zަ ՜լ y?? @ ]MxxՈ՜@A@kմA# # installpath_text: text mode installation type selection dialog # # Copyright 2001-2002 Red Hat, Inc. # # This software may be freely redistributed under the terms of the GNU # library public license. # # You should have received a copy of the GNU Library Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # from snack import * from constants_text import * from rhpl.translate import _ from flags import flags import installclass class InstallPathWindow: def __call__ (self, screen, dispatch, id, method, intf): classes = installclass.availableClasses() choices = [] default = 0 i = 0 orig = None for (name, object, icon) in classes: choices.append(_(name)) if isinstance(id.instClass, object): orig = i elif object.default: default = i i = i + 1 if orig != None: default = orig # CJS # (button, choice) = ListboxChoiceWindow(screen, _("Installation Type"), # _("What type of system would you like to install?"), # choices, [TEXT_OK_BUTTON, TEXT_BACK_BUTTON], # width = 40, default = default, help = "installpath") (button, choice) = ListboxChoiceWindow(screen, _("Installation Type"), _("What type of system would you like to install?"), choices, [TEXT_OK_BUTTON, TEXT_BACK_BUTTON], width = 40, default = default, help = "installpath", scroll = 1 , height = 11 ) # CJS if button == TEXT_BACK_CHECK: return INSTALL_BACK if (choice != orig): (name, objectClass, logo) = classes[choice] c = objectClass(flags.expert) c.setSteps(dispatch) c.setInstallData(id) return INSTALL_OK # # welcome_text.py: text mode welcome window # # Copyright 2001-2002 Red Hat, Inc. # # This software may be freely redistributed under the terms of the GNU # library public license. # # You should have received a copy of the GNU Library Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # from snack import * from constants_text import * from rhpl.translate import _ from constants import * import os class WelcomeWindow: def __call__(self, screen, configFileData): rc = ButtonChoiceWindow(screen, _("%s") % (productName,), _("Welcome to %s!\n\n") % (productName), buttons = [TEXT_OK_BUTTON, TEXT_BACK_BUTTON], width = 50, help = "welcome") if rc == TEXT_BACK_CHECK: return INSTALL_BACK return INSTALL_OK # # complete_text.py: text mode congratulations windows # # Copyright 2001-2002 Red Hat, Inc. # # This software may be freely redistributed under the terms of the GNU # library public license. # # You should have received a copy of the GNU Library Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # from snack import * from constants_text import * from rhpl.translate import _ from constants import * import iutil class FinishedWindow: def __call__ (self, screen): bootstr = "" if iutil.getArch() == "s390": floppystr = _("Press to end the installation process.\n\n") bottomstr = _(" to exit") else: floppystr = _("Remove any installation media (diskettes or " "CD-ROMs) used during the installation process " "and press to reboot your system." "\n\n") bottomstr = _(" to reboot") screen.pushHelpLine (string.center(bottomstr, screen.width)) rc = ButtonChoiceWindow (screen, _("Complete"), _("Congratulations, your %s installation is " "complete.\n\n" "%s" "%s" ) % (productName, floppystr, bootstr), [ _("OK") ], help = "finished", width=60) return INSTALL_OK # # gui.py - Graphical front end for anaconda # # Matt Wilson # Michael Fulbright # # Copyright 1999-2003 Red Hat, Inc. # # This software may be freely redistributed under the terms of the GNU # library public license. # # You should have received a copy of the GNU Library Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # import os import iutil import string import isys import sys import parted import gtk import htmlbuffer import rpm import kudzu from language import expandLangs from splashscreen import splashScreenPop from flags import flags from constants import * from rhpl.log import log from rhpl.translate import _, N_ from product import * rpm.addMacro("_i18ndomains", "redhat-dist") isys.bind_textdomain_codeset("redhat-dist", "UTF-8") StayOnScreen = "stayOnScreen" mainWindow = None stepToClass = { "language" : ("language_gui", "LanguageWindow"), "keyboard" : ("keyboard_gui", "KeyboardWindow"), "mouse" : ("mouse_gui", "MouseWindow"), "welcome" : ("welcome_gui", "WelcomeWindow"), "installtype" : ("installpath_gui", "InstallPathWindow"), "partitionmethod" : ("partmethod_gui", "PartitionMethodWindow"), "partition" : ("partition_gui", "PartitionWindow"), "autopartition" : ("partition_gui", "AutoPartitionWindow"), "findinstall" : ("examine_gui", "UpgradeExamineWindow"), "addswap" : ("upgrade_swap_gui", "UpgradeSwapWindow"), "upgrademigratefs" : ("upgrade_migratefs_gui", "UpgradeMigrateFSWindow"), "fdisk" : ("fdisk_gui", "FDiskWindow"), "bootloader": ("bootloader_main_gui", "MainBootloaderWindow"), "bootloaderadvanced": ("bootloader_advanced_gui", "AdvancedBootloaderWindow"), "upgbootloader": ("upgrade_bootloader_gui", "UpgradeBootloaderWindow"), "network" : ("network_gui", "NetworkWindow"), "firewall" : ("firewall_gui", "FirewallWindow"), "languagesupport" : ("language_support_gui", "LanguageSupportWindow"), "timezone" : ("timezone_gui", "TimezoneWindow"), "accounts" : ("account_gui", "AccountWindow"), "authentication" : ("auth_gui", "AuthWindow"), "desktopchoice": ("desktop_choice_gui", "DesktopChoiceWindow"), "package-selection" : ("package_gui", "PackageSelectionWindow"), "indivpackage" : ("package_gui", "IndividualPackageSelectionWindow"), "dependencies" : ("dependencies_gui", "UnresolvedDependenciesWindow"), "videocard" : ("xconfig_gui", "XConfigWindow"), "monitor" : ("xconfig_gui", "MonitorWindow"), "xcustom" : ("xconfig_gui", "XCustomWindow"), "confirminstall" : ("confirm_gui", "InstallConfirmWindow"), "confirmupgrade" : ("confirm_gui", "UpgradeConfirmWindow"), "finishxconfig" : None, "install" : ("progress_gui", "InstallProgressWindow"), "bootdisk" : ("bootdisk_gui", "BootdiskWindow"), "complete" : ("congrats_gui", "CongratulationWindow"), } if iutil.getArch() == 'sparc': stepToClass["bootloader"] = ("silo_gui", "SiloWindow") elif iutil.getArch() == 's390': stepToClass["bootloader"] = ("zipl_gui", "ZiplWindow") # # Stuff for screenshots # screenshotDir = None screenshotIndex = 0 def copyScreenshots(): global screenshotIndex global screenshotDir # see if any screenshots taken if screenshotIndex == 0: return destDir = "/mnt/sysimage/root/anaconda-screenshots" if not os.access(destDir, os.R_OK): try: os.mkdir(destDir, 0750) except: window = MessageWindow("Error Saving Screenshot", _("An error occurred copying the " "screenshots over."), type="warning") return # copy all png's over for f in os.listdir(screenshotDir): (path, fname) = os.path.split(f) (b, ext) = os.path.splitext(f) if ext == ".png": iutil.copyFile(screenshotDir + '/' + f, destDir + '/' + fname) window = MessageWindow(_("Screenshots Copied"), _("The screenshots have been saved into the " "directory:\n\n" "\t/root/anaconda-screenshots/\n\n" "You can access these when you reboot and " "login as root.")) def takeScreenShot(): global screenshotIndex global screenshotDir if screenshotDir is None: screenshotDir = "/tmp/ramfs/anaconda-screenshots" if not os.access(screenshotDir, os.R_OK): try: os.mkdir(screenshotDir) except: screenshotDir = None return try: screenshot = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, gtk.FALSE, 8, gtk.gdk.screen_width(), gtk.gdk.screen_height()) screenshot.get_from_drawable(gtk.gdk.get_default_root_window(), gtk.gdk.colormap_get_system(), 0, 0, 0, 0, gtk.gdk.screen_width(), gtk.gdk.screen_height()) if screenshot: while (1): sname = "screenshot-%04d.png" % ( screenshotIndex,) if not os.access(screenshotDir + '/' + sname, os.R_OK): break screenshotIndex = screenshotIndex + 1 if screenshotIndex > 9999: log("Too many screenshots!") return screenshot.save (screenshotDir + '/' + sname, "png") screenshotIndex = screenshotIndex + 1 window = MessageWindow(_("Saving Screenshot"), _("A screenshot named '%s' has been saved.") % (sname,) , type="ok") except: window = MessageWindow(_("Error Saving Screenshot"), _("An error occurred while saving " "the screenshot. If this occurred " "during package installation, you may need " "to try several times for it to succeed."), type="warning") def handleShiftPrintScrnRelease (window, event): if (event.keyval == gtk.keysyms.Print and event.state & gtk.gdk.SHIFT_MASK): takeScreenShot() # # HACK to make treeview work # def setupTreeViewFixupIdleHandler(view, store): id = {} id["id"] = gtk.idle_add(scrollToIdleHandler, (view, store, id)) def scrollToIdleHandler((view, store, iddict)): if not view or not store or not iddict: return try: id = iddict["id"] except: return selection = view.get_selection() if not selection: return model, iter = selection.get_selected() if not iter: return path = store.get_path(iter) col = view.get_column(0) view.scroll_to_cell(path, col, gtk.TRUE, 0.5, 0.5) if id: gtk.idle_remove(id) # setup globals def processEvents(): gtk.gdk.flush() while gtk.events_pending(): gtk.main_iteration(gtk.FALSE) def partedExceptionWindow(exc): # if our only option is to cancel, let us handle the exception # in our code and avoid popping up the exception window here. if exc.options == parted.EXCEPTION_CANCEL: return parted.EXCEPTION_UNHANDLED print exc.type_string print exc.message print exc.options win = gtk.Dialog(exc.type_string, mainWindow, gtk.DIALOG_MODAL) addFrame(win) win.set_position(gtk.WIN_POS_CENTER) label = WrappingLabel(exc.message) win.vbox.pack_start (label) numButtons = 0 buttonToAction = {} exflags = ((parted.EXCEPTION_FIX, N_("Fix")), (parted.EXCEPTION_YES, N_("Yes")), (parted.EXCEPTION_NO, N_("No")), (parted.EXCEPTION_OK, N_("OK")), (parted.EXCEPTION_RETRY, N_("Retry")), (parted.EXCEPTION_IGNORE, N_("Ignore")), (parted.EXCEPTION_CANCEL, N_("Cancel"))) for flag, string in exflags: if exc.options & flag: win.add_button(_(string), flag) win.show_all() rc = win.run() win.destroy() return rc def widgetExpander(widget, growTo=None): widget.connect("size-allocate", growToParent, growTo) def growToParent(widget, rect, growTo=None): return if not widget.parent: return ignore = widget.__dict__.get("ignoreEvents") if not ignore: if growTo: x, y, width, height = growTo.get_allocation() widget.set_size_request(width, -1) else: widget.set_size_request(rect.width, -1) widget.ignoreEvents = 1 else: widget.ignoreEvents = 0 _busyCursor = 0 def setCursorToBusy(process=1): root = gtk.gdk.get_default_root_window() cursor = gtk.gdk.Cursor(gtk.gdk.WATCH) root.set_cursor(cursor) if process: processEvents() def setCursorToNormal(): root = gtk.gdk.get_default_root_window() cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR) root.set_cursor(cursor) def rootPushBusyCursor(process=1): global _busyCursor _busyCursor += 1 if _busyCursor > 0: setCursorToBusy(process) def rootPopBusyCursor(): global _busyCursor _busyCursor -= 1 if _busyCursor <= 0: setCursorToNormal() def getBusyCursorStatus(): global _busyCursor return _busyCursor class MnemonicLabel(gtk.Label): def __init__(self, text=""): gtk.Label.__init__(self, "") self.set_text_with_mnemonic(text) class WrappingLabel(gtk.Label): def __init__(self, label=""): gtk.Label.__init__(self, label) self.set_line_wrap(gtk.TRUE) self.ignoreEvents = 0 # self.set_size_request(-1, 1) widgetExpander(self) def titleBarMousePressCB(widget, event, data): if event.type & gtk.gdk.BUTTON_PRESS: data["state"] = 1 data["button"] = event.button data["deltax"] = event.x data["deltay"] = event.y def titleBarMouseReleaseCB(widget, event, data): if data["state"] and event.button == data["button"]: data["state"] = 0 data["button"] = 0 data["deltax"] = 0 data["deltay"] = 0 def titleBarMotionEventCB(widget, event, data): if data["state"]: newx = event.x_root-data["deltax"] newy = event.y_root-data["deltay"] if newx < 0: newx = 0 if newy < 0: newy = 0 (w, h) = data["window"].get_size() if (newx+w) > gtk.gdk.screen_width(): newx = gtk.gdk.screen_width() - w if (newy+20) > (gtk.gdk.screen_height()): newy = gtk.gdk.screen_height() - 20 data["window"].move(newx, newy) def addFrame(dialog, title=None): contents = dialog.get_children()[0] dialog.remove(contents) frame = gtk.Frame() frame.set_shadow_type(gtk.SHADOW_OUT) box = gtk.VBox() try: if title is None: title = dialog.get_title() if title: data = {} data["state"] = 0 data["button"] = 0 data["deltax"] = 0 data["deltay"] = 0 data["window"] = dialog eventBox = gtk.EventBox() eventBox.connect("button-press-event", titleBarMousePressCB, data) eventBox.connect("button-release-event", titleBarMouseReleaseCB, data) eventBox.connect("motion-notify-event", titleBarMotionEventCB,data) titleBox = gtk.HBox(gtk.FALSE, 5) eventBox.add(titleBox) eventBox.modify_bg(gtk.STATE_NORMAL, eventBox.rc_get_style().bg[gtk.STATE_SELECTED]) titlelbl = gtk.Label("") titlelbl.set_markup(""+_(title)+"") titlelbl.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse ("white")) titlelbl.set_property("ypad", 4) titleBox.pack_start(titlelbl) box.pack_start(eventBox, gtk.FALSE, gtk.FALSE) except: pass frame2=gtk.Frame() frame2.set_shadow_type(gtk.SHADOW_NONE) frame2.set_border_width(4) frame2.add(contents) box.pack_start(frame2, gtk.TRUE, gtk.TRUE, padding=5) frame.add(box) frame.show() dialog.add(frame) # make screen shots work dialog.connect ("key-release-event", handleShiftPrintScrnRelease) class WaitWindow: def __init__(self, title, text): self.window = gtk.Window(gtk.WINDOW_POPUP) self.window.set_title(title) self.window.set_position(gtk.WIN_POS_CENTER) self.window.set_modal(gtk.TRUE) label = WrappingLabel(text) box = gtk.Frame() box.set_border_width(10) box.add(label) box.set_shadow_type(gtk.SHADOW_NONE) frame = gtk.Frame () frame.set_shadow_type(gtk.SHADOW_OUT) frame.add (box) self.window.add(frame) self.window.show_all() rootPushBusyCursor() def pop(self): self.window.destroy() rootPopBusyCursor() class ProgressWindow: def __init__(self, title, text, total): self.window = gtk.Window (gtk.WINDOW_POPUP) self.window.set_title (title) self.window.set_position (gt}~k.WIN_POS_CENTER) self.window.set_modal (gtk.TRUE) box = gtk.VBox (gtk.FALSE, 5) box.set_border_width (10) label = WrappingLabel (text) label.set_alignment (0.0, 0.5) box.pack_start (label, gtk.FALSE) self.total = total self.progress = gtk.ProgressBar () box.pack_start (self.progress, gtk.TRUE) frame = gtk.Frame () frame.set_shadow_type (gtk.SHADOW_OUT) frame.add (box) self.window.add (frame) self.window.show_all () rootPushBusyCursor() def set (self, amount): # only update widget if we've changed by 5% curval = self.progress.get_fraction() newval = float (amount) / self.total if newval < 0.998: if (newval - curval) < 0.05 and newval > curval: return self.progress.set_fraction (newval) processEvents () def pop(self): self.window.destroy () rootPopBusyCursor() class ExceptionWindow: def __init__ (self, text): try: floppyDevices = 0 for dev in kudzu.probe(kudzu.CLASS_FLOPPY, kudzu.BUS_UNSPEC, kudzu.PROBE_ALL): if not dev.detached: floppyDevices = floppyDevices + 1 except: floppyDevices = 0 win = gtk.Dialog("Exception Occured", mainWindow, gtk.DIALOG_MODAL) win.add_button("_Debug", 0) if floppyDevices > 0 or DEBUG: win.add_button("_Save to floppy", 1) win.add_button('gtk-ok', 2) buffer = gtk.TextBuffer(None) buffer.set_text(text) textbox = gtk.TextView() textbox.set_buffer(buffer) textbox.set_property("editable", gtk.FALSE) textbox.set_property("cursor_visible", gtk.FALSE) sw = gtk.ScrolledWindow () sw.add (textbox) sw.set_policy (gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) hbox = gtk.HBox (gtk.FALSE) ## file = pixmap_file('gnome-warning.png') ## if file: ## hbox.pack_start (GnomePixmap (file), gtk.FALSE) if floppyDevices > 0: info = WrappingLabel(exceptionText) else: info = WrappingLabel(exceptionTextNoFloppy) info.set_size_request (400, -1) hbox.pack_start (sw, gtk.TRUE) win.vbox.pack_start (info, gtk.FALSE) win.vbox.pack_start (hbox, gtk.TRUE) win.set_size_request (500, 300) win.set_position (gtk.WIN_POS_CENTER) addFrame(win) win.show_all () self.window = win self.rc = self.window.run () # self.window.destroy() def getrc (self): # I did it this way for future expantion # 0 is debug if self.rc == 0: try: # switch to VC1 so we can debug isys.vtActivate (1) except SystemError: pass return 1 # 1 is save if self.rc == 1: return 2 # 2 is OK elif self.rc == 2: return 0 class MessageWindow: def getrc (self): return self.rc def __init__ (self, title, text, type="ok", default=None, custom_buttons=None, custom_icon=None): if flags.autostep: self.rc = 1 return self.rc = None docustom = 0 if type == 'ok': buttons = gtk.BUTTONS_OK style = gtk.MESSAGE_INFO elif type == 'warning': buttons = gtk.BUTTONS_OK style = gtk.MESSAGE_WARNING elif type == 'okcancel': buttons = gtk.BUTTONS_OK_CANCEL style = gtk.MESSAGE_WARNING elif type == 'yesno': buttons = gtk.BUTTONS_YES_NO style = gtk.MESSAGE_QUESTION elif type == 'custom': docustom = 1 buttons = gtk.BUTTONS_NONE style = gtk.MESSAGE_QUESTION if custom_icon == "warning": style = gtk.MESSAGE_WARNING elif custom_icon == "question": style = gtk.MESSAGE_QUESTION elif custom_icon == "error": style = gtk.MESSAGE_ERROR elif custom_icon == "info": style = gtk.MESSAGE_INFO dialog = gtk.MessageDialog(mainWindow, 0, style, buttons, text) if docustom: rid=0 for button in custom_buttons: if button == _("Cancel"): tbutton = "gtk-cancel" else: tbutton = button widget = dialog.add_button(tbutton, rid) rid = rid + 1 defaultchoice = rid - 1 else: if default == "no": defaultchoice = 0 elif default == "yes" or default == "ok": defaultchoice = 1 else: defaultchoice = 0 addFrame(dialog, title=title) dialog.set_position (gtk.WIN_POS_CENTER) dialog.set_default_response(defaultchoice) dialog.show_all () # XXX - Messy - turn off busy cursor if necessary busycursor = getBusyCursorStatus() setCursorToNormal() rc = dialog.run() if rc == gtk.RESPONSE_OK or rc == gtk.RESPONSE_YES: self.rc = 1 elif (rc == gtk.RESPONSE_CANCEL or rc == gtk.RESPONSE_NO or rc == gtk.RESPONSE_CLOSE): self.rc = 0 elif rc == gtk.RESPONSE_DELETE_EVENT: self.rc = 0 else: self.rc = rc dialog.destroy() # restore busy cursor if busycursor: setCursorToBusy() class InstallInterface: def __init__ (self): # figure out if we want to run interface at 800x600 or 640x480 if gtk.gdk.screen_width() >= 800: self.runres = "800x600" else: self.runres = "640x480" def __del__ (self): pass def shutdown (self): pass def setPackageProgressWindow (self, ppw): self.ppw = ppw def waitWindow (self, title, text): return WaitWindow (title, text) def progressWindow (self, title, text, total): return ProgressWindow (title, text, total) def packageProgressWindow (self, total, totalSize): self.ppw.setSizes (total, totalSize) return self.ppw def messageWindow(self, title, text, type="ok", default = None, custom_buttons=None, custom_icon=None): rc = MessageWindow (title, text, type, default, custom_buttons, custom_icon).getrc() return rc def exceptionWindow(self, title, text): print text win = ExceptionWindow (text) return win.getrc () def dumpWindow(self): window = MessageWindow("Save Crash Dump", _("Please insert a floppy now. All contents " "of the disk will be erased, so please " "choose your diskette carefully."), "okcancel") rc = window.getrc() return not rc def getBootdisk (self): return None def run(self, id, dispatch, configFileData): ## from xkb import XKB ## kb = XKB() self.dispatch = dispatch # XXX users complain when the keypad doesn't work for input. ## if 0 and flags.setupFilesystems: ## try: ## kb.setMouseKeys (1) ## except SystemError: ## pass # XXX x_already_set is a hack if id.keyboard and not id.x_already_set: id.keyboard.activate() ## info = id.keyboard.getXKB() ## if info: ## (rules, model, layout, variant, options) = info ## kb.setRule (model, layout, variant, "complete") id.fsset.registerMessageWindow(self.messageWindow) id.fsset.registerProgressWindow(self.progressWindow) id.fsset.registerWaitWindow(self.waitWindow) parted.exception_set_handler(partedExceptionWindow) lang = id.instLanguage.getCurrent() lang = id.instLanguage.getLangNick(lang) self.icw = InstallControlWindow (self, self.dispatch, lang) self.icw.run (self.runres, configFileData) class TextViewBrowser(gtk.TextView): def __init__(self): self.hadj = None self.vadj = None gtk.TextView.__init__(self) self.set_property('editable', gtk.FALSE) self.set_property('cursor_visible', gtk.FALSE) self.set_left_margin(10) self.set_wrap_mode(gtk.WRAP_WORD) self.connect('move-cursor', self.moveCursor) self.connect('set-scroll-adjustments', self.cacheAdjustments) def swallowFocus(self, *args): self.emit_stop_by_name('focus-in-event') def cacheAdjustments(self, view, hadj, vadj): self.hadj = hadj self.vadj = vadj def moveCursor(self, view, step, count, extend_selection): if step == gtk.MOVEMENT_DISPLAY_LINES: if count == -1 and self.vadj != None: self.vadj.value = max(self.vadj.value - self.vadj.step_increment, self.vadj.lower) self.vadj.value_changed() elif count == 1 and self.vadj != None: self.vadj.value = min(self.vadj.value + self.vadj.step_increment - 1, self.vadj.upper - self.vadj.page_increment - 1) self.vadj.value_changed() elif step == gtk.MOVEMENT_PAGES: if count == -1 and self.vadj != None: self.vadj.value = max(self.vadj.value - self.vadj.page_increment, self.vadj.lower) self.vadj.value_changed() elif count == 1 and self.vadj != None: self.vadj.value = min(self.vadj.value + self.vadj.page_increment - 1, self.vadj.upper - self.vadj.page_increment - 1) self.vadj.value_changed() self.emit_stop_by_name ('move-cursor') class InstallControlWindow: def setLanguage (self, locale): #gtk_set_locale () #gtk_rc_init () #gtk_rc_reparse_all () self.langSearchPath = expandLangs(locale) + ['C'] ## found = 0 ## for l in self.langSearchPath: ## if os.access ("/etc/gtk/gtkrc." + l, os.R_OK): ## rc_parse("/etc/gtk/gtkrc." + l) ## found = 1 ## if not found: ## rc_parse("/etc/gtk/gtkrc") ## #_gtk_nuke_rc_mtimes () ## gtk_rc_reparse_all () if not self.__dict__.has_key('window'): return self.reloadRcQueued = 1 self.updateStockButtons() self.helpFrame.set_label (_("Online Help")) self.installFrame.set_label (_("Language Selection")) self.loadReleaseNotes() self.refreshHelp(recreate = 1) def prevClicked (self, *args): try: self.currentWindow.getPrev () except StayOnScreen: return self.dispatch.gotoPrev() self.dir = -1 self.setScreen () def nextClicked (self, *args): try: rc = self.currentWindow.getNext () except StayOnScreen: return self.dispatch.gotoNext() self.dir = 1 self.setScreen () def helpClicked (self, widget, simulated=0): self.hbox.remove (widget) if widget == self.hideHelpButton: self.bin.remove (self.table) self.installFrame.reparent (self.bin) self.showHelpButton.show () self.showHelpButton.set_state (gtk.STATE_NORMAL) self.hbox.pack_start (self.showHelpButton, gtk.FALSE) self.hbox.reorder_child (self.showHelpButton, 0) self.showHelpButton.grab_focus() self.displayHelp = gtk.FALSE else: self.bin.remove (self.installFrame) self.table.attach (self.installFrame, 1, 3, 0, 1, gtk.FILL | gtk.EXPAND, gtk.FILL | gtk.EXPAND) self.bin.add (self.table) self.refreshHelp() self.hideHelpButton.show () self.showHelpButton.set_state (gtk.STATE_NORMAL) self.hbox.pack_start (self.hideHelpButton, gtk.FALSE) self.hbox.reorder_child (self.hideHelpButton, 0) self.hideHelpButton.grab_focus() self.displayHelp = gtk.TRUE def debugClicked (self, *args): try: # switch to VC1 so we can debug isys.vtActivate (1) except SystemError: pass import pdb try: pdb.set_trace() except: sys.exit(-1) try: # switch back isys.vtActivate (7) except SystemError: pass def refreshHelp(self, recreate = 0): buffer = htmlbuffer.HTMLBuffer() ics = self.currentWindow.getICS() buffer.feed(ics.getHTML(self.langSearchPath)) textbuffer = buffer.get_buffer() if recreate == 0: self.help.set_buffer(textbuffer) else: self.help_sw.remove(self.help) self.help = TextViewBrowser() self.help_sw.add(self.help) self.help.set_buffer(textbuffer) self.help.show() # scroll to the top. Do this with a mark so it's done in the idle loop iter = textbuffer.get_iter_at_offset(0) mark = textbuffer.create_mark("top", iter, gtk.FALSE) self.help.scroll_to_mark(mark, 0.0, gtk.FALSE, 0.0, 0.0) def relnotes_closed (self, *args): self.textWin.destroy() # # XXX - disabling this behavior for now due to bug where if you pop up # release notes during package selection, then close it after # package selection is done and install has moved to next screen, # the stockButtons get their state screwed up # # for (icon, name, text, func) in self.stockButtons: # if self.__dict__.has_key(name): # self.__dict__[name].set_sensitive(self.relnotes_buttonstate[name]) return def releaseClicked (self, widget): self.textWin = gtk.Dialog(parent=mainWindow, flags=gtk.DIALOG_MODAL) # # XXX - disabling this behavior for now due to bug where if you pop up # release notes during package selection, then close it after # package selection is done and install has moved to next screen, # the stockButtons get their state screwed up # self.relnotes_buttonstate={} # for (icon, name, text, func) in self.stockButtons: # if self.__dict__.has_key(name): # self.relnotes_buttonstate[name] = self.__dict__[name].get_property("sensitive") # self.__dict__[name].set_sensitive(gtk.FALSE) table = gtk.Table(3, 3, gtk.FALSE) self.textWin.vbox.pack_start(table) self.textWin.add_button('gtk-close', gtk.RESPONSE_NONE) self.textWin.connect("response", self.relnotes_closed) vbox1 = gtk.VBox () vbox1.set_border_width (10) frame = gtk.Frame (_("Release Notes")) frame.add(vbox1) frame.set_label_align (0.5, 0.5) frame.set_shadow_type (gtk.SHADOW_NONE) self.textWin.set_position (gtk.WIN_POS_CENTER) if self.releaseNotesBuffer: text = TextViewBrowser() text.set_buffer(self.releaseNotesBuffer) sw = gtk.ScrolledWindow() sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) sw.set_shadow_type(gtk.SHADOW_IN) sw.add(text) vbox1.pack_start(sw) a = gtk.Alignment (0, 0, 1.0, 1.0) a.add (frame) self.textWin.set_default_size (635, 393) self.textWin.set_size_request (635, 393) self.textWin.set_position (gtk.WIN_POS_CENTER) table.attach (a, 1, 2, 1, 2, gtk.FILL | gtk.EXPAND, gtk.FILL | gtk.EXPAND, 5, 5) self.textWin.set_border_width(0) addFrame(self.textWin, _("Release Notes")) self.textWin.show_all() else: self.textWin.set_position (gtk.WIN_POS_CENTER) label = gtk.Label(_("Unable to load file!")) table.attach (label, 1, 2, 1, 2, gtk.FILL | gtk.EXPAND, gtk.FILL | gtk.EXPAND, 5, 5) self.textWin.set_border_width(0) addFrame(self.textWin) self.textWin.show_all() def loadReleaseNotes(self): langList = self.langSearchPath + [ "" ] sourcepath = self.dispatch.method.getSourcePath() suffixList = [] for lang in langList: if lang: suffixList.append("-%s.html" % (lang,)) suffixList.append(".%s" % (lang,)) else: suffixList.append(".html") suffixList.append("") for suffix in suffixList: fn = "%s/RELEASE-NOTES%s" % (sourcepath, suffix) if os.access(fn, os.R_OK): file = open(fn, "r") if suffix.endswith('.html'): buffer = htmlbuffer.HTMLBuffer() buffer.feed(file.read()) self.releaseNotesBuffer = buffer.get_buffer() else: buffer = gtk.TextBuffer(None) buffer.set_text(file.read()) self.releaseNotesBuffer = buffer file.close() return buffer = gtk.TextBuffer(None) buffer.set_text(_("Release notes are missing.\n")) self.releaseNotesBuffer = buffer def handleRenderCallback(self): self.currentWindow.renderCallback() if flags.autostep: self.nextClicked() else: gtk.idle_remove(self.handle) def setScreen (self): (step, args) = self.dispatch.currentStep() if step is None: gtk.mainquit() return if not stepToClass[step]: if self.dir == 1: return self.nextClicked() else: return self.prevClicked() (file, className) = stepToClass[step] newScreenClass = None s = "from %s import %s; newScreenClass = %s" % (file, className, className) while 1: try: exec s break except ImportError, e: print e win = MessageWindow(_("Error!"), _("An error occurred when attempting " "to load an installer interface " "component.\n\nclassName = %s") % (className,), type="custom", custom_icon="warning", custom_buttons=[_("_Exit"), _("_Retry")]) if not win.getrc(): MessageWindow(_("Rebooting System"), _("Your system will now be rebooted..."), type="custom", custom_icon="warning", custom_buttons=[_("_Reboot")]) sys.exit(0) ics = InstallControlState (self) ics.setPrevEnabled(self.dispatch.canGoBack()) self.destroyCurrentWindow() self.currentWindow = newScreenClass(ics) new_screen = apply(self.currentWindow.getScreen, args) if not new_screen: return self.update (ics) self.installFrame.set_label(ics.getTitle ()) self.installFrame.add(new_screen) self.installFrame.show_all() self.handle = gtk.idle_add(self.handleRenderCallback) if self.reloadRcQueued: self.window.reset_rc_styles() self.reloadRcQueued = 0 if self.displayHelp: self.refreshHelp() def destroyCurrentWindow(self): children = self.installFrame.get_children () if children: child = children[0] self.installFrame.remove (child) child.destroy () self.currentWindow = None def update (self, ics): self.installFrame.set_label (ics.getTitle ()) prevButton = self.prevButtonStock nextButton = self.nextButtonStock if ics.getNextButton(): (icon, text) = ics.getNextButton() button = gtk.Button() box = gtk.HBox(gtk.FALSE, 0) image = gtk.Image() image.set_from_stock(icon, gtk.ICON_SIZE_BUTTON) box.pack_start(image, gtk.FALSE, gtk.FALSE) label = gtk.Label(_(text)) label.set_property("use-underline", gtk.TRUE) box.pack_start(label, gtk.TRUE, gtk.TRUE) button.add(box) button.connect("clicked", self.nextClicked) button.show_all() button.label = label nextButton = button children = self.buttonBox.get_children() if not nextButton in children and self.nextButtonStock in children: pos = children.index(self.nextButtonStock) self.buttonBox.remove(self.nextButtonStock) self.buttonBox.pack_end(nextButton) self.buttonBox.reorder_child(nextButton, pos) self.nextButtonStock = nextButton prevButton.set_sensitive (ics.getPrevEnabled ()) nextButton.set_sensitive (ics.getNextEnabled ()) self.hideHelpButton.set_sensitive (ics.getHelpButtonEnabled ()) self.showHelpButton.set_sensitive (ics.getHelpButtonEnabled ()) if ics.getHelpEnabled () == gtk.FALSE: if self.displayHelp: self.helpClicked (self.hideHelpButton, 1) elif ics.getHelpEnabled () == gtk.TRUE: if not self.displayHelp: self.helpClicked (self.showHelpButton, 1) if (ics.getGrabNext ()): nextButton.grab_focus () def __init__ (self, ii, dispatch, locale): self.prevButtonStock = None self.nextButtonStock = None self.releaseButton = None self.showHelpButton = None self.hideHelpButton = None self.debugButton = None self.stockButtons = (('gtk-go-back', "prevButtonStock", N_("_Back"), self.prevClicked), ('gtk-go-forward', "nextButtonStock", N_("_Next"), self.nextClicked), ('gtk-new', "releaseButton", N_("_Release Notes"), self.releaseClicked), ('gtk-help', "showHelpButton", N_("Show _Help"), self.helpClicked), ('gtk-help', "hideHelpButton", N_("Hide _Help"), self.helpClicked), ('gtk-execute', 'debugButton', N_("_Debug"), self.debugClicked)) self.reloadRcQueued = 0 self.ii = ii self.dispatch = dispatch self.setLanguage(locale) self.handle = None def keyRelease (self, window, event): if ((event.keyval == gtk.keysyms.KP_Delete or event.keyval == gtk.keysyms.Delete) and (event.state & (gtk.gdk.CONTROL_MASK | gtk.gdk.MOD1_MASK))): gtk.mainquit() os._exit(0) # XXX hack: remove me when the accelerators work again. elif (event.keyval == gtk.keysyms.F12 and self.currentWindow.getICS().getNextEnabled()): self.nextClicked() elif (event.keyval == gtk.keysyms.Print and event.state & gtk.gdk.SHIFT_MASK): takeScreenShot() def buildStockButtons(self): for (icon, item, text, action) in self.stockButtons: button = gtk.Button() box = gtk.HBox(gtk.FALSE, 0) image = gtk.Image() image.set_from_stock(icon, gtk.ICON_SIZE_BUTTON) box.pack_start(image, gtk.FALSE, gtk.FALSE) label = gtk.Label(_(text)) label.set_property("use-underline", gtk.TRUE) box.pack_start(label, gtk.TRUE, gtk.TRUE) button.add(box) button.connect("clicked", action) button.show_all() button.label = label self.__dict__[item] = button def updateStockButtons(self): for (icon, item, text, action) in self.stockButtons: button = self.__dict__[item] for child in button.get_children(): button.remove(child) # FIXME: this is cut and pasted from above; make a nicer # function that knows how to replace the contents in the # button for a future release box = gtk.HBox(gtk.FALSE, 0) image = gtk.Image() image.set_from_stock(icon, gtk.ICON_SIZE_BUTTON) box.pack_start(image, gtk.FALSE, gtk.FALSE) label = gtk.Label(_(text)) label.set_property("use-underline", gtk.TRUE) box.pack_start(label, gtk.TRUE, gtk.TRUE) button.add(box) button.show_all() button.label = label button.queue_resize() def findPixmap(self, file): for path in ("/mnt/source/" + productSite + "/RHupdates/pixmaps/", "/mnt/source/" + productSite + "/RHupdates/", "/tmp/updates/pixmaps/", "/tmp/updates/", "/mnt/source/RHupdates/pixmaps/", "/mnt/source/RHupdates/", "/tmp/product/pixmaps/", "/tmp/product/", "/usr/share/anaconda/pixmaps/", "pixmaps/", "/usr/share/pixmaps/", "/usr/share/anaconda/", ""): fn = path + file if os.access(fn, os.R_OK): return fn return None def setup_window (self, runres): self.window = gtk.Window () global mainWindow mainWindow = self.window self.window.set_events (gtk.gdk.KEY_RELEASE_MASK) if runres == '640x480': self.window.set_default_size (640, 480) self.window.set_size_request (640, 480) else: self.window.set_default_size (800, 600) self.window.set_size_request (800, 600) self.window.set_border_width (10) title = _("%s Installer") % (productName,) if os.environ["DISPLAY"][:1] != ':': # from gnome.zvt import * # zvtwin = gtk.Window () # shtitle = _("Red Hat Linux Install Shell") try: f = open ("/tmp/netinfo", "r") except: pass else: lines = f.readlines () f.close () for line in lines: netinf = string.splitfields (line, '=') if netinf[0] == "HOSTNAME": title = _("%s Installer on %s") % (productName, string.strip (netinf[1])) # shtitle = _("Red Hat Linux Install Shell on %s") % string.strip (netinf[1]) break # zvtwin.set_title (shtitle) # zvt = ZvtTerm (80, 24) # if zvt.forkpty() == 0: # os.execv ("/bin/sh", [ "/bin/sh" ]) # zvt.show () # zvtwin.add (zvt) # zvtwin.show_all () self.window.set_title (title) self.window.set_position (gtk.WIN_POS_CENTER) self.window.set_border_width(0) vbox = gtk.VBox (gtk.FALSE, 10) image = self.configFileData["TitleBar"] pixbuf = None # Create header at the top of the installer if runres != '640x480': fn = self.findPixmap(image) if not fn: log("unable to load %s", image) else : pixbuf = gtk.gdk.pixbuf_new_from_file(fn) if pixbuf: p = gtk.Image() p.set_from_pixbuf(pixbuf) a = gtk.Alignment() a.set(0.5, 0.5, 1.0, 1.0) a.add(p) vbox.pack_start(a, gtk.FALSE, gtk.TRUE, 0) else: print _("Unable to load title bar") self.loadReleaseNotes() vbox.set_spacing(0) self.buttonBox = gtk.HButtonBox () self.buttonBox.set_layout (gtk.BUTTONBOX_END) self.buttonBox.set_spacing (30) self.buildStockButtons() group = gtk.AccelGroup() self.window.add_accel_group(group) self.nextButtonStock.add_accelerator('clicked', group, gtk.keysyms.F12, gtk.gdk.RELEASE_MASK, 0); # set up ctrl+alt+delete handler self.window.connect ("key-release-event", self.keyRelease) if DEBUG: self.buttonBox.add (self.debugButton) self.buttonBox.add (self.prevButtonStock) self.buttonBox.add (self.nextButtonStock) self.hbox = gtk.HBox () self.hbox.set_border_width(5) self.hbox.pack_start (self.hideHelpButton, gtk.FALSE) self.hbox.set_spacing (25) self.hbox.pack_start (self.releaseButton, gtk.FALSE) self.hbox.pack_start (self.buttonBox) vbox.pack_end (self.hbox, gtk.FALSE) self.help = TextViewBrowser() self.displayHelp = gtk.TRUE self.helpState = gtk.TRUE self.helpFrame = gtk.Frame (_("Online Help")) self.box = gtk.VBox (gtk.FALSE, 0) self.box.set_spacing(0) self.help_sw = gtk.ScrolledWindow() self.help_sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.help_sw.set_shadow_type(gtk.SHADOW_IN) self.help_sw.add(self.help) self.box.pack_start(self.help_sw, gtk.TRUE) self.helpFrame.add (self.box) table = gtk.Table (1, 3, gtk.TRUE) table.attach (self.helpFrame, 0, 1, 0, 1, gtk.FILL | gtk.EXPAND, gtk.FILL | gtk.EXPAND) self.installFrame = gtk.Frame () self.windowList = [] #self.setStateList (self.steps, 0) self.setScreen () table.attach (self.installFrame, 1, 3, 0, 1, gtk.FILL | gtk.EXPAND, gtk.FILL | gtk.EXPAND) table.set_col_spacing (0, 5) self.bin = gtk.Frame () self.bin.set_shadow_type (gtk.SHADOW_NONE) self.bin.add (table) vbox.pack_end (self.bin, gtk.TRUE, gtk.TRUE) self.table = table self.window.add (vbox) # Popup the ICW and wait for it to wake us back up self.window.show_all () splashScreenPop() def busyCursorPush(self): rootPushBusyCursor() def busyCursorPop(self): rootPopBusyCursor() def run (self, runres, configFileData): self.configFileData = configFileData self.setup_window(runres) gtk.main() class InstallControlState: def __init__ (self, cw): self.searchPath = ("/mnt/source/RHupdates", "./", "/usr/share/anaconda/") self.cw = cw self.prevEnabled = 1 self.nextEnabled = 1 self.nextButtonInfo = None self.helpButtonEnabled = gtk.TRUE self.title = _("Install Window") self.html = "" self.htmlFile = None self.nextButton = 'gtk-next' self.prevButton = 'gtk-prev' self.nextButtonLabel = None self.prevButtonLabel = None # Values other than gtk.TRUE or gtk.FALSE don't change the help setting self.helpEnabled = 3 self.grabNext = 0 def setTitle (self, title): self.title = title self.cw.update (self) def getTitle (self): return self.title def setPrevEnabled (self, value): if value == self.prevEnabled: return self.prevEnabled = value self.cw.update (self) def getPrevEnabled (self): return self.prevEnabled def setNextEnabled (self, value): if value != self.nextEnabled: self.nextEnabled = value self.cw.update (self) def getNextEnabled (self): return self.nextEnabled def setHelpButtonEnabled (self, value): if value == self.helpButtonEnabled: return self.helpButtonEnabled = value self.cw.update (self) def getHelpButtonEnabled (self): return self.helpButtonEnabled def findPixmap(self, file): for path in ("/mnt/source/" + productSite + "/RHupdates/pixmaps/", "/mnt/source/" + productSite + "/RHupdates/", "/tmp/updates/pixmaps/", "/tmp/updates/", "/mnt/source/RHupdates/pixmaps/", "/mnt/source/RHupdates/", "/tmp/product/pixmaps/", "/tmp/product/", "/usr/share/anaconda/pixmaps/", "pixmaps/", "/usr/share/pixmaps/", "/usr/share/anaconda/", ""): fn = path + file if os.access(fn, os.R_OK): return fn return None def readPixmap (self, file, height = None, width = None): fn = self.findPixmap(file) if not fn: log("unable to load %s", file) return None try: pixbuf = gtk.gdk.pixbuf_new_from_file(fn) except RuntimeError, msg: log("unable to read %s: %s", file, msg) return None if (height is not None and width is not None and height != pixbuf.get_height() and width != pixbuf.get_width()): sclpix = pixbuf.scale_simple(height, width, gtk.gdk.INTERP_BILINEAR) p = gtk.Image() p.set_from_pixbuf(sclpix) else: source = gtk.IconSource() source.set_pixbuf(pixbuf) source.set_size(gtk.ICON_SIZE_DIALOG) source.set_size_wildcarded(gtk.FALSE) iconset = gtk.IconSet() iconset.add_source(source) p = gtk.image_new_from_icon_set(iconset, gtk.ICON_SIZE_DIALOG) return p def readPixmapDithered(self, file, height = None, width = None): fn = self.findPixmap(file) if not fn: log("unable to load %s", file) return None try: pixbuf = gtk.gdk.pixbuf_new_from_file(fn) except RuntimeError, msg: log("unable to read %s: %s", file, msg) return None if (height is not None and width is not None and height != pixbuf.get_height() and width != pixbuf.get_width()): pixbuf = pixbuf.scale_simple(height, width, gtk.gdk.INTERP_BILINEAR) (pixmap, mask) = pixbuf.render_pixmap_and_mask() pixbuf.render_to_drawable(pixmap, gtk.gdk.GC(pixmap), 0, 0, 0, 0, pixbuf.get_width(), pixbuf.get_height(), gtk.gdk.RGB_DITHER_MAX, 0, 0) p = gtk.Image() p.set_from_pixmap(pixmap, mask) return p def readHTML (self, file): self.htmlFile = file def setHTML (self, text): self.html = text self.cw.update (self) def getHTML (self, langPath): text = None if self.htmlFile: file = self.htmlFile if self.cw.configFileData.has_key("helptag"): helpTag = "-%s" % (self.cw.configFileData["helptag"],) else: helpTag = "" arch = "-%s" % (iutil.getArch(),) tags = [ "%s%s" % (helpTag, arch), "%s" % (helpTag,), "%s" % (arch,), "" ] found = 0 for path in self.searchPath: if found: break for lang in langPath + ['C']: if found: break for tag in tags: try: text = open("%s/help/%s/s1-help-screens-%s%s.html" % (path, lang, file, tag)).read () found = 1 break except IOError: continue if text: break if text: text = text.replace("@RHL@", productName) text = text.replace("@RHLVER@", productVersion) return text print "Unable to read %s help text" % (file,) return self.html def setScreenPrev (self): self.cw.prevClicked () def setScreenNext (self): self.cw.nextClicked () def setHelpEnabled (self, value): self.helpEnabled = value self.cw.update (self) def getHelpEnabled (self): return self.helpEnabled def setGrabNext (self, value): self.grabNext = value self.cw.update (self) def getGrabNext (self): return self.grabNext def getICW (self): return self.cw def setNextButton(self, icon, text): self.nextButtonInfo = (icon, text) def getNextButton(self): return self.nextButtonInfo # # image.py - Install method for disk image installs (CD & NFS) # # Copyright 1999-2003 Red Hat, Inc. # # This software may be freely redistributed under the terms of the GNU # library public license. # # You should have received a copy of the GNU Library Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # from hdrlist import groupSetFromCompsFile, HeaderListFromFile from installmethod import InstallMethod, FileCopyException import iutil import os import isys import time import stat import kudzu import string import shutil from constants import * from rhpl.log import log from rhpl.translate import _ import product # this sucks, but we want to consider s390x as s390x in here but generally # don't. *sigh* if os.uname()[4] == "s390x": _arch = "s390x" else: _arch = iutil.getArch() class ImageInstallMethod(InstallMethod): def readCompsViaMethod(self, hdlist): fname = self.findBestFileMatch(self.tree, 'comps.xml') if fname is None: raise FileCopyException return groupSetFromCompsFile(fname, hdlist) def getFilename(self, h, timer): if self.isUpdateRPM(h) and self.updatesCopied == 1: log ("going to use cached package for %s" %(h[1000000],)) return "/var/spool/anaconda-updates/" + h[1000000] if self.currentIso is not None and self.currentIso != h[1000002]: log("switching from iso %s to %s for %s-%s-%s.%s" %(self.currentIso, h[1000002], h['name'], h['version'], h['release'], h['arch'])) self.currentIso = h[1000002] #CJS changed if self.isUpdateRPM(h): # path = "/RedHat/Updates/" path = "/" + product.productSite + "/Updates/" else: # path = "/RedHat/RPMS/" path = "/" + product.productDefault + "/RPMS/" return self.tree + path + h[1000000] #CJS changed def readHeaders(self): # if not os.access(self.tree + "/RedHat/base/hdlist", os.R_OK): # raise FileCopyException # return HeaderListFromFile(self.tree + "/RedHat/base/hdlist") log("self.tree is %s, productSite is %s" % (self.tree,product.productSite)) if not os.access(self.tree + "/" + product.productSite + "/base/hdlist", os.R_OK): raise FileCopyException return HeaderListFromFile(self.tree + "/" + product.productSite + "/base/hdlist") #CJS changed def mergeFullHeaders(self, hdlist): # if not os.access(self.tree + "/RedHat/base/hdlist2", os.R_OK): # raise FileCopyException # hdlist.mergeFullHeaders(self.tree + "/RedHat/base/hdlist2") if not os.access(self.tree + "/" + product.productSite + "/base/hdlist2", os.R_OK): raise FileCopyException hdlist.mergeFullHeaders(self.tree + "/" + product.productSite + "/base/hdlist2") def getSourcePath(self): return self.tree def copyFileToTemp(self, filename): tmppath = self.getTempPath() path = tmppath + os.path.basename(filename) shutil.copy(self.tree + "/" + filename, path) return path def __init__(self, tree, rootPath): InstallMethod.__init__(self, rootPath) self.tree = tree self.currentIso = None #CJS added for informational log("product.productSite is %s" % product.productSite) log("product.productDefault is %s" % product.productDefault) class CdromInstallMethod(ImageInstallMethod): def unmountCD(self): done = 0 while done == 0: try: isys.umount("/mnt/source") self.currentDisc = [] break except: self.messageWindow(_("Error"), _("An error occurred unmounting the CD. " "Please make sure you're not accessing " "%s from the shell on tty2 " "and then click OK to retry.") % ("/mnt/source",)) def ejectCD(self): log("ejecting CD") # make /tmp/cdrom again so cd gets ejected isys.makeDevInode(self.device, "/tmp/cdrom") try: isys.ejectCdrom("/tmp/cdrom", makeDevice = 0) except Exception, e: log("eject failed %s" % (e,)) pass def systemUnmounted(self): if self.loopbackFile: isys.makeDevInode("loop0", "/tmp/loop") # isys.lochangefd("/tmp/loop", # "%s/RedHat/base/stage2.img" % self.tree) isys.lochangefd("/tmp/loop", "%s/%s/base/stage2.img" % (self.tree, product.productDefault)) self.loopbackFile = None def systemMounted(self, fsset, chroot): self.loopbackFile = "%s%s%s" % (chroot, fsset.filesystemSpace(chroot)[0][0], "/rhinstall-stage2.img") try: # iutil.copyFile("%s/RedHat/base/stage2.img" % self.tree, # self.loopbackFile, # (self.progressWindow, _("Copying File"), # _("Transferring install image to hard drive..."))) #CJS more changes for cdrom install iutil.copyFile("%s/%s/base/stage2.img" % (self.tree, product.productDefault ) , self.loopbackFile, (self.progressWindow, _("Copying File"), _("Transferring install image to hard drive..."))) except: self.messageWindow(_("Error"), _("An error occurred transferring the install image " "to your hard drive. You are probably out of disk " "space.")) os.unlink(self.loopbackFile) return 1 isys.makeDevInode("loop0", "/tmp/loop") isys.lochangefd("/tmp/loop", self.loopbackFile) def getFilename(self, h, timer): if self.isUpdateRPM(h) and self.updatesCopied == 1: log ("going to use cached package for %s" %(h[1000000],)) return "/var/spool/anaconda-updates/" + h[1000000] if h[1000002] == None: log ("header for %s has no disc location tag, assuming it's" "on the current CD", h[1000000]) elif h[1000002] not in self.currentDisc: timer.stop() log("switching from iso %s to %s for %s-%s-%s.%s" %(self.currentDisc, h[1000002], h['name'], h['version'], h['release'], h['arch'])) if os.access("/mnt/source/.discinfo", os.R_OK): f = open("/mnt/source/.discinfo") timestamp = f.readline().strip() f.close() else: timestamp = self.timestamp if self.timestamp is None: self.timestamp = timestamp needed = h[1000002] # if self.currentDisc is empty, then we shouldn't have anything # mounted. double-check by trying to unmount, but we don't want # to get into a loop of trying to unmount forever. if # self.currentDisc is set, then it should still be mounted and # we want to loop until it unmounts successfully if not self.currentDisc: try: isys.umount("/mnt/source") except: pass else: self.unmountCD() done = 0 cdlist = [] for (dev, something, descript) in \ kudzu.probe(kudzu.CLASS_CDROM, kudzu.BUS_UNSPEC, 0): cdlist.append(dev) for dev in cdlist: try: if not isys.mount(dev, "/mnt/source", fstype = "iso9660", readOnly = 1): if os.access("/mnt/source/.discinfo", os.R_OK): f = open("/mnt/source/.discinfo") newStamp = f.readline().strip() try: descr = f.readline().strip() except: descr = None try: arch = f.readline().strip() except: arch = None try: discNum = getDiscNums(f.readline().strip()) except: discNum = [ 0 ] f.close() if (newStamp == timestamp and arch == _arch and needed in discNum): done = 1 self.currentDisc = discNum if not done: isys.umount("/mnt/source") except: pass if done: break if not done: isys.ejectCdrom(self.device) while not done: self.messageWindow(_("Change CDROM"), _("Please insert disc %d to continue.") % needed) try: if isys.mount(self.device, "/mnt/source", fstype = "iso9660", readOnly = 1): time.sleep(3) isys.mount(self.device, "/mnt/source", fstype = "iso9660", readOnly = 1) if os.access("/mnt/source/.discinfo", os.R_OK): f = open("/mnt/source/.discinfo") newStamp = f.readline().strip() try: descr = f.readline().strip() except: descr = None try: arch = f.readline().strip() except: arch = None try: discNum = getDiscNums(f.readline().strip()) except: discNum = [ 0 ] f.close() if (newStamp == timestamp and arch == _arch and needed in discNum): done = 1 self.currentDisc = discNum # make /tmp/cdrom again so cd gets ejected isys.makeDevInode(self.device, "/tmp/cdrom") if not done: self.messageWindow(_("Wrong CDROM"), _("That's not the correct %s CDROM.") % (productName,)) isys.umount("/mnt/source") isys.ejectCdrom(self.device) except: self.messageWindow(_("Error"), _("The CDROM could not be mounted.")) timer.start() # if we haven't read a timestamp yet, let's try to get one if (self.timestamp is None and os.access("/mnt/source/.discinfo", os.R_OK)): try: f = open("/mnt/source/.discinfo") self.timestamp = f.readline().strip() f.close() except: pass tmppath = self.getTempPath() tries = 0 # FIXME: should retry a few times then prompt for new cd #CJS for cdrom while tries < 5: try: if self.isUpdateRPM(h): # path = "/RedHat/Updates/" path = "/" + product.productSite + "/Updates/" else: # path = "/RedHat/RPMS/" path = "/" + product.productDefault + "/RPMS/" shutil.copy(self.tree + path + h[1000000], tmppath + h[1000000]) except IOError, (errnum, msg): log("IOError %s occurred copying %s: %s", errnum, h[1000000], str(msg)) time.sleep(5) else: break tries = tries + 1 if tries >= 5: raise FileCopyException return tmppath + h[1000000] def unlinkFilename(self, fullName): os.remove(fullName) def filesDone(self): # we're trying to unmount the CD here. if it fails, oh well, # they'll reboot soon enough I guess :) try: isys.umount("/mnt/source") except: log("unable to unmount source in filesDone") if not self.loopbackFile: return try: # this isn't the exact right place, but it's close enough os.unlink(self.loopbackFile) except SystemError: pass def __init__(self, url, messageWindow, progressWindow, rootPath): (self.device, tree) = string.split(url, ":", 1) if not tree.startswith("/"): tree = "/%s" %(tree,) self.messageWindow = messageWindow self.progressWindow = progressWindow self.loopbackFile = None # figure out which disc is in. if we fail for any reason, # assume it's just disc1. if os.access("/mnt/source/.discinfo", os.R_OK): try: f = open("/mnt/source/.discinfo") self.timestamp = f.readline().strip() f.readline() # descr f.readline() # arch self.currentDisc = getDiscNums(f.readline().strip()) f.close() except: self.currentDisc = [ 1 ] self.timestamp = None else: self.currentDisc = [ 1 ] ImageInstallMethod.__init__(self, tree, rootPath) self.needUpdateCache = 1 class NfsInstallMethod(ImageInstallMethod): def __init__(self, tree, rootPath): ImageInstallMethod.__init__(self, tree, rootPath) def getDiscNums(line): # get the disc numbers for this disc nums = line.split(",") discNums = [] for num in nums: discNums.append(int(num)) return discNums def findIsoImages(path, messageWindow): files = os.listdir(path) arch = _arch discImages = {} for file in files: what = path + '/' + file if not isys.isIsoImage(what): continue isys.makeDevInode("loop2", "/tmp/loop2") try: isys.losetup("/tmp/loop2", what, readOnly = 1) except SystemError: continue try: isys.mount("loop2", "/mnt/cdimage", fstype = "iso9660", readOnly = 1) for num in range(1, 10): if os.access("/mnt/cdimage/.discinfo", os.R_OK): f = open("/mnt/cdimage/.discinfo") try: f.readline() # skip timestamp f.readline() # skip release description discArch = string.strip(f.readline()) # read architecture discNum = getDiscNums(f.readline().strip()) except: discArch = None discNum = [ 0 ] f.close() if num not in discNum or discArch != arch: continue # if it's disc1, it needs to have RedHat/base/stage2.img if (num == 1 and not # os.access("/mnt/cdimage/RedHat/base/stage2.img", # os.R_OK)): os.access("/mnt/cdimage/" + product.productDefault + "/base/stage2.img", os.R_OK)): continue # warn user if images appears to be wrong size if os.stat(what)[stat.ST_SIZE] % 2048: rc = messageWindow(_("Warning"), "The ISO image %s has a size which is not " "a multiple of 2048 bytes. This may mean " "it was corrupted on transfer to this computer." "\n\nPress OK to continue (but installation will " "probably fail), or Cancel to exit the " "installer (RECOMMENDED). " % file, type = "okcancel") if rc: import sys sys.exit(0) discImages[num] = file isys.umount("/mnt/cdimage") except SystemError: pass isys.makeDevInode("loop2", '/tmp/' + "loop2") isys.unlosetup("/tmp/loop2") return discImages class NfsIsoInstallMethod(NfsInstallMethod): def getFilename(self, h, timer): if self.imageMounted != h[1000002]: log("switching from iso %s to %s for %s-%s-%s.%s" %(self.imageMounted, h[1000002], h['name'], h['version'], h['release'], h['arch'])) self.umountImage() self.mountImage(h[1000002]) if self.isUpdateRPM(h): # path = "/RedHat/Updates/" path = "/" + product.productSite + "/Updates/" else: # path = "/RedHat/RPMS/" path = "/" + product.productDefault + "/RPMS/" return self.mntPoint + path + h[1000000] def umountImage(self): if self.imageMounted: isys.umount(self.mntPoint) isys.makeDevInode("loop3", "/tmp/loop3") isys.unlosetup("/tmp/loop3") self.mntPoint = None self.imageMounted = 0 def mountImage(self, cdNum): if (self.imageMounted): raise SystemError, "trying to mount already-mounted iso image!" isoImage = self.isoPath + '/' + self.discImages[cdNum] isys.makeDevInode("loop3", "/tmp/loop3") isys.losetup("/tmp/loop3", isoImage, readOnly = 1) isys.mount("loop3", "/tmp/isomedia", fstype = 'iso9660', readOnly = 1); self.mntPoint = "/tmp/isomedia/" self.imageMounted = cdNum def filesDone(self): # if we can't unmount the cd image, we really don't care much # let them go along and don't complain try: self.umountImage() except: log("unable to unmount iimage in filesDone") pass def __init__(self, tree, messageWindow, rootPath): self.imageMounted = None self.isoPath = tree # the tree points to the directory that holds the iso images # even though we already have the main one mounted once, it's # easiest to just mount it again so that we can treat all of the # images the same way -- we use loop3 for everything self.discImages = findIsoImages(tree, messageWindow) self.mountImage(1) ImageInstallMethod.__init__(self, self.mntPoint, rootPath) # # installmethod.py - Base class for install methods # # Copyright 1999-2002 Red Hat, Inc. # # This software may be freely redistributed under the terms of the GNU # library public license. # # You should have received a copy of the GNU Library Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # import os import string from hdrlist import groupSetFromCompsFile import isys import iutil import shutil from rhpl.log import log from rhpl.translate import _ import product class FileCopyException(Exception): def __init__(self, s = ""): self.args = s class InstallMethod: # find best match from several locations for a file def findBestFileMatch(self, treebase, file): # look in /tmp/updates first log("looking for %s in %s", file, treebase) rc = None tryloc = ["/tmp/updates"] if treebase is not None: tryloc.append(treebase + "/RHupdates") tryloc.append(treebase + "/" + product.productSite + "/base") tryloc.append(treebase + "/" + product.productDefault + "/base") # tryloc.append(treebase + "/RedHat/base") for pre in tryloc: tmpname = pre + "/" + file log("looking for %s ", tmpname) if os.access(tmpname, os.R_OK): log("Using file://%s", tmpname) return "file://%s" %(tmpname,) log("Unable to find %s", file) return None def protectedPartitions(self): return None def readCompsViaMethod(self, hdlist): pass def readComps(self, hdlist): # see if there is a comps in PYTHONPATH, otherwise fall thru # to method dependent location path = None if os.environ.has_key('PYTHONPATH'): for f in string.split(os.environ['PYTHONPATH'], ":"): if os.access (f+"/comps", os.X_OK): path = f+"/comps" break if path: return groupSetFromCompsFile(path, hdlist) else: return self.readCompsViaMethod(hdlist) pass def getTempPath(self): root = self.rootPath pathlist = [ "/var/tmp", "/tmp", "/." ] tmppath = None for p in pathlist: if (os.access(root + p, os.X_OK)): tmppath = root + p + "/" break if tmppath is None: log("Unable to find temp path, going to use ramfs path") return "/tmp/" return tmppath def getFilename(self, h, timer): pass def readHeaders(self): pass def systemUnmounted(self): pass def systemMounted(self, fstab, mntPoint): pass def isUpdateRPM(self, hdr): if (1000005 in hdr.keys()) and (hdr[1000005] is not None): # if (hdr[1000005] is not None): return 1 return 0 def cacheUpdates(self, chroot, hdlist, intf): if self.needUpdateCache == 0: log("not caching updates") return log("going to cache updates") size = 0 num = 0 for h in hdlist.values(): if h.isSelected() and self.isUpdateRPM(h): log("%s is selected, size is %s" %(h.nevra(), h[1000001])) size += h[1000001] # FILESIZE_TAG num += 1 # make sure it looks like we have space + a fudge factor if (size / 1024.0 / 1024.0) > (isys.fsSpaceAvailable("/mnt/sysimage/var") + 50): log("only %s free on var and want %s, not caching updates" %(isys.fsSpaceAvailable("/mnt/sysimage/var"), size)) return if num == 0: return if intf: win = intf.progressWindow(_("Copying Files"), _("Transferring updated packages"), num) iutil.mkdirChain(chroot + "/var/spool/anaconda-updates") num = 0 for h in hdlist.values(): if h.isSelected() and self.isUpdateRPM(h): # path = "/RedHat/Updates/" path = "/" + product.productSite + "/Updates/" shutil.copy(self.tree + path + h[1000000], "%s/var/spool/anaconda-updates/%s" % (chroot, h[1000000])) num += 1 if intf: win.set(num) if intf: win.pop() self.updatesCopied = 1 def filesDone(self): pass def unlinkFilename(self, fullName): pass def __init__(self, rootpath): self.rootPath = rootpath self.needUpdateCache = 0 self.updatesCopied = 0 try: f = open("/proc/cmdline") line = f.readline() if string.find(line, " cacheupdates") != -1: self.needUpdateCache = 1 f.close() except: pass def getSourcePath(self): pass def unmountCD(self): pass def ejectCD(self): pass # this handles any cleanup needed for the method. it occurs *very* late # (ie immediately before the congratulations screen). main use right now # is ejecting the cdrom def doMethodComplete(method): method.ejectCD() # # packages.py: package management - mainly package installation # # Erik Troan # Matt Wilson # Michael Fulbright # Jeremy Katz # # Copyright 2001-2003 Red Hat, Inc. # # This software may be freely redistributed under the terms of the GNU # library public license. # # You should have received a copy of the GNU Library Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # import iutil import isys import rpm import os import timer import time import sys import string import pcmcia import language import fsset import kudzu from flags import flags from product import * from constants import * from syslogd import syslog from hdrlist import PKGTYPE_MANDATORY, PKGTYPE_DEFAULT, DependencyChecker from installmethod import FileCopyException from rhpl.log import log from rhpl.translate import _ import rhpl.arch def queryUpgradeContinue(intf, dir): if dir == DISPATCH_FORWARD: return rc = intf.messageWindow(_("Proceed with upgrade?"), _("The file systems of the Linux installation " "you have chosen to upgrade have already been " "mounted. You cannot go back past this point. " "\n\n") + _( "Would you like to continue with the upgrade?"), type = "yesno") if rc == 0: sys.exit(0) return DISPATCH_FORWARD def doPostAction(id, instPath): id.instClass.postAction(instPath, flags.serial) def firstbootConfiguration(id, instPath): if id.firstboot == FIRSTBOOT_RECONFIG: f = open(instPath + '/etc/reconfigSys', 'w+') f.close() elif id.firstboot == FIRSTBOOT_SKIP: f = open(instPath + '/etc/sysconfig/firstboot', 'w+') f.write('RUN_FIRSTBOOT=NO') f.close() return def writeConfiguration(id, instPath): log("Writing main configuration") if not flags.test: id.write(instPath) def writeKSConfiguration(id, instPath): log("Writing autokickstart file") if not flags.test: fn = instPath + "/root/anaconda-ks.cfg" else: fn = "/tmp/anaconda-ks.cfg" id.writeKS(fn) def writeXConfiguration(id, instPath): testmode = flags.test # comment out to test if testmode: return # end code to comment to test # uncomment to test writing X config in test mode # try: # os.mkdir("/tmp/etc") # except: # pass # try: # os.mkdir("/tmp/etc/X11") # except: # pass # instPath = '/' # end code for test writing if id.xsetup.skipx: return xserver = id.videocard.primaryCard().getXServer() if not xserver: return log("Writing X configuration") if not testmode: fn = instPath if os.access (instPath + "/etc/X11/X", os.R_OK): os.rename (instPath + "/etc/X11/X", instPath + "/etc/X11/X.rpmsave") try: os.unlink (instPath + "/etc/X11/X") except OSError: pass os.symlink ("../../usr/X11R6/bin/" + xserver, instPath + "/etc/X11/X") else: fn = "/tmp/" id.xsetup.write(fn+"/etc/X11", id.mouse, id.keyboard) id.desktop.write(instPath) def readPackages(intf, method, id): if id.grpset: grpset = id.grpset hdrlist = id.grpset.hdrlist doselect = 0 else: grpset = None hdrlist = None doselect = 1 while hdrlist is None: w = intf.waitWindow(_("Reading"), _("Reading package information...")) try: hdrlist = method.readHeaders() except FileCopyException: w.pop() method.unmountCD() intf.messageWindow(_("Error"), _("Unable to read header list. This may be " "due to a missing file or bad media. " "Press to try again.")) continue w.pop() while grpset is None: try: grpset = method.readComps(hdrlist) except FileCopyException: method.unmountCD() intf.messageWindow(_("Error"), _("Unable to read comps file. This may be " "due to a missing file or bad media. " "Press to try again.")) continue while iutil.getArch() == "ia64": try: method.mergeFullHeaders(hdrlist) break except FileCopyException: method.unmountCD() intf.messageWindow(_("Error"), _("Unable to merge header list. This may be " "due to a missing file or bad media. " "Press to try again.")) # this is a crappy hack, but I don't want bug reports from these people if (iutil.getArch() == "i386") and (not grpset.hdrlist.has_key("kernel")): intf.messageWindow(_("Error"), _("You are trying to install on a machine " "which isn't supported by this release of " "%s.") %(productName,), type="custom", custom_icon="error", custom_buttons=[_("_Exit")]) sys.exit(0) id.grpset = grpset if doselect: id.instClass.setGroupSelection(grpset, intf) id.instClass.setPackageSelection(hdrlist, intf) def handleX11Packages(dir, intf, disp, id, instPath): if dir == DISPATCH_BACK: return # skip X setup if it is not being installed if (not id.grpset.hdrlist.has_key('XFree86') or not id.grpset.hdrlist['XFree86'].isSelected()): disp.skipStep("videocard") disp.skipStep("monitor") disp.skipStep("xcustom") disp.skipStep("writexconfig") id.xsetup.skipx = 1 elif disp.stepInSkipList("videocard"): # if X is being installed, but videocard step skipped # need to turn it back on disp.skipStep("videocard", skip=0) disp.skipStep("monitor", skip=0) disp.skipStep("xcustom", skip=0) disp.skipStep("writexconfig", skip=0) id.xsetup.skipx = 0 # set default runlevel based on packages gnomeSelected = (id.grpset.hdrlist.has_key('gnome-session') and id.grpset.hdrlist['gnome-session'].isSelected()) kdeSelected = (id.grpset.hdrlist.has_key('kdebase') and id.grpset.hdrlist['kdebase'].isSelected()) if gnomeSelected: id.desktop.setDefaultDesktop("GNOME") elif kdeSelected: id.desktop.setDefaultDesktop("KDE") if gnomeSelected or kdeSelected: id.desktop.setDefaultRunLevel(5) def getAnacondaTS(instPath = None): if instPath: ts = rpm.TransactionSet(instPath) else: ts = rpm.TransactionSet() ts.setVSFlags(~(rpm.RPMVSF_NORSA|rpm.RPMVSF_NODSA)) ts.setFlags(rpm.RPMTRANS_FLAG_ANACONDA) # set color if needed. FIXME: why isn't this the default :/ if (rhpl.arch.canonArch.startswith("ppc64") or rhpl.arch.canonArch in ("s390x", "sparc64", "x86_64", "ia64")): ts.setColor(3) return ts def checkDependencies(dir, intf, disp, id, instPath): if dir == DISPATCH_BACK: return win = intf.waitWindow(_("Dependency Check"), _("Checking dependencies in packages selected for installation...")) # FIXME: we really don't need to build up a ts more than once # granted, this is better than before still if id.upgrade.get(): ts = getAnacondaTS(instPath) how = "u" else: ts = getAnacondaTS() how = "i" # set the rpm log file to /dev/null so that we don't segfault f = open("/dev/null", "w+") rpm.setLogFile(f) ts.scriptFd = f.fileno() for p in id.grpset.hdrlist.pkgs.values(): if p.isSelected(): ts.addInstall(p.hdr, p.hdr, how) depcheck = DependencyChecker(id.grpset, how) id.dependencies = ts.check(depcheck.callback) win.pop() if depcheck.added and id.handleDeps == CHECK_DEPS: disp.skipStep("dependencies", skip = 0) log("FIXME: had dependency problems. resolved them without informing the user") disp.skipStep("dependencies") else: disp.skipStep("dependencies") return # FIXME: I BROKE IT # this is kind of hackish, but makes kickstart happy if id.handleDeps == CHECK_DEPS: pass elif id.handleDeps == IGNORE_DEPS: id.comps.selectDepCause(id.dependencies) id.comps.unselectDeps(id.dependencies) elif id.handleDeps == RESOLVE_DEPS: id.comps.selectDepCause(id.dependencies) id.comps.selectDeps(id.dependencies) class InstallCallback: def cb(self, what, amount, total, h, (param)): # first time here means we should pop the window telling # user to wait until we get here if not self.beenCalled: self.beenCalled = 1 self.initWindow.pop() if (what == rpm.RPMCALLBACK_TRANS_START): # step 6 is the bulk of the transaction set # processing time if amount == 6: self.progressWindow = \ self.progressWindowClass (_("Processing"), _("Preparing to install..."), total) try: self.incr = total / 10 except: pass if (what == rpm.RPMCALLBACK_TRANS_PROGRESS): if self.progressWindow and amount > self.lastprogress + self.incr: self.progressWindow.set (amount) self.lastprogress = amount if (what == rpm.RPMCALLBACK_TRANS_STOP and self.progressWindow): self.progressWindow.pop () if (what == rpm.RPMCALLBACK_INST_OPEN_FILE): # We don't want to start the timer until we get to the first # file. self.pkgTimer.start() self.progress.setPackage(h) self.progress.setPackageScale(0, 1) self.instLog.write (self.modeText % (h[rpm.RPMTAG_NAME], h[rpm.RPMTAG_VERSION], h[rpm.RPMTAG_RELEASE], h[rpm.RPMTAG_ARCH])) self.instLog.flush () self.rpmFD = -1 self.size = h[rpm.RPMTAG_SIZE] while self.rpmFD < 0: try: fn = self.method.getFilename(h, self.pkgTimer) self.rpmFD = os.open(fn, os.O_RDONLY) # Make sure this package seems valid try: hdr = self.ts.hdrFromFdno(self.rpmFD) os.lseek(self.rpmFD, 0, 0) # if we don't have a valid package, throw an error if not hdr: raise SystemError except: try: os.close(self.rpmFD) except: pass self.rpmFD = -1 raise FileCopyException except Exception, e: log("exception was: %s" %(e,)) self.method.unmountCD() self.messageWindow(_("Error"), _("The package %s-%s-%s cannot be opened. This is due " "to a missing file or perhaps a corrupt package. " "If you are installing from CD media this usually " "means the CD media is corrupt, or the CD drive is " "unable to read the media.\n\n" "Press to try again.") % (h['name'], h['version'], h['release'])) fn = self.method.unlinkFilename(fn) return self.rpmFD elif (what == rpm.RPMCALLBACK_INST_PROGRESS): # RPM returns strange values sometimes if amount > total: amount = total if not total or total == 0 or total == "0": total = amount self.progress.setPackageScale(amount, total) elif (what == rpm.RPMCALLBACK_INST_CLOSE_FILE): os.close (self.rpmFD) self.progress.completePackage(h, self.pkgTimer) self.progress.processEvents() elif ((what == rpm.RPMCALLBACK_UNPACK_ERROR) or (what == rpm.RPMCALLBACK_CPIO_ERROR)): # we may want to make this error more fine-grained at some # point pkg = "%s-%s-%s" % (h[rpm.RPMTAG_NAME], h[rpm.RPMTAG_VERSION], h[rpm.RPMTAG_RELEASE]) self.messageWindow(_("Error Installing Package"), _("There was an error installing %s. This " "can indicate media failure, lack of disk " "space, and/or hardware problems. This is " "a fatal error and your install will be " "aborted. Please verify your media and try " "your install again.\n\n" "Press the OK button to reboot " "your system.") % (pkg,)) sys.exit(0) else: pass self.progress.processEvents() def __init__(self, messageWindow, progress, pkgTimer, method, progressWindowClass, instLog, modeText, ts): self.messageWindow = messageWindow self.progress = progress self.pkgTimer = pkgTimer self.method = method self.progressWindowClass = progressWindowClass self.progressWindow = None self.lastprogress = 0 self.incr = 20 self.instLog = instLog self.modeText = modeText self.beenCalled = 0 self.initWindow = None self.ts = ts def sortPackages(first, second): # install packages in cd order (cd tag is 1000002) one = None two = None if first[1000003] != None: one = first[1000003] if second[1000003] != None: two = second[1000003] if one == None or two == None: one = 0 two = 0 if first[1000002] != None: one = first[1000002] if second[1000002] != None: two = second[1000002] if one < two: return -1 elif one > two: return 1 elif (string.lower(first[rpm.RPMTAG_NAME]) < string.lower(second[rpm.RPMTAG_NAME])): return -1 elif (string.lower(first[rpm.RPMTAG_NAME]) > string.lower(second[rpm.RPMTAG_NAME])): return 1 return 0 class rpmErrorClass: def cb(self): self.f.write (rpm.errorString () + "\n") def __init__(self, f): self.f = f def doMigrateFilesystems(dir, thefsset, diskset, upgrade, instPath): if dir == DISPATCH_BACK: return DISPATCH_NOOP if thefsset.haveMigratedFilesystems(): return DISPATCH_NOOP thefsset.migrateFilesystems (instPath) def turnOnFilesystems(dir, thefsset, diskset, partitions, upgrade, instPath): if dir == DISPATCH_BACK: log("unmounting filesystems") thefsset.umountFilesystems(instPath) return if flags.setupFilesystems: if not upgrade.get(): partitions.doMetaDeletes(diskset) thefsset.setActive(diskset) if not thefsset.isActive(): diskset.savePartitions () thefsset.checkBadblocks(instPath) thefsset.createLogicalVolumes(instPath) thefsset.formatSwap(instPath) thefsset.turnOnSwap(instPath) thefsset.makeFilesystems (instPath) log("mounting filesystems") thefsset.mountFilesystems (instPath) def setupTimezone(timezone, upgrade, instPath, dir): # we don't need this on an upgrade or going backwards if upgrade.get() or (dir == DISPATCH_BACK): return os.environ["TZ"] = timezone.tz tzfile = "/usr/share/zoneinfo/" + timezone.tz if not os.access(tzfile, os.R_OK): log("unable to set timezone") else: try: iutil.copyFile(tzfile, "/etc/localtime") except OSError, (errno, msg): log("Error copying timezone (from %s): %s" %(tzfile, msg)) if iutil.getArch() == "s390": return args = [ "/usr/sbin/hwclock", "--hctosys" ] if timezone.utc: args.append("-u") elif timezone.arc: args.append("-a") try: iutil.execWithRedirect(args[0], args, stdin = None, stdout = "/dev/tty5", stderr = "/dev/tty5") except RuntimeError: log("Failed to set clock") def doPreInstall(method, id, intf, instPath, dir): if dir == DISPATCH_BACK: return arch = iutil.getArch () # this is a crappy hack, but I don't want bug reports from these people if (arch == "i386") and (not id.grpset.hdrlist.has_key("kernel")): intf.messageWindow(_("Error"), _("You are trying to install on a machine " "which isn't supported by this release of " "%s.") %(productName,), type="custom", custom_icon="error", custom_buttons=[_("_Exit")]) sys.exit(0) # shorthand upgrade = id.upgrade.get() def select(hdrlist, name): if hdrlist.has_key(name): hdrlist[name].select(isManual = 1) if not upgrade: # this is NICE and LATE. It lets kickstart/server/workstation # installs detect this properly if arch == "s390": if (string.find(os.uname()[2], "tape") > -1): select(id.grpset.hdrlist, 'kernel-tape') elif arch == "ppc" and iutil.getPPCMachine() == "pSeries": select(id.grpset.hdrlist, 'kernel-pseries') elif arch == "ppc" and iutil.getPPCMachine() == "iSeries": select(id.grpset.hdrlist, "kernel-iseries") if (isys.smpAvailable() or isys.htavailable()) and not rhpl.arch.canonArch == "ia32e": select(id.grpset.hdrlist, 'kernel-smp') ## Hook up our kernel-module-XXX packages - jaroslaw.polok@cern.ch if id.grpset.hdrlist['kernel-module-openafs-2.4.21-20.EL'].isSelected(): select(id.grpset.hdrlist, 'kernel-module-openafs-2.4.21-20.ELsmp') if iutil.needsEnterpriseKernel(): select(id.grpset.hdrlist, "kernel-bigmem") if isys.summitavailable(): select(id.grpset.hdrlist, "kernel-summit") # we *always* need a kernel installed select(id.grpset.hdrlist, 'kernel') # if NIS is configured, install ypbind and dependencies: if id.auth.useNIS: select(id.grpset.hdrlist, 'ypbind') select(id.grpset.hdrlist, 'yp-tools') select(id.grpset.hdrlist, 'portmap') if id.auth.useLdap: select(id.grpset.hdrlist, 'nss_ldap') select(id.grpset.hdrlist, 'openldap') select(id.grpset.hdrlist, 'perl') if id.auth.useKrb5: select(id.grpset.hdrlist, 'pam_krb5') select(id.grpset.hdrlist, 'krb5-workstation') select(id.grpset.hdrlist, 'krbafs') select(id.grpset.hdrlist, 'krb5-libs') if id.auth.useSamba: select(id.grpset.hdrlist, 'pam_smb') if iutil.getArch() == "i386" and id.bootloader.useGrubVal == 0: select(id.grpset.hdrlist, 'lilo') elif iutil.getArch() == "i386" and id.bootloader.useGrubVal == 1: select(id.grpset.hdrlist, 'grub') elif iutil.getArch() == "s390": select(id.grpset.hdrlist, 's390utils') elif iutil.getArch() == "ppc": select(id.grpset.hdrlist, 'yaboot') elif iutil.getArch() == "ia64": select(id.grpset.hdrlist, 'elilo') if pcmcia.pcicType(): select(id.grpset.hdrlist, 'kernel-pcmcia-cs') if flags.test: return # make sure that all comps that include other comps are # selected (i.e. - recurse down the selected comps and turn # on the children while 1: try: method.mergeFullHeaders(id.grpset.hdrlist) except FileCopyException: method.unmountCD() intf.messageWindow(_("Error"), _("Unable to merge header list. This may be " "due to a missing file or bad media. " "Press to try again.")) else: break if upgrade: # An old mtab can cause confusion (esp if loop devices are # in it) f = open(instPath + "/etc/mtab", "w+") f.close() if method.systemMounted (id.fsset, instPath): id.fsset.umountFilesystems(instPath) return DISPATCH_BACK for i in ( '/var', '/var/lib', '/var/lib/rpm', '/tmp', '/dev', '/etc', '/etc/sysconfig', '/etc/sysconfig/network-scripts', '/etc/X11', '/root', '/var/tmp', '/etc/rpm' ): try: os.mkdir(instPath + i) except os.error, (errno, msg): pass # log("Error making directory %s: %s" % (i, msg)) if flags.setupFilesystems: # setup /etc/rpm/platform for the post-install environment iutil.writeRpmPlatform(instPath) try: # FIXME: making the /var/lib/rpm symlink here is a hack to # workaround db->close() errors from rpm iutil.mkdirChain("/var/lib") iutil.mkdirChain("/var/spool") for path in ("/var/tmp", "/var/lib/rpm","/var/spool/anaconda-updates"): if os.path.exists(path) and not os.path.islink(path): iutil.rmrf(path) if not os.path.islink(path): os.symlink("/mnt/sysimage/%s" %(path,), "%s" %(path,)) else: log("%s already exists as a symlink to %s" %(path, os.readlink(path),)) except Exception, e: # how this could happen isn't entirely clear; log it in case # it does and causes problems later log("error creating symlink, continuing anyway: %s" %(e,)) # try to copy the comps package. if it doesn't work, don't worry about it if (product.productDefault == product.productSite) : tmparea = productDefault + "/base/comps.rpm" else: tmparea = productSite + "/base/comps.rpm" try: id.compspkg = method.copyFileToTemp(tmparea) except: log("Unable to copy comps package") id.compspkg = None # write out the fstab if not upgrade: id.fsset.write(instPath) # rootpath mode doesn't have this file around if os.access("/tmp/modules.conf", os.R_OK): iutil.copyFile("/tmp/modules.conf", instPath + "/etc/modules.conf") # make a /etc/mtab so mkinitrd can handle certain hw (usb) correctly f = open(instPath + "/etc/mtab", "w+") f.write(id.fsset.mtab()) f.close() # we need to cache packages that are "updates" if we're doing a cd # install. we write them to under /var since that's where up2date # will end up putting updates try: method.cacheUpdates(instPath, id.grpset.hdrlist, intf) except Exception, e: log("Problem caching updates: %s" %(e,)) # delay writing migrate adjusted fstab till later, in case # rpm transaction set determines they don't have enough space to upgrade # else: # id.fsset.migratewrite(instPath) def doInstall(method, id, intf, instPath): if flags.test: return # set up dependency white outs import whiteout upgrade = id.upgrade.get() ts = getAnacondaTS(instPath) total = 0 totalSize = 0 if upgrade: how = "u" else: how = "i" rpm.addMacro("__dbi_htconfig", "hash nofsync %{__dbi_other} %{__dbi_perms}") l = [] for p in id.grpset.hdrlist.values(): if p.isSelected(): l.append(p) l.sort(sortPackages) progress = intf.progressWindow(_("Processing"), _("Preparing RPM transaction..."), len(l)) # this is kind of a hack, but has to be done so we can have a chance # with broken triggers if upgrade and len(id.upgradeRemove) > 0: # simple rpm callback since erasure doesn't need anything def install_callback(what, bytes, total, h, user): pass for pkg in id.upgradeRemove: ts.addErase(pkg) # if we hit problems, it's not like there's anything we can # do about it ts.run(install_callback, 0) # new transaction set ts.closeDB() del ts ts = getAnacondaTS(instPath) # we don't want to try to remove things more than once (#84221) id.upgradeRemove = [] i = 0 updcount = 0 updintv = len(l) / 25 for p in l: ts.addInstall(p.hdr, p.hdr, how) total = total + 1 totalSize = totalSize + (p[rpm.RPMTAG_SIZE] / 1024) i = i + 1 # HACK - dont overload progress bar with useless requests updcount = updcount + 1 if updcount > updintv: progress.set(i) updcount = 0 progress.pop() depcheck = DependencyChecker(id.grpset) if not id.grpset.hdrlist.preordered(): log ("WARNING: not all packages in hdlist had order tag") # have to call ts.check before ts.order() to set up the alIndex ts.check(depcheck.callback) ts.order() else: ts.check(depcheck.callback) if upgrade: logname = '/root/upgrade.log' else: logname = '/root/install.log' instLogName = instPath + logname try: iutil.rmrf (instLogName) except OSError: pass instLog = open(instLogName, "w+") syslogname = "%s%s.syslog" % (instPath, logname) try: iutil.rmrf (syslogname) except OSError: pass syslog.start (instPath, syslogname) if id.compspkg is not None: num = i + 1 else: num = i if upgrade: instLog.write(_("Upgrading %s packages\n\n") % (num,)) else: instLog.write(_("Installing %s packages\n\n") % (num,)) ts.scriptFd = instLog.fileno () rpm.setLogFile(instLog) # the transaction set dup()s the file descriptor and will close the # dup'd when we go out of scope if upgrade: modeText = _("Upgrading %s-%s-%s.%s.\n") else: modeText = _("Installing %s-%s-%s.%s.\n") errors = rpmErrorClass(instLog) pkgTimer = timer.Timer(start = 0) id.instProgress.setSizes(total, totalSize) id.instProgress.processEvents() cb = InstallCallback(intf.messageWindow, id.instProgress, pkgTimer, method, intf.progressWindow, instLog, modeText, ts) # write out migrate adjusted fstab so kernel RPM can get initrd right if upgrade: id.fsset.migratewrite(instPath) if id.upgradeDeps: instLog.write(_("\n\nThe following packages were automatically\n" "selected to be installed:" "\n" "%s" "\n\n") % (id.upgradeDeps,)) cb.initWindow = intf.waitWindow(_("Install Starting"), _("Starting install process, this may take several minutes...")) ts.setProbFilter(~rpm.RPMPROB_FILTER_DISKSPACE) problems = ts.run(cb.cb, 0) if problems: # restore old fstab if we did anything for migrating if upgrade: id.fsset.restoreMigratedFstab(instPath) spaceneeded = {} nodeneeded = {} size = 12 for (descr, (type, mount, need)) in problems: log("(%s, (%s, %s, %s))" %(descr, type, mount, need)) if mount and mount.startswith(instPath): mount = mount[len(instPath):] if not mount: mount = '/' if type == rpm.RPMPROB_DISKSPACE: if spaceneeded.has_key (mount) and spaceneeded[mount] < need: spaceneeded[mount] = need else: spaceneeded[mount] = need elif type == nodeprob: if nodeneeded.has_key (mount) and nodeneeded[mount] < need: nodeneeded[mount] = need else: nodeneeded[mount] = need else: if descr is None: descr = "no description" log ("WARNING: unhandled problem returned from " "transaction set type %d (%s)", type, descr) probs = "" if spaceneeded: probs = probs + _("You don't appear to have enough disk space " "to install the packages you've selected. " "You need more space on the following " "file systems:\n\n") probs = probs + ("%-15s %s\n") % (_("Mount Point"), _("Space Needed")) for (mount, need) in spaceneeded.items (): log("(%s, %s)" %(mount, need)) if need > (1024*1024): need = (need + 1024 * 1024 - 1) / (1024 * 1024) suffix = "M" else: need = (need + 1023) / 1024 suffix = "k" prob = "%-15s %d %c\n" % (mount, need, suffix) probs = probs + prob if nodeneeded: if probs: probs = probs + '\n' probs = probs + _("You don't appear to have enough file nodes " "to install the packages you've selected. " "You need more file nodes on the following " "file systems:\n\n") probs = probs + ("%-15s %s\n") % (_("Mount Point"), _("Nodes Needed")) for (mount, need) in nodeneeded.items (): prob = "%-15s %d\n" % (mount, need) probs = probs + prob if len(probs) == 0: probs = ("ERROR: NO! An unexpected problem has occurred with " "your transaction set. Please see tty3 for more " "information") intf.messageWindow (_("Disk Space"), probs) ts.closeDB() del ts instLog.close() syslog.stop() method.systemUnmounted () return DISPATCH_BACK # This should close the RPM database so that you can # do RPM ops in the chroot in a %post ks script ts.closeDB() del ts # make sure the window gets popped (#82862) if not cb.beenCalled: cb.initWindow.pop() method.filesDone () # rpm environment files go bye-bye for file in ["__db.001", "__db.002", "__db.003"]: try: os.unlink("%s/var/lib/rpm/%s" %(instPath, file)) except Exception, e: log("failed to unlink /var/lib/rpm/%s: %s" %(file,e)) # FIXME: remove the /var/lib/rpm symlink that keeps us from having # db->close error messages shown. I don't really like this though :( try: os.unlink("/var/lib/rpm") except Exception, e: log("failed to unlink /var/lib/rpm: %s" %(e,)) if upgrade: instLog.write(_("\n\nThe following packages were available in " "this version but NOT upgraded:\n")) lines = [] for p in id.grpset.hdrlist.values(): if not p.isSelected(): lines.append("%s-%s-%s.%s.rpm\n" % (p.hdr[rpm.RPMTAG_NAME], p.hdr[rpm.RPMTAG_VERSION], p.hdr[rpm.RPMTAG_RELEASE], p.hdr[rpm.RPMTAG_ARCH])) lines.sort() for line in lines: instLog.write(line) instLog.close () if id.grpset.hdrlist.has_key("rhgb") and id.grpset.hdrlist["rhgb"].isSelected() and os.access("/tmp/product/.userhgb", os.R_OK): log("rhgb installed, adding to boot loader config") id.bootloader.args.append("rhgb quiet") id.instProgress = None def doPostInstall(method, id, intf, instPath): if flags.test: return w = intf.progressWindow(_("Post Install"), _("Performing post install configuration..."), 6) upgrade = id.upgrade.get() arch = iutil.getArch () if upgrade: logname = '/root/upgrade.log' else: logname = '/root/install.log' instLogName = instPath + logname instLog = open(instLogName, "a") try: if not upgrade: w.set(1) copyExtraModules(instPath, id.grpset, id.extraModules) w.set(2) # pcmcia is supported only on i386 at the moment if arch == "i386": pcmcia.createPcmciaConfig( instPath + "/etc/sysconfig/pcmcia") # we need to write out the network bits before kudzu runs # to avoid getting devices in the wrong order (#102276) id.network.write(instPath) w.set(3) # blah. If we're on a serial mouse, and we have X, we need to # close the mouse device, then run kudzu, then open it again. # turn it off mousedev = None # XXX currently Bad Things (X async reply) happen when doing # Mouse Magic on Sparc (Mach64, specificly) # The s390 doesn't even have a mouse! if os.environ.get('DISPLAY') == ':1' and arch != 'sparc': try: import xmouse mousedev = xmouse.get()[0] except RuntimeError: pass if mousedev: try: os.rename (mousedev, "/dev/disablemouse") except OSError: pass try: xmouse.reopen() except RuntimeError: pass if arch != "s390": # we need to unmount usbdevfs before mounting it usbWasMounted = iutil.isUSBDevFSMounted() if usbWasMounted: isys.umount('/proc/bus/usb', removeDir = 0) # see if unmount suceeded, if not pretent it isnt mounted # because we're screwed anywyas if system is going to # lock up if iutil.isUSBDevFSMounted(): usbWasMounted = 0 unmountUSB = 0 try: isys.mount('/usbdevfs', instPath+'/proc/bus/usb', 'usbdevfs') unmountUSB = 1 except: log("Mount of /proc/bus/usb in chroot failed") pass argv = [ "/usr/sbin/kudzu", "-q" ] if id.grpset.hdrlist.has_key("kernel"): ver = "%s-%s" %(id.grpset.hdrlist["kernel"][rpm.RPMTAG_VERSION], id.grpset.hdrlist["kernel"][rpm.RPMTAG_RELEASE]) argv.extend(["-k", ver]) devnull = os.open("/dev/null", os.O_RDWR) iutil.execWithRedirect(argv[0], argv, root = instPath, stdout = devnull) # turn it back on if mousedev: try: os.rename ("/dev/disablemouse", mousedev) except OSError: pass try: xmouse.reopen() except RuntimeError: pass if unmountUSB: try: isys.umount(instPath + '/proc/bus/usb', removeDir = 0) except SystemError: # if we fail to unmount, then we should just not # try to remount it. this protects us from random # suckage usbWasMounted = 0 if usbWasMounted: isys.mount('/usbdevfs', '/proc/bus/usb', 'usbdevfs') w.set(4) if upgrade and id.dbpath is not None: # remove the old rpmdb try: iutil.rmrf (id.dbpath) except OSError: pass if upgrade: # needed for prior systems which were not xinetd based migrateXinetd(instPath, instLogName) w.set(5) # FIXME: hack to install the comps package if (id.compspkg is not None and os.access(id.compspkg, os.R_OK)): log("found the comps package") try: # ugly hack path = id.compspkg.split("/mnt/sysimage")[1] args = ["/bin/rpm", "-Uvh", path] rc = iutil.execWithRedirect(args[0], args, stdout = "/dev/tty5", stderr = "/dev/tty5", root = instPath) ts = rpm.TransactionSet() ts.setVSFlags(~(rpm.RPMVSF_NORSA|rpm.RPMVSF_NODSA)) ts.closeDB() fd = os.open(id.compspkg, os.O_RDONLY) h = ts.hdrFromFdno(fd) os.close(fd) if upgrade: text = _("Upgrading %s-%s-%s.%s.\n") else: text = _("Installing %s-%s-%s.%s.\n") instLog.write(text % (h['name'], h['version'], h['release'], h['arch'])) os.unlink(id.compspkg) del ts except Exception, e: log("comps.rpm failed to install: %s" %(e,)) try: os.unlink(id.compspkg) except: pass else: log("no comps package found") w.set(6) finally: pass # XXX hack - we should really write a proper /etc/lvmtab. but for now # just create the lvmtab if they have /sbin/vgscan and some VGs if (os.access(instPath + "/sbin/vgscan", os.X_OK) and os.access(instPath + "/proc/lvm", os.R_OK) and len(os.listdir("/proc/lvm/VGs")) > 0): rc = iutil.execWithRedirect("/sbin/vgscan", ["vgscan", "-v"], stdout = "/dev/tty5", stderr = "/dev/tty5", root = instPath, searchPath = 1) # write out info on install method used try: if id.methodstr is not None: if os.access (instPath + "/etc/sysconfig/installinfo", os.R_OK): os.rename (instPath + "/etc/sysconfig/installinfo", instPath + "/etc/sysconfig/installinfo.rpmsave") f = open(instPath + "/etc/sysconfig/installinfo", "w+") f.write("INSTALLMETHOD=%s\n" % (string.split(id.methodstr, ':')[0],)) try: ii = open("/tmp/isoinfo", "r") il = ii.readlines() ii.close() for line in il: f.write(line) except: pass f.close() else: log("methodstr not set for some reason") except: log("Failed to write out installinfo") w.pop () sys.stdout.flush() syslog.stop() def migrateXinetd(instPath, instLog): if not os.access (instPath + "/usr/sbin/inetdconvert", os.X_OK): return if not os.access (instPath + "/etc/inetd.conf.rpmsave", os.R_OK): return argv = [ "/usr/sbin/inetdconvert", "--convertremaining", "--inetdfile", "/etc/inetd.conf.rpmsave" ] logfile = os.open (instLog, os.O_APPEND) iutil.execWithRedirect(argv[0], argv, root = instPath, stdout = logfile, stderr = logfile) os.close(logfile) def copyExtraModules(instPath, grpset, extraModules): kernelVersions = grpset.kernelVersionList() foundModule = 0 try: f = open("/etc/arch") arch = f.readline().strip() del f except IOError: arch = os.uname()[2] for (path, name) in extraModules: if not path: path = "/modules.cgz" pattern = "" names = "" for (n, tag) in kernelVersions: if tag == "up": pkg = "kernel" else: pkg = "kernel-%s" %(tag,) arch = grpset.hdrlist[pkg][rpm.RPMTAG_ARCH] # version 1 path pattern = pattern + " %s/%s/%s.o " % (n, arch, name) # version 0 path pattern = pattern + " %s/%s.o " % (n, name) names = names + " %s.o" % (name,) command = ("cd %s/lib/modules; gunzip < %s | " "%s/bin/cpio --quiet -iumd %s" % (instPath, path, instPath, pattern)) log("running: '%s'" % (command, )) os.system(command) for (n, tag) in kernelVersions: if tag == "up": pkg = "kernel" else: pkg = "kernel-%s" %(tag,) toDir = "%s/lib/modules/%s/updates" % \ (instPath, n) to = "%s/%s.o" % (toDir, name) if (os.path.isdir("%s/lib/modules/%s" %(instPath, n)) and not os.path.isdir("%s/lib/modules/%s/updates" %(instPath, n))): os.mkdir("%s/lib/modules/%s/updates" %(instPath, n)) if not os.path.isdir(toDir): continue arch = grpset.hdrlist[pkg][rpm.RPMTAG_ARCH] for p in ("%s/%s.o" %(arch, name), "%s.o" %(name,)): fromFile = "%s/lib/modules/%s/%s" % (instPath, n, p) if (os.access(fromFile, os.R_OK)): log("moving %s to %s" % (fromFile, to)) os.rename(fromFile, to) # the file might not have been owned by root in the cgz os.chown(to, 0, 0) foundModule = 1 else: log("missing DD module %s (this may be okay)" % fromFile) if foundModule == 1: for (n, tag) in kernelVersions: recreateInitrd(n, instPath) #Recreate initrd for use when driver disks add modules def recreateInitrd (kernelTag, instRoot): log("recreating initrd for %s" % (kernelTag,)) iutil.execWithRedirect("/sbin/new-kernel-pkg", [ "/sbin/new-kernel-pkg", "--mkinitrd", "--depmod", "--install", kernelTag ], stdout = None, stderr = None, searchPath = 1, root = instRoot) # XXX Deprecated. Is this ever called anymore? def depmodModules(comps, instPath): kernelVersions = comps.kernelVersionList() for (version, tag) in kernelVersions: iutil.execWithRedirect ("/sbin/depmod", [ "/sbin/depmod", "-a", version, "-F", "/boot/System.map-" + version ], root = instPath, stderr = '/dev/null') def betaNagScreen(intf, dir): publicBetas = { "Red Hat Linux": "Red Hat Linux Public Beta", "Red Hat Enterprise Linux": "Red Hat Enterprise Linux Public Beta" } if dir == DISPATCH_BACK: return DISPATCH_NOOP fileagainst = None for (key, val) in publicBetas.items(): if productName.startswith(key): fileagainst = val if fileagainst is None: fileagainst = "%s Beta" %(productName,) while 1: rc = intf.messageWindow( _("Warning! This is a beta!"), _("Thank you for downloading this " "%s Beta release.\n\n" "This is not a final " "release and is not intended for use " "on production systems. The purpose of " "this release is to collect feedback " "from testers, and it is not suitable " "for day to day usage.\n\n" "To report feedback, please visit:\n\n" " http://bugzilla.redhat.com/bugzilla\n\n" "and file a report against '%s'.\n" %(productName, fileagainst)), type="custom", custom_icon="warning", custom_buttons=[_("_Exit"), _("_Install BETA")]) if not rc: rc = intf.messageWindow( _("Rebooting System"), _("Your system will now be rebooted..."), type="custom", custom_icon="warning", custom_buttons=[_("_Back"), _("_Reboot")]) if rc: sys.exit(0) else: break # FIXME: this is a kind of poor way to do this, but it will work for now def selectLanguageSupportGroups(grpset, langSupport): sup = langSupport.supported if len(sup) == 0: sup = langSupport.getAllSupported() for group in grpset.groups.values(): xmlgrp = grpset.compsxml.groups[group.basename] langs = [] for name in sup: try: lang = langSupport.langInfoByName[name][0] langs.extend(language.expandLangs(lang)) except: continue if group.langonly is not None and group.langonly in langs: group.select() for package in xmlgrp.pkgConditionals.keys(): req = xmlgrp.pkgConditionals[package] if not grpset.hdrlist.has_key(package): log("Missing %s which is in a langsupport conditional" %(package,)) continue # add to the deps in the dependencies structure for the # package. this should take care of whenever we're # selected grpset.hdrlist[req].addDeps([package], main = 0) if grpset.hdrlist[req].isSelected(): grpset.hdrlist[package].select() sys.stdout.flush() grpset.hdrlist[package].usecount += grpset.hdrlist[req].usecount - 1 group.selectDeps([package], uses = grpset.hdrlist[req].usecount) # # product.py: product identification string # # Copyright 2003 Red Hat, Inc. # # This software may be freely redistributed under the terms of the GNU # library public license. # # You should have received a copy of the GNU Library Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. import os if os.access("/tmp/product/.buildstamp", os.R_OK): path = "/tmp/product/.buildstamp" elif os.access("/.buildstamp", os.R_OK): path = "/.buildstamp" else: path = None if path is None: productName = "anaconda" productVersion = "bluesky" productSite = "SL" productDefault = "SL" else: f = open(path, "r") lines = f.readlines() if len(lines) < 4: productName = "SL" productVersion = "3.0.1" productDefault = "SL" productSite = "SL" productSiteDir = "sites" else: productName = lines[1][:-1] productVersion = lines[2][:-1] productDefault = lines[3][:-1] productSite = lines[4][:-1] productSiteDir = lines[5][:-1] # # splashscreen.py: a quick splashscreen window that displays during ipl # # Matt Wilson # # Copyright 2001-2002 Red Hat, Inc. # # This software may be freely redistributed under the terms of the GNU # library public license. # # You should have received a copy of the GNU Library Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # import os os.environ["PYGTK_DISABLE_THREADS"] = "1" os.environ["GNOME_DISABLE_CRASH_DIALOG"] = "1" import gtk from flags import flags from rhpl.translate import cat from rhpl.log import log import product # for GTK+ 2.0 cat.setunicode(1) splashwindow = None def splashScreenShow(configFileData): #set the background to a dark gray if flags.setupFilesystems: path = ("/usr/X11R6/bin/xsetroot",) args = ("-solid", "gray45") child = os.fork() if (child == 0): os.execv(path[0], path + args) try: pid, status = os.waitpid(child, 0) except OSError, (errno, msg): print __name__, "waitpid:", msg root = gtk.gdk.get_default_root_window() cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR) root.set_cursor(cursor) def findPixmap(file): for path in ("/mnt/source/" + product.productSite + "/RHupdates/pixmaps/", "/mnt/source/" + product.productSite + "/RHupdates/", "/tmp/updates/pixmaps/", "/tmp/updates/", "/mnt/source/RHupdates/pixmaps/", "/mnt/source/RHupdates/", "/tmp/product/pixmaps/", "/tmp/product/", "/usr/share/anaconda/pixmaps/", "pixmaps/", "/usr/share/pixmaps/", "/usr/share/anaconda/", ""): fn = path + file log("Looking for %s in splash findPixmap", fn) if os.access(fn, os.R_OK): return fn return None def load_image(file): p = gtk.Image() log("going to try to load %s", file) fn = findPixmap(file) if fn: log("We found fn it is %s", fn) pixbuf = gtk.gdk.pixbuf_new_from_file(fn) else: log("We didnt find fn we are using %s", file) pixbuf = gtk.gdk.pixbuf_new_from_file(file) if pixbuf: (pixmap, mask) = pixbuf.render_pixmap_and_mask() pixbuf.render_to_drawable(pixmap, gtk.gdk.GC(pixmap), 0, 0, 0, 0, pixbuf.get_width(), pixbuf.get_height(), gtk.gdk.RGB_DITHER_MAX, 0, 0) p.set_from_pixmap(pixmap, mask) return p global splashwindow width = gtk.gdk.screen_width() p = None # If the xserver is running at 800x600 res or higher, use the # 800x600 splash screen. if width >= 800: image = configFileData["Splashscreen"] p = load_image(image) else: p = load_image('pixmaps/first-lowres.png') if p: splashwindow = gtk.Window() splashwindow.set_position(gtk.WIN_POS_CENTER) box = gtk.EventBox() box.modify_bg(gtk.STATE_NORMAL, box.get_style().white) box.add(p) splashwindow.add(box) box.show_all() splashwindow.show_now() gtk.gdk.flush() while gtk.events_pending(): gtk.main_iteration(gtk.FALSE) def splashScreenPop(): global splashwindow if splashwindow: splashwindow.destroy() # # text.py - text mode frontend to anaconda # # Erik Troan # Matt Wilson # # Copyright 1999-2002 Red Hat, Inc. # # This software may be freely redistributed under the terms of the GNU # library public license. # # You should have received a copy of the GNU Library Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # from snack import * import sys import os import isys import iutil import time import signal import parted import string import kudzu from language import expandLangs from flags import flags from constants_text import * from constants import * from rhpl.log import log from rhpl.translate import _, cat, N_ stepToClasses = { "language" : ("language_text", "LanguageWindow"), "keyboard" : ("keyboard_text", "KeyboardWindow"), "mouse" : ("mouse_text", ("MouseWindow", "MouseDeviceWindow")), "welcome" : ("welcome_text", "WelcomeWindow"), "installtype" : ("installpath_text", "InstallPathWindow"), "autopartition" : ("partition_text", "AutoPartitionWindow"), "custom-upgrade" : ("upgrade_text", "UpgradeExamineWindow"), "addswap" : ("upgrade_text", "UpgradeSwapWindow"), "upgrademigratefs" : ("upgrade_text", "UpgradeMigrateFSWindow"), "fdisk" : ("fdisk_text", "fdiskPartitionWindow"), "partitionmethod" : ("partmethod_text", ("PartitionMethod")), "partition": ("partition_text", ("PartitionWindow")), "findinstall" : ("upgrade_text", "UpgradeExamineWindow"), "addswap" : ("upgrade_text", "UpgradeSwapWindow"), "upgbootloader": ("upgrade_bootloader_text", "UpgradeBootloaderWindow"), "bootloader" : ("bootloader_text", ("BootloaderChoiceWindow", "BootloaderAppendWindow", "BootloaderPasswordWindow")), "bootloaderadvanced" : ("bootloader_text", ("BootloaderImagesWindow", "BootloaderLocationWindow")), "network" : ("network_text", ("NetworkDeviceWindow", "NetworkGlobalWindow", "HostnameWindow")), "firewall" : ("firewall_text", "FirewallWindow"), "languagesupport" : ("language_text", ("LanguageSupportWindow", "LanguageDefaultWindow")), "timezone" : ("timezone_text", "TimezoneWindow"), "accounts" : ("userauth_text", "RootPasswordWindow"), "authentication" : ("userauth_text", ("AuthConfigWindow")), "desktopchoice": ("desktop_choice_text", "DesktopChoiceWindow"), "package-selection" : ("packages_text", "PackageGroupWindow"), "indivpackage" : ("packages_text", ("IndividualPackageWindow")), "dependencies" : ("packages_text", "PackageDepWindow"), "videocard" : ("xconfig_text", "XConfigWindowCard"), "monitor" : ("xconfig_text", "MonitorWindow"), "xcustom" : ("xconfig_text", "XCustomWindow"), "confirminstall" : ("confirm_text", "BeginInstallWindow"), "confirmupgrade" : ("confirm_text", "BeginUpgradeWindow"), "install" : ("progress_text", "setupForInstall"), "bootdisk" : ("bootdisk_text", ("BootDiskWindow")), "complete" : ("complete_text", "FinishedWindow"), } if iutil.getArch() == 'sparc': stepToClasses["bootloader"] = ("silo_text", ("SiloAppendWindow", "SiloWindow" "SiloImagesWindow")) if iutil.getArch() == 's390': stepToClasses["bootloader"] = ("zipl_text", ( "ZiplWindow")) class InstallWindow: def __call__ (self, screen): raise RuntimeError, "Unimplemented screen" class WaitWindow: def pop(self): self.screen.popWindow() self.screen.refresh() def __init__(self, screen, title, text): self.screen = screen width = 40 if (len(text) < width): width = len(text) t = TextboxReflowed(width, text) g = GridForm(self.screen, title, 1, 1) g.add(t, 0, 0) g.draw() self.screen.refresh() class OkCancelWindow: def getrc(self): return self.rc def __init__(self, screen, title, text): rc = ButtonChoiceWindow(screen, title, text, buttons=[TEXT_OK_BUTTON, _("Cancel")]) if rc == string.lower(_("Cancel")): self.rc = 1 else: self.rc = 0 class ProgressWindow: def pop(self): self.screen.popWindow() self.screen.refresh() del self.scale self.scale = None def set(self, amount): self.scale.set(amount) self.screen.refresh() def __init__(self, screen, title, text, total): self.screen = screen width = 55 if (len(text) > width): width = len(text) t = TextboxReflowed(width, text) g = GridForm(self.screen, title, 1, 2) g.add(t, 0, 0, (0, 0, 0, 1), anchorLeft=1) self.scale = Scale(width, total) g.add(self.scale, 0, 1) g.draw() self.screen.refresh() class InstallInterface: def helpWindow(self, screen, key): lang = self.instLanguage.getCurrent() lang = self.instLanguage.getLangNick(lang) self.langSearchPath = expandLangs(lang) + ['C'] if key == "helponhelp": if self.showingHelpOnHelp: return None else: self.showingHelpOnHelp = 1 try: f = None if self.configFileData.has_key("helptag"): helpTag = "-%s" % (self.configFileData["helptag"],) else: helpTag = "" arch = "-%s" % (iutil.getArch(),) tags = [ "%s%s" % (helpTag, arch), "%s" % (helpTag,), "%s" % (arch,), "" ] # XXX # # HelpWindow can't get to the langauge found = 0 for path in ("./text-", "/mnt/source/RHupdates/", "/usr/share/anaconda/"): if found: break for lang in self.langSearchPath: for tag in tags: fn = "%shelp/%s/s1-help-screens-%s%s.txt" \ % (path, lang, key, tag) try: f = open(fn) except IOError, msg: continue found = 1 break if not f: ButtonChoiceWindow(screen, _("Help not available"), _("No help is available for this " "step of the install."), buttons=[TEXT_OK_BUTTON]) return None lines = f.readlines() for l in lines: l = l.replace("@RHL@", productName) l = l.replace("@RHLVER@", productVersion) while not string.strip(l[0]): l = l[1:] title = string.strip(l[0]) l = l[1:] while not string.strip(l[0]): l = l[1:] f.close() height = 10 scroll = 1 if len(l) < height: height = len(l) scroll = 0 width = len(title) + 6 stream = "" for line in l: line = string.strip(line) stream = stream + line + "\n" if len(line) > width: width = len(line) bb = ButtonBar(screen, [TEXT_OK_BUTTON]) t = Textbox(width, height, stream, scroll=scroll) g = GridFormHelp(screen, title, "helponhelp", 1, 2) g.add(t, 0, 0, padding=(0, 0, 0, 1)) g.add(bb, 0, 1, growx=1) g.runOnce() self.showingHelpOnHelp = 0 except: import traceback (type, value, tb) = sys.exc_info() from string import joinfields list = traceback.format_exception(type, value, tb) text = joinfields(list, "") rc = self.exceptionWindow(_("Exception Occurred"), text) if rc: import pdb pdb.post_mortem(tb) os._exit(1) def progressWindow(self, title, text, total): return ProgressWindow(self.screen, title, text, total) def messageWindow(self, title, text, type="ok", default = None, custom_icon=None, custom_buttons=[]): if type == "ok": ButtonChoiceWindow(self.screen, title, text, buttons=[TEXT_OK_BUTTON]) elif type == "yesno": if default and default == "no": btnlist = [TEXT_NO_BUTTON, TEXT_YES_BUTTON] else: btnlist = [TEXT_YES_BUTTON, TEXT_NO_BUTTON] rc = ButtonChoiceWindow(self.screen, title, text, buttons=btnlist) if rc == "yes": return 1 else: return 0 elif type == "custom": tmpbut = [] for but in custom_buttons: tmpbut.append(string.replace(but,"_","")) rc = ButtonChoiceWindow(self.screen, title, text, width=60, buttons=tmpbut) idx = 0 for b in tmpbut: if string.lower(b) == rc: return idx != 0 idx = idx + 1 return 0 else: return OkCancelWindow(self.screen, title, text) def dumpWindow(self): rc = ButtonChoiceWindow(self.screen, _("Save Crash Dump"), _("Please insert a floppy now. All contents of the disk " "will be erased, so please choose your diskette carefully."), [TEXT_OK_BUTTON, _("Cancel")]) if rc == string.lower(_("Cancel")): return 1 return 0 def exceptionWindow(self, title, text): try: floppyDevices = 0 for dev in kudzu.probe(kudzu.CLASS_FLOPPY, kudzu.BUS_UNSPEC, kudzu.PROBE_ALL): if not dev.detached: floppyDevices = floppyDevices + 1 except: floppyDevices = 0 if floppyDevices > 0 or DEBUG: ugh = "%s\n\n" % (exceptionText,) buttons=[TEXT_OK_BUTTON, _("Save"), _("Debug")] else: ugh = "%s\n\n" % (exceptionTextNoFloppy,) buttons=[TEXT_OK_BUTTON, _("Debug")] rc = ButtonChoiceWindow(self.screen, title, ugh + text, buttons) if rc == string.lower(_("Debug")): return 1 elif rc == string.lower(_("Save")): return 2 return None def partedExceptionWindow(self, exc): # if our only option is to cancel, let us handle the exception # in our code and avoid popping up the exception window here. if exc.options == parted.EXCEPTION_CANCEL: return parted.EXCEPTION_UNHANDLED buttons = [] buttonToAction = {} flags = ((parted.EXCEPTION_FIX, N_("Fix")), (parted.EXCEPTION_YES, N_("Yes")), (parted.EXCEPTION_NO, N_("No")), (parted.EXCEPTION_OK, N_("OK")), (parted.EXCEPTION_RETRY, N_("Retry")), (parted.EXCEPTION_IGNORE, N_("Ignore")), (parted.EXCEPTION_CANCEL, N_("Cancel"))) for flag, errorstring in flags: if exc.options & flag: buttons.append(_(errorstring)) buttonToAction[string.lower(_(errorstring))] = flag rc = None while not buttonToAction.has_key(rc): rc = ButtonChoiceWindow(self.screen, exc.type_string, exc.message, buttons=buttons) return buttonToAction[rc] def waitWindow(self, title, text): return WaitWindow(self.screen, title, text) def drawFrame(self): self.welcomeText = _("%s ") % (productName,) self.screen.drawRootText (0, 0, self.welcomeText) self.screen.drawRootText (len(_(self.welcomeText)), 0, (self.screen.width - len(_(self.welcomeText))) * " ") if (os.access("/usr/share/anaconda/help/C/s1-help-screens-lang.txt", os.R_OK)): self.screen.pushHelpLine(_(" for help | between elements | selects | next screen")) else: self.screen.pushHelpLine(_(" / between elements | selects | next screen")) def setScreen(self, screen): self.screen = screen def shutdown(self): self.screen.finish() self.screen = None def __init__(self): signal.signal(signal.SIGINT, signal.SIG_IGN) signal.signal(signal.SIGTSTP, signal.SIG_IGN) self.screen = None self.showingHelpOnHelp = 0 def __del__(self): if self.screen: self.screen.finish() def run(self, id, dispatch, configFileData): # set up for CJK text mode if needed if (flags.setupFilesystems and (id.instLanguage.getFontFile(id.instLanguage.getCurrent()) == "bterm") and not isys.isPsudoTTY(0) and not flags.serial): log("starting bterm") try: rc = isys.startBterm() time.sleep(1) except Exception, e: log("got an exception starting bterm: %s" %(e,)) self.screen = SnackScreen() self.configFileData = configFileData self.screen.helpCallback(self.helpWindow) # uncomment this line to make the installer quit on # handy for quick debugging. # self.screen.suspendCallback(killSelf, self.screen) # uncomment this line to drop into the python debugger on # --VERY handy-- if DEBUG or flags.test: self.screen.suspendCallback(debugSelf, self.screen) if flags.serial or isys.isPsudoTTY(0) or isys.isVioConsole(): self.screen.suspendCallback(spawnShell, self.screen) # clear out the old root text by writing spaces in the blank # area on the right side of the screen #self.screen.drawRootText (len(_(self.welcomeText)), 0, #(self.screen.width - len(_(self.welcomeText))) * " ") #self.screen.drawRootText (0 - len(_(step[0])), 0, _(step[0])) langname = id.instLanguage.getCurrent() lang = id.instLanguage.getLangNick(langname) self.langSearchPath = expandLangs(lang) + ['C'] self.instLanguage = id.instLanguage # draw the frame after setting up the fallback self.drawFrame() # draw the frame after setting up the fallback self.drawFrame() id.fsset.registerMessageWindow(self.messageWindow) id.fsset.registerProgressWindow(self.progressWindow) id.fsset.registerWaitWindow(self.waitWindow) parted.exception_set_handler(self.partedExceptionWindow) lastrc = INSTALL_OK (step, args) = dispatch.currentStep() while step: (file, classNames) = stepToClasses[step] if type(classNames) != type(()): classNames = (classNames,) if lastrc == INSTALL_OK: step = 0 else: step = len(classNames) - 1 while step >= 0 and step < len(classNames): # reget the args. they could change (especially direction) (foo, args) = dispatch.currentStep() nextWindow = None s = "from %s import %s; nextWindow = %s" % \ (file, classNames[step], classNames[step]) exec s win = nextWindow() #log("TUI running step %s (class %s, file %s)" % #(step, file, classNames)) rc = apply(win, (self.screen, ) + args) if rc == INSTALL_NOOP: rc = lastrc if rc == INSTALL_BACK: step = step - 1 dispatch.dir = DISPATCH_BACK elif rc == INSTALL_OK: step = step + 1 dispatch.dir = DISPATCH_FORWARD lastrc = rc if step == -1: if not dispatch.canGoBack(): ButtonChoiceWindow(self.screen, _("Cancelled"), _("I can't go to the previous step " "from here. You will have to try " "again."), buttons=[_("OK")]) dispatch.gotoPrev() else: dispatch.gotoNext() (step, args) = dispatch.currentStep() self.screen.finish() def killSelf(screen): screen.finish() os._exit(0) def debugSelf(screen): screen.suspend() import pdb try: pdb.set_trace() except: sys.exit(-1) screen.resume() def spawnShell(screen): screen.suspend() print "\n\nType to return to the install program.\n" iutil.execWithRedirect("/bin/sh", ["-/bin/sh"]) time.sleep(5) screen.resume() # # urlinstall.py - URL based install source method # # Erik Troan # # Copyright 1999-2002 Red Hat, Inc. # # This software may be freely redistributed under the terms of the GNU # library public license. # # You should have received a copy of the GNU Library Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # from hdrlist import groupSetFromCompsFile, HeaderList from installmethod import InstallMethod, FileCopyException import os import rpm import time import urllib2 import string import struct import socket # we import these explicitly because urllib loads them dynamically, which # stinks -- and we need to have them imported for the --traceonly option import ftplib import httplib import StringIO from rhpl.log import log import product FILENAME = 1000000 DISCNUM = 1000002 def urlretrieve(location, file): """Downloads from location and saves to file.""" try: url = urllib2.urlopen(location) except urllib2.HTTPError, e: raise IOError(e.code, e.msg) except urllib2.URLError, e: raise IOError(-1, e.reason) f = open(file, 'w+') f.write(url.read()) f.close() url.close() class UrlInstallMethod(InstallMethod): def readCompsViaMethod(self, hdlist): fname = self.findBestFileMatch(None, 'comps.xml') # if not local then assume its on host if fname is None: if (product.productDefault == product.productSite) : fname = self.baseUrl + "/" + product.productDefault + "/base/comps.xml" else: fname = self.baseUrl + "/" + product.productSite + "/base/comps.xml" log("Comps not in update dirs, using %s",fname) return groupSetFromCompsFile(fname, hdlist) def getFilename(self, h, timer): tmppath = self.getTempPath() # h doubles as a filename -- gross if type("/") == type(h): fullPath = self.baseUrl + "/" + h else: if self.multiDiscs: base = "%s/disc%d" % (self.pkgUrl, h[DISCNUM]) else: base = self.pkgUrl if self.isUpdateRPM(h): # path = "/RedHat/Updates/" path = "/" + product.productSite + "/Updates/" else: # path = "/RedHat/RPMS/" path = "/" + product.productDefault + "/RPMS/" fullPath = base + path + h[FILENAME] file = tmppath + os.path.basename(fullPath) tries = 0 while tries < 5: try: urlretrieve(fullPath, file) except IOError, (errnum, msg): log("IOError %s occurred getting %s: %s" %(errnum, fullPath, str(msg))) time.sleep(5) else: break tries = tries + 1 if tries >= 5: raise FileCopyException return file def copyFileToTemp(self, filename): tmppath = self.getTempPath() if self.multiDiscs: base = "%s/disc1" % (self.pkgUrl,) else: base = self.pkgUrl fullPath = base + "/" + filename file = tmppath + "/" + os.path.basename(fullPath) tries = 0 while tries < 5: try: urlretrieve(fullPath, file) except IOError, (errnum, msg): log("IOError %s occurred getting %s: %s", errnum, fullPath, str(msg)) time.sleep(5) else: break tries = tries + 1 if tries >= 5: raise FileCopyException return file def unlinkFilename(self, fullName): os.remove(fullName) def readHeaders(self): tries = 0 while tries < 5: if (product.productDefault == product.productSite) : hdurl = self.baseUrl + "/" + product.productDefault + "/base/hdlist" else: log("product.productSiteDir is %s", product.productSiteDir) log("product.productSite is %s ", product.productSite) hdurl = self.baseUrl + "/" + product.productSite + "/base/hdlist" try: url = urllib2.urlopen(hdurl) except urllib2.HTTPError, e: log("HTTPError: %s occurred getting %s", hdurl, e) except urllib2.URLError, e: log("URLError: %s occurred getting %s", hdurl, e) except IOError, (errnum, msg): log("IOError %s occurred getting %s: %s", errnum, hdurl, msg) else: break time.sleep(5) tries = tries + 1 if tries >= 5: raise FileCopyException raw = url.read(16) if raw is None or len(raw) < 1: raise TypeError, "header list is empty!" hl = [] while (raw and len(raw)>0): info = struct.unpack("iiii", raw) magic1 = socket.ntohl(info[0]) & 0xffffffff if (magic1 != 0x8eade801 or info[1]): raise TypeError, "bad magic in header" il = socket.ntohl(info[2]) dl = socket.ntohl(info[3]) totalSize = il * 16 + dl; hdrString = raw[8:] + url.read(totalSize) hdr = rpm.headerLoad(hdrString) hl.append(hdr) raw = url.read(16) return HeaderList(hl) def mergeFullHeaders(self, hdlist): if (product.productDefault == product.productSite) : fn = self.getFilename("/" + product.productDefault + "/base/hdlist2", None) else: fn = self.getFilename("/" + product.productSite + "/base/hdlist2", None) hdlist.mergeFullHeaders(fn) os.unlink(fn) def __init__(self, url, rootPath): InstallMethod.__init__(self, rootPath) if url.startswith("ftp"): isFtp = 1 else: isFtp = 0 # build up the url. this is tricky so that we can replace # the first instance of // with /%3F to do absolute URLs right i = string.index(url, '://') + 3 self.baseUrl = url[:i] rem = url[i:] i = string.index(rem, '/') + 1 self.baseUrl = self.baseUrl + rem[:i] rem = rem[i:] # encoding fun so that we can handle absolute paths if rem.startswith("/") and isFtp: rem = "%2F" + rem[1:] self.baseUrl = self.baseUrl + rem if self.baseUrl[-1] == "/": self.baseUrl = self.baseUrl[:-1] # self.baseUrl points at the path which contains the 'RedHat' # directory with the hdlist. if self.baseUrl[-6:] == "/disc1": self.multiDiscs = 1 self.pkgUrl = self.baseUrl[:-6] else: self.multiDiscs = 0 self.pkgUrl = self.baseUrl