Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | 529 |
Merged at revision: | 446 |
Proposed branch: | lp:click/devel |
Merge into: | lp:click |
Diff against target: |
706 lines (+263/-100) 14 files modified
click/chroot.py (+31/-6) click/commands/chroot.py (+19/-9) click/commands/framework.py (+29/-0) click/commands/install.py (+1/-1) click/install.py (+7/-0) click/tests/helpers.py (+8/-0) click/tests/integration/test_chroot.py (+31/-25) click/tests/test_database.py (+2/-12) click/tests/test_user.py (+28/-2) debian/changelog (+24/-0) doc/manpage.rst (+6/-0) lib/click/database.vala (+26/-45) lib/click/hooks.vala (+1/-0) lib/click/user.vala (+50/-0) |
To merge this branch: | bzr merge lp:click/devel |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson | Approve | ||
Review via email: mp+236345@code.launchpad.net |
Commit message
Click 0.4.33: Add scope APIs; fix GC; add "click chroot --name"; make "click destroy" more robust; stop apps when uninstalling them.
Description of the change
[ Pete Woods ]
* Add scope-facing APIs to chroot build (LP: #1370727).
[ Colin Watson ]
* Warn that "click install" without a registration may result in later
garbage-
* Rearrange garbage-collection to remove versions of packages that have no
user registrations and are not running, rather than using the artificial
@gcinuse registration which never really worked properly.
* Run garbage-collection immediately before running system hooks on system
startup (LP: #1342858).
* Add new -n/--name option to "click chroot", defaulting to "click"
(LP: #1364327).
[ Michael Vogt ]
* Make click destroy more robust by unmounting any mounted filesystem
inside the schroot first (LP: #1346723).
* Stop apps if necessary when uninstalling them (LP: #1232130).
* Add new "click framework {info,get-field}" subcommands.
Colin Watson (cjwatson) : | # |
Preview Diff
1 | === modified file 'click/chroot.py' |
2 | --- click/chroot.py 2014-08-06 22:38:57 +0000 |
3 | +++ click/chroot.py 2014-09-29 14:09:31 +0000 |
4 | @@ -77,6 +77,11 @@ |
5 | ], |
6 | "ubuntu-sdk-14.04": [ |
7 | "cmake", |
8 | + "google-mock:TARGET", |
9 | + "libboost1.54-dev:TARGET", |
10 | + "libjsoncpp-dev:TARGET", |
11 | + "libprocess-cpp-dev:TARGET", |
12 | + "libproperties-cpp-dev:TARGET", |
13 | "libqt5svg5-dev:TARGET", |
14 | "libqt5webkit5-dev:TARGET", |
15 | "libqt5xmlpatterns5-dev:TARGET", |
16 | @@ -99,7 +104,13 @@ |
17 | ], |
18 | "ubuntu-sdk-14.10": [ |
19 | "cmake", |
20 | + "google-mock:TARGET", |
21 | + "libboost1.55-dev:TARGET", |
22 | "libcontent-hub-dev:TARGET", |
23 | + "libjsoncpp-dev:TARGET", |
24 | + "libnet-cpp-dev:TARGET", |
25 | + "libprocess-cpp-dev:TARGET", |
26 | + "libproperties-cpp-dev:TARGET", |
27 | "libqt5keychain0:TARGET", |
28 | "libqt5sensors5-dev:TARGET", |
29 | "libqt5svg5-dev:TARGET", |
30 | @@ -546,12 +557,26 @@ |
31 | return self.clean() |
32 | |
33 | def destroy(self): |
34 | - if not self.exists(): |
35 | - raise ClickChrootDoesNotExistException( |
36 | - "Chroot %s does not exist" % self.full_name) |
37 | - os.remove(self.chroot_config) |
38 | - mount = "%s/%s" % (self.chroots_dir, self.full_name) |
39 | - shutil.rmtree(mount) |
40 | + # remove config |
41 | + if os.path.exists(self.chroot_config): |
42 | + os.remove(self.chroot_config) |
43 | + # find all schroot mount points, this is actually quite complicated |
44 | + mount_dir = os.path.abspath( |
45 | + os.path.join(self.chroots_dir, "..", "mount")) |
46 | + needle = os.path.join(mount_dir, self.full_name) |
47 | + all_mounts = [] |
48 | + with open("/proc/mounts") as f: |
49 | + for line in f.readlines(): |
50 | + mp = line.split()[1] |
51 | + if mp.startswith(needle): |
52 | + all_mounts.append(mp) |
53 | + # reverse order is important in case of submounts |
54 | + for mp in sorted(all_mounts, key=len, reverse=True): |
55 | + subprocess.call(["umount", mp]) |
56 | + # now remove the rest |
57 | + chroot_dir = "%s/%s" % (self.chroots_dir, self.full_name) |
58 | + if os.path.exists(chroot_dir): |
59 | + shutil.rmtree(chroot_dir) |
60 | return 0 |
61 | |
62 | def begin_session(self): |
63 | |
64 | === modified file 'click/commands/chroot.py' |
65 | --- click/commands/chroot.py 2014-07-01 07:12:49 +0000 |
66 | +++ click/commands/chroot.py 2014-09-29 14:09:31 +0000 |
67 | @@ -61,7 +61,8 @@ |
68 | "debootstrap not installed and configured; install click-dev and " |
69 | "debootstrap") |
70 | requires_root(parser) |
71 | - chroot = ClickChroot(args.architecture, args.framework, series=args.series) |
72 | + chroot = ClickChroot( |
73 | + args.architecture, args.framework, name=args.name, series=args.series) |
74 | with message_on_error( |
75 | ClickChrootAlreadyExistsException, ErrorMessages.EXISTS): |
76 | return chroot.create(args.keep_broken_chroot) |
77 | @@ -71,7 +72,7 @@ |
78 | |
79 | def install(parser, args): |
80 | packages = args.packages |
81 | - chroot = ClickChroot(args.architecture, args.framework) |
82 | + chroot = ClickChroot(args.architecture, args.framework, name=args.name) |
83 | with message_on_error( |
84 | ClickChrootDoesNotExistException, ErrorMessages.NOT_EXISTS): |
85 | return chroot.install(*packages) |
86 | @@ -82,7 +83,7 @@ |
87 | def destroy(parser, args): |
88 | requires_root(parser) |
89 | # ask for confirmation? |
90 | - chroot = ClickChroot(args.architecture, args.framework) |
91 | + chroot = ClickChroot(args.architecture, args.framework, name=args.name) |
92 | with message_on_error( |
93 | ClickChrootDoesNotExistException, ErrorMessages.NOT_EXISTS): |
94 | return chroot.destroy() |
95 | @@ -95,7 +96,8 @@ |
96 | if not program: |
97 | program = ["/bin/bash"] |
98 | chroot = ClickChroot( |
99 | - args.architecture, args.framework, session=args.session) |
100 | + args.architecture, args.framework, name=args.name, |
101 | + session=args.session) |
102 | with message_on_error( |
103 | ClickChrootDoesNotExistException, ErrorMessages.NOT_EXISTS): |
104 | return chroot.run(*program) |
105 | @@ -108,7 +110,8 @@ |
106 | if not program: |
107 | program = ["/bin/bash"] |
108 | chroot = ClickChroot( |
109 | - args.architecture, args.framework, session=args.session) |
110 | + args.architecture, args.framework, name=args.name, |
111 | + session=args.session) |
112 | with message_on_error( |
113 | ClickChrootDoesNotExistException, ErrorMessages.NOT_EXISTS): |
114 | return chroot.maint(*program) |
115 | @@ -117,7 +120,7 @@ |
116 | |
117 | |
118 | def upgrade(parser, args): |
119 | - chroot = ClickChroot(args.architecture, args.framework) |
120 | + chroot = ClickChroot(args.architecture, args.framework, name=args.name) |
121 | with message_on_error( |
122 | ClickChrootDoesNotExistException, ErrorMessages.NOT_EXISTS): |
123 | return chroot.upgrade() |
124 | @@ -127,7 +130,8 @@ |
125 | |
126 | def begin_session(parser, args): |
127 | chroot = ClickChroot( |
128 | - args.architecture, args.framework, session=args.session) |
129 | + args.architecture, args.framework, name=args.name, |
130 | + session=args.session) |
131 | with message_on_error( |
132 | ClickChrootDoesNotExistException, ErrorMessages.NOT_EXISTS): |
133 | return chroot.begin_session() |
134 | @@ -137,7 +141,8 @@ |
135 | |
136 | def end_session(parser, args): |
137 | chroot = ClickChroot( |
138 | - args.architecture, args.framework, session=args.session) |
139 | + args.architecture, args.framework, name=args.name, |
140 | + session=args.session) |
141 | with message_on_error( |
142 | ClickChrootDoesNotExistException, ErrorMessages.NOT_EXISTS): |
143 | return chroot.end_session() |
144 | @@ -146,7 +151,7 @@ |
145 | |
146 | |
147 | def exists(parser, args): |
148 | - chroot = ClickChroot(args.architecture, args.framework) |
149 | + chroot = ClickChroot(args.architecture, args.framework, name=args.name) |
150 | # return shell exit codes 0 on success, 1 on failure |
151 | if chroot.exists(): |
152 | return 0 |
153 | @@ -169,6 +174,11 @@ |
154 | "-s", "--series", |
155 | help="series to use for a newly-created chroot (defaults to a series " |
156 | "appropriate for the framework)") |
157 | + parser.add_argument( |
158 | + "-n", "--name", default="click", |
159 | + help=( |
160 | + "name of the chroot (default: click; the framework and " |
161 | + "architecture will be appended)")) |
162 | create_parser = subparsers.add_parser( |
163 | "create", |
164 | help="create a chroot of the provided architecture") |
165 | |
166 | === modified file 'click/commands/framework.py' |
167 | --- click/commands/framework.py 2014-05-20 07:02:53 +0000 |
168 | +++ click/commands/framework.py 2014-09-29 14:09:31 +0000 |
169 | @@ -28,6 +28,17 @@ |
170 | return 0 |
171 | |
172 | |
173 | +def info(parser, args): |
174 | + framework = Click.Framework.open(args.framework_name) |
175 | + for field in sorted(framework.get_fields()): |
176 | + print("%s: %s" % (field, framework.get_field(field))) |
177 | + |
178 | + |
179 | +def get_field(parser, args): |
180 | + framework = Click.Framework.open(args.framework_name) |
181 | + print(framework.get_field(args.field_name)) |
182 | + |
183 | + |
184 | def run(argv): |
185 | parser = ArgumentParser("click framework") |
186 | subparsers = parser.add_subparsers() |
187 | @@ -35,6 +46,24 @@ |
188 | "list", |
189 | help="list available frameworks") |
190 | list_parser.set_defaults(func=list) |
191 | + info_parser = subparsers.add_parser( |
192 | + "info", |
193 | + help="show info about a specific framework") |
194 | + info_parser.add_argument( |
195 | + "framework_name", |
196 | + help="framework name with the information") |
197 | + info_parser.set_defaults(func=info) |
198 | + get_field_parser = subparsers.add_parser( |
199 | + "get-field", |
200 | + help="get a field from a given framework") |
201 | + get_field_parser.add_argument( |
202 | + "framework_name", |
203 | + help="framework name with the information") |
204 | + get_field_parser.add_argument( |
205 | + "field_name", |
206 | + help="the field name (e.g. base-version)") |
207 | + get_field_parser.set_defaults(func=get_field) |
208 | + |
209 | args = parser.parse_args(argv) |
210 | if not hasattr(args, "func"): |
211 | parser.print_help() |
212 | |
213 | === modified file 'click/commands/install.py' |
214 | --- click/commands/install.py 2014-08-07 21:25:48 +0000 |
215 | +++ click/commands/install.py 2014-09-29 14:09:31 +0000 |
216 | @@ -45,7 +45,7 @@ |
217 | help="register package for all users") |
218 | parser.add_option( |
219 | "--allow-unauthenticated", default=False, action="store_true", |
220 | - help="allow installing packages with no sigantures") |
221 | + help="allow installing packages with no signatures") |
222 | options, args = parser.parse_args(argv) |
223 | if len(args) < 1: |
224 | parser.error("need package file name") |
225 | |
226 | === modified file 'click/install.py' |
227 | --- click/install.py 2014-09-04 14:15:08 +0000 |
228 | +++ click/install.py 2014-09-29 14:09:31 +0000 |
229 | @@ -38,6 +38,7 @@ |
230 | import subprocess |
231 | import sys |
232 | import tempfile |
233 | +from textwrap import dedent |
234 | |
235 | from contextlib import closing |
236 | |
237 | @@ -450,6 +451,12 @@ |
238 | else: |
239 | registry = Click.User.for_user(self.db, name=user) |
240 | registry.set_version(package_name, package_version) |
241 | + else: |
242 | + print(dedent("""\ |
243 | + %s %s has not been registered for any users. |
244 | + It may be garbage-collected the next time the system starts. |
245 | + To avoid this, use "click register". |
246 | + """) % (package_name, package_version)) |
247 | |
248 | if old_version is not None: |
249 | self.db.maybe_remove(package_name, old_version) |
250 | |
251 | === modified file 'click/tests/helpers.py' |
252 | --- click/tests/helpers.py 2014-06-23 21:14:06 +0000 |
253 | +++ click/tests/helpers.py 2014-09-29 14:09:31 +0000 |
254 | @@ -326,3 +326,11 @@ |
255 | def touch(path): |
256 | with mkfile(path, mode="a"): |
257 | pass |
258 | + |
259 | + |
260 | +def make_file_with_content(filename, content, mode=0o644): |
261 | + """Create a file with the given content and mode""" |
262 | + Click.ensuredir(os.path.dirname(filename)) |
263 | + with open(filename, "w") as f: |
264 | + f.write(content) |
265 | + os.chmod(filename, mode) |
266 | |
267 | === modified file 'click/tests/integration/test_chroot.py' |
268 | --- click/tests/integration/test_chroot.py 2014-09-05 09:04:33 +0000 |
269 | +++ click/tests/integration/test_chroot.py 2014-09-29 14:09:31 +0000 |
270 | @@ -27,52 +27,58 @@ |
271 | class TestChroot(ClickTestCase): |
272 | |
273 | @classmethod |
274 | + def command(cls, arch, *args): |
275 | + return [cls.click_binary, "chroot", "-a", arch] + list(args) |
276 | + |
277 | + @classmethod |
278 | def setUpClass(cls): |
279 | super(TestChroot, cls).setUpClass() |
280 | require_root() |
281 | require_network() |
282 | cls.arch = subprocess.check_output( |
283 | ["dpkg", "--print-architecture"], universal_newlines=True).strip() |
284 | - subprocess.check_call([ |
285 | - cls.click_binary, |
286 | - "chroot", "-a", cls.arch, |
287 | - "create"]) |
288 | + subprocess.check_call(cls.command(cls.arch, "create")) |
289 | |
290 | @classmethod |
291 | def tearDownClass(cls): |
292 | - subprocess.check_call([ |
293 | - cls.click_binary, |
294 | - "chroot", "-a", cls.arch, |
295 | - "destroy"]) |
296 | + subprocess.check_call(cls.command(cls.arch, "destroy")) |
297 | |
298 | def test_upgrade(self): |
299 | - subprocess.check_call([ |
300 | - self.click_binary, "chroot", "-a", self.arch, |
301 | - "upgrade"]) |
302 | + subprocess.check_call(self.command(self.arch, "upgrade")) |
303 | |
304 | def test_install(self): |
305 | - subprocess.check_call([ |
306 | - self.click_binary, "chroot", "-a", self.arch, |
307 | - "install", "apt-utils"]) |
308 | + subprocess.check_call(self.command(self.arch, "install", "apt-utils")) |
309 | |
310 | def test_run(self): |
311 | - output = subprocess.check_output([ |
312 | - self.click_binary, "chroot", "-a", self.arch, |
313 | - "run", "echo", "hello world"], universal_newlines=True) |
314 | + output = subprocess.check_output( |
315 | + self.command(self.arch, "run", "echo", "hello world"), |
316 | + universal_newlines=True) |
317 | self.assertEqual(output, "hello world\n") |
318 | |
319 | def test_maint(self): |
320 | - output = subprocess.check_output([ |
321 | - self.click_binary, "chroot", "-a", self.arch, |
322 | - "maint", "id"], universal_newlines=True) |
323 | + output = subprocess.check_output( |
324 | + self.command(self.arch, "maint", "id"), |
325 | + universal_newlines=True) |
326 | self.assertEqual(output, "uid=0(root) gid=0(root) groups=0(root)\n") |
327 | |
328 | def test_exists_ok(self): |
329 | - subprocess.check_call([ |
330 | - self.click_binary, "chroot", "-a", self.arch, "exists"]) |
331 | + subprocess.check_call(self.command(self.arch, "exists")) |
332 | |
333 | def test_exists_no(self): |
334 | with self.assertRaises(subprocess.CalledProcessError): |
335 | - subprocess.check_call([ |
336 | - self.click_binary, |
337 | - "chroot", "-a", "arch-that-does-not-exist"]) |
338 | + subprocess.check_call(self.command("arch-that-does-not-exist")) |
339 | + |
340 | + |
341 | +class TestChrootName(TestChroot): |
342 | + """Run the chroot tests again with a different --name.""" |
343 | + |
344 | + @classmethod |
345 | + def command(cls, arch, *args): |
346 | + return super(TestChrootName, cls).command( |
347 | + arch, "-n", "testname", *args) |
348 | + |
349 | + def test_exists_different_name_fails(self): |
350 | + # "click chroot exists" fails for a non-existent name. |
351 | + with self.assertRaises(subprocess.CalledProcessError): |
352 | + subprocess.check_call(super(TestChrootName, self).command( |
353 | + self.arch, "-n", "testname2", "exists")) |
354 | |
355 | === modified file 'click/tests/test_database.py' |
356 | --- click/tests/test_database.py 2014-07-11 17:20:51 +0000 |
357 | +++ click/tests/test_database.py 2014-09-29 14:09:31 +0000 |
358 | @@ -331,14 +331,6 @@ |
359 | self.g_spawn_sync_side_effect, {b"ubuntu-app-pid": 0}) |
360 | preloads["click_find_on_path"].return_value = True |
361 | self.db.maybe_remove("a", "1.0") |
362 | - gcinuse_path = os.path.join( |
363 | - self.temp_dir, ".click", "users", "@gcinuse", "a") |
364 | - self.assertTrue(os.path.islink(gcinuse_path)) |
365 | - self.assertEqual(version_path, os.readlink(gcinuse_path)) |
366 | - self.assertTrue(os.path.exists(version_path)) |
367 | - self.db.maybe_remove("a", "1.0") |
368 | - self.assertTrue(os.path.islink(gcinuse_path)) |
369 | - self.assertEqual(version_path, os.readlink(gcinuse_path)) |
370 | self.assertTrue(os.path.exists(version_path)) |
371 | |
372 | def test_maybe_remove_not_running(self): |
373 | @@ -358,9 +350,6 @@ |
374 | self.g_spawn_sync_side_effect, {b"ubuntu-app-pid": 1 << 8}) |
375 | preloads["click_find_on_path"].return_value = True |
376 | self.db.maybe_remove("a", "1.0") |
377 | - gcinuse_path = os.path.join( |
378 | - self.temp_dir, ".click", "users", "@gcinuse", "a") |
379 | - self.assertFalse(os.path.islink(gcinuse_path)) |
380 | self.assertFalse(os.path.exists(os.path.join(self.temp_dir, "a"))) |
381 | |
382 | def test_gc(self): |
383 | @@ -399,8 +388,9 @@ |
384 | preloads["click_find_on_path"].return_value = True |
385 | self.db.gc() |
386 | self.assertTrue(os.path.exists(a_path)) |
387 | + self.assertFalse(os.path.exists(b_gcinuse_path)) |
388 | self.assertFalse(os.path.exists(b_path)) |
389 | - self.assertTrue(os.path.exists(c_path)) |
390 | + self.assertFalse(os.path.exists(c_path)) |
391 | |
392 | def test_gc_ignores_non_directory(self): |
393 | with self.run_in_subprocess( |
394 | |
395 | === modified file 'click/tests/test_user.py' |
396 | --- click/tests/test_user.py 2014-07-09 12:56:21 +0000 |
397 | +++ click/tests/test_user.py 2014-09-29 14:09:31 +0000 |
398 | @@ -26,12 +26,18 @@ |
399 | import json |
400 | import os |
401 | import shutil |
402 | +from textwrap import dedent |
403 | |
404 | from gi.repository import Click, GLib |
405 | |
406 | from click.json_helpers import json_array_to_python, json_object_to_python |
407 | from click.tests.gimock_types import Passwd |
408 | -from click.tests.helpers import TestCase, mkfile, touch |
409 | +from click.tests.helpers import ( |
410 | + TestCase, |
411 | + mkfile, |
412 | + make_file_with_content, |
413 | + touch, |
414 | +) |
415 | |
416 | |
417 | class TestClickUser(TestCase): |
418 | @@ -53,7 +59,8 @@ |
419 | a_1_0 = os.path.join(self.temp_dir, "custom", "a", "1.0") |
420 | os.makedirs(a_1_0) |
421 | with mkfile(os.path.join(a_1_0, ".click", "info", "a.manifest")) as m: |
422 | - json.dump({"name": "a", "version": "1.0"}, m) |
423 | + json.dump({"name": "a", "version": "1.0", |
424 | + "hooks": {"a-app": {}}}, m) |
425 | b_2_0 = os.path.join(self.temp_dir, "custom", "b", "2.0") |
426 | os.makedirs(b_2_0) |
427 | with mkfile(os.path.join(b_2_0, ".click", "info", "b.manifest")) as m: |
428 | @@ -549,3 +556,22 @@ |
429 | self.assertEqual("2.0", registry.get_version("b")) |
430 | self.assertEqual(b_overlay, registry.get_path("b")) |
431 | self.assertTrue(registry.is_removable("b")) |
432 | + |
433 | + def test_app_stops_on_remove(self): |
434 | + fake_app_stop = os.path.join(self.temp_dir, "bin", "ubuntu-app-stop") |
435 | + fake_app_stop_output = os.path.join(self.temp_dir, "fake-app-stop.out") |
436 | + fake_app_stop_content = dedent("""\ |
437 | + #!/bin/sh |
438 | + echo "$@" >> %s |
439 | + """ % fake_app_stop_output) |
440 | + make_file_with_content(fake_app_stop, fake_app_stop_content, 0o755) |
441 | + # its ok to modify env here, click.helpers.TestCase will take care |
442 | + # of it |
443 | + os.environ["PATH"] = "%s:%s" % ( |
444 | + os.path.dirname(fake_app_stop), os.environ["PATH"]) |
445 | + # get a app with manifest etc all |
446 | + _, registry = self._setUpMultiDB() |
447 | + registry.remove("a") |
448 | + # ensure that stop was called with the right app |
449 | + with open(fake_app_stop_output) as f: |
450 | + self.assertEqual("a_a-app_1.1", f.read().strip()) |
451 | |
452 | === modified file 'debian/changelog' |
453 | --- debian/changelog 2014-09-09 10:02:01 +0000 |
454 | +++ debian/changelog 2014-09-29 14:09:31 +0000 |
455 | @@ -1,3 +1,27 @@ |
456 | +click (0.4.33) UNRELEASED; urgency=medium |
457 | + |
458 | + [ Pete Woods ] |
459 | + * Add scope-facing APIs to chroot build (LP: #1370727). |
460 | + |
461 | + [ Colin Watson ] |
462 | + * Warn that "click install" without a registration may result in later |
463 | + garbage-collection. |
464 | + * Rearrange garbage-collection to remove versions of packages that have no |
465 | + user registrations and are not running, rather than using the artificial |
466 | + @gcinuse registration which never really worked properly. |
467 | + * Run garbage-collection immediately before running system hooks on system |
468 | + startup (LP: #1342858). |
469 | + * Add new -n/--name option to "click chroot", defaulting to "click" |
470 | + (LP: #1364327). |
471 | + |
472 | + [ Michael Vogt ] |
473 | + * Make click destroy more robust by unmounting any mounted filesystem |
474 | + inside the schroot first (LP: #1346723). |
475 | + * Stop apps if necessary when uninstalling them (LP: #1232130). |
476 | + * Add new "click framework {info,get-field}" subcommands. |
477 | + |
478 | + -- Colin Watson <cjwatson@ubuntu.com> Wed, 10 Sep 2014 12:47:46 +0100 |
479 | + |
480 | click (0.4.32.1) utopic; urgency=low |
481 | |
482 | [ Michael Vogt ] |
483 | |
484 | === modified file 'doc/manpage.rst' |
485 | --- doc/manpage.rst 2014-09-05 16:18:38 +0000 |
486 | +++ doc/manpage.rst 2014-09-29 14:09:31 +0000 |
487 | @@ -245,6 +245,12 @@ |
488 | development versions of SDKs which have not yet put a framework declaration |
489 | in place. |
490 | |
491 | +You should always register installed packages either for a specific user or |
492 | +for all users; if you do not do this then the packages may be |
493 | +garbage-collected later. You can do this using the ``--user`` or |
494 | +``--all-users`` options to this command, or using the ``click register`` |
495 | +command. |
496 | + |
497 | Options: |
498 | |
499 | --root=PATH Install packages underneath PATH. |
500 | |
501 | === modified file 'lib/click/database.vala' |
502 | --- lib/click/database.vala 2014-06-23 16:00:56 +0000 |
503 | +++ lib/click/database.vala 2014-09-29 14:09:31 +0000 |
504 | @@ -330,12 +330,8 @@ |
505 | private void |
506 | remove_unless_running (string package, string version) throws Error |
507 | { |
508 | - if (any_app_running (package, version)) { |
509 | - var gc_in_use_user_db = |
510 | - new User.for_gc_in_use (master_db); |
511 | - gc_in_use_user_db.set_version (package, version); |
512 | + if (any_app_running (package, version)) |
513 | return; |
514 | - } |
515 | |
516 | var version_path = get_path (package, version); |
517 | if (show_messages ()) |
518 | @@ -386,14 +382,10 @@ |
519 | * Remove a package version if it is not in use. |
520 | * |
521 | * "In use" may mean registered for another user, or running. In |
522 | - * the latter case we construct a fake registration so that we can |
523 | - * tell the difference later between a package version that was in |
524 | - * use at the time of removal and one that was never registered for |
525 | - * any user. |
526 | - * |
527 | - * (This is unfortunately complex, and perhaps some day we can |
528 | - * require that installations always have some kind of registration |
529 | - * to avoid this complexity.) |
530 | + * either case, we do nothing. We will already have removed at |
531 | + * least one registration by this point, so if no registrations are |
532 | + * left but it is running, then gc will be able to come along later |
533 | + * and clean things out. |
534 | */ |
535 | public void |
536 | maybe_remove (string package, string version) throws Error |
537 | @@ -408,11 +400,8 @@ |
538 | continue; |
539 | } |
540 | if (reg_version == version) { |
541 | - if (user_db.is_gc_in_use) |
542 | - user_db.remove (package); |
543 | - else |
544 | - /* In use. */ |
545 | - return; |
546 | + /* In use. */ |
547 | + return; |
548 | } |
549 | } |
550 | |
551 | @@ -422,33 +411,33 @@ |
552 | /** |
553 | * gc: |
554 | * |
555 | - * Remove package versions with no user registrations. |
556 | - * |
557 | - * To avoid accidentally removing packages that were installed |
558 | - * without ever having a user registration, we only garbage-collect |
559 | - * packages that were not removed by maybe_remove() due to having a |
560 | - * running application at the time. |
561 | - * |
562 | - * (This is unfortunately complex, and perhaps some day we can |
563 | - * require that installations always have some kind of registration |
564 | - * to avoid this complexity.) |
565 | + * Remove package versions that have no user registrations and that |
566 | + * are not running. |
567 | + * |
568 | + * This is rather like maybe_remove, but is suitable for bulk use, |
569 | + * since it only needs to scan the database once rather than once |
570 | + * per package. |
571 | + * |
572 | + * For historical reasons, we don't count @gcinuse as a real user |
573 | + * registration, and remove any such registrations we find. We can |
574 | + * drop this once we no longer care about upgrading versions from |
575 | + * before this change to something more current in a single step. |
576 | */ |
577 | public void |
578 | gc () throws Error |
579 | { |
580 | var users_db = new Users (master_db); |
581 | var user_reg = new Gee.HashMultiMap<string, string> (); |
582 | - var gc_in_use = new Gee.HashMultiMap<string, string> (); |
583 | foreach (var user_name in users_db.get_user_names ()) { |
584 | var user_db = users_db.get_user (user_name); |
585 | foreach (var package in user_db.get_package_names ()) { |
586 | var version = user_db.get_version (package); |
587 | + if (version == "current") |
588 | + continue; |
589 | /* Odd multimap syntax; this should really |
590 | * be more like foo[package] += version. |
591 | */ |
592 | - if (user_db.is_gc_in_use) |
593 | - gc_in_use[package] = version; |
594 | - else |
595 | + if (! user_db.is_gc_in_use) |
596 | user_reg[package] = version; |
597 | } |
598 | } |
599 | @@ -462,22 +451,14 @@ |
600 | continue; |
601 | foreach (var version in Click.Dir.open |
602 | (package_path)) { |
603 | + if (version == "current") |
604 | + continue; |
605 | if (version in user_reg[package]) |
606 | /* In use. */ |
607 | continue; |
608 | - if (! (version in gc_in_use[package])) { |
609 | - if (show_messages ()) { |
610 | - var version_path = |
611 | - Path.build_filename |
612 | - (package_path, |
613 | - version); |
614 | - message ("Not removing %s " + |
615 | - "(never registered).", |
616 | - version_path); |
617 | - } |
618 | - continue; |
619 | - } |
620 | - gc_in_use_user_db.remove (package); |
621 | + if (gc_in_use_user_db.has_package_name |
622 | + (package)) |
623 | + gc_in_use_user_db.remove (package); |
624 | remove_unless_running (package, version); |
625 | } |
626 | } |
627 | |
628 | === modified file 'lib/click/hooks.vala' |
629 | --- lib/click/hooks.vala 2014-05-19 13:19:16 +0000 |
630 | +++ lib/click/hooks.vala 2014-09-29 14:09:31 +0000 |
631 | @@ -1170,6 +1170,7 @@ |
632 | public void |
633 | run_system_hooks (DB db) throws Error |
634 | { |
635 | + db.gc (); |
636 | db.ensure_ownership (); |
637 | string[] failed = {}; |
638 | foreach (var hook in Hook.open_all (db)) { |
639 | |
640 | === modified file 'lib/click/user.vala' |
641 | --- lib/click/user.vala 2014-07-11 17:16:35 +0000 |
642 | +++ lib/click/user.vala 2014-09-29 14:09:31 +0000 |
643 | @@ -598,6 +598,48 @@ |
644 | old_version, version, name); |
645 | } |
646 | |
647 | + private bool |
648 | + stop_running_app (string package, string version) |
649 | + { |
650 | + var res = true; |
651 | + if (! find_on_path ("ubuntu-app-stop")) |
652 | + return false; |
653 | + |
654 | + Json.Object manifest; |
655 | + try { |
656 | + manifest = get_manifest (package); |
657 | + } catch (Error e) { |
658 | + warning ("Can not get manifest for %s", package); |
659 | + return false; |
660 | + } |
661 | + |
662 | + if (! manifest.has_member ("hooks")) { |
663 | + warning ("No hooks in manifest %s", package); |
664 | + return false; |
665 | + } |
666 | + var hooks = manifest.get_object_member ("hooks"); |
667 | + foreach (unowned string app_name in hooks.get_members ()) |
668 | + { |
669 | + // FIXME: move this into a "stop_single_app" helper |
670 | + string[] command = { |
671 | + "ubuntu-app-stop", |
672 | + @"$(package)_$(app_name)_$(version)" |
673 | + }; |
674 | + try { |
675 | + int exit_status; |
676 | + Process.spawn_sync |
677 | + (null, command, null, |
678 | + SpawnFlags.SEARCH_PATH | |
679 | + SpawnFlags.STDOUT_TO_DEV_NULL, |
680 | + null, null, null, out exit_status); |
681 | + res &= Process.check_exit_status (exit_status); |
682 | + } catch (Error e) { |
683 | + res &= false; |
684 | + } |
685 | + } |
686 | + return res; |
687 | + } |
688 | + |
689 | /** |
690 | * remove: |
691 | * @package: A package name. |
692 | @@ -636,6 +678,14 @@ |
693 | regain_privileges (); |
694 | } |
695 | } |
696 | + |
697 | + drop_privileges (); |
698 | + try { |
699 | + stop_running_app (package, old_version); |
700 | + } finally { |
701 | + regain_privileges (); |
702 | + } |
703 | + |
704 | if (! is_pseudo_user) |
705 | package_remove_hooks (db, package, old_version, name); |
706 | } |