The GUI now returns an XML selections document in all cases, rather than
[zeroinstall.git] / zeroinstall / injector / cli.py
blob8e402d44370ef021e6e86d7ebe7cbc5e903a778d
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)
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)
200 if not sels: return # Aborted
201 if options.get_selections:
202 doc = sels.toDOM()
203 doc.writexml(sys.stdout)
204 sys.stdout.write('\n')
205 elif not options.download_only:
206 run.execute_selections(sels, prog_args, options.dry_run, options.main)
207 else:
208 #program_log('download_and_execute ' + iface_uri)
209 policy.download_and_execute(prog_args, refresh = bool(options.refresh), main = options.main)
210 except autopolicy.NeedDownload, ex:
211 # This only happens for dry runs
212 print ex
214 def _fork_gui(iface_uri, gui_args):
215 gui_policy = autopolicy.AutoPolicy(namespaces.injector_gui_uri)
216 # Try to start the GUI without using the network.
217 # The GUI can refresh itself if it wants to.
218 gui_policy.freshness = 0
219 gui_policy.network_use = model.network_offline
220 gui_policy.recalculate_with_dl()
221 assert gui_policy.ready # Should always be some version available
222 gui_policy.start_downloading_impls()
223 gui_policy.handler.wait_for_downloads()
225 from zeroinstall.injector import run
226 import socket
227 cli, gui = socket.socketpair()
228 try:
229 child = os.fork()
230 if child == 0:
231 # We are the child
232 try:
233 try:
234 cli.close()
235 os.dup2(gui.fileno(), 1)
236 run.execute(gui_policy, gui_args + ['--', iface_uri])
237 except:
238 import traceback
239 traceback.print_exc()
240 finally:
241 os._exit(1)
242 gui.close()
243 gui = None
245 xml = ""
246 while True:
247 got = cli.recv(256)
248 if not got: break
249 xml += got
250 pid, status = os.waitpid(child, 0)
251 assert pid == child
252 if status == 1 << 8:
253 return None # Aborted
254 if status != 0:
255 raise Exception("Error from GUI: code = %d" % status)
256 finally:
257 if cli is not None: cli.close()
258 if gui is not None: gui.close()
260 from StringIO import StringIO
261 from zeroinstall.injector import qdom, selections
262 return selections.Selections(qdom.parse(StringIO(xml)))
264 def _get_selections(policy):
265 import selections
266 doc = selections.Selections(policy).toDOM()
267 doc.writexml(sys.stdout)
268 sys.stdout.write('\n')
270 class UsageError(Exception): pass
272 def main(command_args):
273 """Act as if 0launch was run with the given arguments.
274 @arg command_args: array of arguments (e.g. C{sys.argv[1:]})
275 @type command_args: [str]
277 # Ensure stdin, stdout and stderr FDs exist, to avoid confusion
278 for std in (0, 1, 2):
279 try:
280 os.fstat(std)
281 except OSError:
282 fd = os.open('/dev/null', os.O_RDONLY)
283 if fd != std:
284 os.dup2(fd, std)
285 os.close(fd)
287 parser = OptionParser(usage="usage: %prog [options] interface [args]\n"
288 " %prog --list [search-term]\n"
289 " %prog --import [signed-interface-files]\n"
290 " %prog --feed [interface]")
291 parser.add_option("", "--before", help="choose a version before this", metavar='VERSION')
292 parser.add_option("-c", "--console", help="never use GUI", action='store_false', dest='gui')
293 parser.add_option("-d", "--download-only", help="fetch but don't run", action='store_true')
294 parser.add_option("-D", "--dry-run", help="just print actions", action='store_true')
295 parser.add_option("-f", "--feed", help="add or remove a feed", action='store_true')
296 parser.add_option("", "--get-selections", help="write selected versions as XML", action='store_true')
297 parser.add_option("-g", "--gui", help="show graphical policy editor", action='store_true')
298 parser.add_option("-i", "--import", help="import from files, not from the network", action='store_true')
299 parser.add_option("-l", "--list", help="list all known interfaces", action='store_true')
300 parser.add_option("-m", "--main", help="name of the file to execute")
301 parser.add_option("", "--not-before", help="minimum version to choose", metavar='VERSION')
302 parser.add_option("-o", "--offline", help="try to avoid using the network", action='store_true')
303 parser.add_option("-r", "--refresh", help="refresh all used interfaces", action='store_true')
304 parser.add_option("", "--set-selections", help="run versions specified in XML file", metavar='FILE')
305 parser.add_option("-s", "--source", help="select source code", action='store_true')
306 parser.add_option("-v", "--verbose", help="more verbose output", action='count')
307 parser.add_option("-V", "--version", help="display version information", action='store_true')
308 parser.disable_interspersed_args()
310 (options, args) = parser.parse_args(command_args)
312 if options.verbose:
313 logger = logging.getLogger()
314 if options.verbose == 1:
315 logger.setLevel(logging.INFO)
316 else:
317 logger.setLevel(logging.DEBUG)
318 import zeroinstall
319 logging.info("Running 0launch %s %s; Python %s", zeroinstall.version, repr(args), sys.version)
321 try:
322 if options.list:
323 _list_interfaces(args)
324 elif options.version:
325 import zeroinstall
326 print "0launch (zero-install) " + zeroinstall.version
327 print "Copyright (C) 2006 Thomas Leonard"
328 print "This program comes with ABSOLUTELY NO WARRANTY,"
329 print "to the extent permitted by law."
330 print "You may redistribute copies of this program"
331 print "under the terms of the GNU General Public License."
332 print "For more information about these matters, see the file named COPYING."
333 elif options.set_selections:
334 from zeroinstall.injector import selections, qdom, run
335 sels = selections.Selections(qdom.parse(file(options.set_selections)))
336 run.execute_selections(sels, args, options.dry_run, options.main)
337 elif getattr(options, 'import'):
338 _import_interface(args)
339 elif options.feed:
340 _manage_feeds(options, args)
341 else:
342 _normal_mode(options, args)
343 except UsageError:
344 parser.print_help()
345 sys.exit(1)
346 except model.SafeException, ex:
347 if options.verbose: raise
348 print >>sys.stderr, ex
349 sys.exit(1)