diff options
Diffstat (limited to 'drivers/acpi/bay.c')
-rw-r--r-- | drivers/acpi/bay.c | 592 |
1 files changed, 592 insertions, 0 deletions
diff --git a/drivers/acpi/bay.c b/drivers/acpi/bay.c new file mode 100644 index 000000000000..1cfc0b74b58c --- /dev/null +++ b/drivers/acpi/bay.c | |||
@@ -0,0 +1,592 @@ | |||
1 | /* | ||
2 | * bay.c - ACPI removable drive bay driver | ||
3 | * | ||
4 | * Copyright (C) 2006 Kristen Carlson Accardi <kristen.c.accardi@intel.com> | ||
5 | * | ||
6 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
7 | * | ||
8 | * This program is free software; you can redistribute it and/or modify | ||
9 | * it under the terms of the GNU General Public License as published by | ||
10 | * the Free Software Foundation; either version 2 of the License, or (at | ||
11 | * your option) any later version. | ||
12 | * | ||
13 | * This program is distributed in the hope that it will be useful, but | ||
14 | * WITHOUT ANY WARRANTY; without even the implied warranty of | ||
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
16 | * General Public License for more details. | ||
17 | * | ||
18 | * You should have received a copy of the GNU General Public License along | ||
19 | * with this program; if not, write to the Free Software Foundation, Inc., | ||
20 | * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. | ||
21 | * | ||
22 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
23 | */ | ||
24 | #include <linux/kernel.h> | ||
25 | #include <linux/module.h> | ||
26 | #include <linux/init.h> | ||
27 | #include <linux/types.h> | ||
28 | #include <linux/notifier.h> | ||
29 | #include <acpi/acpi_bus.h> | ||
30 | #include <acpi/acpi_drivers.h> | ||
31 | #include <linux/proc_fs.h> | ||
32 | #include <linux/seq_file.h> | ||
33 | #include <asm/uaccess.h> | ||
34 | |||
35 | #define ACPI_BAY_DRIVER_NAME "ACPI Removable Drive Bay Driver" | ||
36 | |||
37 | ACPI_MODULE_NAME("bay") | ||
38 | MODULE_AUTHOR("Kristen Carlson Accardi"); | ||
39 | MODULE_DESCRIPTION(ACPI_BAY_DRIVER_NAME); | ||
40 | MODULE_LICENSE("GPL"); | ||
41 | #define ACPI_BAY_CLASS "bay" | ||
42 | #define ACPI_BAY_COMPONENT 0x10000000 | ||
43 | #define _COMPONENT ACPI_BAY_COMPONENT | ||
44 | #define bay_dprintk(h,s) {\ | ||
45 | char prefix[80] = {'\0'};\ | ||
46 | struct acpi_buffer buffer = {sizeof(prefix), prefix};\ | ||
47 | acpi_get_name(h, ACPI_FULL_PATHNAME, &buffer);\ | ||
48 | printk(KERN_DEBUG PREFIX "%s: %s\n", prefix, s); } | ||
49 | |||
50 | static void bay_notify(acpi_handle handle, u32 event, void *data); | ||
51 | static int acpi_bay_add(struct acpi_device *device); | ||
52 | static int acpi_bay_remove(struct acpi_device *device, int type); | ||
53 | static int acpi_bay_match(struct acpi_device *device, | ||
54 | struct acpi_driver *driver); | ||
55 | |||
56 | static struct acpi_driver acpi_bay_driver = { | ||
57 | .name = ACPI_BAY_DRIVER_NAME, | ||
58 | .class = ACPI_BAY_CLASS, | ||
59 | .ops = { | ||
60 | .add = acpi_bay_add, | ||
61 | .remove = acpi_bay_remove, | ||
62 | .match = acpi_bay_match, | ||
63 | }, | ||
64 | }; | ||
65 | |||
66 | struct bay { | ||
67 | acpi_handle handle; | ||
68 | char *name; | ||
69 | struct list_head list; | ||
70 | struct proc_dir_entry *proc; | ||
71 | }; | ||
72 | |||
73 | LIST_HEAD(drive_bays); | ||
74 | |||
75 | static struct proc_dir_entry *acpi_bay_dir; | ||
76 | |||
77 | /***************************************************************************** | ||
78 | * Drive Bay functions * | ||
79 | *****************************************************************************/ | ||
80 | /** | ||
81 | * is_ejectable - see if a device is ejectable | ||
82 | * @handle: acpi handle of the device | ||
83 | * | ||
84 | * If an acpi object has a _EJ0 method, then it is ejectable | ||
85 | */ | ||
86 | static int is_ejectable(acpi_handle handle) | ||
87 | { | ||
88 | acpi_status status; | ||
89 | acpi_handle tmp; | ||
90 | |||
91 | status = acpi_get_handle(handle, "_EJ0", &tmp); | ||
92 | if (ACPI_FAILURE(status)) | ||
93 | return 0; | ||
94 | return 1; | ||
95 | } | ||
96 | |||
97 | /** | ||
98 | * bay_present - see if the bay device is present | ||
99 | * @bay: the drive bay | ||
100 | * | ||
101 | * execute the _STA method. | ||
102 | */ | ||
103 | static int bay_present(struct bay *bay) | ||
104 | { | ||
105 | unsigned long sta; | ||
106 | acpi_status status; | ||
107 | |||
108 | if (bay) { | ||
109 | status = acpi_evaluate_integer(bay->handle, "_STA", NULL, &sta); | ||
110 | if (ACPI_SUCCESS(status) && sta) | ||
111 | return 1; | ||
112 | } | ||
113 | return 0; | ||
114 | } | ||
115 | |||
116 | /** | ||
117 | * eject_device - respond to an eject request | ||
118 | * @handle - the device to eject | ||
119 | * | ||
120 | * Call this devices _EJ0 method. | ||
121 | */ | ||
122 | static void eject_device(acpi_handle handle) | ||
123 | { | ||
124 | struct acpi_object_list arg_list; | ||
125 | union acpi_object arg; | ||
126 | |||
127 | bay_dprintk(handle, "Ejecting device"); | ||
128 | |||
129 | arg_list.count = 1; | ||
130 | arg_list.pointer = &arg; | ||
131 | arg.type = ACPI_TYPE_INTEGER; | ||
132 | arg.integer.value = 1; | ||
133 | |||
134 | if (ACPI_FAILURE(acpi_evaluate_object(handle, "_EJ0", | ||
135 | &arg_list, NULL))) | ||
136 | pr_debug("Failed to evaluate _EJ0!\n"); | ||
137 | } | ||
138 | |||
139 | |||
140 | /** | ||
141 | * is_ata - see if a device is an ata device | ||
142 | * @handle: acpi handle of the device | ||
143 | * | ||
144 | * If an acpi object has one of 4 ATA ACPI methods defined, | ||
145 | * then it is an ATA device | ||
146 | */ | ||
147 | static int is_ata(acpi_handle handle) | ||
148 | { | ||
149 | acpi_handle tmp; | ||
150 | |||
151 | if ((ACPI_SUCCESS(acpi_get_handle(handle, "_GTF", &tmp))) || | ||
152 | (ACPI_SUCCESS(acpi_get_handle(handle, "_GTM", &tmp))) || | ||
153 | (ACPI_SUCCESS(acpi_get_handle(handle, "_STM", &tmp))) || | ||
154 | (ACPI_SUCCESS(acpi_get_handle(handle, "_SDD", &tmp)))) | ||
155 | return 1; | ||
156 | |||
157 | return 0; | ||
158 | } | ||
159 | |||
160 | /** | ||
161 | * parent_is_ata(acpi_handle handle) | ||
162 | * | ||
163 | */ | ||
164 | static int parent_is_ata(acpi_handle handle) | ||
165 | { | ||
166 | acpi_handle phandle; | ||
167 | |||
168 | if (acpi_get_parent(handle, &phandle)) | ||
169 | return 0; | ||
170 | |||
171 | return is_ata(phandle); | ||
172 | } | ||
173 | |||
174 | /** | ||
175 | * is_ejectable_bay - see if a device is an ejectable drive bay | ||
176 | * @handle: acpi handle of the device | ||
177 | * | ||
178 | * If an acpi object is ejectable and has one of the ACPI ATA | ||
179 | * methods defined, then we can safely call it an ejectable | ||
180 | * drive bay | ||
181 | */ | ||
182 | static int is_ejectable_bay(acpi_handle handle) | ||
183 | { | ||
184 | if ((is_ata(handle) || parent_is_ata(handle)) && is_ejectable(handle)) | ||
185 | return 1; | ||
186 | return 0; | ||
187 | } | ||
188 | |||
189 | /** | ||
190 | * eject_removable_drive - try to eject this drive | ||
191 | * @dev : the device structure of the drive | ||
192 | * | ||
193 | * If a device is a removable drive that requires an _EJ0 method | ||
194 | * to be executed in order to safely remove from the system, do | ||
195 | * it. ATM - always returns success | ||
196 | */ | ||
197 | int eject_removable_drive(struct device *dev) | ||
198 | { | ||
199 | acpi_handle handle = DEVICE_ACPI_HANDLE(dev); | ||
200 | |||
201 | if (handle) { | ||
202 | bay_dprintk(handle, "Got device handle"); | ||
203 | if (is_ejectable_bay(handle)) | ||
204 | eject_device(handle); | ||
205 | } else { | ||
206 | printk("No acpi handle for device\n"); | ||
207 | } | ||
208 | |||
209 | /* should I return an error code? */ | ||
210 | return 0; | ||
211 | } | ||
212 | EXPORT_SYMBOL_GPL(eject_removable_drive); | ||
213 | |||
214 | static int acpi_bay_add(struct acpi_device *device) | ||
215 | { | ||
216 | bay_dprintk(device->handle, "adding bay device"); | ||
217 | strcpy(acpi_device_name(device), "Dockable Bay"); | ||
218 | strcpy(acpi_device_class(device), "bay"); | ||
219 | return 0; | ||
220 | } | ||
221 | |||
222 | static int acpi_bay_status_seq_show(struct seq_file *seq, void *offset) | ||
223 | { | ||
224 | struct bay *bay = (struct bay *)seq->private; | ||
225 | |||
226 | if (!bay) | ||
227 | return 0; | ||
228 | |||
229 | if (bay_present(bay)) | ||
230 | seq_printf(seq, "present\n"); | ||
231 | else | ||
232 | seq_printf(seq, "removed\n"); | ||
233 | |||
234 | return 0; | ||
235 | } | ||
236 | |||
237 | static ssize_t | ||
238 | acpi_bay_write_eject(struct file *file, | ||
239 | const char __user * buffer, | ||
240 | size_t count, loff_t * data) | ||
241 | { | ||
242 | struct seq_file *m = (struct seq_file *)file->private_data; | ||
243 | struct bay *bay = (struct bay *)m->private; | ||
244 | char str[12] = { 0 }; | ||
245 | u32 state = 0; | ||
246 | |||
247 | /* FIXME - our only valid value here is 1 */ | ||
248 | if (!bay || count + 1 > sizeof str) | ||
249 | return -EINVAL; | ||
250 | |||
251 | if (copy_from_user(str, buffer, count)) | ||
252 | return -EFAULT; | ||
253 | |||
254 | str[count] = 0; | ||
255 | state = simple_strtoul(str, NULL, 0); | ||
256 | if (state) | ||
257 | eject_device(bay->handle); | ||
258 | |||
259 | return count; | ||
260 | } | ||
261 | |||
262 | static int | ||
263 | acpi_bay_status_open_fs(struct inode *inode, struct file *file) | ||
264 | { | ||
265 | return single_open(file, acpi_bay_status_seq_show, | ||
266 | PDE(inode)->data); | ||
267 | } | ||
268 | |||
269 | static int | ||
270 | acpi_bay_eject_open_fs(struct inode *inode, struct file *file) | ||
271 | { | ||
272 | return single_open(file, acpi_bay_status_seq_show, | ||
273 | PDE(inode)->data); | ||
274 | } | ||
275 | |||
276 | static struct file_operations acpi_bay_status_fops = { | ||
277 | .open = acpi_bay_status_open_fs, | ||
278 | .read = seq_read, | ||
279 | .llseek = seq_lseek, | ||
280 | .release = single_release, | ||
281 | }; | ||
282 | |||
283 | static struct file_operations acpi_bay_eject_fops = { | ||
284 | .open = acpi_bay_eject_open_fs, | ||
285 | .read = seq_read, | ||
286 | .write = acpi_bay_write_eject, | ||
287 | .llseek = seq_lseek, | ||
288 | .release = single_release, | ||
289 | }; | ||
290 | #if 0 | ||
291 | static struct file_operations acpi_bay_insert_fops = { | ||
292 | .open = acpi_bay_insert_open_fs, | ||
293 | .read = seq_read, | ||
294 | .llseek = seq_lseek, | ||
295 | .release = single_release, | ||
296 | }; | ||
297 | #endif | ||
298 | static int acpi_bay_add_fs(struct bay *bay) | ||
299 | { | ||
300 | struct proc_dir_entry *entry = NULL; | ||
301 | |||
302 | if (!bay) | ||
303 | return -EINVAL; | ||
304 | |||
305 | /* | ||
306 | * create a proc entry for this device | ||
307 | * we need to do this a little bit differently than normal | ||
308 | * acpi device drivers because our device may not be present | ||
309 | * at the moment, and therefore we have no acpi_device struct | ||
310 | */ | ||
311 | |||
312 | bay->proc = proc_mkdir(bay->name, acpi_bay_dir); | ||
313 | |||
314 | /* 'status' [R] */ | ||
315 | entry = create_proc_entry("status", | ||
316 | S_IRUGO, bay->proc); | ||
317 | if (!entry) | ||
318 | return -EIO; | ||
319 | else { | ||
320 | entry->proc_fops = &acpi_bay_status_fops; | ||
321 | entry->data = bay; | ||
322 | entry->owner = THIS_MODULE; | ||
323 | } | ||
324 | /* 'eject' [W] */ | ||
325 | entry = create_proc_entry("eject", | ||
326 | S_IWUGO, bay->proc); | ||
327 | if (!entry) | ||
328 | return -EIO; | ||
329 | else { | ||
330 | entry->proc_fops = &acpi_bay_eject_fops; | ||
331 | entry->data = bay; | ||
332 | entry->owner = THIS_MODULE; | ||
333 | } | ||
334 | #if 0 | ||
335 | /* 'insert' [W] */ | ||
336 | entry = create_proc_entry("insert", | ||
337 | S_IWUGO, bay->proc); | ||
338 | if (!entry) | ||
339 | return -EIO; | ||
340 | else { | ||
341 | entry->proc_fops = &acpi_bay_insert_fops; | ||
342 | entry->data = bay; | ||
343 | entry->owner = THIS_MODULE; | ||
344 | } | ||
345 | #endif | ||
346 | return 0; | ||
347 | } | ||
348 | |||
349 | static void acpi_bay_remove_fs(struct bay *bay) | ||
350 | { | ||
351 | if (!bay) | ||
352 | return; | ||
353 | |||
354 | if (bay->proc) { | ||
355 | remove_proc_entry("status", bay->proc); | ||
356 | remove_proc_entry("eject", bay->proc); | ||
357 | #if 0 | ||
358 | remove_proc_entry("insert", bay->proc); | ||
359 | #endif | ||
360 | remove_proc_entry(bay->name, acpi_bay_dir); | ||
361 | bay->proc = NULL; | ||
362 | } | ||
363 | } | ||
364 | |||
365 | static int bay_is_dock_device(acpi_handle handle) | ||
366 | { | ||
367 | acpi_handle parent; | ||
368 | |||
369 | acpi_get_parent(handle, &parent); | ||
370 | |||
371 | /* if the device or it's parent is dependent on the | ||
372 | * dock, then we are a dock device | ||
373 | */ | ||
374 | return (is_dock_device(handle) || is_dock_device(parent)); | ||
375 | } | ||
376 | |||
377 | static int bay_add(acpi_handle handle) | ||
378 | { | ||
379 | acpi_status status; | ||
380 | struct bay *new_bay; | ||
381 | struct acpi_buffer nbuffer = {ACPI_ALLOCATE_BUFFER, NULL}; | ||
382 | acpi_get_name(handle, ACPI_FULL_PATHNAME, &nbuffer); | ||
383 | |||
384 | bay_dprintk(handle, "Adding notify handler"); | ||
385 | |||
386 | /* | ||
387 | * if this is the first bay device found, make the root | ||
388 | * proc entry | ||
389 | */ | ||
390 | if (acpi_bay_dir == NULL) | ||
391 | acpi_bay_dir = proc_mkdir(ACPI_BAY_CLASS, acpi_root_dir); | ||
392 | |||
393 | /* | ||
394 | * Initialize bay device structure | ||
395 | */ | ||
396 | new_bay = kmalloc(GFP_ATOMIC, sizeof(*new_bay)); | ||
397 | INIT_LIST_HEAD(&new_bay->list); | ||
398 | new_bay->handle = handle; | ||
399 | new_bay->name = (char *)nbuffer.pointer; | ||
400 | list_add(&new_bay->list, &drive_bays); | ||
401 | acpi_bay_add_fs(new_bay); | ||
402 | |||
403 | /* register for events on this device */ | ||
404 | status = acpi_install_notify_handler(handle, ACPI_SYSTEM_NOTIFY, | ||
405 | bay_notify, new_bay); | ||
406 | if (ACPI_FAILURE(status)) { | ||
407 | printk(KERN_ERR PREFIX "Error installing bay notify handler\n"); | ||
408 | } | ||
409 | |||
410 | /* if we are on a dock station, we should register for dock | ||
411 | * notifications. | ||
412 | */ | ||
413 | if (bay_is_dock_device(handle)) { | ||
414 | bay_dprintk(handle, "Is dependent on dock\n"); | ||
415 | register_hotplug_dock_device(handle, bay_notify, new_bay); | ||
416 | } | ||
417 | printk(KERN_INFO PREFIX "Bay [%s] Added\n", new_bay->name); | ||
418 | return 0; | ||
419 | } | ||
420 | |||
421 | static int acpi_bay_remove(struct acpi_device *device, int type) | ||
422 | { | ||
423 | /*** FIXME: do something here */ | ||
424 | return 0; | ||
425 | } | ||
426 | |||
427 | static int acpi_bay_match(struct acpi_device *device, | ||
428 | struct acpi_driver *driver) | ||
429 | { | ||
430 | if (!device || !driver) | ||
431 | return -EINVAL; | ||
432 | |||
433 | if (is_ejectable_bay(device->handle)) { | ||
434 | bay_dprintk(device->handle, "matching bay device"); | ||
435 | return 0; | ||
436 | } | ||
437 | |||
438 | return -ENODEV; | ||
439 | } | ||
440 | |||
441 | /** | ||
442 | * bay_create_acpi_device - add new devices to acpi | ||
443 | * @handle - handle of the device to add | ||
444 | * | ||
445 | * This function will create a new acpi_device for the given | ||
446 | * handle if one does not exist already. This should cause | ||
447 | * acpi to scan for drivers for the given devices, and call | ||
448 | * matching driver's add routine. | ||
449 | * | ||
450 | * Returns a pointer to the acpi_device corresponding to the handle. | ||
451 | */ | ||
452 | static struct acpi_device * bay_create_acpi_device(acpi_handle handle) | ||
453 | { | ||
454 | struct acpi_device *device = NULL; | ||
455 | struct acpi_device *parent_device; | ||
456 | acpi_handle parent; | ||
457 | int ret; | ||
458 | |||
459 | bay_dprintk(handle, "Trying to get device"); | ||
460 | if (acpi_bus_get_device(handle, &device)) { | ||
461 | /* | ||
462 | * no device created for this object, | ||
463 | * so we should create one. | ||
464 | */ | ||
465 | bay_dprintk(handle, "No device for handle"); | ||
466 | acpi_get_parent(handle, &parent); | ||
467 | if (acpi_bus_get_device(parent, &parent_device)) | ||
468 | parent_device = NULL; | ||
469 | |||
470 | ret = acpi_bus_add(&device, parent_device, handle, | ||
471 | ACPI_BUS_TYPE_DEVICE); | ||
472 | if (ret) { | ||
473 | pr_debug("error adding bus, %x\n", | ||
474 | -ret); | ||
475 | return NULL; | ||
476 | } | ||
477 | } | ||
478 | return device; | ||
479 | } | ||
480 | |||
481 | /** | ||
482 | * bay_notify - act upon an acpi bay notification | ||
483 | * @handle: the bay handle | ||
484 | * @event: the acpi event | ||
485 | * @data: our driver data struct | ||
486 | * | ||
487 | */ | ||
488 | static void bay_notify(acpi_handle handle, u32 event, void *data) | ||
489 | { | ||
490 | struct acpi_device *dev; | ||
491 | struct bay *bay = data; | ||
492 | |||
493 | bay_dprintk(handle, "Bay event"); | ||
494 | |||
495 | switch(event) { | ||
496 | case ACPI_NOTIFY_BUS_CHECK: | ||
497 | printk("Bus Check\n"); | ||
498 | case ACPI_NOTIFY_DEVICE_CHECK: | ||
499 | printk("Device Check\n"); | ||
500 | dev = bay_create_acpi_device(handle); | ||
501 | if (dev) | ||
502 | acpi_bus_generate_event(dev, event, 0); | ||
503 | else | ||
504 | printk("No device for generating event\n"); | ||
505 | /* wouldn't it be a good idea to just rescan SATA | ||
506 | * right here? | ||
507 | */ | ||
508 | break; | ||
509 | case ACPI_NOTIFY_EJECT_REQUEST: | ||
510 | printk("Eject request\n"); | ||
511 | dev = bay_create_acpi_device(handle); | ||
512 | if (dev) | ||
513 | acpi_bus_generate_event(dev, event, 0); | ||
514 | else | ||
515 | printk("No device for generating eventn"); | ||
516 | |||
517 | /* wouldn't it be a good idea to just call the | ||
518 | * eject_device here if we were a SATA device? | ||
519 | */ | ||
520 | break; | ||
521 | default: | ||
522 | printk("unknown event %d\n", event); | ||
523 | } | ||
524 | } | ||
525 | |||
526 | static acpi_status | ||
527 | find_bay(acpi_handle handle, u32 lvl, void *context, void **rv) | ||
528 | { | ||
529 | int *count = (int *)context; | ||
530 | |||
531 | /* | ||
532 | * there could be more than one ejectable bay. | ||
533 | * so, just return AE_OK always so that every object | ||
534 | * will be checked. | ||
535 | */ | ||
536 | if (is_ejectable_bay(handle)) { | ||
537 | bay_dprintk(handle, "found ejectable bay"); | ||
538 | bay_add(handle); | ||
539 | (*count)++; | ||
540 | } | ||
541 | return AE_OK; | ||
542 | } | ||
543 | |||
544 | static int __init bay_init(void) | ||
545 | { | ||
546 | int bays = 0; | ||
547 | |||
548 | acpi_bay_dir = NULL; | ||
549 | INIT_LIST_HEAD(&drive_bays); | ||
550 | |||
551 | /* look for dockable drive bays */ | ||
552 | acpi_walk_namespace(ACPI_TYPE_DEVICE, ACPI_ROOT_OBJECT, | ||
553 | ACPI_UINT32_MAX, find_bay, &bays, NULL); | ||
554 | |||
555 | if (bays) { | ||
556 | if ((acpi_bus_register_driver(&acpi_bay_driver) < 0)) { | ||
557 | printk(KERN_ERR "Unable to register bay driver\n"); | ||
558 | if (acpi_bay_dir) | ||
559 | remove_proc_entry(ACPI_BAY_CLASS, | ||
560 | acpi_root_dir); | ||
561 | } | ||
562 | } | ||
563 | |||
564 | if (!bays) | ||
565 | return -ENODEV; | ||
566 | |||
567 | return 0; | ||
568 | } | ||
569 | |||
570 | static void __exit bay_exit(void) | ||
571 | { | ||
572 | struct bay *bay, *tmp; | ||
573 | |||
574 | list_for_each_entry_safe(bay, tmp, &drive_bays, list) { | ||
575 | if (is_dock_device(bay->handle)) | ||
576 | unregister_hotplug_dock_device(bay->handle); | ||
577 | acpi_bay_remove_fs(bay); | ||
578 | acpi_remove_notify_handler(bay->handle, ACPI_SYSTEM_NOTIFY, | ||
579 | bay_notify); | ||
580 | kfree(bay->name); | ||
581 | kfree(bay); | ||
582 | } | ||
583 | |||
584 | if (acpi_bay_dir) | ||
585 | remove_proc_entry(ACPI_BAY_CLASS, acpi_root_dir); | ||
586 | |||
587 | acpi_bus_unregister_driver(&acpi_bay_driver); | ||
588 | } | ||
589 | |||
590 | postcore_initcall(bay_init); | ||
591 | module_exit(bay_exit); | ||
592 | |||