diff --git a/examples/mitop.py b/examples/mitop.py index 84b52168..7b6d4e42 100644 --- a/examples/mitop.py +++ b/examples/mitop.py @@ -10,15 +10,28 @@ import mitogen.utils class Host(object): + """ + A target host from the perspective of the master process. + """ + #: String hostname. name = None + + #: mitogen.parent.Context used to call functions on the host. context = None + + #: mitogen.core.Receiver the target delivers state updates to. recv = None def __init__(self): - self.procs = {} #: pid -> Process() + #: Mapping of pid -> Process() for each process described + #: in the host's previous status update. + self.procs = {} class Process(object): + """ + A single process running on a target host. + """ host = None user = None pid = None @@ -30,14 +43,20 @@ class Process(object): rss = None -@mitogen.core.takes_router -def remote_main(context_id, handle, delay, router): - context = mitogen.core.Context(router, context_id) - sender = mitogen.core.Sender(context, handle) - +def child_main(sender, delay): + """ + Executed on the main thread of the Python interpreter running on the target + machine, using context.call() by the master. It simply sends the output of + the UNIX 'ps' command at regular intervals toward a Receiver on master. + + :param mitogen.core.Sender sender: + The Sender to use for delivering our result. This could target + anywhere, but the sender supplied by the master simply causes results + to be delivered to the master's associated per-host Receiver. + """ args = ['ps', '-axwwo', 'user,pid,ppid,pgid,%cpu,rss,command'] while True: - sender.put(subprocess.check_output(args)) + sender.send(subprocess.check_output(args)) time.sleep(delay) @@ -124,7 +143,12 @@ class Painter(object): self.stdscr.refresh() -def local_main(painter, router, select, delay): +def master_main(painter, router, select, delay): + """ + Loop until CTRL+C is pressed, waiting for the next result delivered by the + Select. Use parse_output() to turn that result ('ps' command output) into + rich data, and finally repaint the screen if the repaint delay has passed. + """ next_paint = 0 while True: msg = select.get() @@ -134,8 +158,13 @@ def local_main(painter, router, select, delay): painter.paint() -def main(router, argv): - mitogen.utils.log_to_file() +@mitogen.main() +def main(router): + """ + Main program entry point. @mitogen.main() is just a helper to handle + reliable setup/destruction of Broker, Router and the logging package. + """ + argv = sys.argv[1:] if not len(argv): print 'mitop: Need a list of SSH hosts to connect to.' sys.exit(1) @@ -144,36 +173,60 @@ def main(router, argv): select = mitogen.master.Select(oneshot=False) hosts = [] + # For each hostname on the command line, create a Host instance, a Mitogen + # connection, a Receiver to accept messages from the host, and finally + # start child_main() on the host to pump messages into the receiver. for hostname in argv: print 'Starting on', hostname host = Host() host.name = hostname + if host.name == 'localhost': host.context = router.local() else: host.context = router.ssh(hostname=host.name) + # A receiver wires up a handle (via Router.add_handler()) to an + # internal thread-safe queue object, which can be drained through calls + # to recv.get(). host.recv = mitogen.core.Receiver(router) host.recv.host = host + + # But we don't want to receive data from just one receiver, we want to + # receive data from many. In this case we can use a Select(). It knows + # how to efficiently sleep while waiting for the first message sent to + # many receivers. select.add(host.recv) - call_recv = host.context.call_async(remote_main, - mitogen.context_id, host.recv.handle, delay) + # The inverse of a Receiver is a Sender. Unlike receivers, senders are + # serializable, so we can call the .to_sender() helper method to create + # one equivalent to our host's receiver, and pass it directly to the + # host as a function parameter. + sender = host.recv.to_sender() + + # Finally invoke the function in the remote target. Since child_main() + # is an infinite loop, using .call() would block the parent, since + # child_main() never returns. Instead use .call_async(), which returns + # another Receiver. We also want to wait for results from receiver -- + # even child_main() never returns, if there is an exception, it will be + # delivered instead. + call_recv = host.context.call_async(child_main, sender, delay) + call_recv.host = host # Adding call_recv to the select will cause CallError to be thrown by - # .get() if startup in the context fails, halt local_main() and cause + # .get() if startup in the context fails, halt master_main() and cause # the exception to be printed. select.add(call_recv) hosts.append(host) + # Painter just wraps up all the prehistory ncurses code and keeps it out of + # master_main(). painter = Painter(hosts) try: try: - local_main(painter, router, select, delay) + master_main(painter, router, select, delay) except KeyboardInterrupt: + # Shut down gracefully when the user presses CTRL+C. pass finally: painter.close() - -if __name__ == '__main__': - mitogen.utils.run_with_router(main, sys.argv[1:])