Package zeroinstall :: Package injector :: Module run
[frames] | no frames]

Source Code for Module zeroinstall.injector.run

  1  """ 
  2  Executes a set of implementations as a program. 
  3  """ 
  4   
  5  # Copyright (C) 2009, Thomas Leonard 
  6  # See the README file for details, or visit http://0install.net. 
  7   
  8  from __future__ import print_function 
  9   
 10  from zeroinstall import _ 
 11  import os, sys 
 12  from logging import info, debug 
 13  from string import Template 
 14   
 15  from zeroinstall.injector.model import SafeException, EnvironmentBinding, ExecutableBinding, Command, Dependency 
 16  from zeroinstall.injector import namespaces, qdom 
 17  from zeroinstall.support import basedir 
 18   
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])
32
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) 56 57 info(_("Waiting for test process to finish...")) 58 59 pid, status = os.waitpid(child, 0) 60 assert pid == child 61 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() 68 69 return results
70
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))
76
77 -class Setup(object):
78 """@since: 1.2""" 79 stores = None 80 selections = None 81 _exec_bindings = None 82 _checked_runenv = False 83
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
89
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]""" 100 101 assert command_name 102 103 prog_args = [] 104 sels = self.selections.selections 105 106 while command_name: 107 command_sel = sels[command_iface] 108 109 if user_command is None: 110 command = command_sel.get_command(command_name) 111 else: 112 command = user_command 113 user_command = None 114 115 command_args = [] 116 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 126 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(self._get_implementation_path(command_sel), command_path) 137 138 assert prog_path is not None 139 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}) 145 146 command_args.append(prog_path) 147 148 # Add extra arguments for program 149 _process_args(command_args, command.qdom) 150 151 prog_args = command_args + prog_args 152 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>") 157 158 return prog_args
159
160 - def _get_implementation_path(self, impl):
161 if impl.id.startswith('package:'): return None 162 return impl.local_path or self.stores.lookup_any(impl.digests)
163
164 - def prepare_env(self):
165 """Do all the environment bindings in the selections (setting os.environ).""" 166 self._exec_bindings = [] 167 168 def _do_bindings(impl, bindings, iface): 169 for b in bindings: 170 self.do_binding(impl, b, iface)
171 172 def _do_deps(deps): 173 for dep in deps: 174 dep_impl = sels.get(dep.interface, None) 175 if dep_impl is None: 176 assert dep.importance != Dependency.Essential, dep 177 else: 178 _do_bindings(dep_impl, dep.bindings, dep.interface)
179 180 sels = self.selections.selections 181 for selection in sels.values(): 182 _do_bindings(selection, selection.bindings, selection.interface) 183 _do_deps(selection.dependencies) 184 185 # Process commands' dependencies' bindings too 186 for command in selection.get_commands().values(): 187 _do_bindings(selection, command.bindings, selection.interface) 188 _do_deps(command.requires) 189 190 # Do these after <environment>s, because they may do $-expansion 191 for binding, iface in self._exec_bindings: 192 self.do_exec_binding(binding, iface) 193 self._exec_bindings = None 194
195 - def do_binding(self, impl, binding, iface):
196 """Called by L{prepare_env} for each binding. 197 Sub-classes may wish to override this. 198 @param impl: the selected implementation 199 @type impl: L{selections.Selection} 200 @param binding: the binding to be processed 201 @type binding: L{model.Binding} 202 @param iface: the interface containing impl 203 @type iface: L{model.Interface} 204 """ 205 if isinstance(binding, EnvironmentBinding): 206 do_env_binding(binding, self._get_implementation_path(impl)) 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))
213
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) 221 222 if not self._checked_runenv: 223 self._check_runenv() 224 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) 229 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) 236 237 import json 238 args = self.build_command(iface, binding.command) 239 os.environ["0install-runenv-" + name] = json.dumps(args)
240
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 246 247 actual_contents = None 248 if os.path.exists(runenv): 249 with open(runenv) as s: 250 actual_contents = s.read() 251 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, 0555) 259 os.rename(tmp.name, runenv) 260 261 self._checked_runenv = True
262
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. 278 """ 279 #assert stores is not None 280 if stores is None: 281 from zeroinstall import zerostore 282 stores = zerostore.Stores() 283 284 setup = Setup(stores, selections) 285 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 305 306 setup.prepare_env() 307 prog_args = setup.build_command(selections.interface, selections.command, user_command) + prog_args 308 309 if wrapper: 310 prog_args = ['/bin/sh', '-c', wrapper + ' "$@"', '-'] + list(prog_args) 311 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 os.execv(prog_args[0], prog_args) 320 except OSError as ex: 321 raise SafeException(_("Failed to run '%(program_path)s': %(exception)s") % {'program_path': prog_args[0], 'exception': str(ex)})
322