~usb-creator-hackers/usb-creator/trunk

« back to all changes in this revision

Viewing changes to usbcreator/backend.py

  • Committer: Evan Dandrea
  • Date: 2009-05-31 17:11:33 UTC
  • mfrom: (97.2.7 future)
  • Revision ID: evan.dandrea@canonical.com-20090531171133-nx3su4avxx17v1gt
Merge with the ~evand/usb-creator/future branch.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2008 Canonical Ltd.
 
1
# Copyright (C) 2008-2009 Canonical Ltd.
2
2
 
3
3
# This program is free software: you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License version 3,
13
13
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
14
14
 
15
15
import os
 
16
import signal
16
17
import subprocess, sys
17
18
import stat
18
19
import shutil
23
24
import tempfile
24
25
 
25
26
from remtimest import RemainingTimeEstimator
26
 
 
27
 
class Logger:
28
 
    def __init__ (self):
29
 
        if 'SUDO_USER' in os.environ:
30
 
            log_file = '%s/.usb-creator.log' % \
31
 
                os.path.expanduser('~' + os.environ['SUDO_USER'])
32
 
        else:
33
 
            log_file = '%s/.usb-creator.log' % \
34
 
                os.path.expanduser('~')
35
 
        self.fp = open(log_file, 'a')
36
 
        self.save = sys.stderr
37
 
        sys.stderr = self
38
 
        self.new_line = 1
39
 
        l = '\n-- Starting up at %s --\n' % time.strftime('%H:%M:%S')
40
 
        self.fp.write(l)
41
 
        self.fp.flush()
42
 
        sys.stdout.write(l)
43
 
        sys.stdout.flush()
44
 
 
45
 
    def __del__(self):
46
 
        self.fp.close()
47
 
        self.stderr = self.save
48
 
 
49
 
    def fileno(self):
50
 
        return self.fp.fileno()
51
 
 
52
 
    def write(self, line):
53
 
        if (self.new_line):
54
 
            l = '[%s] ' % time.strftime('%H:%M:%S')
55
 
            self.new_line = 0
56
 
            l = l + line
57
 
        else:
58
 
            l = line
59
 
 
60
 
        self.fp.write(l)
61
 
        self.fp.flush()
62
 
        sys.stdout.write(l)
63
 
        sys.stdout.flush()
64
 
        if (line[-1] == '\n'):
65
 
            self.new_line = 1
 
27
import logging
 
28
import logging.handlers
 
29
 
 
30
UPDATE_FREE_INTERVAL = 2000 # in milliseconds.
 
31
 
 
32
class USBCreatorProcessException(Exception):
 
33
    pass
66
34
 
67
35
def popen(cmd):
68
 
    print >>sys.stderr, str(cmd)
 
36
    logging.debug(str(cmd))
69
37
    process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
70
38
        stderr=sys.stderr, stdin=subprocess.PIPE)
71
 
    res = process.communicate()
72
 
    return process, res
 
39
    out, err = process.communicate()
 
40
    if process.returncode is None:
 
41
        process.wait()
 
42
    elif process.returncode != 0:
 
43
        raise USBCreatorProcessException(err)
 
44
    return out
73
45
 
74
46
def free_space(dev):
75
47
    try:
80
52
        return 0
81
53
    return stat.f_bsize * stat.f_bavail
82
54
 
 
55
def setup_logging():
 
56
    l = os.environ.get('USBCREATOR_LOG_LEVEL', None)
 
57
    if l:
 
58
        l = l.lower()
 
59
    if l == 'debug':
 
60
        level = logging.DEBUG
 
61
    elif l == 'critical':
 
62
        level = logging.CRITICAL
 
63
    else:
 
64
        level = logging.INFO
 
65
 
 
66
    logging.basicConfig(level=level,
 
67
                        format='usb-creator %(asctime)s (%(levelname)s) %(filename)s:%(lineno)d: %(message)s',
 
68
                        datefmt='%H:%M:%S')
 
69
    handler = logging.handlers.SysLogHandler('/dev/log')
 
70
    logging.getLogger().addHandler(handler)
 
71
 
83
72
class Backend:
 
73
    IMG, ISO, CD = range(3)
 
74
    CAN_USE, CANNOT_USE, NEED_SPACE = range(3)
84
75
    def __init__(self, frontend):
85
76
        self.devices = {}
 
77
        self.partitions = {}
86
78
        self.cds = {}
87
79
        self.timeouts = {}
88
 
        self.copy_timeout = 0
89
 
        self.original_size = 0
 
80
        #self.copy_timeout = 0
 
81
        #self.original_size = 0
 
82
        self.mountpoints = []
90
83
        self.progress_description = ''
91
84
        self.frontend = frontend
92
 
        self.logger = Logger()
 
85
        setup_logging()
93
86
        DBusGMainLoop(set_as_default=True)
94
 
        self.bus = dbus.Bus(dbus.Bus.TYPE_SYSTEM)
 
87
        self.bus = dbus.SystemBus()
95
88
        hal_obj = self.bus.get_object('org.freedesktop.Hal',
96
89
            '/org/freedesktop/Hal/Manager')
97
90
        self.hal = dbus.Interface(hal_obj, 'org.freedesktop.Hal.Manager')
98
91
        self.pipe = None
99
92
        self.estimator = RemainingTimeEstimator()
100
 
 
101
 
    def log(self, line):
102
 
        # It might make more sense to just reassign stdout since we're logging
103
 
        # everything anyway.
104
 
        line = str(line) + '\n'
105
 
        self.logger.write(line)
106
 
 
107
 
    def set_install_source(self, source):
108
 
        # TODO: Make more consistent with install_target
109
 
        self.install_source = source
110
 
 
111
 
    def set_install_target(self, target):
112
 
        self.install_target = self.devices[target]
113
 
 
114
 
    def set_persistence_size(self, persist):
115
 
        self.persistence_size = persist
116
 
 
117
 
    def format_device(self, device):
118
 
        udi = self.hal.FindDeviceStringMatch('block.device', device)
119
 
        udi = udi[0]
120
 
        children = self.hal.FindDeviceStringMatch('info.parent', udi)
121
 
        for child in children:
122
 
            dev_obj = self.bus.get_object('org.freedesktop.Hal', child)
123
 
            child = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device')
124
 
            dev = str(child.GetProperty('block.device'))
125
 
            popen(['umount', dev])
126
 
        popen(['umount', device])
127
 
 
128
 
        # TODO: This could really use a progress dialog.
129
 
        res = popen(['parted', '-s', device, 'mklabel', 'msdos'])
130
 
        if res[0].returncode:
131
 
            message = _('Unable to create a partition table:') + '\n' + str(res[1][0])
132
 
            self.log(message)
133
 
            self.frontend.notify(message)
134
 
            return
135
 
        res = popen(['parted', '-s', device, 'mkpartfs', 'primary', 'fat32', '0', '--', '-0'])
136
 
        if res[0].returncode:
137
 
            message = _('Unable to format device:') + '\n' + str(res[1][0])
138
 
            self.log(message)
139
 
            self.frontend.notify(message)
140
 
        else:
141
 
            self.devices.pop(device)
142
 
            self.frontend.device_removed(device, source=False)
143
 
    
 
93
        
 
94
        self.bus.add_signal_receiver(self.device_added,
 
95
            'DeviceAdded',
 
96
            'org.freedesktop.Hal.Manager',
 
97
            'org.freedesktop.Hal',
 
98
            '/org/freedesktop/Hal/Manager')
 
99
        self.bus.add_signal_receiver(self.device_removed,
 
100
            'DeviceRemoved',
 
101
            'org.freedesktop.Hal.Manager',
 
102
            'org.freedesktop.Hal',
 
103
            '/org/freedesktop/Hal/Manager')
 
104
        self.detect_devices()
 
105
 
 
106
    # Device detection functions
 
107
 
144
108
    def device_added(self, udi):
145
 
        self.log('possibly adding: ' + str(udi))
146
 
        dev_obj = self.bus.get_object("org.freedesktop.Hal", udi)
147
 
        dev = dbus.Interface(dev_obj, "org.freedesktop.Hal.Device")
148
 
        if dev.PropertyExists('volume.is_disc') and dev.GetProperty('volume.is_disc'):
149
 
            if not dev.GetProperty('volume.disc.is_blank'):
150
 
                self.log('got a disc: %s' % dev.GetProperty('volume.label'))
151
 
                self.bus.add_signal_receiver(self.property_modified,
152
 
                'PropertyModified', 'org.freedesktop.Hal.Device',
153
 
                'org.freedesktop.Hal', udi, path_keyword='udi')
154
 
        # Look for the volume first as it may not have appeared yet when you
155
 
        # check the parent.
156
 
        success = False
157
 
        if dev.PropertyExists('block.is_volume') and dev.GetProperty('block.is_volume'):
158
 
            if (dev.PropertyExists('storage.bus') and
159
 
            dev.GetProperty('storage.bus') == 'usb') and \
160
 
            dev.GetProperty('storage.removable'):
161
 
                if dev.GetProperty('volume.fstype') == 'vfat':
162
 
                    success = True
163
 
                else:
164
 
                    self.log('didnt add because not vfat')
165
 
            else:
166
 
                p = dev.GetProperty('info.parent')
167
 
                dev_obj = self.bus.get_object('org.freedesktop.Hal', p)
168
 
                d = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device')
169
 
                if (d.PropertyExists('storage.bus') and
170
 
                d.GetProperty('storage.bus') == 'usb') and \
171
 
                d.GetProperty('storage.removable'):
172
 
                    if dev.GetProperty('volume.fstype') == 'vfat':
173
 
                        success = True
174
 
            if success:
175
 
                self.add_device(dev)
176
 
                return
177
 
        # Look for empty devices.
178
 
        if self.IsStorageDevice(dev):
179
 
            children = self.hal.FindDeviceStringMatch('info.parent', udi)
180
 
            c = False
181
 
            for child in children:
182
 
                dev_obj = self.bus.get_object('org.freedesktop.Hal', child)
183
 
                child = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device')
184
 
                if child.GetProperty('block.is_volume') and child.GetProperty('volume.fstype') == 'vfat':
185
 
                    c = True
186
 
                self.log('children: ' + str(children))
187
 
            if not c and dev.PropertyExists('storage.removable.media_size'):
188
 
                self.log('no children or children not vfat')
189
 
                device = str(dev.GetProperty('block.device'))
190
 
                self.devices[device] = {
191
 
                    'label' : '',
192
 
                    'fstype' : '',
193
 
                    'uuid' : '',
194
 
                    'mountpoint' : '',
195
 
                    'udi' : udi,
196
 
                    'free' : dev.GetProperty('storage.removable.media_size'),
197
 
                    'capacity' : dev.GetProperty('storage.removable.media_size'),
198
 
                    'device' : device
199
 
                }
200
 
                self.frontend.add_dest(device)
201
 
 
202
 
    def property_modified(self, num_changes, change_list, udi):
203
 
        dev_obj = self.bus.get_object("org.freedesktop.Hal", udi)
204
 
        dev = dbus.Interface(dev_obj, "org.freedesktop.Hal.Device")
205
 
        if dev.PropertyExists('volume.is_disc') and dev.GetProperty('volume.is_disc'):
206
 
            if not dev.GetProperty('volume.is_mounted'):
207
 
                return
208
 
            mountpoint = dev.GetProperty('volume.mount_point')
209
 
            # TODO: Is the most appropriate check?
210
 
            if mountpoint and os.path.exists('%s/.disk/info' % mountpoint):
211
 
                add = False
212
 
                if not self.cds.has_key(udi):
213
 
                    add = True
 
109
        logging.debug('Possibly adding: %s' % str(udi))
 
110
        dev_obj = self.bus.get_object("org.freedesktop.Hal", udi)
 
111
        dev = dbus.Interface(dev_obj, "org.freedesktop.Hal.Device")
 
112
 
 
113
        # TODO evand 2009-04-21: Confirm these are the only, and correct,
 
114
        # values.
 
115
        bus_types = ('usb', 'mmc')
 
116
        drive_types = ('compact_flash', 'memory_stick', 'smart_media', 'sd_mmc')
 
117
 
 
118
        storage = False
 
119
        volume = False
 
120
        fs = False
 
121
        disc = False
 
122
        bus = False
 
123
        type = False
 
124
 
 
125
        try:
 
126
            # TODO evand 2009-04-22: Use GetAllProperties() instead?  See
 
127
            # lshal.py
 
128
            udi = dev.GetProperty('info.udi')
 
129
            # I hate HAL.  info.subsystem would make the most sense here, but
 
130
            # despite the specification saying that it is a mandatory property,
 
131
            # it is not always present.
 
132
            if dev.PropertyExists('info.category'):
 
133
                # FIXME evand 2009-04-26: QueryCapability to be consistent with
 
134
                # detect_devices?
 
135
                storage = dev.GetProperty('info.category') == 'storage'
 
136
                volume = dev.GetProperty('info.category') == 'volume'
 
137
            if volume:
 
138
                fs = dev.GetProperty('volume.fstype') == 'vfat'
 
139
                # FIXME evand 2009-04-26: Need to check for .disk/info.
 
140
                disc = dev.GetProperty('volume.is_disc') and not \
 
141
                       dev.GetProperty('volume.disc.is_blank')
 
142
                label = dev.GetProperty('volume.label')
 
143
                capacity = dev.GetProperty('volume.size')
 
144
                parent = dev.GetProperty('info.parent')
 
145
            if storage:
 
146
                bus = dev.GetProperty('storage.bus') in bus_types
 
147
                type = dev.GetProperty('storage.drive_type') in drive_types
 
148
                if bus or type:
 
149
                    label = dev.GetProperty('storage.model')
 
150
                    if dev.PropertyExists('storage.removable'):
 
151
                        capacity = dev.GetProperty('storage.removable.media_size')
 
152
                    else:
 
153
                        capacity = dev.GetProperty('storage.size')
 
154
            if storage or volume:
 
155
                # Keybuk can't tell the difference between the gvfs volume
 
156
                # names.
 
157
                device = dev.GetProperty('block.device')
 
158
                    
 
159
        except dbus.DBusException, e:
 
160
            if e.get_dbus_name() == 'org.freedesktop.Hal.NoSuchProperty':
 
161
                # FIXME evand 2009-04-21: Add warning support to the frontend.
 
162
                # Warnings are like errors, only the program can continue after
 
163
                # warnings.
 
164
                # self.frontend.warning(str(e))
 
165
                logging.critical('No such property: %s' % str(e))
 
166
                return
 
167
        
 
168
        logging.debug('    volume: %s' % volume)
 
169
        logging.debug('    disc: %s' % disc)
 
170
        logging.debug('    fs: %s' % fs)
 
171
        logging.debug('    storage: %s' % storage)
 
172
        logging.debug('    bus: %s' % bus)
 
173
        logging.debug('    type: %s' % type)
 
174
 
 
175
        # Only add properties that are needed by the frontend.  The rest can be
 
176
        # queried using the UDI as a key.
 
177
        if volume:
 
178
            # CD (source)
 
179
            if disc:
 
180
                # TODO evand 2009-04-21: Rename to self.sources.
214
181
                self.cds[udi] = {
215
 
                    'label' : str(dev.GetProperty('volume.label')),
216
 
                    'uuid' : str(dev.GetProperty('volume.uuid')),
 
182
                    'label' : label,
 
183
                    'type' : Backend.CD,
 
184
                    'capacity' : capacity,
217
185
                    'mountpoint' : mountpoint,
218
 
                    'udi' : udi,
219
 
                    'size' : dev.GetProperty('volume.size'),
220
 
                    'filename' : '',
221
 
                }
222
 
                if add:
223
 
                    self.frontend.add_source(udi)
224
 
                self.frontend.update_all_rows(None)
225
 
        else:
226
 
            mountpoint = str(dev.GetProperty('volume.mount_point'))
227
 
            device = str(dev.GetProperty('block.device'))
228
 
            self.devices[device] = {
229
 
                'label' : str(dev.GetProperty('volume.label')).replace(' ', '_'),
230
 
                'fstype' : str(dev.GetProperty('volume.fstype')),
231
 
                'uuid' : str(dev.GetProperty('volume.uuid')),
232
 
                'mountpoint' : mountpoint,
233
 
                'udi' : str(dev.GetProperty('info.udi')),
234
 
                'free' : mountpoint and free_space(mountpoint) or 0,
235
 
                'capacity' : dev.GetProperty('volume.size'),
236
 
                'device' : device
237
 
            }
238
 
            self.frontend.update_dest_row(device)
239
 
        self.log('prop modified')
240
 
        self.log('device_udi: %s' % udi)
241
 
        self.log('num_changes: %d' % num_changes)
 
186
                    # TODO evand 2009-04-21: Needed?
 
187
                    'device' : device,
 
188
                }
 
189
                self.frontend.add_source(udi)
 
190
                self.bus.add_signal_receiver(self.property_modified,
 
191
                                             'PropertyModified',
 
192
                                             'org.freedesktop.Hal.Device',
 
193
                                             'org.freedesktop.Hal', udi,
 
194
                                             path_keyword='udi')
 
195
            # Partition (target)
 
196
            if fs:
 
197
                self.partitions[udi] = {
 
198
                    'label' : label,
 
199
                    'capacity' : capacity,
 
200
                    'free' : 0,
 
201
                    'device' : device,
 
202
                    'status' : Backend.CAN_USE,
 
203
                }
 
204
                logging.debug('    %s' % str(self.partitions[udi]))
 
205
                if parent in self.devices:
 
206
                    self.devices[parent]['partitions'].append(udi)
 
207
                    self.refresh_targets()
 
208
                else:
 
209
                    self.devices[parent] = {
 
210
                        'partitions' : [udi],
 
211
                    }
 
212
                    # Don't refresh the frontend, as we know we're going to be
 
213
                    # adding a device entry soon.
 
214
                self.bus.add_signal_receiver(self.property_modified,
 
215
                                             'PropertyModified',
 
216
                                             'org.freedesktop.Hal.Device',
 
217
                                             'org.freedesktop.Hal', udi,
 
218
                                             path_keyword='udi')
 
219
                # TODO evand 2009-05-05: Move to refresh_targets()?  We don't
 
220
                # want to mount partitions if we never care about their free
 
221
                # space.
 
222
                self.timeouts[udi] = gobject.timeout_add(UPDATE_FREE_INTERVAL,
 
223
                                                         self.update_free, udi)
 
224
        elif storage:
 
225
            if bus or type:
 
226
                # Drive (target)
 
227
                if udi in self.devices:
 
228
                    partitions = self.devices[udi]['partitions']
 
229
                else:
 
230
                    partitions = []
 
231
                self.devices[udi] = {
 
232
                    'label' : label,
 
233
                    'capacity' : capacity,
 
234
                    'device' : device,
 
235
                    'partitions' : partitions,
 
236
                    'free' : 0,
 
237
                    'status' : Backend.CAN_USE,
 
238
                }
 
239
                self.bus.add_signal_receiver(self.property_modified,
 
240
                                             'PropertyModified',
 
241
                                             'org.freedesktop.Hal.Device',
 
242
                                             'org.freedesktop.Hal', udi,
 
243
                                             path_keyword='udi')
 
244
                self.refresh_targets()
 
245
        
 
246
        logging.debug('devices: %s' % str(self.devices))
 
247
 
 
248
    def refresh_targets(self):
 
249
        source = self.frontend.get_source()
 
250
        ret = []
 
251
        if source:
 
252
            type = self.cds[source]['type']
 
253
            if type == Backend.CD or type == Backend.ISO:
 
254
                # We want to show partitions and devices with no usable
 
255
                # partitions.
 
256
                for device in self.devices:
 
257
                    if self.devices[device]['partitions']:
 
258
                        for partition in self.devices[device]['partitions']:
 
259
                            self.update_free(partition)
 
260
                            ret.append(partition)
 
261
                    else:
 
262
                        ret.append(device)
 
263
            elif type == Backend.IMG:
 
264
                # We only want to show the devices.
 
265
                ret = self.devices.keys()
 
266
        else:
 
267
            # FIXME evand 2009-04-25: I'm not entirely confident this is the
 
268
            # right approach, but we should give the user some indication that
 
269
            # the USB disk they've plugged in has been recognized.
 
270
            ret = self.partitions.keys()
 
271
            for r in ret:
 
272
                self.update_free(r)
 
273
        if ret:
 
274
            for r in ret:
 
275
                self.update_state(r)
 
276
            self.frontend.set_targets(ret)
 
277
 
 
278
    def device_removed(self, udi):
 
279
        logging.debug('Removing %s' % str(udi))
 
280
        self.bus.remove_signal_receiver(self.property_modified, path=udi)
 
281
        if udi in self.cds:
 
282
            self.cds.pop(udi)
 
283
            self.frontend.remove_source(udi)
 
284
        # FIXME evand 2009-04-26: This is getting really ugly really quickly.
 
285
        # Fold partitions back into devices?
 
286
        elif udi in self.devices:
 
287
            self.devices.pop(udi)
 
288
            self.refresh_targets()
 
289
        elif udi in self.partitions:
 
290
            self.partitions.pop(udi)
 
291
            for u in self.devices:
 
292
                if udi in self.devices[u]['partitions']:
 
293
                    self.devices[u]['partitions'].remove(udi)
 
294
                    break
 
295
            self.refresh_targets()
 
296
        else:
 
297
            logging.critical('Was notified of changes to a device we do not' \
 
298
                             ' care about: %s' % str(udi))
 
299
    
 
300
    def property_modified(self, num_changes, change_list, udi):
 
301
        dev_obj = self.bus.get_object('org.freedesktop.Hal', udi)
 
302
        dev = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device')
 
303
        # FIXME evand 2009-04-29:
 
304
        logging.debug('device_udi: %s' % udi)
 
305
        logging.debug('num_changes: %d' % num_changes)
 
306
        print change_list
242
307
        for c in change_list:
243
 
            self.log('change: %s' % str(c[0]))
244
 
 
245
 
    def device_removed(self, udi):
246
 
        d = None
247
 
        for device in self.devices.itervalues():
248
 
            if device['udi'] == udi:
249
 
                self.log('removing %s' % udi)
250
 
                d = device['device']
251
 
                break
252
 
        if d:
253
 
            self.devices.pop(d)
254
 
            self.frontend.device_removed(d, source=False)
255
 
            gobject.source_remove(self.timeouts[d])
256
 
            self.timeouts.pop(d)
257
 
        else:
258
 
            # TODO: Ick.
259
 
            d = None
260
 
            for device in self.cds.itervalues():
261
 
                if device['udi'] == udi:
262
 
                    self.log('removing %s' % udi)
263
 
                    d = udi
264
 
                    break
265
 
            if d:
266
 
                self.cds.pop(d)
267
 
                self.frontend.device_removed(d, source=True)
268
 
            
269
 
    def add_device(self, dev):
270
 
        # Remove parent device if present as this means there is not an empty
271
 
        # partition table.
272
 
        dev_obj = self.bus.get_object("org.freedesktop.Hal",
273
 
            dev.GetProperty('info.parent'))
274
 
        device = dbus.Interface(dev_obj, "org.freedesktop.Hal.Device")
275
 
        d = device.GetProperty('block.device')
276
 
        if self.devices.has_key(d):
277
 
            self.devices.pop(d)
278
 
            self.frontend.device_removed(d, source=False)
279
 
 
280
 
        udi = dev.GetProperty('info.udi')
281
 
        mountpoint = str(dev.GetProperty('volume.mount_point'))
282
 
        device = str(dev.GetProperty('block.device'))
283
 
        self.devices[device] = {
284
 
            'label' : str(dev.GetProperty('volume.label')).replace(' ', '_'),
285
 
            'fstype' : str(dev.GetProperty('volume.fstype')),
286
 
            'uuid' : str(dev.GetProperty('volume.uuid')),
287
 
            'mountpoint' : mountpoint,
288
 
            'udi' : str(dev.GetProperty('info.udi')),
289
 
            'free' : mountpoint and free_space(mountpoint) or 0,
290
 
            'capacity' : dev.GetProperty('volume.size'),
291
 
            'device' : device
292
 
        }
293
 
        self.bus.add_signal_receiver(self.property_modified,
294
 
        'PropertyModified', 'org.freedesktop.Hal.Device',
295
 
        'org.freedesktop.Hal', udi, path_keyword='udi')
296
 
        self.log('new device:\n%s' % self.devices[device])
297
 
        self.frontend.add_dest(device)
298
 
        def update_free(device):
299
 
            dev = self.devices[device]
300
 
            mp = dev['mountpoint']
301
 
            free = dev['free']
302
 
            if mp:
303
 
                dev['free'] = free_space(mp)
304
 
                if free != dev['free']:
305
 
                    self.frontend.update_dest_row(device)
306
 
            else:
307
 
                # TODO: Is here really the best place for this?
308
 
                fstype = dev['fstype']
309
 
                if not fstype:
310
 
                    return True
311
 
                dev_obj = self.bus.get_object("org.freedesktop.Hal", str(dev['udi']))
312
 
                d = dbus.Interface(dev_obj, "org.freedesktop.Hal.Device")
313
 
                if d.GetProperty('volume.mount_point'):
314
 
                    return True
315
 
                tmpdir = tempfile.mkdtemp()
316
 
                res = popen(['mount', '-t', fstype, device, tmpdir])
317
 
                if res[0].returncode:
318
 
                    self.log('Error mounting %s: %s' % (device, res[1]))
319
 
                    popen(['rmdir', tmpdir])
320
 
                else:
321
 
                    dev['mountpoint'] = tmpdir
322
 
            return True
323
 
        self.timeouts[device] = gobject.timeout_add(2000, update_free, device)
 
308
            logging.debug('change: %s %s' % (str(c[0]), str(c[1])))
 
309
        # TODO evand 2009-04-26: What happens if a device doesn't have a
 
310
        # property value we want now, but does so later?  Will that ever
 
311
        # happen?
 
312
        try:
 
313
            dev_obj = self.bus.get_object("org.freedesktop.Hal", udi)
 
314
            dev = dbus.Interface(dev_obj, "org.freedesktop.Hal.Device")
 
315
            if udi in self.cds:
 
316
                self.cds[udi]['mountpoint'] = dev.GetProperty('volume.mount_point')
 
317
                logging.debug('changed: %s' % str(self.cds[udi]))
 
318
        except dbus.DBusException, e:
 
319
            logging.debug('Unable to read the new mount point from %s' % udi)
324
320
    
325
321
    def detect_devices(self):
326
 
        # TODO: Handle devices with empty partition tables.
327
 
        # CDs
328
 
        devices = self.hal.FindDeviceByCapability('volume')
329
 
        for device in devices:
330
 
            dev_obj = self.bus.get_object('org.freedesktop.Hal', device)
331
 
            dev = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device')
332
 
            if dev.PropertyExists('volume.is_disc') and dev.GetProperty('volume.is_disc'):
333
 
                if not dev.GetProperty('volume.disc.is_blank'):
334
 
                    self.log('got a disc: %s' % dev.GetProperty('volume.label'))
335
 
                    udi = dev.GetProperty('info.udi')
336
 
                    if not dev.GetProperty('volume.is_mounted'):
337
 
                        self.bus.add_signal_receiver(self.property_modified,
338
 
                        'PropertyModified', 'org.freedesktop.Hal.Device',
339
 
                        'org.freedesktop.Hal', udi, path_keyword='udi')
340
 
                    else:
341
 
                        mountpoint = dev.GetProperty('volume.mount_point')
342
 
                        # TODO: Is the most appropriate check?
343
 
                        if mountpoint and os.path.exists('%s/.disk/info' % mountpoint):
344
 
                            self.cds[udi] = {
345
 
                                'label' : str(dev.GetProperty('volume.label')),
346
 
                                'uuid' : str(dev.GetProperty('volume.uuid')),
347
 
                                'mountpoint' : mountpoint,
348
 
                                'udi' : udi,
349
 
                                'size' : dev.GetProperty('volume.size'),
350
 
                                'filename' : '',
351
 
                            }
352
 
                            self.frontend.add_source(udi)
353
 
        # USB disks
354
 
        devices = self.hal.FindDeviceByCapability('storage')
355
 
        for device in devices:
356
 
            dev_obj = self.bus.get_object('org.freedesktop.Hal', device)
357
 
            d = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device')
358
 
            # Only check physical storage devices (not partitions)
359
 
            if not self.IsStorageDevice(d):
360
 
                continue
361
 
            # Check if this device has a filesystem but no partitions.
362
 
            if d.GetProperty('block.is_volume'):
363
 
                if d.GetProperty('volume.fstype') == 'vfat':
364
 
                    self.add_device(d) 
365
 
            else:
366
 
                # If not, find all of this device's child partitions
367
 
                children = self.hal.FindDeviceStringMatch('info.parent', device)
368
 
                c = False
369
 
                for child in children:
370
 
                    dev_obj = self.bus.get_object('org.freedesktop.Hal', child)
371
 
                    child = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device')
372
 
                    if child.GetProperty('block.is_volume'):
373
 
                        if child.GetProperty('volume.fstype') == 'vfat':
374
 
                            c = True
375
 
                            self.add_device(child)
376
 
                # If no partitions were found, check for an empty partition table.
377
 
                if not c and d.PropertyExists('storage.removable.media_size'):
378
 
                    udi = d.GetProperty('info.udi')
379
 
                    device = str(d.GetProperty('block.device'))
380
 
                    self.devices[device] = {
381
 
                        'label' : '',
382
 
                        'fstype' : '',
383
 
                        'uuid' : '',
384
 
                        'mountpoint' : '',
385
 
                        'udi' : udi,
386
 
                        'free' : d.GetProperty('storage.removable.media_size'),
387
 
                        'capacity' : d.GetProperty('storage.removable.media_size'),
388
 
                        'device' : device
389
 
                    }
390
 
                    self.frontend.add_dest(device)
391
 
 
392
 
        self.bus.add_signal_receiver(self.device_added,
393
 
            "DeviceAdded",
394
 
            "org.freedesktop.Hal.Manager",
395
 
            "org.freedesktop.Hal",
396
 
            "/org/freedesktop/Hal/Manager")
397
 
        self.bus.add_signal_receiver(self.device_removed,
398
 
            "DeviceRemoved",
399
 
            "org.freedesktop.Hal.Manager",
400
 
            "org.freedesktop.Hal",
401
 
            "/org/freedesktop/Hal/Manager")
402
 
 
403
 
    def mount_iso(self, filename):
404
 
        # HAL doesn't support loop mounted filesystems and indeed when manually
405
 
        # mounted, the device does not appear in d-feet (searching the
406
 
        # mountpoint).  So we have to just manually construct the addition to
407
 
        # self.devices.
408
 
        self.log('mounting %s' % filename)
409
 
        tmpdir = tempfile.mkdtemp()
410
 
        res = popen(['mount', '-t', 'iso9660', '-o', 'loop,ro', filename, tmpdir])
411
 
        if res[0].returncode:
412
 
            self.log('unable to mount %s to %s' % (filename, tmpdir))
413
 
            self.log(res[1])
 
322
        volumes = self.hal.FindDeviceByCapability('volume')
 
323
        for volume in volumes:
 
324
            self.device_added(volume)
 
325
        disks = self.hal.FindDeviceByCapability('storage')
 
326
        for disk in disks:
 
327
            self.device_added(disk)
 
328
 
 
329
    # Manual device addition.
 
330
    
 
331
    # HAL doesn't support loop mounted filesystems, despite udev apparently
 
332
    # supporting them (according to Keybuk), and indeed when manually mounted,
 
333
    # the device does not appear in hal-device.  So we have to just manually
 
334
    # add the device.
 
335
 
 
336
    def add_file_source(self, filename):
 
337
        try:
 
338
            res = popen(['file', '-b', filename])
 
339
        except USBCreatorProcessException, e:
 
340
            logging.error(e)
 
341
            return
 
342
        if res.startswith('x86 boot sector'):
 
343
            self.add_image_source(filename)
 
344
        elif res.startswith('ISO 9660'):
 
345
            self.add_iso_source(filename)
 
346
        else:
 
347
            logging.error('Unknown file type: %s' % str(res))
 
348
 
 
349
    def add_image_source(self, filename):
 
350
        size = os.stat(filename).st_size
 
351
        # TODO: Rename to self.imgs
 
352
        self.cds[filename] = { 'label' : '',
 
353
                               'type' : Backend.IMG,
 
354
                               'mountpoint' : '',
 
355
                               'device' : os.path.basename(filename),
 
356
                               'capacity' : size }
 
357
        self.frontend.add_source(filename)
 
358
        
 
359
    def add_iso_source(self, filename):
 
360
        # TODO evand 2009-04-28: Replace with generic mount function that
 
361
        # watches all mountpoints and unmounts them in the end.
 
362
        mount_dir = tempfile.mkdtemp()
 
363
        try:
 
364
            # Really need to use HAL for all mounting and unmounting.  Be sure
 
365
            # to handle org.freedesktop.Hal.Device.Volume.NotMountedByHal .
 
366
            # Oh, not possible is it, given ISO?  At least do it for all
 
367
            # partitions? It might not be us mounting it elsewhere. Mounted by
 
368
            # hal property.  Also catch Volume.NotMounted and Volume.Busy?
 
369
            res = popen(['mount', '-t', 'iso9660', '-o', 'loop,ro', filename, mount_dir])
 
370
        except USBCreatorProcessException, e:
 
371
            logging.debug('unable to mount %s to %s:\n%s' % (filename, mount_dir, str(e)))
414
372
            self.frontend.notify(_('Unable to mount the image %s.\n\n'
415
373
                'Please see ~/.usb-creator.log for more details') % filename)
416
374
            return
 
375
 
417
376
        fp = None
 
377
        label = ''
418
378
        try:
419
 
            fp = open('%s/.disk/info' % tmpdir)
420
 
            line = fp.readline().strip('\0')
421
 
            line = ' '.join(line.split(' ')[:2])
 
379
            fp = open('%s/.disk/info' % mount_dir)
 
380
            label = fp.readline().strip('\0')
 
381
            label = ' '.join(label.split(' ')[:2])
 
382
        except Exception, e:
 
383
            logging.debug('Not an Ubuntu CD: %s' % str(e))
 
384
        finally:
 
385
            fp and fp.close()
 
386
            try:
 
387
                popen(['umount', mount_dir])
 
388
                popen(['rmdir', mount_dir])
 
389
            except USBCreatorProcessException, e:
 
390
                logging.error('Unable to unmount %s: %s' % (mount_dir, str(e)))
 
391
 
 
392
        if label:
422
393
            size = os.stat(filename).st_size
423
 
            self.cds[filename] = { 'label' : line,
424
 
                                   'uuid' : '',
425
 
                                   'udi' : '',
 
394
            self.cds[filename] = { 'label' : label,
 
395
                                   'type' : Backend.ISO,
426
396
                                   'mountpoint' : '',
427
 
                                   'filename' : filename,
428
 
                                   'size' : size }
 
397
                                   'device' : os.path.basename(filename),
 
398
                                   'capacity' : size }
429
399
            self.frontend.add_source(filename)
430
 
        except Exception, e:
431
 
            self.log('Unable to find %s/.disk/info, not using %s' %
432
 
                (tmpdir, filename))
433
 
            self.log(str(e))
434
 
            self.frontend.notify(_('This is not a desktop install CD and '
435
 
                'thus cannot be used by this application.'))
436
 
            return
 
400
 
 
401
    # Utility functions
 
402
 
 
403
    def update_state(self, udi):
 
404
        '''Determines whether a disk/partition has sufficient free space for
 
405
        the selected source, or if the source is larger than the disk/partition
 
406
        itself and therefore the disk is unusable with that image.'''
 
407
        source = self.frontend.get_source()
 
408
        if not source:
 
409
            return
 
410
 
 
411
        source = self.cds[source]
 
412
        source_capacity = source['capacity']
 
413
 
 
414
        # XXX evand 2009-05-05: We assume that a formatted disk will have a
 
415
        # partition that has the same size as the disk's capacity, which is
 
416
        # never the case, but we have no way of accurately guessing the size of
 
417
        # the resulting filesystem.
 
418
 
 
419
        # TODO evand 2009-05-06: Need to factor in the removal of Ubuntu CD
 
420
        # directories (casper, pool, etc).
 
421
        if udi in self.devices:
 
422
            device = self.devices[udi]
 
423
            if source_capacity > device['capacity']:
 
424
                # The image is larger than this disk, we cannot use it.
 
425
                device['status'] = Backend.CANNOT_USE
 
426
            else:
 
427
                # We can use the disk.
 
428
                device['status'] = Backend.CAN_USE
 
429
        else:
 
430
            if udi in self.partitions:
 
431
                partition = self.partitions[udi]
 
432
                if source_capacity > partition['capacity']:
 
433
                    partition['status'] = Backend.CANNOT_USE
 
434
                elif source_capacity > partition['free']:
 
435
                    partition['status'] = Backend.NEED_SPACE
 
436
                else:
 
437
                    partition['status'] = Backend.CAN_USE
 
438
 
 
439
    def update_free(self, udi):
 
440
        '''Peroidically called via a gobject.timeout to update the amount of
 
441
        free space on a partition target.  If the amount of space has changed,
 
442
        it calls update_state, which will determine if there is sufficient free
 
443
        space to hold the selected source image, and then notifies the frontend
 
444
        that the partition has changed and it needs to update its UI to reflect
 
445
        that.'''
 
446
        # FIXME evand 2009-05-05: Don't poll, use an inotify watch instead.
 
447
        dev_obj = self.bus.get_object('org.freedesktop.Hal', udi)
 
448
        dev = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device')
 
449
        if not dev.GetProperty('volume.is_mounted'):
 
450
            d = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device.Volume')
 
451
            try:
 
452
                d.Mount(self.partitions[udi]['label'], 'vfat', [])
 
453
                self.mountpoints.append(udi)
 
454
            except dbus.DBusException, e:
 
455
                logging.error(str(e))
 
456
                logging.error('Cannot mount %s, not updating free space.' % udi)
 
457
                return False
 
458
        mp = dev.GetProperty('volume.mount_point')
 
459
 
 
460
        if mp:
 
461
            free = free_space(mp)
 
462
            if self.partitions[udi]['free'] != free:
 
463
                logging.debug('%s now has %d B free.' % (udi, free))
 
464
                self.partitions[udi]['free'] = free
 
465
                self.update_state(udi)
 
466
                self.frontend.update_target(udi)
 
467
        return True
 
468
 
 
469
    def unmount(self, partitions_list):
 
470
        '''Unmount a list of Device.Volumes, specified by UDI.'''
 
471
        for partition in partitions_list:
 
472
            if partition in self.cds:
 
473
                mp = self.cds[partition]['mountpoint']
 
474
                mp and popen(['umount', mp])
 
475
            dev_obj = self.bus.get_object('org.freedesktop.Hal', partition)
 
476
            dev = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device')
 
477
            if dev.GetProperty('volume.is_mounted'):
 
478
                d = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device.Volume')
 
479
                # TODO evand 2009-04-28: Catch unmount exceptions.
 
480
                try:
 
481
                    d.Unmount([])
 
482
                except dbus.DBusException, e:
 
483
                    n = e.get_dbus_name()
 
484
                    if n == 'org.freedesktop.Hal.Device.Volume.NotMountedByHal':
 
485
                        mountpoint = dev.GetProperty('volume.mount_point')
 
486
                        popen(['umount', mountpoint])
 
487
                    elif n == 'org.freedesktop.Hal.Device.Volume.Busy':
 
488
                        # TODO evand 2009-05-01: Need to call into the frontend
 
489
                        # for an error dialog here.
 
490
                        raise
 
491
 
 
492
    def format_device(self, udi):
 
493
        '''Format the disk device.  If the UDI specified is a Device.Volume,
 
494
           find its parent and format it instead.'''
 
495
 
 
496
        device = None
 
497
        if udi in self.devices:
 
498
            device = udi
 
499
        else:
 
500
            for dev in self.devices:
 
501
                if udi in self.devices[dev]['partitions']:
 
502
                    device = dev
 
503
                    break
 
504
        if not device:
 
505
            logging.critical('Could not find device to format: %s' % udi)
 
506
            return
 
507
        dev_obj = self.bus.get_object('org.freedesktop.Hal', udi)
 
508
        dev = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device')
 
509
        # TODO evand 2009-04-28: Catch lock exceptions.
 
510
        dev.Lock('Formatting device')
 
511
        partitions = self.devices[device]['partitions']
 
512
        try:
 
513
            self.unmount(partitions)
 
514
        except dbus.DBusException, e:
 
515
            logging.error('Unable to unmount partitions before formatting.')
 
516
            return
 
517
 
 
518
        device = self.devices[device]['device']
 
519
        try:
 
520
            # TODO: This could really use a progress dialog.
 
521
            popen(['parted', '-s', device, 'mklabel', 'msdos'])
 
522
            popen(['parted', '-s', device, 'mkpartfs', 'primary',
 
523
                   'fat32', '0', '--', '-0'])
 
524
        except USBCreatorProcessException, e:
 
525
            message = _('Unable to format the device')
 
526
            logging.debug('%s %s: %s' % (message, device, str(e)))
 
527
            self.frontend.notify(message)
 
528
            return
 
529
        # Probably unnecessary.
 
530
        try:
 
531
            dev.Unlock()
 
532
        except dbus.DBusException:
 
533
            return
 
534
 
 
535
    # Install routines
 
536
 
 
537
    def install(self):
 
538
        # TODO evand 2009-05-03: Perhaps raise USBCreatorInstallException on
 
539
        # recoverable errors?  Or FatalInstallException and InstallException?
 
540
        source = self.frontend.get_source()
 
541
        target = self.frontend.get_target()
 
542
        persist = self.frontend.get_persistence() * 1024 * 1024
 
543
        if not (source and target):
 
544
            # We do not fail here because the user can go back and select a
 
545
            # proper source and target, should they somehow get here.
 
546
            logging.error('Pressed install without a source or target.')
 
547
            return
 
548
 
 
549
        try:
 
550
            dev_obj = self.bus.get_object('org.freedesktop.Hal', target)
 
551
            dev = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device')
 
552
            dev.Lock('Writing %s to the disk' % source)
 
553
            if self.cds[source]['type'] == Backend.IMG:
 
554
                self.unmount(self.devices[target]['partitions'])
 
555
                self.write_image(source, target)
 
556
            else:
 
557
                # TODO evand 2009-05-06: Call self.unmount()
 
558
                self.install_bootloader(target)
 
559
                self.copy_files(source, target, persist)
 
560
        except (dbus.DBusException, Exception), e:
 
561
            import traceback
 
562
            print traceback.print_exc()
 
563
            message = _('An error occured during the installation, and it ' \
 
564
                        'was unable to complete.')
 
565
            # FIXME evand 2009-05-03: Unmount both source and target here.
 
566
            self.cleanup()
 
567
            self.frontend.failed(message)
437
568
        finally:
438
 
            if fp:
439
 
                fp.close()
440
 
            popen(['umount', tmpdir])
441
 
            popen(['rmdir', tmpdir])
442
 
 
443
 
    def install_bootloader(self):
444
 
        # TODO: Needs to be moved into the generic install routine.
 
569
            try:
 
570
                dev.Unlock()
 
571
            except dbus.DBusException:
 
572
                pass
 
573
 
 
574
    def dd_status(self):
 
575
        try:
 
576
            os.kill(self.pipe.pid, signal.SIGUSR1)
 
577
        except OSError:
 
578
            return False
 
579
        return True
 
580
 
 
581
    def dd_progress(self, source, condition, source_size):
 
582
        char = source.readline()
 
583
        splitted = char.split()
 
584
        # XXX evand 2009-05-05: Ick.
 
585
        if len(splitted) > 1 and splitted[1] == 'bytes':
 
586
            try:
 
587
                now = float(splitted[0])
 
588
                per = (now / source_size) * 100
 
589
                remaining, speed = self.estimator.estimate(now, source_size)
 
590
                self.frontend.progress(per, remaining, speed, _('Copying files'))
 
591
            except ValueError:
 
592
                self.frontend.progress(0, 0, 0, _('Copying files'))
 
593
        return True
 
594
 
 
595
    def write_image(self, source, target):
 
596
        print 'Writing %s to %s' % (source, target)
 
597
        target = self.devices[target]
 
598
        cmd = ['dd', 'bs=1M', 'if=%s' % source,
 
599
               'of=%s' % target['device'], 'conv=fsync']
 
600
        logging.debug(cmd)
 
601
        # TODO evand 2009-05-06: Either use popen() or write another function
 
602
        # that wraps errors in exceptions.
 
603
        self.pipe = subprocess.Popen(cmd, stdout=sys.stdout,
 
604
            stderr=subprocess.PIPE, universal_newlines=True,
 
605
            env={'LC_ALL':'C'})
 
606
        source_size = float(self.cds[source]['capacity'])
 
607
        self.watch = gobject.io_add_watch(self.pipe.stderr,
 
608
                 gobject.IO_IN | gobject.IO_HUP,
 
609
                 self.dd_progress, source_size)
 
610
        self.copy_timeout = gobject.timeout_add(5000, self.dd_status)
 
611
        # Wait for the process to complete
 
612
        gobject.child_watch_add(self.pipe.pid, self.on_end)
 
613
 
 
614
    def install_bootloader(self, target):
 
615
        # TODO evand 2009-04-29: Perhaps detect bootloader code instead and
 
616
        # offer to overwrite.
 
617
        try:
 
618
            dev_obj = self.bus.get_object('org.freedesktop.Hal', target)
 
619
            dev = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device')
 
620
            partition_num = str(dev.GetProperty('volume.partition.number'))
 
621
            device = ''
 
622
            rootdevice = ''
 
623
            assert target in self.partitions
 
624
            device = self.partitions[target]['device']
 
625
            for dev in self.devices:
 
626
                if target in self.devices[dev]['partitions']:
 
627
                    rootdevice = self.devices[dev]['device']
 
628
                    break
 
629
            assert device and rootdevice
 
630
        except (AssertionError, dbus.DBusException), e:
 
631
            logging.error('Could not determine the device: %s' % str(e))
 
632
            # We don't exit because we haven't actually done anything to the device.
 
633
            return
445
634
        
446
 
        # Ugly, i18n.
447
635
        self.frontend.progress(0, None, None, _('Installing the bootloader'))
448
 
        import re
449
 
        device = self.install_target['device']
450
 
        udi = self.install_target['udi']
451
 
        dev_obj = self.bus.get_object("org.freedesktop.Hal", udi)
452
 
        dev = dbus.Interface(dev_obj, "org.freedesktop.Hal.Device")
453
 
        dev_obj = self.bus.get_object("org.freedesktop.Hal", dev.GetProperty('info.parent'))
454
 
        dev = dbus.Interface(dev_obj, "org.freedesktop.Hal.Device")
455
 
        rootdev = dev.GetProperty('block.device')
456
 
        tmp = device.lstrip(rootdev)
457
 
        match = re.match('.*([0-9]+)$', tmp)
458
 
        num = None
459
 
        if match:
460
 
            num = match.groups()[0]
461
 
        self.log('Marking partition %s as active.' % num)
462
 
        if not num:
463
 
            self.frontend.failed(_('Unable to determine the partition number.'))
464
 
 
465
 
        # Install the bootloader to the MBR.
466
 
        self.log('installing the bootloader to %s.' % rootdev)
467
 
        process = popen(['dd', 'if=/usr/lib/syslinux/mbr.bin',
468
 
            'of=%s' % rootdev, 'bs=446', 'count=1', 'conv=sync'])
469
 
        bootloader_failed = _('Error installing the bootloader.')
470
 
        if process[0].returncode:
471
 
            self.frontend.failed(bootloader_failed)
472
 
        self.log('installing the bootloader to %s.' % device)
473
 
        args = ['syslinux']
474
 
        if 'USBCREATOR_SAFE' in os.environ:
475
 
            args.append('-s')
476
 
        args.append(device)
477
 
        process = popen(args)[0]
478
 
        if process.returncode:
479
 
            self.frontend.failed(bootloader_failed)
480
 
        popen(['parted', '-s', rootdev, 'set', num, 'boot', 'on'])
481
 
 
482
 
    def copy_files(self):
483
 
        tmpdir = ''
484
 
        def _copy_files(source, target, persist):
485
 
            cmd = ['/usr/share/usb-creator/install.py', '-s', '%s/.' % source,
486
 
                '-t', '%s' % target, '-p', '%d' % persist]
487
 
            self.log(cmd)
488
 
            self.pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
489
 
                stderr=sys.stderr, universal_newlines=True)
490
 
            self.watch = gobject.io_add_watch(self.pipe.stdout,
 
636
 
 
637
        try:
 
638
            # Install the bootloader to the MBR.
 
639
            popen(['dd', 'if=/usr/lib/syslinux/mbr.bin',
 
640
                'of=%s' % rootdevice, 'bs=446', 'count=1', 'conv=sync'])
 
641
            args = ['syslinux']
 
642
            if 'USBCREATOR_SAFE' in os.environ:
 
643
                args.append('-s')
 
644
            args.append(device)
 
645
            popen(args)
 
646
            logging.debug('Marking partition %s as active.' % partition_num)
 
647
            popen(['parted', '-s', rootdevice, 'set', partition_num, 'boot', 'on'])
 
648
        except USBCreatorProcessException, e:
 
649
            logging.error('Failed to install the bootloader: %s' % str(e))
 
650
            # FIXME evand 2009-04-29: exit properly.
 
651
            sys.exit(1)
 
652
 
 
653
    def copy_files(self, source, target, persist):
 
654
        source_obj = self.cds[source]
 
655
        target_obj = self.partitions[target]
 
656
 
 
657
        # TODO evand 2009-05-05: Need to push this into its own function.
 
658
        if not source_obj['mountpoint'] and source_obj['type'] == Backend.ISO:
 
659
            mount_point = tempfile.mkdtemp()
 
660
            # mount(options [] = None, source, mount_point)?
 
661
            #  mounts, adds to mountpoint list
 
662
            #  what happens if it fails?  Let the exception bubble up to install()
 
663
            popen(['mount', '-o', 'loop,ro', source, mount_point])
 
664
            source_obj['mountpoint'] = mount_point
 
665
            self.mountpoints.append(source)
 
666
        elif not source_obj['mountpoint'] and source_obj['type'] == Backend.CD:
 
667
            # This should use HAL to mount.
 
668
            mount_point = tempfile.mkdtemp()
 
669
            popen(['mount', '-o', 'ro', source_obj['device'], mount_point])
 
670
            self.mountpoints.append(source)
 
671
            source_obj['mountpoint'] = mount_point
 
672
 
 
673
        dev_obj = self.bus.get_object('org.freedesktop.Hal', target)
 
674
        dev = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device')
 
675
        if not dev.GetProperty('volume.is_mounted'):
 
676
            dev_obj = self.bus.get_object('org.freedesktop.Hal', target)
 
677
            d = dbus.Interface(dev_obj, 'org.freedesktop.Hal.Device.Volume')
 
678
            # TODO evand 2009-05-01: Should we use sync as a mount option here?
 
679
            # Ask Keybuk.
 
680
            d.Mount(target_obj['label'], 'vfat', [])
 
681
            self.mountpoints.append(target)
 
682
            mp = dev.GetProperty('volume.mount_point')
 
683
            if not mp:
 
684
                logging.critical('Unable to find mount point for target media.')
 
685
                # TODO evand 2009-05-05: FAIL
 
686
        target_mp = dev.GetProperty('volume.mount_point')
 
687
        # FIXME 2009-05-1: What if a CD is not mounted?
 
688
        source_mp = source_obj['mountpoint']
 
689
 
 
690
        # We don't care about updating the main window anymore.
 
691
        #for timeout in self.timeouts.itervalues():
 
692
        #    gobject.source_remove(timeout)
 
693
        #self.timeouts = {}
 
694
        #self.bus.remove_signal_receiver(self.property_modified)
 
695
 
 
696
        # FIXME evand 2009-04-29: Move into scripts/install.py?  Can't, need to
 
697
        # do it before calculating the original size.
 
698
        # Remove files we're going to copy.
 
699
        try:
 
700
            for obj in os.listdir(source_mp):
 
701
                obj = os.path.join(target_mp, obj)
 
702
                if os.path.exists(obj):
 
703
                    popen(['rm', '-rf', obj])
 
704
            casper_rw = os.path.join(target_mp, 'casper-rw')
 
705
            popen(['rm', casper_rw])
 
706
        except USBCreatorProcessException, e:
 
707
            # Chances are these files will not exist and rm will return
 
708
            # nonzero.
 
709
            pass
 
710
        
 
711
        # Update the progress by calculating the free space on the partition.
 
712
        original_size = free_space(target_mp)
 
713
        print 'persist', persist
 
714
        print 'capacity', source_obj['capacity']
 
715
        source_size = float(persist) + float(source_obj['capacity'])
 
716
        self.copy_timeout = gobject.timeout_add(2000, self.copy_progress,
 
717
                                                original_size,
 
718
                                                target_mp,
 
719
                                                source_size)
 
720
        
 
721
        cmd = ['/usr/share/usb-creator/install.py', '-s', '%s/.' % source_mp,
 
722
               '-t', '%s' % target_mp, '-p', '%d' % persist]
 
723
        self.pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
 
724
                                     stderr=sys.stderr,
 
725
                                     universal_newlines=True)
 
726
        self.watch = gobject.io_add_watch(self.pipe.stdout,
491
727
                     gobject.IO_IN | gobject.IO_HUP,
492
728
                     self.data_available)
493
 
            # Wait for the process to complete
494
 
            gobject.child_watch_add(self.pipe.pid, self.on_end)
495
 
 
496
 
        source = self.cds[self.install_source]
497
 
        if not source['udi']:
498
 
            tmpdir = tempfile.mkdtemp()
499
 
            popen(['mount', '-o', 'loop,ro', self.install_source, tmpdir])
500
 
            source['mountpoint'] = tmpdir
501
 
        elif not source['mountpoint']:
502
 
            tmpdir = tempfile.mkdtemp()
503
 
            popen(['mount', '-o', 'ro', self.install_source, tmpdir])
504
 
            source['mountpoint'] = tmpdir
505
 
        if not self.install_target['mountpoint']:
506
 
            tmpdir = tempfile.mkdtemp()
507
 
            popen(['mount', self.install_target['device'], tmpdir])
508
 
            self.install_target['mountpoint'] = tmpdir
509
 
 
510
 
        # We don't care about updating the main window anymore.
511
 
        for timeout in self.timeouts.itervalues():
512
 
            gobject.source_remove(timeout)
513
 
        self.timeouts = {}
514
 
        self.bus.remove_signal_receiver(self.property_modified)
515
 
        # Remove files we're going to copy.
516
 
        t = self.install_target['mountpoint']
517
 
        for obj in os.listdir(self.cds[self.install_source]['mountpoint']):
518
 
            obj = os.path.join(t, obj)
519
 
            popen(['rm', '-rf', obj])
520
 
        popen(['rm', os.path.join(t, 'casper-rw')])
521
 
        self.original_size = free_space(self.install_target['mountpoint'])
522
 
        _copy_files(source['mountpoint'], self.install_target['mountpoint'],
523
 
            self.persistence_size)
524
 
 
525
 
    def shutdown(self):
526
 
        self.log('Forcing shutdown of the install process.')
527
 
        import signal
 
729
        # Wait for the process to complete
 
730
        gobject.child_watch_add(self.pipe.pid, self.on_end)
 
731
        
 
732
 
 
733
    def abort(self):
 
734
        '''Called when the user has canceled the installation.'''
 
735
        logging.debug('Forcing shutdown of the install process.')
 
736
        import errno
528
737
        try:
529
738
            if self.pipe:
530
739
                os.kill(self.pipe.pid, signal.SIGTERM)
531
 
        except OSError:
532
 
            pass
533
 
        source = self.cds[self.install_source]
534
 
        if source['mountpoint'].startswith('/tmp'):
535
 
            self.log('Unmounting source volume.')
536
 
            popen(['umount', source['mountpoint']])
537
 
            popen(['rmdir', source['mountpoint']])
538
 
        if self.install_target['mountpoint'].startswith('/tmp'):
539
 
            self.log('Unmounting target volume.')
540
 
            popen(['umount', self.install_target['mountpoint']])
541
 
            popen(['rmdir', self.install_target['mountpoint']])
 
740
        except OSError, e:
 
741
            if e.errno == errno.ESRCH:
 
742
                # Already gone.
 
743
                pass
 
744
            else:
 
745
                logging.error('Unable to kill %d: %s' % self.pipe.pid, str(e))
 
746
        self.cleanup()
 
747
 
 
748
    def copy_progress(self, original_size, target_mountpoint, source_size):
 
749
        now = free_space(target_mountpoint)
 
750
        done = original_size - now
 
751
        per = (done / float(source_size)) * 100
 
752
        remaining, speed = self.estimator.estimate(done, source_size)
 
753
        self.frontend.progress(per, remaining, speed, self.progress_description)
 
754
        if per < 100:
 
755
            return True
 
756
        else:
 
757
            return False
 
758
 
 
759
    def data_available(self, source, condition):
 
760
        text = source.readline()
 
761
        if len(text) > 0:
 
762
            self.progress_description = text.strip('\n')
 
763
            return True
 
764
        else:
 
765
            return False
 
766
    
 
767
    # Exit routines
 
768
 
 
769
    def cleanup(self):
 
770
        t = {}
 
771
        for udi, timeout in self.timeouts.iteritems():
 
772
            if not gobject.source_remove(timeout):
 
773
                t[udi] = timeout
 
774
        self.timeouts = t
 
775
        print 'unmounting', self.mountpoints
 
776
        self.unmount(self.mountpoints)
542
777
 
543
778
    def quit(self):
544
 
        for dev in self.devices:
545
 
            mp = self.devices[dev]['mountpoint']
546
 
            if mp and mp.startswith('/tmp'):
547
 
                popen(['umount', mp])
548
 
                popen(['rmdir', mp])
 
779
        '''Called when the user has quit the application.'''
 
780
        self.cleanup()
549
781
 
550
782
    def on_end(self, pid, error_code):
551
 
        source = self.cds[self.install_source]
552
 
        if source['mountpoint'].startswith('/tmp'):
553
 
            self.log('Unmounting source volume.')
554
 
            popen(['umount', source['mountpoint']])
555
 
            popen(['rmdir', source['mountpoint']])
556
 
        if self.install_target['mountpoint'].startswith('/tmp'):
557
 
            self.log('Unmounting target volume.')
558
 
            popen(['umount', self.install_target['mountpoint']])
559
 
            popen(['rmdir', self.install_target['mountpoint']])
560
 
        self.log('Install command exited with code: %d' % error_code)
 
783
        # FIXME evand 2009-04-29:
 
784
        # unmount source and dest
 
785
        gobject.source_remove(self.watch)
 
786
        if self.copy_timeout:
 
787
            gobject.source_remove(self.copy_timeout)
 
788
        logging.debug('Install command exited with code: %d' % error_code)
561
789
        if error_code != 0:
562
790
            self.frontend.failed()
563
 
        # TODO: Needed?
564
 
        #gobject.source_remove(self.watch)
565
791
        self.frontend.finished()
566
 
 
567
 
    def copy_progress(self):
568
 
        now = free_space(self.install_target['mountpoint'])
569
 
        source = float(self.cds[self.install_source]['size'])
570
 
        source = source + (self.persistence_size * 1024 * 1024)
571
 
        done = (self.original_size - now)
572
 
        per = (done / source) * 100
573
 
        remaining, speed = self.estimator.estimate(done, source)
574
 
        self.frontend.progress(per, remaining, speed, self.progress_description)
575
 
        return True
576
 
 
577
 
    def data_available(self, source, condition):
578
 
        text = source.readline()
579
 
        if len(text) > 0:
580
 
            self.progress_description = text.strip('\n')
581
 
            if not self.copy_timeout:
582
 
                self.copy_timeout = gobject.timeout_add(2000, self.copy_progress)
583
 
            return True
584
 
        else:
585
 
            if self.copy_timeout:
586
 
                gobject.source_remove(self.copy_timeout)
587
 
            return False
588
 
 
589
 
    def IsStorageDevice(self, d):
590
 
        # We only care about usb devices currently
591
 
        if d.GetProperty('storage.bus') not in ['usb', 'mmc']:
592
 
            return False
593
 
        
594
 
        # Card readers are not removable, so nix this test.
595
 
        #if d.GetProperty('storage.removable') == False:
596
 
        #    return False
597
 
 
598
 
        # Known good drive types
599
 
        drive_types = ['disk',
600
 
                       'sd_mmc',
601
 
                       'smart_media',
602
 
                       'memory_stick',
603
 
                       'compact_flash']
604
 
        if d.GetProperty('storage.drive_type') in drive_types:
605
 
            return True
606