Added --wrapper option for debugging and tracing.
[zeroinstall.git] / zeroinstall / injector / cli.py
blob4e1a6cea56c987e7cea90a3dfa62afe04cbea74a
1 """
2 The B{0launch} command-line interface.
4 This code is here, rather than in B{0launch} itself, simply so that it gets byte-compiled at
5 install time.
6 """
8 import os, sys
9 from optparse import OptionParser
10 import logging
12 from zeroinstall.injector import model, download, autopolicy, namespaces
14 #def program_log(msg): os.access('MARK: 0launch: ' + msg, os.F_OK)
15 #import __main__
16 #__main__.__builtins__.program_log = program_log
17 #program_log('0launch ' + ' '.join((sys.argv[1:])))
19 def _list_interfaces(args):
20 from zeroinstall.injector.iface_cache import iface_cache
21 if len(args) == 0:
22 matches = iface_cache.list_all_interfaces()
23 elif len(args) == 1:
24 match = args[0].lower()
25 matches = [i for i in iface_cache.list_all_interfaces() if match in i.lower()]
26 else:
27 raise UsageError()
29 matches.sort()
30 for i in matches:
31 print i
33 def _import_interface(args):
34 from zeroinstall.injector import gpg, handler, trust
35 from zeroinstall.injector.iface_cache import iface_cache, PendingFeed
36 from xml.dom import minidom
37 for x in args:
38 if not os.path.isfile(x):
39 raise model.SafeException("File '%s' does not exist" % x)
40 logging.info("Importing from file '%s'", x)
41 signed_data = file(x)
42 data, sigs = gpg.check_stream(signed_data)
43 doc = minidom.parseString(data.read())
44 uri = doc.documentElement.getAttribute('uri')
45 if not uri:
46 raise model.SafeException("Missing 'uri' attribute on root element in '%s'" % x)
47 iface = iface_cache.get_interface(uri)
48 logging.info("Importing information about interface %s", iface)
49 signed_data.seek(0)
51 def keys_ready():
52 if not iface_cache.update_interface_if_trusted(iface, pending.sigs, pending.new_xml):
53 handler.confirm_trust_keys(iface, pending.sigs, pending.new_xml)
54 trust.trust_db.watchers.append(lambda: keys_ready())
56 pending = PendingFeed(uri, signed_data)
57 iface_cache.add_pending(pending)
59 handler = handler.Handler()
60 pending.begin_key_downloads(handler, keys_ready)
61 handler.wait_for_downloads()
63 def _manage_feeds(options, args):
64 from zeroinstall.injector import iface_cache, writer
65 from xml.dom import minidom
66 if not args: raise UsageError()
67 for x in args:
68 print "Feed '%s':\n" % x
69 x = model.canonical_iface_uri(x)
70 policy = autopolicy.AutoPolicy(x, download_only = True, dry_run = options.dry_run)
71 if options.offline:
72 policy.network_use = model.network_offline
73 policy.recalculate_with_dl()
74 interfaces = policy.get_feed_targets(policy.root)
75 for i in range(len(interfaces)):
76 feed = interfaces[i].get_feed(x)
77 if feed:
78 print "%d) Remove as feed for '%s'" % (i + 1, interfaces[i].uri)
79 else:
80 print "%d) Add as feed for '%s'" % (i + 1, interfaces[i].uri)
81 print
82 while True:
83 try:
84 i = raw_input('Enter a number, or CTRL-C to cancel [1]: ').strip()
85 except KeyboardInterrupt:
86 print
87 raise model.SafeException("Aborted at user request.")
88 if i == '':
89 i = 1
90 else:
91 try:
92 i = int(i)
93 except ValueError:
94 i = 0
95 if i > 0 and i <= len(interfaces):
96 break
97 print "Invalid number. Try again. (1 to %d)" % len(interfaces)
98 iface = interfaces[i - 1]
99 feed = iface.get_feed(x)
100 if feed:
101 iface.feeds.remove(feed)
102 else:
103 iface.feeds.append(model.Feed(x, arch = None, user_override = True))
104 writer.save_interface(iface)
105 print "\nFeed list for interface '%s' is now:" % iface.get_name()
106 if iface.feeds:
107 for f in iface.feeds:
108 print "- " + f.uri
109 else:
110 print "(no feeds)"
112 def _normal_mode(options, args):
113 if len(args) < 1:
114 # You can use -g on its own to edit the GUI's own policy
115 # Otherwise, failing to give an interface is an error
116 if options.gui:
117 args = [namespaces.injector_gui_uri]
118 options.download_only = True
119 else:
120 raise UsageError()
122 iface_uri = model.canonical_iface_uri(args[0])
124 # Singleton instance used everywhere...
125 policy = autopolicy.AutoPolicy(iface_uri,
126 download_only = bool(options.download_only),
127 dry_run = options.dry_run,
128 src = options.source)
130 if options.before or options.not_before:
131 policy.root_restrictions.append(model.Restriction(model.parse_version(options.before),
132 model.parse_version(options.not_before)))
134 if options.offline:
135 policy.network_use = model.network_offline
137 if options.get_selections:
138 if len(args) > 1:
139 raise model.SafeException("Can't use arguments with --get-selections")
140 if options.main:
141 raise model.SafeException("Can't use --main with --get-selections")
143 # Note that need_download() triggers a recalculate()
144 if options.refresh or options.gui:
145 # We could run immediately, but the user asked us not to
146 can_run_immediately = False
147 else:
148 can_run_immediately = (not policy.need_download()) and policy.ready
150 if options.download_only and policy.stale_feeds:
151 can_run_immediately = False
153 if can_run_immediately:
154 if policy.stale_feeds:
155 if policy.network_use == model.network_offline:
156 logging.debug("No doing background update because we are in off-line mode.")
157 else:
158 # There are feeds we should update, but we can run without them.
159 # Do the update in the background while the program is running.
160 import background
161 background.spawn_background_update(policy, options.verbose > 0)
162 if options.get_selections:
163 _get_selections(policy)
164 else:
165 policy.execute(args[1:], main = options.main, wrapper = options.wrapper)
166 assert options.dry_run or options.download_only
167 return
169 # If the user didn't say whether to use the GUI, choose for them.
170 if options.gui is None and os.environ.get('DISPLAY', None):
171 options.gui = True
172 # If we need to download anything, we might as well
173 # refresh all the interfaces first. Also, this triggers
174 # the 'checking for updates' box, which is non-interactive
175 # when there are no changes to the selection.
176 options.refresh = True
177 logging.info("Switching to GUI mode... (use --console to disable)")
179 prog_args = args[1:]
181 try:
182 if options.gui:
183 from zeroinstall.injector import run
184 gui_args = []
185 if options.download_only:
186 # Just changes the button's label
187 gui_args.append('--download-only')
188 if options.refresh:
189 # Just changes the button's label
190 gui_args.append('--refresh')
191 if options.not_before:
192 gui_args.insert(0, options.not_before)
193 gui_args.insert(0, '--not-before')
194 if options.before:
195 gui_args.insert(0, options.before)
196 gui_args.insert(0, '--before')
197 if options.source:
198 gui_args.insert(0, '--source')
199 sels = _fork_gui(iface_uri, gui_args, prog_args, options)
200 if not sels:
201 sys.exit(1) # Aborted
202 if options.get_selections:
203 doc = sels.toDOM()
204 doc.writexml(sys.stdout)
205 sys.stdout.write('\n')
206 elif not options.download_only:
207 run.execute_selections(sels, prog_args, options.dry_run, options.main, options.wrapper)
208 else:
209 #program_log('download_and_execute ' + iface_uri)
210 policy.download_and_execute(prog_args, refresh = bool(options.refresh), main = options.main)
211 except autopolicy.NeedDownload, ex:
212 # This only happens for dry runs
213 print ex
215 def _read_bytes(fd, nbytes, null_ok = False):
216 """Read exactly nbytes from fd."""
217 data = ''
218 while nbytes:
219 got = os.read(fd, min(256, nbytes))
220 if not got:
221 if null_ok and not data:
222 return None
223 raise Exception("Unexpected end-of-stream. Data so far %s; expecting %d bytes mode."
224 % (repr(data), nbytes))
225 data += got
226 nbytes -= len(got)
227 logging.debug("Message from GUI: %s" % repr(data))
228 return data
230 def _fork_gui(iface_uri, gui_args, prog_args, options = None):
231 """Run the GUI to get the selections.
232 prog_args and options are used only if the GUI requests a test.
234 import selections
236 gui_policy = autopolicy.AutoPolicy(namespaces.injector_gui_uri)
237 if iface_uri != namespaces.injector_gui_uri and (gui_policy.need_download() or gui_policy.stale_feeds):
238 # The GUI itself needs updating. Do that first.
239 logging.info("The GUI could do with updating first.")
240 gui_sel = _fork_gui(namespaces.injector_gui_uri, [], [])
241 if gui_sel is None:
242 logging.info("Aborted at user request")
243 return None # Aborted by user
244 else:
245 logging.info("GUI is up-to-date.")
246 # Try to start the GUI without using the network.
247 # The GUI can refresh itself if it wants to.
248 gui_policy.freshness = 0
249 gui_policy.network_use = model.network_offline
250 gui_policy.recalculate_with_dl()
251 assert gui_policy.ready # Should always be some version available
252 gui_policy.start_downloading_impls()
253 gui_policy.handler.wait_for_downloads()
254 gui_sel = selections.Selections(gui_policy)
256 from zeroinstall.injector import run
257 cli_from_gui, gui_to_cli = os.pipe() # socket.socketpair() not in Python 2.3 :-(
258 gui_from_cli, cli_to_gui = os.pipe()
259 try:
260 child = os.fork()
261 if child == 0:
262 # We are the child
263 try:
264 try:
265 os.close(cli_from_gui)
266 os.close(cli_to_gui)
267 os.dup2(gui_to_cli, 1)
268 os.dup2(gui_from_cli, 0)
269 run.execute_selections(gui_sel, gui_args + ['--', iface_uri])
270 except:
271 import traceback
272 traceback.print_exc(file = sys.stderr)
273 finally:
274 sys.stderr.flush()
275 os._exit(1)
276 os.close(gui_from_cli)
277 gui_from_cli = None
278 os.close(gui_to_cli)
279 gui_to_cli = None
281 while True:
282 logging.info("Waiting for selections from GUI...")
284 reply = _read_bytes(cli_from_gui, len('Length:') + 9, null_ok = True)
285 if reply:
286 assert reply.startswith('Length:')
287 xml = _read_bytes(cli_from_gui, int(reply.split(':', 1)[1], 16))
289 from StringIO import StringIO
290 from zeroinstall.injector import qdom, selections
291 dom = qdom.parse(StringIO(xml))
292 sels = selections.Selections(dom)
294 if dom.getAttribute('run-test'):
295 logging.info("Testing program, as requested by GUI...")
296 output = run.test_selections(sels, prog_args,
297 bool(options and options.dry_run),
298 options and options.main)
299 logging.info("Sending results to GUI...")
300 output = ('Length:%8x\n' % len(output)) + output
301 logging.debug("Sending: %s" % `output`)
302 while output:
303 sent = os.write(cli_to_gui, output)
304 output = output[sent:]
305 continue
306 else:
307 sels = None
309 pid, status = os.waitpid(child, 0)
310 assert pid == child
311 if status == 1 << 8:
312 logging.info("User cancelled the GUI; aborting")
313 return None # Aborted
314 if status != 0:
315 raise Exception("Error from GUI: code = %d" % status)
316 break
317 finally:
318 for fd in [cli_to_gui, cli_from_gui, gui_to_cli, gui_from_cli]:
319 if fd is not None: os.close(fd)
321 return sels
323 def _get_selections(policy):
324 import selections
325 doc = selections.Selections(policy).toDOM()
326 doc.writexml(sys.stdout)
327 sys.stdout.write('\n')
329 class UsageError(Exception): pass
331 def main(command_args):
332 """Act as if 0launch was run with the given arguments.
333 @arg command_args: array of arguments (e.g. C{sys.argv[1:]})
334 @type command_args: [str]
336 # Ensure stdin, stdout and stderr FDs exist, to avoid confusion
337 for std in (0, 1, 2):
338 try:
339 os.fstat(std)
340 except OSError:
341 fd = os.open('/dev/null', os.O_RDONLY)
342 if fd != std:
343 os.dup2(fd, std)
344 os.close(fd)
346 parser = OptionParser(usage="usage: %prog [options] interface [args]\n"
347 " %prog --list [search-term]\n"
348 " %prog --import [signed-interface-files]\n"
349 " %prog --feed [interface]")
350 parser.add_option("", "--before", help="choose a version before this", metavar='VERSION')
351 parser.add_option("-c", "--console", help="never use GUI", action='store_false', dest='gui')
352 parser.add_option("-d", "--download-only", help="fetch but don't run", action='store_true')
353 parser.add_option("-D", "--dry-run", help="just print actions", action='store_true')
354 parser.add_option("-f", "--feed", help="add or remove a feed", action='store_true')
355 parser.add_option("", "--get-selections", help="write selected versions as XML", action='store_true')
356 parser.add_option("-g", "--gui", help="show graphical policy editor", action='store_true')
357 parser.add_option("-i", "--import", help="import from files, not from the network", action='store_true')
358 parser.add_option("-l", "--list", help="list all known interfaces", action='store_true')
359 parser.add_option("-m", "--main", help="name of the file to execute")
360 parser.add_option("", "--not-before", help="minimum version to choose", metavar='VERSION')
361 parser.add_option("-o", "--offline", help="try to avoid using the network", action='store_true')
362 parser.add_option("-r", "--refresh", help="refresh all used interfaces", action='store_true')
363 parser.add_option("", "--set-selections", help="run versions specified in XML file", metavar='FILE')
364 parser.add_option("-s", "--source", help="select source code", action='store_true')
365 parser.add_option("-v", "--verbose", help="more verbose output", action='count')
366 parser.add_option("-V", "--version", help="display version information", action='store_true')
367 parser.add_option("-w", "--wrapper", help="execute program using a debugger, etc", metavar='COMMAND')
368 parser.disable_interspersed_args()
370 (options, args) = parser.parse_args(command_args)
372 if options.verbose:
373 logger = logging.getLogger()
374 if options.verbose == 1:
375 logger.setLevel(logging.INFO)
376 else:
377 logger.setLevel(logging.DEBUG)
378 import zeroinstall
379 logging.info("Running 0launch %s %s; Python %s", zeroinstall.version, repr(args), sys.version)
381 try:
382 if options.list:
383 _list_interfaces(args)
384 elif options.version:
385 import zeroinstall
386 print "0launch (zero-install) " + zeroinstall.version
387 print "Copyright (C) 2006 Thomas Leonard"
388 print "This program comes with ABSOLUTELY NO WARRANTY,"
389 print "to the extent permitted by law."
390 print "You may redistribute copies of this program"
391 print "under the terms of the GNU General Public License."
392 print "For more information about these matters, see the file named COPYING."
393 elif options.set_selections:
394 from zeroinstall.injector import selections, qdom, run
395 sels = selections.Selections(qdom.parse(file(options.set_selections)))
396 run.execute_selections(sels, args, options.dry_run, options.main, options.wrapper)
397 elif getattr(options, 'import'):
398 _import_interface(args)
399 elif options.feed:
400 _manage_feeds(options, args)
401 else:
402 _normal_mode(options, args)
403 except UsageError:
404 parser.print_help()
405 sys.exit(1)
406 except model.SafeException, ex:
407 if options.verbose: raise
408 print >>sys.stderr, ex
409 sys.exit(1)