1 """
2 Executes a set of implementations as a program.
3 """
4
5
6
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
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
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
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
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
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
78 """@since: 1.2"""
79 stores = None
80 selections = None
81 _exec_bindings = None
82 _checked_runenv = False
83
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
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
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
149 _process_args(command_args, command.qdom)
150
151 prog_args = command_args + prog_args
152
153
154
155 if command.path is None:
156 raise SafeException("Missing 'path' attribute on <command>")
157
158 return prog_args
159
163
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
186 for command in selection.get_commands().values():
187 _do_bindings(selection, command.bindings, selection.interface)
188 _do_deps(command.requires)
189
190
191 for binding, iface in self._exec_bindings:
192 self.do_exec_binding(binding, iface)
193 self._exec_bindings = None
194
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
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
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
242
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
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
289 if main.startswith('/'):
290 main = main[1:]
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)
295
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