initial commit
[bubblebox.git] / bubblebox.py
1 import sys, os, glob, random, string, subprocess
2 from pprint import pprint
3
4 HOME = os.environ["HOME"]
5 XDG_RUNTIME_DIR = os.environ["XDG_RUNTIME_DIR"]
6 BUBBLEBOX_DIR = XDG_RUNTIME_DIR + "/bubblebox"
7 os.makedirs(BUBBLEBOX_DIR, exist_ok=True)
8
9 def flat_map(f, xs):
10     """Concatenate the result of applying `f` to each element of `xs` to a list.
11     `None` is treated like the empty list."""
12     ys = []
13     for x in xs:
14         x_mapped = f(x)
15         if x_mapped is not None:
16             ys.extend(x_mapped)
17     return ys
18
19 def globexpand(base, names):
20     return flat_map(lambda x: glob.glob(base + "/" + x), names)
21
22 def randname():
23     # choose from all lowercase letter
24     letters = string.ascii_lowercase
25     return ''.join(random.choice(letters) for i in range(8))
26
27 class BoxFlags:
28     """Flags that configure the bubblebox"""
29     def __init__(self, bwrap_flags = None, dbus_proxy_flags = None):
30         self.bwrap_flags = bwrap_flags
31         self.dbus_proxy_flags = dbus_proxy_flags
32
33 def launch_dbus_proxy(flags):
34     """Launches the dbus proxy and returns the bwrap flags to be used to talk to it."""
35     # Prepare a pipe to coordinate shutdown of bwrap and the proxy
36     bwrap_end, other_end = os.pipe() # both FDs are "non-inheritable" now
37     # Invoke the debus-proxy
38     filename = BUBBLEBOX_DIR + "/bus-" + randname()
39     args = ["/usr/bin/xdg-dbus-proxy", "--fd="+str(other_end)]
40     args += [os.environ["DBUS_SESSION_BUS_ADDRESS"], filename, "--filter"] + flags
41     #pprint(args)
42     subprocess.Popen(
43         args,
44         pass_fds = [other_end], # default is to pass only the std FDs!
45     )
46     # Wait until the proxy is ready
47     os.read(bwrap_end, 1)
48     assert os.path.exists(filename)
49     # Make sure bwrap can access the other end of the pipe
50     os.set_inheritable(bwrap_end, True)
51     # Put this at the usual location for the bus insode the sandbox.
52     # TODO: What if DBUS_SESSION_BUS_ADDRESS says something else?
53     return ["--bind", filename, XDG_RUNTIME_DIR + "/bus", "--sync-fd", str(bwrap_end)]
54
55 # Constructors that should be used instead of directly mentioning the class above.
56 def bwrap_flags(*flags):
57     return BoxFlags(bwrap_flags=flags)
58 def dbus_proxy_flags(*flags):
59     return BoxFlags(dbus_proxy_flags=flags)
60 def collect_flags(*flags):
61     bwrap_flags = flat_map(lambda x: x.bwrap_flags, flags)
62     dbus_proxy_flags = flat_map(lambda x: x.dbus_proxy_flags, flags)
63     return BoxFlags(bwrap_flags, dbus_proxy_flags)
64
65 # Run the application in the bubblebox with the given flags.
66 def bubblebox(*flags):
67     flags = collect_flags(*flags)
68     bwrap = "/usr/bin/bwrap"
69     extraflags = []
70     if flags.dbus_proxy_flags:
71         extraflags += launch_dbus_proxy(flags.dbus_proxy_flags)
72     args = [bwrap] + flags.bwrap_flags + extraflags + ["--"] + sys.argv[1:]
73     #pprint(args)
74     os.execvp(args[0], args)
75
76 # Convenient methods to give access to the host file system
77 def ro_host_access(*names):
78     return bwrap_flags(*flat_map(lambda x: ["--ro-bind", x, x], names))
79 def rw_host_access(*names):
80     return bwrap_flags(*flat_map(lambda x: ["--bind", x, x], names))
81 def dev_host_access(*names):
82     return bwrap_flags(*flat_map(lambda x: ["--dev-bind", x, x], names))
83
84 # Give all instances of the same box a shared XDG_RUNTIME_DIR
85 def shared_runtime_dir(boxname):
86     dirname = BUBBLEBOX_DIR + "/" + boxname
87     os.makedirs(dirname, exist_ok=True)
88     return bwrap_flags("--bind", dirname, XDG_RUNTIME_DIR)
89
90 # Profile the profiles when importing bubblebox.
91 import profiles