Tell the user when a background update completes, not when it starts
[zeroinstall/solver.git] / zeroinstall / injector / run.py
blob23ce8855c1b1ab5ca07df4adcbc8bf5a3db392dd
1 """
2 Executes a set of implementations as a program.
3 """
5 # Copyright (C) 2009, Thomas Leonard
6 # See the README file for details, or visit http://0install.net.
8 from __future__ import print_function
10 from zeroinstall import _
11 import os, sys
12 from logging import info, debug
13 from string import Template
15 from zeroinstall.injector.model import SafeException, EnvironmentBinding, ExecutableBinding, Command, Dependency
16 from zeroinstall.injector import namespaces, qdom
17 from zeroinstall.support import basedir
19 def do_env_binding(binding, path):
20 """Update this process's environment by applying the binding.
21 @param binding: the binding to apply
22 @type binding: L{model.EnvironmentBinding}
23 @param path: the selected implementation
24 @type path: str"""
25 if binding.insert is not None and path is None:
26 # Skip insert bindings for package implementations
27 debug("not setting %s as we selected a package implementation", binding.name)
28 return
29 os.environ[binding.name] = binding.get_value(path,
30 os.environ.get(binding.name, None))
31 info("%s=%s", binding.name, os.environ[binding.name])
33 def test_selections(selections, prog_args, dry_run, main):
34 """Run the program in a child process, collecting stdout and stderr.
35 @return: the output produced by the process
36 @since: 0.27
37 """
38 import tempfile
39 output = tempfile.TemporaryFile(prefix = '0launch-test')
40 try:
41 child = os.fork()
42 if child == 0:
43 # We are the child
44 try:
45 try:
46 os.dup2(output.fileno(), 1)
47 os.dup2(output.fileno(), 2)
48 execute_selections(selections, prog_args, dry_run, main)
49 except:
50 import traceback
51 traceback.print_exc()
52 finally:
53 sys.stdout.flush()
54 sys.stderr.flush()
55 os._exit(1)
57 info(_("Waiting for test process to finish..."))
59 pid, status = os.waitpid(child, 0)
60 assert pid == child
62 output.seek(0)
63 results = output.read()
64 if status != 0:
65 results += _("Error from child process: exit code = %d") % status
66 finally:
67 output.close()
69 return results
71 def _process_args(args, element):
72 """Append each <arg> under <element> to args, performing $-expansion."""
73 for child in element.childNodes:
74 if child.uri == namespaces.XMLNS_IFACE and child.name == 'arg':
75 args.append(Template(child.content).substitute(os.environ))
77 class Setup(object):
78 """@since: 1.2"""
79 stores = None
80 selections = None
81 _exec_bindings = None
82 _checked_runenv = False
84 def __init__(self, stores, selections):
85 """@param stores: where to find cached implementations
86 @type stores: L{zerostore.Stores}"""
87 self.stores = stores
88 self.selections = selections
90 def build_command(self, command_iface, command_name, user_command = None):
91 """Create a list of strings to be passed to exec to run the <command>s in the selections.
92 @param command_iface: the interface of the program being run
93 @type command_iface: str
94 @param command_name: the name of the command being run
95 @type command_name: str
96 @param user_command: a custom command to use instead
97 @type user_command: L{model.Command}
98 @return: the argument list
99 @rtype: [str]"""
101 assert command_name or user_command
103 prog_args = []
104 sels = self.selections.selections
106 while command_name or user_command:
107 command_sel = sels[command_iface]
109 if user_command is None:
110 command = command_sel.get_command(command_name)
111 else:
112 command = user_command
113 user_command = None
115 command_args = []
117 # Add extra arguments for runner
118 runner = command.get_runner()
119 if runner:
120 command_iface = runner.interface
121 command_name = runner.command
122 _process_args(command_args, runner.qdom)
123 else:
124 command_iface = None
125 command_name = None
127 # Add main program path
128 command_path = command.path
129 if command_path is not None:
130 if command_sel.id.startswith('package:'):
131 prog_path = command_path
132 else:
133 if command_path.startswith('/'):
134 raise SafeException(_("Command path must be relative, but '%s' starts with '/'!") %
135 command_path)
136 prog_path = os.path.join(command_sel.get_path(self.stores), command_path)
138 assert prog_path is not None
140 if not os.path.exists(prog_path):
141 raise SafeException(_("File '%(program_path)s' does not exist.\n"
142 "(implementation '%(implementation_id)s' + program '%(main)s')") %
143 {'program_path': prog_path, 'implementation_id': command_sel.id,
144 'main': command_path})
146 command_args.append(prog_path)
148 # Add extra arguments for program
149 _process_args(command_args, command.qdom)
151 prog_args = command_args + prog_args
153 # Each command is run by the next, but the last one is run by exec, and we
154 # need a path for that.
155 if command.path is None:
156 raise SafeException("Missing 'path' attribute on <command>")
158 return prog_args
160 def prepare_env(self):
161 """Do all the environment bindings in the selections (setting os.environ)."""
162 self._exec_bindings = []
164 def _do_bindings(impl, bindings, iface):
165 for b in bindings:
166 self.do_binding(impl, b, iface)
168 def _do_deps(deps):
169 for dep in deps:
170 dep_impl = sels.get(dep.interface, None)
171 if dep_impl is None:
172 assert dep.importance != Dependency.Essential, dep
173 else:
174 _do_bindings(dep_impl, dep.bindings, dep.interface)
176 sels = self.selections.selections
177 for selection in sels.values():
178 _do_bindings(selection, selection.bindings, selection.interface)
179 _do_deps(selection.dependencies)
181 # Process commands' dependencies' bindings too
182 for command in selection.get_commands().values():
183 _do_bindings(selection, command.bindings, selection.interface)
184 _do_deps(command.requires)
186 # Do these after <environment>s, because they may do $-expansion
187 for binding, iface in self._exec_bindings:
188 self.do_exec_binding(binding, iface)
189 self._exec_bindings = None
191 def do_binding(self, impl, binding, iface):
192 """Called by L{prepare_env} for each binding.
193 Sub-classes may wish to override this.
194 @param impl: the selected implementation
195 @type impl: L{selections.Selection}
196 @param binding: the binding to be processed
197 @type binding: L{model.Binding}
198 @param iface: the interface containing impl
199 @type iface: L{model.Interface}
201 if isinstance(binding, EnvironmentBinding):
202 if impl.id.startswith('package:'):
203 path = None # (but still do the binding, e.g. for values)
204 else:
205 path = impl.get_path(self.stores)
206 do_env_binding(binding, path)
207 elif isinstance(binding, ExecutableBinding):
208 if isinstance(iface, Dependency):
209 import warnings
210 warnings.warn("Pass an interface URI instead", DeprecationWarning, 2)
211 iface = iface.interface
212 self._exec_bindings.append((binding, iface))
214 def do_exec_binding(self, binding, iface):
215 assert iface is not None
216 name = binding.name
217 if '/' in name or name.startswith('.') or "'" in name:
218 raise SafeException("Invalid <executable> name '%s'" % name)
219 exec_dir = basedir.save_cache_path(namespaces.config_site, namespaces.config_prog, 'executables', name)
220 exec_path = os.path.join(exec_dir, name)
222 if not self._checked_runenv:
223 self._check_runenv()
225 if not os.path.exists(exec_path):
226 # Symlink ~/.cache/0install.net/injector/executables/$name/$name to runenv.py
227 os.symlink('../../runenv.py', exec_path)
228 os.chmod(exec_dir, 0o500)
230 if binding.in_path:
231 path = os.environ["PATH"] = exec_dir + os.pathsep + os.environ["PATH"]
232 info("PATH=%s", path)
233 else:
234 os.environ[name] = exec_path
235 info("%s=%s", name, exec_path)
237 import json
238 args = self.build_command(iface, binding.command)
239 os.environ["0install-runenv-" + name] = json.dumps(args)
241 def _check_runenv(self):
242 # Create the runenv.py helper script under ~/.cache if missing or out-of-date
243 main_dir = basedir.save_cache_path(namespaces.config_site, namespaces.config_prog)
244 runenv = os.path.join(main_dir, 'runenv.py')
245 expected_contents = "#!%s\nfrom zeroinstall.injector import _runenv; _runenv.main()\n" % sys.executable
247 actual_contents = None
248 if os.path.exists(runenv):
249 with open(runenv) as s:
250 actual_contents = s.read()
252 if actual_contents != expected_contents:
253 import tempfile
254 tmp = tempfile.NamedTemporaryFile('w', dir = main_dir, delete = False)
255 info("Updating %s", runenv)
256 tmp.write(expected_contents)
257 tmp.close()
258 os.chmod(tmp.name, 0o555)
259 os.rename(tmp.name, runenv)
261 self._checked_runenv = True
263 def execute_selections(selections, prog_args, dry_run = False, main = None, wrapper = None, stores = None):
264 """Execute program. On success, doesn't return. On failure, raises an Exception.
265 Returns normally only for a successful dry run.
266 @param selections: the selected versions
267 @type selections: L{selections.Selections}
268 @param prog_args: arguments to pass to the program
269 @type prog_args: [str]
270 @param dry_run: if True, just print a message about what would have happened
271 @type dry_run: bool
272 @param main: the name of the binary to run, or None to use the default
273 @type main: str
274 @param wrapper: a command to use to actually run the binary, or None to run the binary directly
275 @type wrapper: str
276 @since: 0.27
277 @precondition: All implementations are in the cache.
279 #assert stores is not None
280 if stores is None:
281 from zeroinstall import zerostore
282 stores = zerostore.Stores()
284 setup = Setup(stores, selections)
286 commands = selections.commands
287 if main is not None:
288 # Replace first command with user's input
289 if main.startswith('/'):
290 main = main[1:] # User specified a path relative to the package root
291 else:
292 old_path = commands[0].path
293 assert old_path, "Can't use a relative replacement main when there is no original one!"
294 main = os.path.join(os.path.dirname(old_path), main) # User main is relative to command's name
295 # Copy all child nodes (e.g. <runner>) except for the arguments
296 user_command_element = qdom.Element(namespaces.XMLNS_IFACE, 'command', {'path': main})
297 if commands:
298 for child in commands[0].qdom.childNodes:
299 if child.uri == namespaces.XMLNS_IFACE and child.name == 'arg':
300 continue
301 user_command_element.childNodes.append(child)
302 user_command = Command(user_command_element, None)
303 else:
304 user_command = None
306 setup.prepare_env()
307 prog_args = setup.build_command(selections.interface, selections.command, user_command) + prog_args
309 if wrapper:
310 prog_args = ['/bin/sh', '-c', wrapper + ' "$@"', '-'] + list(prog_args)
312 if dry_run:
313 print(_("Would execute: %s") % ' '.join(prog_args))
314 else:
315 info(_("Executing: %s"), prog_args)
316 sys.stdout.flush()
317 sys.stderr.flush()
318 try:
319 env = os.environ.copy()
320 for x in ['0install-runenv-ZEROINSTALL_GPG', 'ZEROINSTALL_GPG']:
321 if x in env:
322 del env[x]
324 os.execve(prog_args[0], prog_args, env)
325 except OSError as ex:
326 raise SafeException(_("Failed to run '%(program_path)s': %(exception)s") % {'program_path': prog_args[0], 'exception': str(ex)})