class Mouse: def __init__(self, conn, root): self.conn = conn self.root = root self.log = Log("mouse") def move(self, x=None, y=None, dx=0, dy=0, window=None): if window is None: window = self.root xcb_reply = self.conn.core.QueryPointer(window.wid).reply() new_x = xcb_reply.win_x new_y = xcb_reply.win_y if x: new_x = x if y: new_y = y if dx: new_x += dx if dy: new_y += dy self.log.debug("relocating to ({}, {})".format(new_x, new_y)) self.conn.core.WarpPointerChecked( 0, window.wid, # src_window, dst_window 0, 0, # src_x, src_y 0, 0, # src_width, src_height new_x, new_y # dest_x, dest_y )
class Worker(Thread): def __init__(self, timeline): super().__init__(daemon=True) self.timeline = timeline self.log = Log("worker %s" % self.name) def run(self): queue = self.timeline.outq schedule = self.timeline.schedule while True: c = queue.get() self.log.debug("running %s" % c) r, out = c._check() schedule(c)
class Worker(Thread): """ Get tasks that are ready to run and execute them. """ def __init__(self, scheduler): super().__init__(daemon=True) self.scheduler = scheduler self.log = Log("worker %s" % self.name) def run(self): queue = self.scheduler.ready schedule = self.scheduler.schedule history = self.scheduler.history while True: checker = queue.get() self.log.debug("running %s" % checker) r, out = checker._check() self.log.debug("result: %s %s" %(r, out)) schedule(checker) history.appendleft((time.time(), r, out))
class Checker: last_checked = None last_status = None, "<no check were performed yet>" def __init__(self, interval=60, name="<no name>", descr=None): self.interval = interval self.name = name self.descr = descr self.statuses = deque(maxlen=6) self.log = Log("checker %s" % self.__class__.__name__) global checks; checks += [self] def _check(self): if self.last_checked: delta = time.time() - self.last_checked if delta > (self.interval+1): log.critical("behind schedule for %ss" % delta) self.last_status = self.check() self.last_checked = time.time() self.statuses += [self.last_status] return self.last_status def check(self): return ERR, "<this is a generic check>" def get_next_check(self): if not self.last_checked: self.log.debug("was never checked, requesting immediate check") return -1 now = time.time() next_check = self.last_checked + self.interval if next_check < now: return now return next_check def __lt__(self, other): return True
class Scheduler(Thread): """ Schedules and executes tasks using threaded workers """ def __init__(self, workers=5): super().__init__(daemon=True) self.lock = Lock() # lock queue manipulations self.inq = PriorityQueue() self.outq = Queue() # fired checks to be executed self.timer = MyTimer(0) # timer placeholder for .schedule() so it can call self.timer.cancel() during first time self.log = Log("scheduler") for i in range(workers): worker = Worker(self) worker.start() def schedule(self, checker): time = checker.get_next_check() with self.lock: self.inq.put((time, checker)) self.timer.cancel() # trigger recalculate because this task may go before pending def run(self): while True: t, c = self.inq.get() with self.lock: now = time.time() self.timer = MyTimer(t-now) try: self.timer.start() self.log.debug("sleeping for %s" % self.timer.interval) self.timer.join() except TimerCanceled: self.log.notice("timer aborted, recalculating timeouts") with self.lock: print(t,c) self.inq.put((t,c)) continue self.outq.put(c)
class WPAMonitor(Thread): def __init__(self, ifname): self.log = Log("monitor") mon_path = "/tmp/wpa_mon_%s" % getpid() atexit.register(lambda: unlink(mon_path)) server_path = "/var/run/wpa_supplicant/%s" % ifname self.log.debug("connecting to %s" % server_path) self.socket = socket(AF_UNIX, SOCK_DGRAM) self.socket.bind(mon_path) self.socket.connect(server_path) super().__init__(daemon=True) def run(self): self.socket.send(b"AUTOSCAN periodic:10") self.socket.send(b"AP_SCAN 1") self.socket.send(b"ATTACH") while True: try: data = self.socket.recv(65535).strip().decode('ascii', 'ignore') self.log.debug("got %s" % data) if data == 'OK': continue if data == 'FAIL': raise Exception("Failure detected") mask, evtype = data.split('>', 1) if evtype == 'CTRL-EVENT-SCAN-RESULTS': print("scan results") for cb in events['scan_results']: cb() else: self.log.info("unknown event %s" % data) except Exception as e: self.log.critical(e) sys.exit(e)
class WPAClient: def __init__(self, ifname): self.log = Log("WPA %s" % ifname) client_path = "/tmp/wpa_client_%s" % getpid() atexit.register(lambda: unlink(client_path)) server_path = "/var/run/wpa_supplicant/%s" % ifname self.socket = socket(AF_UNIX, SOCK_DGRAM) self.socket.bind(client_path) self.log.debug("using %s wpa socket..." % server_path) self.socket.connect(server_path) self.lock = Lock() def send(self, msg): self.log.debug("sending: %s" % msg) if isinstance(msg, str): msg = msg.encode('ascii', errors='ignore') self.socket.send(msg) def recv(self, bufsize=65535): r = self.socket.recv(bufsize) return r.strip().decode('ascii', errors='ignore') def run(self, cmd, check=True): with self.lock: self.send(cmd) r = self.recv() self.log.debug("received: %s" % r) if check: assert r not in ['FAIL', 'UNKNOWN COMMAND'] return r def status(self): raw_status = self.run("STATUS", check=False) return parse_status(raw_status) def scan_results(self): result = [] raw_results = self.run("SCAN_RESULTS") for line in raw_results.splitlines()[1:]: bssid, freq, signal, flags, ssid = line.split() r = Struct(ssid=ssid, signal=signal, bssid=bssid, freq=freq, flags=flags) result.append(r) return result def connect(self, network): nid = self.run("ADD_NETWORK") for cmd in network.wpacfg(): self.run(s("SET_NETWORK ${nid} ${cmd}")) self.run(s("SELECT_NETWORK ${nid}")) self.run(s("ENABLE_NETWORK ${nid}"))
class WM: """ Provides basic building blocks to make a window manager. It hides many dirty details about XCB. It was intended to provide minimum functionality, the rest supposed to be implemented by user in configuration file. """ root = None # type: Window atoms = None # type: AtomVault def __init__(self, display=None, desktops=None, loop=None): self.log = Log("WM") # INIT SOME BASIC STUFF self.hook = Hook() self.windows = {} # mapping between window id and Window self.win2desk = {} if not display: display = os.environ.get("DISPLAY") try: self._conn = xcffib.connect(display=display) except xcffib.ConnectionException: sys.exit("cannot connect to %s" % display) self.atoms = AtomVault(self._conn) self.desktops = desktops or [Desktop()] self.cur_desktop = self.desktops[0] self.cur_desktop.show() # CREATE ROOT WINDOW xcb_setup = self._conn.get_setup() xcb_screens = [i for i in xcb_setup.roots] self.xcb_default_screen = xcb_screens[self._conn.pref_screen] root_wid = self.xcb_default_screen.root self.root = Window(self, wid=root_wid, atoms=self.atoms, mapped=True) self.windows[root_wid] = self.root # for desktop in self.desktops: # desktop.windows.append(self.root) self.root.set_attr( eventmask=( EventMask.StructureNotify | EventMask.SubstructureNotify | EventMask.FocusChange # | EventMask.SubstructureRedirect | EventMask.EnterWindow # | EventMask.LeaveWindow # | EventMask.PropertyChange | EventMask.OwnerGrabButton ) ) # INFORM X WHICH FEATURES WE SUPPORT self.root.props[self.atoms._NET_SUPPORTED] = [self.atoms[a] for a in SUPPORTED_ATOMS] # PRETEND TO BE A WINDOW MANAGER supporting_wm_check_window = self.create_window(-1, -1, 1, 1) supporting_wm_check_window.props['_NET_WM_NAME'] = "SWM" self.root.props['_NET_SUPPORTING_WM_CHECK'] = supporting_wm_check_window.wid self.root.props['_NET_NUMBER_OF_DESKTOPS'] = len(self.desktops) self.root.props['_NET_CURRENT_DESKTOP'] = 0 # TODO: set cursor # EVENTS THAT HAVE LITTLE USE FOR US... self.ignoreEvents = { "KeyRelease", "ReparentNotify", # "CreateNotify", # DWM handles this to help "broken focusing windows". # "MapNotify", "ConfigureNotify", "LeaveNotify", "FocusOut", "FocusIn", "NoExposure", } # KEYBOARD self.kbd = Keyboard(xcb_setup, self._conn) self.mouse = Mouse(conn=self._conn, root=self.root) # FLUSH XCB BUFFER self.xsync() # apply settings # the event loop is not yet there, but we might have some pending # events... self._xpoll() # TODO: self.grabMouse # NOW IT'S TIME TO GET PHYSICAL SCREEN CONFIGURATION self.xrandr = Xrandr(root=self.root, conn=self._conn) # TODO: self.update_net_desktops() # SETUP EVENT LOOP if not loop: loop = asyncio.new_event_loop() self._eventloop = loop self._eventloop.add_signal_handler(signal.SIGINT, self.stop) self._eventloop.add_signal_handler(signal.SIGTERM, self.stop) self._eventloop.add_signal_handler(signal.SIGCHLD, self.on_sigchld) self._eventloop.set_exception_handler( lambda loop, ctx: self.log.error( "Got an exception in {}: {}".format(loop, ctx)) ) fd = self._conn.get_file_descriptor() self._eventloop.add_reader(fd, self._xpoll) # HANDLE STANDARD EVENTS self.hook.register("MapRequest", self.on_map_request) self.hook.register("MapNotify", self.on_map_notify) self.hook.register("UnmapNotify", self.on_window_unmap) self.hook.register("KeyPress", self.on_key_press) # self.hook.register("KeyRelease", self.on_key_release) # self.hook.register("CreateNotify", self.on_window_create) self.hook.register("PropertyNotify", self.on_property_notify) self.hook.register("ClientMessage", self.on_client_message) self.hook.register("DestroyNotify", self.on_window_destroy) self.hook.register("EnterNotify", self.on_window_enter) self.hook.register("ConfigureRequest", self.on_configure_window) self.hook.register("MotionNotify", self.on_mouse_event) self.hook.register("ButtonPress", self.on_mouse_event) self.hook.register("ButtonRelease", self.on_mouse_event) def on_property_notify(self, evname, xcb_event): # TODO: messy ugly code wid = xcb_event.window atom = self.atoms.get_name(xcb_event.atom) # window = self.windows.get(wid, Window(wm=self, wid=wid, mapped=True)) self.log.error("PropertyNotify: %s" % atom) run_("xprop -id %s %s" % (wid, atom)) # TODO: dirty code, relocate to config def on_client_message(self, evname, xcb_event): self.log.error(dir(xcb_event)) data = xcb_event.data resp_type = self.atoms.get_name(xcb_event.response_type) type = self.atoms.get_name(xcb_event.type) wid = xcb_event.window self.log.error("client message: resp_type={resp_type} window={wid} type={type} data={data}" \ .format(**locals())) # 'bufsize', 'data', 'format', 'pack', 'response_type', 'sequence', 'synthetic', 'type', 'window'] if type == '_NET_ACTIVE_WINDOW': window = self.windows[wid] window.rise() self.focus_on(window) def on_sigchld(self): """ Rip orphans. """ while True: try: pid, status = os.waitpid(-1, os.WNOHANG) if (pid, status) == (0, 0): # no child to rip break self.log.notice("ripped child PID=%s" % pid) except ChildProcessError: break def on_map_request(self, evname, xcb_event): """ Map request is a request to draw the window on screen. """ wid = xcb_event.window if wid not in self.windows: window = self.on_new_window(wid) else: window = self.windows[wid] self.log.on_map_request.debug("map request for %s" % window) window.show() if window.above_all: window.rise() if window.can_focus: window.focus() def on_new_window(self, wid): """ Registers new window. """ window = Window(wm=self, wid=wid, atoms=self.atoms, mapped=True) # call configuration hood first # to setup attributes like 'sticky' self.hook.fire("new_window", window) self.log.on_new_window.debug( "new window is ready: %s" % window) self.windows[wid] = window self.win2desk[window] = self.cur_desktop if window.sticky: for desktop in self.desktops: desktop.add(window) else: self.cur_desktop.windows.append(window) return window def on_map_notify(self, evname, xcb_event): wid = xcb_event.window if wid not in self.windows: window = self.on_new_window(wid) else: window = self.windows[wid] window.mapped = True if window.above_all: window.rise() # if window.can_focus: # window.focus() self.log.on_map_notify.debug("map notify for %s" % window) def on_window_unmap(self, evname, xcb_event): wid = xcb_event.window if wid not in self.windows: return window = self.windows[wid] window.mapped = False self.hook.fire("window_unmap", window) def on_window_destroy(self, evname, xcb_event): wid = xcb_event.window if wid not in self.windows: return window = self.windows[wid] assert isinstance(window, Window), "it's not a window: %s (%s)" % ( window, type(window)) for desktop in self.desktops: try: desktop.windows.remove(window) self.log.debug("%s removed from %s" % (self, desktop)) except ValueError: pass del self.windows[wid] if window in self.win2desk: del self.win2desk[window] def on_window_enter(self, evname, xcb_event): wid = xcb_event.event if wid not in self.windows: # self.log.on_window_enter.error("no window with wid=%s" % wid) self.hook.fire("unknown_window", wid) return window = self.windows[wid] # self.log.on_window_enter("window_enter: %s %s" % (wid, window)) self.hook.fire("window_enter", window) def grab_key(self, modifiers, key, owner_events=False, window=None): """ Intercept this key when it is pressed. If owner_events=False then the window in focus will not receive it. This is useful from WM hotkeys. """ # TODO: check if key already grabbed? # Here is how X works with keys: # key => keysym => keycode # where `key' is something like 'a', 'b' or 'Enter', # `keysum' is what should be written on they key cap (physical keyboard) # and `keycode' is a number reported by the keyboard when the key is pressed. # Modifiers are keys like Shift, Alt, Win and some other buttons. self.log.grab_key.debug("intercept keys: %s %s" % (modifiers, key)) if window is None: window = self.root keycode = self.kbd.key_to_code(key) modmask = get_modmask(modifiers) # TODO: move to Keyboard event = ("on_key_press", modmask, keycode) pointer_mode = xproto.GrabMode.Async keyboard_mode = xproto.GrabMode.Async self._conn.core.GrabKey( owner_events, window.wid, modmask, keycode, pointer_mode, keyboard_mode ) self.flush() # TODO: do we need this? return event def on_key_press(self, evname, xcb_event): # TODO: ignore capslock, scrolllock and other modifiers? modmap = xcb_event.state keycode = xcb_event.detail event = ("on_key_press", modmap, keycode) self.hook.fire(event) def on_key_release(self, evname, xcb_event): modmap = xcb_event.state keycode = xcb_event.detail event = ("on_key_release", modmap, keycode) self.hook.fire(event) def grab_mouse(self, modifiers, button, owner_events=False, window=None): # http://www.x.org/archive/X11R7.7/doc/man/man3/xcb_grab_button.3.xhtml wid = (window or self.root).wid event_mask = xcffib.xproto.EventMask.ButtonPress | \ xcffib.xproto.EventMask.ButtonRelease | \ xcffib.xproto.EventMask.Button1Motion modmask = get_modmask(modifiers) pointer_mode = xproto.GrabMode.Async # I don't know what it is keyboard_mode = xproto.GrabMode.Async # do not block other keyboard events confine_to = xcffib.xproto.Atom._None # do not restrict cursor movements cursor = xcffib.xproto.Atom._None # do not change cursor event = ("on_mouse", modmask, button) # event to be used in hooks self._conn.core.GrabButton( owner_events, wid, event_mask, pointer_mode, keyboard_mode, confine_to, cursor, button, modmask, ) self.flush() # TODO: do we need this? return event def hotkey(self, keys, cmd): """ Setup hook to launch a command on specific hotkeys. """ @self.hook(self.grab_key(*keys)) def cb(event): run_(cmd) def focus_on(self, window, warp=False): """ Focuses on given window. """ self.cur_desktop.focus_on(window, warp) self.root.set_prop('_NET_ACTIVE_WINDOW', window.wid) def switch_to(self, desktop: Desktop): """ Switches to another desktop. """ if isinstance(desktop, int): desktop = self.desktops[desktop] if self.cur_desktop == desktop: self.log.notice("attempt to switch to the same desktop") return self.log.debug("switching from {} to {}".format( self.cur_desktop, desktop)) self.cur_desktop.hide() self.cur_desktop = desktop self.cur_desktop.show() # TODO: move this code to Desktop.show() self.root.props[self.atom._NET_CURRENT_DESKTOP] = desktop.id def relocate_to(self, window: Window, to_desktop: Desktop): """ Relocates window to a specific desktop. """ if window.sticky: self.log.debug( "%s is meant to be on all desktops, cannot relocate to specific one" % window) return from_desktop = self.cur_desktop if from_desktop == to_desktop: self.log.debug( "no need to relocate %s because remains on the same desktop" % window) return from_desktop.remove(window) to_desktop.add(window) def on_mouse_event(self, evname, xcb_event): """evname is one of ButtonPress, ButtonRelease or MotionNotify.""" # l = [(attr, getattr(xcb_event, attr)) for attr in sorted(dir(xcb_event)) if not attr.startswith('_')] # print(evname) # print(l) modmask = xcb_event.state & 0xff # TODO: is the mask correct? if evname == 'MotionNotify': button = 1 # TODO else: button = xcb_event.detail event = ('on_mouse', modmask, button) # print(event) self.hook.fire(event, evname, xcb_event) def on_configure_window(self, _, event): # This code is so trivial that I just took it from fpwm as is :) values = [] if event.value_mask & ConfigWindow.X: values.append(event.x) if event.value_mask & ConfigWindow.Y: values.append(event.y) if event.value_mask & ConfigWindow.Width: values.append(event.width) if event.value_mask & ConfigWindow.Height: values.append(event.height) if event.value_mask & ConfigWindow.BorderWidth: values.append(event.border_width) if event.value_mask & ConfigWindow.Sibling: values.append(event.sibling) if event.value_mask & ConfigWindow.StackMode: values.append(event.stack_mode) self._conn.core.ConfigureWindow(event.window, event.value_mask, values) def create_window(self, x, y, width, height): """ Create a window. Right now only used for initialization, see __init__. """ wid = self._conn.generate_id() self._conn.core.CreateWindow( self.xcb_default_screen.root_depth, wid, self.xcb_default_screen.root, x, y, width, height, 0, WindowClass.InputOutput, self.xcb_default_screen.root_visual, CW.BackPixel | CW.EventMask, [ self.xcb_default_screen.black_pixel, EventMask.StructureNotify | EventMask.Exposure ] ) return Window(self, wid=wid, atoms=self.atoms) def scan(self, focus=True): """ Gets all windows in the system. """ self.log.debug("performing scan of all mapped windows") q = self._conn.core.QueryTree(self.root.wid).reply() for wid in q.children: # attrs=self._conn.core.GetWindowAttributes(wid).reply() # print(attrs, type(attrs)) # if attrs.map_state == xproto.MapState.Unmapped: # self.log.scan.debug( # "window %s is not mapped, skipping" % wid) # TODO # continue if wid not in self.windows: self.on_new_window(wid) self.log.scan.info("the following windows are active: %s" % sorted(self.windows.values())) if focus: windows = sorted(self.windows.values()) windows = list(filter(lambda w: w != self.root and not w.skip, windows)) if windows: # on empty desktop there is nothing to focus on self.cur_desktop.focus_on(windows[-1], warp=True) def finalize(self): """ This code is run when event loop is terminated. """ pass # currently nothing to do here def flush(self): """ Force pending X request to be sent. By default XCB aggressevly buffers for performance reasons. """ return self._conn.flush() def xsync(self): """ Flush XCB queue and wait till it is processed by X server. """ # The idea here is that pushing an innocuous request through the queue # and waiting for a response "syncs" the connection, since requests are # serviced in order. self._conn.core.GetInputFocus().reply() def stop(self, xserver_dead=False): """ Stop WM to quit. """ self.hook.fire("on_exit") # display all hidden windows try: if not xserver_dead: for window in self.windows.values(): window.show() self.xsync() except Exception as err: self.log.stop.error("error on stop: %s" % err) self.log.stop.debug("stopping event loop") self._eventloop.stop() def replace(self, execv_args): self.log.notice("replacing current process with %s" % (execv_args,)) self.stop() import os os.execv(*execv_args) def loop(self): """ DITTO """ self.scan() try: self._eventloop.run_forever() finally: self.finalize() def _xpoll(self): """ Fetch incomming events (if any) and call hooks. """ # OK, kids, today I'll teach you how to write reliable enterprise # software! You just catch all the exceptions in the top-level loop # and ignore them. No, I'm kidding, these exceptions are no use # for us because we don't care if a window cannot be drawn or something. # We actually only need to handle just a few events and ignore the rest. # Exceptions happen because of the async nature of X. while True: try: xcb_event = self._conn.poll_for_event() if not xcb_event: break evname = xcb_event.__class__.__name__ if evname.endswith("Event"): evname = evname[:-5] if evname in self.ignoreEvents: self.log._xpoll.info("ignoring %s" % xcb_event) continue self.log._xpoll.critical("got %s %s" % (evname, xcb_event)) self.hook.fire(evname, xcb_event) self.flush() # xcb doesn't flush implicitly except (WindowError, AccessError, DrawableError): self.log.debug("(minor exception)") except Exception as e: self.log._xpoll.error(traceback.format_exc()) error_code = self._conn.has_error() if error_code: error_string = XCB_CONN_ERRORS[error_code] self.log.critical("Shutting down due to X connection error %s (%s)" % (error_string, error_code)) self.stop(xserver_dead=True) break
class Scheduler(Thread): """ Schedules and executes tasks using threaded workers. """ def __init__(self, workers=5, histlen=10000): super().__init__(daemon=True) self.pending = PriorityQueue() self.ready = Queue() # checks to be executed self.timer = MyTimer(0) # timer placeholder for .schedule() so it can call self.timer.cancel() during the first call self.lock = Lock() # lock pending queue self.lockev = Event() # set by .run() when lock is acquired self.history = deque(maxlen=histlen) # keep track of the history self.log = Log("scheduler") for i in range(workers): worker = Worker(self) worker.start() def flush(self): """ Request immidiate check. """ self.log.debug("flushing pending queue") with self.lock: self.lockev.clear() self.pending.put((-1000, None)) self.timer.cancel() self.lockev.wait() queued = [] while True: try: _, check = self.pending.get(block=False) queued.append(check) except Empty: break for checker in queued: self.ready.put(checker) self.log.debug("flushing done") def schedule(self, checker): t = checker.get_next_check() with self.lock: self.pending.put((t, checker)) self.timer.cancel() def run(self): pending = self.pending ready = self.ready while True: t, c = pending.get() if c is None: self.lockev.set() with self.lock: continue with self.lock: delta = t - time.time() self.log.debug("sleeping for %.2f" % delta) self.timer = MyTimer(delta) self.timer.start() try: self.timer.join() ready.put(c) except TimerCanceled: self.log.debug("new item scheduled, restarting scheduler (this is normal)") pending.put((t, c))
class Desktop: """ Support for virtual desktops. """ def __init__(self, id, windows=None, name=None): self.id = id if not name: name = "(desktop %s)" % id(self) self.log = Log("desktop %s" % name) if not windows: windows = [] self.windows = windows self.name = name self.cur_focus = None self.prev_focus = None self.were_mapped = [] self.hidden = True # TODO: rename to active def show(self): self.hidden = False for window in self.were_mapped: self.log.debug("showing window %s" % window) window.show() else: self.log.debug("no windows on this desktop to show") self.were_mapped.clear() if self.cur_focus: self.cur_focus.focus() # TODO: self.wm.root.set_prop('_NET_CURRENT_DESKTOP', self.id) def hide(self): self.hidden = True for window in self.windows: if window.mapped: self.log.debug("hiding window %s" % window) window.hide() self.were_mapped.append(window) self.log.debug("followind windows were hidden: %s" % self.were_mapped) def add(self, window): self.windows.append(window) if self.hidden: self.were_mapped.append(window) else: window.show() window.focus() self.cur_focus = window def remove(self, window): if window not in self.windows: self.log.error("NO WINDOW %s" % window) self.log.error("current windows: %s", self.windows) return self.windows.remove(window) if not self.hidden: window.hide() if window == self.cur_focus: self.cur_focus = None def focus_on(self, window, warp=False): assert window in self.windows, "window %s is not on current desktop" % window assert not self.hidden, "cannot focus while desktop is hidden" # if self.cur_focus: # self.cur_focus.lower() # Achtung! Order here is very important or focus will now work # correctly self.log("focusing on %s" % window) if warp: window.show() window.rise() window.warp() window.focus() self.cur_focus = window def __repr__(self): return "Desktop(%s)" % self.name
class Hook: """ Simple callback dispatcher. """ def __init__(self): self.cb_map = defaultdict(list) self.log = Log("hook") self.suppressed = set() def decor(self, event): def wrap(cb): self.register(event, cb) return cb return wrap __call__ = decor def register(self, event, cb): self.cb_map[event].append(cb) def has_hook(self, event): return event in self.cb_map def suppress(self, event): hook = self class Context: def __enter__(self): hook.log.debug("suppressing %s" % event) hook.suppressed.add(event) def __exit__(self, *args): hook.log.debug("un-suppressing %s" % event) if event in hook.suppressed: hook.suppressed.remove(event) else: hook.log.notice("uhm, event is not suppressed: %s" % event) return Context() def fire(self, event, *args, **kwargs): self.log.debug("{} {} {}".format(event, args, kwargs)) if event not in self.cb_map: self.log.notice("no handler for {}".format(event)) return if event in self.suppressed: self.log.debug("event suppressed: {} {} {}".format( event, args, kwargs)) return handlers = self.cb_map[event] for handler in handlers: try: handler(event, *args, **kwargs) # except SupressEvent: # break except Exception as err: # msg = "error on event {ev}: {err} ({typ}) (in {hdl})" \ # .format(err=err, typ=type(err), ev=event, hdl=handler) msg = traceback.format_exc() self.log.error(msg)
# wordpress = "siege -c 100 -t 666h http://localhost/", # matrix = "/home/sources/perftest/benches/matrix.py -s 1024 -r 1000", matrix = BENCHES + "matrix 2048", # sdag = BENCHES + "test_SDAG/test_sdag -t 5 -q 1000 /home/sources/perftest/benches/test_SDAG/dataset.dat", # sdagp = BENCHES + "test_SDAG/test_sdag+ -t 5 -q 1000 /home/sources/perftest/benches/test_SDAG/dataset.dat", blosc = BENCHES + "pyblosc.py -r 10000000", burnP6 = "burnP6", ffmpeg = "ffmpeg -i /home/sources/ToS-4k-1920.mov" \ " -threads 1 -y -strict -2 -loglevel panic" \ " -acodec aac -aq 100" \ " -vcodec libx264 -preset fast -crf 22" \ " -f mp4 /dev/null", ) log = Log(['profile']) for k,v in basis.items(): log.debug("{:<10} {}".format(k,v)) class cfg: sys_ipc_time = 3 task_profile_time = 0.1 sys_optimize_samples = 10 warmup_time = 3 idleness = 100 cpu_mask = 0b1111 def generate_load(num): tasks = [] all_tasks = list(basis.items()) for i in range(num): name, cmd = all_tasks.pop(0) p = Popen(shlex.split(cmd), stdout=DEVNULL, stderr=DEVNULL)
class Manager(CLI): """ Class to orchestrate several instances at once. """ autostart_delay = 3 def __init__(self, name="default"): self.instances = OrderedDict() self.name = name self.log = Log(name) def add_instance(self, inst): assert inst.name not in self.instances, \ "we already have a machine with the name %s" % inst.name self.instances[inst.name] = inst def check_instance(self, name): if name not in self.instances: raise UnknownInstance("no such instance: %s" % name) @command("gen mac") def genmac(self): mac = gen_mac() print(mac) return mac @command("list") def do_list(self): return self.instances.keys() @command("autostart") def autostart(self): log.debug("starting all stopped instances with auto=True") sleep = 0 # do not do a pause if there is only one instance for instance in self.instances.values(): time.sleep(sleep) if not instance.auto: self.log.debug("%s is skipped because it has auto=False" % instance) continue if instance.is_running(): log.debug("skipping %s because it is already started" % instance) continue log.info("Starting %s" % instance) instance.start() sleep = self.autostart_delay @command("[name] start") @command("start [name]") def start(self, name): assert isinstance(name, str), "name should be string" self.log.debug("Starting %s" % name) self.check_instance(name) inst = self.instances[name] inst.start() return inst @command("stop all") @command("shutdown all") def stop_all(self): for inst in self.instances.values(): inst.stop() @command("stop [name]") @command("[name] stop") @command("shutdown [name]") @command("[name] shutdown") def stop(self, name): self.check_instance(name) self.instances[name].stop() @command("[name] reboot") @command("reboot [name]") def reboot(self, name=None): self.check_instance(name) self.instances[name].reboot() @command("[name] reset") @command("reset [name]") def reset(self, name=None): self.check_instance(name) self.instances[name].reset() @command("unfreeze all") @command("defrost all") def kill_defrost(self): for inst in self.instances.values(): inst.unfreeze() @command("killall") @command("kill all") def kill_all(self): self.log.critical("KILLING ALL instances (even with auto=False)") for inst in self.instances.values(): inst.kill() @command("[name] kill") @command("kill [name]") def kill(self, name): self.check_instance(name) self.instances[name].kill() @command("show cmd [name]") def show_cmd(self, name): print(self.instances[name].get_cmd()) @command("console [name]") @command("[name] console") def console(self, name=None): self.log.debug("attaching to %s" % name) if name and not self.instances[name].is_running(): sys.exit("Instance is not started") self.instances[name].tmux.attach(name=name) @command("status") def status(self): for inst in self.instances.values(): print(inst.format_status()) @command("wait all timeout [timeout]") def wait_all(self, timeout): timeout = int(timeout) while True: running = 0 for inst in self.instances.values(): if inst.is_running(): running = 1 if not running: break timeout -= 1 if timeout < 0: raise TimeoutError("instances still running") time.sleep(1) print('.', end='', file=sys.stderr, flush=True) @command("graceful stop timeout [timeout]") def graceful(self, timeout=30): self.log.info("stopping ALL instances (even with auto=False)") timeout = int(timeout) self.stop_all() try: self.wait_all(timeout) except TimeoutError: self.log.critical("kvms still running: %s" \ % list(filter(lambda x: x.is_running(), self.instances.values()))) self.kill_all()
class KVM: name = None mem = 256 cores = 1 cpu = "qemu64" runas = None cmd = "qemu-system-x86_64 -enable-kvm -curses" tmux = TMUX(socket="virt", session="KVM") auto = True net = None drives = None mgr = manager cpus = None # CPU affinity kernel = None append = None initrd = None boot = None devs = [] def __init__(self, **kwargs): self.__dict__.update(kwargs) self.pidfile = os.path.join(PREFIX, "kvm_%s.pid"%self.name) self.monfile = os.path.join(PREFIX, "kvm_%s.mon"%self.name) self.log = Log("KVM %s" % self.name) self.qmpsock = None assert self.name, "name is mandatory" if self.mgr: #self.log.debug("adding %s to %s" % (self, self.mgr)) self.mgr.add_instance(self) def get_cmd(self): """ Get cmd that launches instance. """ cmd = self.cmd cmd += " -name %s" % self.name cmd += " -m %s" % self.mem if self.cpu: cmd += " -cpu %s" % self.cpu cmd += " -smp %s" % self.cores cmd += " -qmp unix:%s,server,nowait " % self.monfile cmd += " -pidfile %s " % self.pidfile if self.net: cmd += stringify(self.net) if self.drives: cmd += stringify(self.drives) if self.runas: if os.geteuid() != 0: cmd = "sudo " + cmd cmd += " -runas %s" % self.runas if self.kernel: cmd += " -kernel %s" % self.kernel if self.append: cmd += " -append %s" % self.append if self.initrd: cmd += " -initrd %s" % self.initrd if self.boot: cmd += " -boot %s" % self.boot if self.devs: for device in self.devs: cmd += " %s" % device return cmd def is_running(self): """ Returns either pid of the process or False if kvm is not running. """ try: pid = int(open(self.pidfile).readline().strip()) except IOError as err: if err.errno == errno.EACCES: raise StatusUnknown("cannot read pidfile:", err) elif err.errno == errno.ENOENT: return False raise try: os.kill(pid, 0) return pid except ProcessLookupError: os.unlink(self.pidfile) return False @property def pid(self): return self.is_running() def start(self): if self.is_running(): self.log.debug("Instance is already started!") return False for device in self.devs: device.on_start(self) self.log.debug("spawning %s" % self.get_cmd()) self.tmux.run(self.get_cmd(), name=self.name) for x in range(100): pid = self.is_running() if pid: break time.sleep(0.2) self.log.debug("waiting for VM") else: raise StatusUnknown("KVM %s doesn't want to start" % self.name) if self.cpus: self.set_cpus(self.cpus) return pid def set_cpus(self, cpus): self.cpus = cpus pid = self.pid if not pid: return self.log.critical("VM is not running, not setting affinity") cpulist = ",".join(map(str,self.cpus)) self.log.debug("setting CPU affinity to %s" % cpulist) cmd = "taskset -a -c -p %s %s" % (cpulist, pid) try: run(cmd, stdout=DEVNULL) except Exception as e: self.log.critical("set affinity with taskset failed: %s" % e) def reboot(self): """ Send Ctrl+Alt+Del. """ data = """{ "execute": "send-key", "arguments": { 'keys': [ {'type':'qcode', 'data': 'ctrl'}, {'type':'qcode', 'data': 'alt'}, {'type':'qcode', 'data': 'delete'} ]}}""" self.send_qmp(data) def reset(self): """ Do hard reset. """ self.send_qmp('{"execute": "system_reset"}') def freeze(self): """ stop virtual CPU """ self.send_qmp('{"execute": "stop"}') def unfreeze(self): """ resume after freeze """ self.send_qmp('{"execute": "cont"}') def shutdown(self): """ Attempt to do graceful shutdown. Success is not guaranteed. """ if self.is_running(): try: self.send_qmp('{"execute": "system_powerdown"}') except Exception as err: self.log.critical("shutdown command failed with %s" % err) for device in self.devs: device.on_stop(self) stop = shutdown # stop is alias for shutdown def kill(self): """ Kill the guest using all possible means. """ for device in self.devs: device.on_kill(self) pid = self.is_running() if not pid: self.log.debug("%s: It's Dead, Jim!" % self) return False try: self.send_qmp("{'execute': 'quit'}") self.qmp_disconnect() timeout = KILL_TIMEOUT while timeout > 0: time.sleep(POLL_INTERVAL) timeout -= POLL_INTERVAL if not self.is_running(): return 1 except Exception as err: self.log.critical("cannot send qmp command: %s" % err) self.log.critical("It doesn't want to die, killing by SIGKILL") try: os.kill(pid, signal.SIGKILL) except ProcessLookupError: pass return 2 def qmp_connect(self): s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s.settimeout(3) #self.log.debug("connecting to %s" % self.monfile) s.connect(self.monfile) answer = s.recv(BUF_SIZE) #self.log.debug("initial handshake: %s" % answer.decode(errors='replace')) s.send(b'{"execute": "qmp_capabilities"}') # handshake answer = s.recv(BUF_SIZE) #self.log.debug("capabilities: %s" % answer.decode(errors='replace')) self.qmpsock = s def qmp_disconnect(self): if self.qmpsock: self.qmpsock.close() self.qmpsock = None def send_qmp(self, cmd): if isinstance(cmd, str): cmd = cmd.encode() if not self.qmpsock: self.qmp_connect() #self.log.debug("sending cmd %s" % cmd) self.qmpsock.send(cmd) answer = self.qmpsock.recv(BUF_SIZE) if len(answer) == BUF_SIZE: self.log.error("too long answer was truncated :(") #self.log.debug("result: %s" % answer.decode(errors='replace')) return answer def console(self): self.tmux.attach(name=self.name) def format_status(self): formated = "%s\n" % self.name formated += " noauto" if not self.auto else " " try: pid = self.is_running() if pid: formated += " UP (pid %s)\n" % pid if pid else "DOWN" else: formated += " DOWN\n" except StatusUnknown as err: formated += " UNKNOWN (%s)\n" % err return formated def __repr__(self): return "KVM(\"{name}\")".format(name=self.name) def __exit__(self): self.shutdown() for x in range(300): time.sleep(0.1) if not self.is_running(): return self.log.critical("it doesn't want to die, killing") self.kill()
class Hook: """ Simple callback dispatcher. """ def __init__(self): self.cb_map = defaultdict(list) self.log = Log("hook") self.suppressed = set() def decor(self, event): def wrap(cb): self.register(event, cb) return cb return wrap __call__ = decor def register(self, event, cb): self.cb_map[event].append(cb) def has_hook(self, event): return event in self.cb_map def suppress(self, event): hook = self class Context: def __enter__(self): hook.log.debug("suppressing %s" % event) hook.suppressed.add(event) def __exit__(self, *args): hook.log.debug("un-suppressing %s" % event) if event in hook.suppressed: hook.suppressed.remove(event) else: hook.log.notice("uhm, event is not suppressed: %s" % event) return Context() def fire(self, event, *args, **kwargs): self.log.debug("{} {} {}".format(event, args, kwargs)) if event not in self.cb_map: self.log.notice("no handler for {}".format(event)) return if event in self.suppressed: self.log.debug("event suppressed: {} {} {}".format(event, args, kwargs)) return handlers = self.cb_map[event] for handler in handlers: try: handler(event, *args, **kwargs) # except SupressEvent: # break except Exception as err: # msg = "error on event {ev}: {err} ({typ}) (in {hdl})" \ # .format(err=err, typ=type(err), ev=event, hdl=handler) msg = traceback.format_exc() self.log.error(msg)