Report errors during key downloads when using --import.
[zeroinstall.git] / zeroinstall / injector / cli.py
blob6df7f09c1b9f51d1e61e76a5845fb63a6821283a
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 errors = handler.wait_for_downloads()
62 if errors:
63 raise model.SafeException("Errors during download: " + '\n'.join(errors))
65 def _manage_feeds(options, args):
66 from zeroinstall.injector import iface_cache, writer
67 from xml.dom import minidom
68 if not args: raise UsageError()
69 for x in args:
70 print "Feed '%s':\n" % x
71 x = model.canonical_iface_uri(x)
72 policy = autopolicy.AutoPolicy(x, download_only = True, dry_run = options.dry_run)
73 if options.offline:
74 policy.network_use = model.network_offline
75 policy.recalculate_with_dl()
76 interfaces = policy.get_feed_targets(policy.root)
77 for i in range(len(interfaces)):
78 feed = interfaces[i].get_feed(x)
79 if feed:
80 print "%d) Remove as feed for '%s'" % (i + 1, interfaces[i].uri)
81 else:
82 print "%d) Add as feed for '%s'" % (i + 1, interfaces[i].uri)
83 print
84 while True:
85 try:
86 i = raw_input('Enter a number, or CTRL-C to cancel [1]: ').strip()
87 except KeyboardInterrupt:
88 print
89 raise model.SafeException("Aborted at user request.")
90 if i == '':
91 i = 1
92 else:
93 try:
94 i = int(i)
95 except ValueError:
96 i = 0
97 if i > 0 and i <= len(interfaces):
98 break
99 print "Invalid number. Try again. (1 to %d)" % len(interfaces)
100 iface = interfaces[i - 1]
101 feed = iface.get_feed(x)
102 if feed:
103 iface.extra_feeds.remove(feed)
104 else:
105 iface.extra_feeds.append(model.Feed(x, arch = None, user_override = True))
106 writer.save_interface(iface)
107 print "\nFeed list for interface '%s' is now:" % iface.get_name()
108 if iface.feeds:
109 for f in iface.feeds:
110 print "- " + f.uri
111 else:
112 print "(no feeds)"
114 def _normal_mode(options, args):
115 if len(args) < 1:
116 # You can use -g on its own to edit the GUI's own policy
117 # Otherwise, failing to give an interface is an error
118 if options.gui:
119 args = [namespaces.injector_gui_uri]
120 options.download_only = True
121 else:
122 raise UsageError()
124 iface_uri = model.canonical_iface_uri(args[0])
126 policy = autopolicy.AutoPolicy(iface_uri,
127 download_only = bool(options.download_only),
128 dry_run = options.dry_run,
129 src = options.source)
131 if options.before or options.not_before:
132 policy.root_restrictions.append(model.Restriction(model.parse_version(options.before),
133 model.parse_version(options.not_before)))
135 if options.offline:
136 policy.network_use = model.network_offline
138 if options.get_selections:
139 if len(args) > 1:
140 raise model.SafeException("Can't use arguments with --get-selections")
141 if options.main:
142 raise model.SafeException("Can't use --main with --get-selections")
144 # Note that need_download() triggers a recalculate()
145 if options.refresh or options.gui:
146 # We could run immediately, but the user asked us not to
147 can_run_immediately = False
148 else:
149 can_run_immediately = (not policy.need_download()) and policy.ready
151 if options.download_only and policy.stale_feeds:
152 can_run_immediately = False
154 if can_run_immediately:
155 if policy.stale_feeds:
156 if policy.network_use == model.network_offline:
157 logging.debug("No doing background update because we are in off-line mode.")
158 else:
159 # There are feeds we should update, but we can run without them.
160 # Do the update in the background while the program is running.
161 import background
162 background.spawn_background_update(policy, options.verbose > 0)
163 if options.get_selections:
164 _get_selections(policy)
165 else:
166 policy.execute(args[1:], main = options.main, wrapper = options.wrapper)
167 assert options.dry_run or options.download_only
168 return
170 # If the user didn't say whether to use the GUI, choose for them.
171 if options.gui is None and os.environ.get('DISPLAY', None):
172 options.gui = True
173 # If we need to download anything, we might as well
174 # refresh all the interfaces first. Also, this triggers
175 # the 'checking for updates' box, which is non-interactive
176 # when there are no changes to the selection.
177 options.refresh = True
178 logging.info("Switching to GUI mode... (use --console to disable)")
180 prog_args = args[1:]
182 try:
183 if options.gui:
184 from zeroinstall.injector import run
185 gui_args = []
186 if options.download_only:
187 # Just changes the button's label
188 gui_args.append('--download-only')
189 if options.refresh:
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 _fork_gui(iface_uri, gui_args, prog_args, options = None):
216 """Run the GUI to get the selections.
217 prog_args and options are used only if the GUI requests a test.
219 from zeroinstall import helpers
220 def test_callback(sels):
221 from zeroinstall.injector import run
222 return run.test_selections(sels, prog_args,
223 bool(options and options.dry_run),
224 options and options.main)
225 return helpers.get_selections_gui(iface_uri, gui_args, test_callback)
227 def _get_selections(policy):
228 import selections
229 doc = selections.Selections(policy).toDOM()
230 doc.writexml(sys.stdout)
231 sys.stdout.write('\n')
233 class UsageError(Exception): pass
235 def main(command_args):
236 """Act as if 0launch was run with the given arguments.
237 @arg command_args: array of arguments (e.g. C{sys.argv[1:]})
238 @type command_args: [str]
240 # Ensure stdin, stdout and stderr FDs exist, to avoid confusion
241 for std in (0, 1, 2):
242 try:
243 os.fstat(std)
244 except OSError:
245 fd = os.open('/dev/null', os.O_RDONLY)
246 if fd != std:
247 os.dup2(fd, std)
248 os.close(fd)
250 parser = OptionParser(usage="usage: %prog [options] interface [args]\n"
251 " %prog --list [search-term]\n"
252 " %prog --import [signed-interface-files]\n"
253 " %prog --feed [interface]")
254 parser.add_option("", "--before", help="choose a version before this", metavar='VERSION')
255 parser.add_option("-c", "--console", help="never use GUI", action='store_false', dest='gui')
256 parser.add_option("-d", "--download-only", help="fetch but don't run", action='store_true')
257 parser.add_option("-D", "--dry-run", help="just print actions", action='store_true')
258 parser.add_option("-f", "--feed", help="add or remove a feed", action='store_true')
259 parser.add_option("", "--get-selections", help="write selected versions as XML", action='store_true')
260 parser.add_option("-g", "--gui", help="show graphical policy editor", action='store_true')
261 parser.add_option("-i", "--import", help="import from files, not from the network", action='store_true')
262 parser.add_option("-l", "--list", help="list all known interfaces", action='store_true')
263 parser.add_option("-m", "--main", help="name of the file to execute")
264 parser.add_option("", "--not-before", help="minimum version to choose", metavar='VERSION')
265 parser.add_option("-o", "--offline", help="try to avoid using the network", action='store_true')
266 parser.add_option("-r", "--refresh", help="refresh all used interfaces", action='store_true')
267 parser.add_option("", "--set-selections", help="run versions specified in XML file", metavar='FILE')
268 parser.add_option("-s", "--source", help="select source code", action='store_true')
269 parser.add_option("-v", "--verbose", help="more verbose output", action='count')
270 parser.add_option("-V", "--version", help="display version information", action='store_true')
271 parser.add_option("-w", "--wrapper", help="execute program using a debugger, etc", metavar='COMMAND')
272 parser.disable_interspersed_args()
274 (options, args) = parser.parse_args(command_args)
276 if options.verbose:
277 logger = logging.getLogger()
278 if options.verbose == 1:
279 logger.setLevel(logging.INFO)
280 else:
281 logger.setLevel(logging.DEBUG)
282 import zeroinstall
283 logging.info("Running 0launch %s %s; Python %s", zeroinstall.version, repr(args), sys.version)
285 try:
286 if options.list:
287 _list_interfaces(args)
288 elif options.version:
289 import zeroinstall
290 print "0launch (zero-install) " + zeroinstall.version
291 print "Copyright (C) 2007 Thomas Leonard"
292 print "This program comes with ABSOLUTELY NO WARRANTY,"
293 print "to the extent permitted by law."
294 print "You may redistribute copies of this program"
295 print "under the terms of the GNU Lesser General Public License."
296 print "For more information about these matters, see the file named COPYING."
297 elif options.set_selections:
298 from zeroinstall.injector import selections, qdom, run
299 sels = selections.Selections(qdom.parse(file(options.set_selections)))
300 run.execute_selections(sels, args, options.dry_run, options.main, options.wrapper)
301 elif getattr(options, 'import'):
302 _import_interface(args)
303 elif options.feed:
304 _manage_feeds(options, args)
305 else:
306 _normal_mode(options, args)
307 except UsageError:
308 parser.print_help()
309 sys.exit(1)
310 except model.SafeException, ex:
311 if options.verbose: raise
312 print >>sys.stderr, ex
313 sys.exit(1)