Use libudev to detect available cd/dvd devices (bnc#590707,fate#308980)
[platform/upstream/libzypp.git] / zypp / media / MediaCD.cc
1 /*---------------------------------------------------------------------\
2 |                          ____ _   __ __ ___                          |
3 |                         |__  / \ / / . \ . \                         |
4 |                           / / \ V /|  _/  _/                         |
5 |                          / /__ | | | | | |                           |
6 |                         /_____||_| |_| |_|                           |
7 |                                                                      |
8 \---------------------------------------------------------------------*/
9 /** \file zypp/media/MediaCD.cc
10  *
11 */
12 extern "C"
13 {
14 #include <sys/ioctl.h>
15 #include <linux/cdrom.h>
16 #include <libudev.h>
17 }
18
19 #include <cstring> // strerror
20 #include <cstdlib> // getenv
21 #include <iostream>
22
23 #include "zypp/base/Logger.h"
24 #include "zypp/ExternalProgram.h"
25 #include "zypp/media/Mount.h"
26 #include "zypp/media/MediaCD.h"
27 #include "zypp/media/MediaManager.h"
28 #include "zypp/Url.h"
29 #include "zypp/AutoDispose.h"
30
31
32
33 /*
34 ** if to throw exception on eject errors or ignore them
35 */
36 #define  REPORT_EJECT_ERRORS     1
37
38 /*
39 ** If defined to the full path of the eject utility,
40 ** it will be used additionally to the eject-ioctl.
41 */
42 #define EJECT_TOOL_PATH "/bin/eject"
43
44
45 using namespace std;
46
47 namespace zypp {
48   namespace media {
49
50 ///////////////////////////////////////////////////////////////////
51 //
52 //  CLASS NAME : MediaCD
53 //
54 ///////////////////////////////////////////////////////////////////
55
56
57   ///////////////////////////////////////////////////////////////////
58   //
59   //
60   //  METHOD NAME : MediaCD::MediaCD
61   //  METHOD TYPE : Constructor
62   //
63   //  DESCRIPTION :
64   //
65   MediaCD::MediaCD( const Url &      url_r,
66                     const Pathname & attach_point_hint_r )
67     : MediaHandler( url_r, attach_point_hint_r,
68                     url_r.getPathName(), // urlpath below attachpoint
69                     false ),
70       _lastdev(-1), _lastdev_tried(-1)
71   {
72     MIL << "MediaCD::MediaCD(" << url_r << ", " << attach_point_hint_r << ")"
73         << endl;
74
75     if( url_r.getScheme() != "dvd" && url_r.getScheme() != "cd")
76     {
77       ERR << "Unsupported schema in the Url: " << url_r.asString() << endl;
78       ZYPP_THROW(MediaUnsupportedUrlSchemeException(_url));
79     }
80
81     string devices = _url.getQueryParam("devices");
82     if (!devices.empty())
83     {
84       string::size_type pos;
85       DBG << "parse " << devices << endl;
86       while(!devices.empty())
87       {
88         pos = devices.find(',');
89         string device = devices.substr(0,pos);
90         if (!device.empty())
91         {
92           MediaSource media("cdrom", device, 0, 0);
93           _devices.push_back( media);
94           DBG << "use device (delayed verify)" << device << endl;
95         }
96         if (pos!=string::npos)
97           devices=devices.substr(pos+1);
98         else
99           devices.erase();
100       }
101     }
102     else
103     {
104       DBG << "going to use on-demand device list" << endl;
105       return;
106     }
107
108     if( _devices.empty())
109     {
110       ERR << "Unable to find any cdrom drive for " << _url.asString() << endl;
111       ZYPP_THROW(MediaBadUrlEmptyDestinationException(_url));
112     }
113   }
114
115   ///////////////////////////////////////////////////////////////////
116   //
117   //
118   //  METHOD NAME : MediaCD::openTray
119   //  METHOD TYPE : bool
120   //
121   bool MediaCD::openTray( const std::string & device_r )
122   {
123     int fd = ::open( device_r.c_str(), O_RDONLY|O_NONBLOCK );
124     int res = -1;
125
126     if ( fd != -1)
127     {
128       res = ::ioctl( fd, CDROMEJECT );
129       ::close( fd );
130     }
131
132     if ( res )
133     {
134       if( fd == -1)
135       {
136         WAR << "Unable to open '" << device_r
137             << "' (" << ::strerror( errno ) << ")" << endl;
138       }
139       else
140       {
141         WAR << "Eject " << device_r
142             << " failed (" << ::strerror( errno ) << ")" << endl;
143       }
144
145 #if defined(EJECT_TOOL_PATH)
146       DBG << "Try to eject " << device_r << " using "
147         << EJECT_TOOL_PATH << " utility" << std::endl;
148
149       const char *cmd[3];
150       cmd[0] = EJECT_TOOL_PATH;
151       cmd[1] = device_r.c_str();
152       cmd[2] = NULL;
153       ExternalProgram eject(cmd, ExternalProgram::Stderr_To_Stdout);
154
155       for(std::string out( eject.receiveLine());
156           out.length(); out = eject.receiveLine())
157       {
158         DBG << " " << out;
159       }
160
161       if(eject.close() != 0)
162       {
163         WAR << "Eject of " << device_r << " failed." << std::endl;
164         return false;
165       }
166 #else
167       return false;
168 #endif
169     }
170     MIL << "Eject of " << device_r << " successful." << endl;
171     return true;
172   }
173
174   ///////////////////////////////////////////////////////////////////
175   //
176   //
177   //    METHOD NAME : MediaCD::closeTray
178   //    METHOD TYPE : bool
179   //
180   bool MediaCD::closeTray( const std::string & device_r )
181   {
182     int fd = ::open( device_r.c_str(), O_RDONLY|O_NONBLOCK );
183     if ( fd == -1 ) {
184       WAR << "Unable to open '" << device_r << "' (" << ::strerror( errno ) << ")" << endl;
185       return false;
186     }
187     int res = ::ioctl( fd, CDROMCLOSETRAY );
188     ::close( fd );
189     if ( res ) {
190       WAR << "Close tray " << device_r << " failed (" << ::strerror( errno ) << ")" << endl;
191       return false;
192     }
193     DBG << "Close tray " << device_r << endl;
194     return true;
195   }
196
197   ///////////////////////////////////////////////////////////////////
198   //
199   //
200   //  METHOD NAME : MediaCD::detectDevices
201   //  METHOD TYPE : MediaCD::DeviceList
202   //
203   MediaCD::DeviceList MediaCD::detectDevices( bool supportingDVD ) const
204   {
205     zypp::AutoDispose<struct udev *> udev( ::udev_new(), ::udev_unref );
206     if ( ! udev )
207     {
208       ERR << "Can't create udev context." << endl;
209       return DeviceList();
210     }
211
212     zypp::AutoDispose<struct udev_enumerate *> enumerate( ::udev_enumerate_new(udev), ::udev_enumerate_unref );
213     if ( ! enumerate )
214     {
215       ERR << "Can't create udev list entry." << endl;
216       return DeviceList();
217     }
218
219     ::udev_enumerate_add_match_subsystem( enumerate, "block" );
220     ::udev_enumerate_add_match_property( enumerate, "ID_CDROM", "1" );
221     ::udev_enumerate_scan_devices( enumerate );
222
223     DeviceList detected;
224     struct udev_list_entry * entry = 0;
225     udev_list_entry_foreach( entry, ::udev_enumerate_get_list_entry( enumerate ) )
226     {
227       zypp::AutoDispose<struct udev_device *> device( ::udev_device_new_from_syspath( ::udev_enumerate_get_udev( enumerate ),
228                                                                                       ::udev_list_entry_get_name( entry ) ),
229                                                       ::udev_device_unref );
230       if ( ! device )
231       {
232         ERR << "Can't create udev device." << endl;
233         continue;
234       }
235
236       if ( supportingDVD && ! ::udev_device_get_property_value( device, "ID_CDROM_DVD" ) )
237       {
238         continue;       // looking for dvd only
239       }
240
241       const char * devnodePtr( ::udev_device_get_devnode( device ) );
242       if ( ! devnodePtr )
243       {
244         ERR << "Got NULL devicenode." << endl;
245         continue;
246       }
247
248       // In case we need it someday:
249       //const char * mountpath = ::udev_device_get_property_value( device, "FSTAB_DIR" );
250
251       PathInfo devnode( devnodePtr );
252       if ( devnode.isBlk() )
253       {
254         MediaSource media( "cdrom", devnode.path().asString(), devnode.major(), devnode.minor() );
255         DBG << "Found (udev): " << media << std::endl;
256         detected.push_back( media );
257       }
258     }
259     return detected;
260   }
261
262
263   ///////////////////////////////////////////////////////////////////
264   //
265   //
266   //  METHOD NAME : MediaCD::attachTo
267   //  METHOD TYPE : PMError
268   //
269   //  DESCRIPTION : Asserted that not already attached, and attachPoint is a directory.
270   //
271   void MediaCD::attachTo(bool next)
272   {
273     DBG << "next " << next << " last " << _lastdev << " last tried " << _lastdev_tried << endl;
274     if (next && _lastdev == -1)
275       ZYPP_THROW(MediaNotSupportedException(url()));
276
277     DeviceList detected(
278       detectDevices(_url.getScheme() == "dvd" ? true : false));
279
280     if(_devices.empty())
281     {
282       DBG << "creating on-demand device list" << endl;
283       //default is /dev/cdrom; for dvd: /dev/dvd if it exists
284       string device( "/dev/cdrom" );
285       if ( _url.getScheme() == "dvd" && PathInfo( "/dev/dvd" ).isBlk() )
286       {
287         device = "/dev/dvd";
288       }
289
290       PathInfo dinfo(device);
291       if( dinfo.isBlk())
292       {
293         MediaSource media("cdrom", device, dinfo.major(), dinfo.minor());
294
295         DeviceList::const_iterator d( detected.begin());
296         for( ; d != detected.end(); ++d)
297         {
298           // /dev/cdrom or /dev/dvd to the front
299           if( media.equals( *d))
300             _devices.push_front( *d);
301           else
302             _devices.push_back( *d);
303         }
304       }
305       else
306       {
307         // no /dev/cdrom or /dev/dvd link
308         _devices = detected;
309       }
310     }
311
312     Mount mount;
313     string mountpoint = attachPoint().asString();
314     bool mountsucceeded = false;
315     int count = 0;
316     MediaMountException merr;
317
318     string options = _url.getQueryParam("mountoptions");
319     if (options.empty())
320     {
321       options="ro";
322     }
323
324     //TODO: make configurable
325     list<string> filesystems;
326
327     // if DVD, try UDF filesystem before iso9660
328     if ( _url.getScheme() == "dvd" )
329       filesystems.push_back("udf");
330
331     filesystems.push_back("iso9660");
332
333     // try all devices in sequence
334     for (DeviceList::iterator it = _devices.begin()
335     ; !mountsucceeded && it != _devices.end()
336     ; ++it, count++ )
337     {
338       DBG << "count " << count << endl;
339       if (next && count <=_lastdev_tried )
340       {
341         DBG << "skipping device " << it->name << endl;
342         continue;
343       }
344
345       _lastdev_tried = count;
346
347       MediaSource temp( *it);
348       bool        valid=false;
349       PathInfo    dinfo(temp.name);
350       if( dinfo.isBlk())
351       {
352         temp.maj_nr = dinfo.major();
353         temp.min_nr = dinfo.minor();
354
355         DeviceList::const_iterator d( detected.begin());
356         for( ; d != detected.end(); ++d)
357         {
358           if( temp.equals( *d))
359           {
360             valid = true;
361             break;
362           }
363         }
364       }
365       if( !valid)
366       {
367         DBG << "skipping invalid device: " << it->name << endl;
368         continue;
369       }
370
371       MediaSourceRef media( new MediaSource(temp));
372       AttachedMedia ret( findAttachedMedia( media));
373
374       if( ret.mediaSource && ret.attachPoint &&
375          !ret.attachPoint->empty())
376       {
377         DBG << "Using a shared media "
378             << ret.mediaSource->name
379             << " attached on "
380             << ret.attachPoint->path
381             << endl;
382         removeAttachPoint();
383         setAttachPoint(ret.attachPoint);
384         setMediaSource(ret.mediaSource);
385         _lastdev = count;
386         mountsucceeded = true;
387         break;
388       }
389
390       {
391         MediaManager  manager;
392         MountEntries  entries( manager.getMountEntries());
393         MountEntries::const_iterator e;
394         for( e = entries.begin(); e != entries.end(); ++e)
395         {
396           bool        is_device = false;
397           std::string dev_path(Pathname(e->src).asString());
398           PathInfo    dev_info;
399
400           if( dev_path.compare(0, sizeof("/dev/")-1, "/dev/") == 0 &&
401               dev_info(e->src) && dev_info.isBlk())
402           {
403             is_device = true;
404           }
405
406           if( is_device && media->maj_nr == dev_info.major() &&
407                            media->min_nr == dev_info.minor())
408           {
409             AttachPointRef ap( new AttachPoint(e->dir, false));
410             AttachedMedia  am( media, ap);
411             {
412               DBG << "Using a system mounted media "
413                   << media->name
414                   << " attached on "
415                   << ap->path
416                   << endl;
417
418               media->iown = false; // mark attachment as foreign
419
420               setMediaSource(media);
421               setAttachPoint(ap);
422               _lastdev = count;
423               mountsucceeded = true;
424               break;
425             }
426           }
427         }
428         if( mountsucceeded)
429           break;
430       }
431
432       // close tray
433       closeTray( it->name );
434
435       // try all filesystems in sequence
436       for(list<string>::iterator fsit = filesystems.begin()
437           ; !mountsucceeded && fsit != filesystems.end()
438           ; ++fsit)
439       {
440         try
441         {
442           if( !isUseableAttachPoint(Pathname(mountpoint)))
443           {
444             mountpoint = createAttachPoint().asString();
445             setAttachPoint( mountpoint, true);
446             if( mountpoint.empty())
447             {
448               ZYPP_THROW( MediaBadAttachPointException(url()));
449             }
450           }
451
452           mount.mount(it->name, mountpoint, *fsit, options);
453
454           setMediaSource(media);
455
456           // wait for /etc/mtab update ...
457           // (shouldn't be needed)
458           int limit = 5;
459           while( !(mountsucceeded=isAttached()) && --limit)
460           {
461             sleep(1);
462           }
463
464           if( mountsucceeded)
465           {
466             _lastdev = count;
467           }
468           else
469           {
470             setMediaSource(MediaSourceRef());
471             try
472             {
473               mount.umount(attachPoint().asString());
474             }
475             catch (const MediaException & excpt_r)
476             {
477               ZYPP_CAUGHT(excpt_r);
478             }
479             ZYPP_THROW(MediaMountException(
480               "Unable to verify that the media was mounted",
481               it->name, mountpoint
482             ));
483           }
484         }
485         catch (const MediaMountException &e)
486         {
487           merr = e;
488           removeAttachPoint();
489           ZYPP_CAUGHT(e);
490         }
491         catch (const MediaException & excpt_r)
492         {
493           removeAttachPoint();
494           ZYPP_CAUGHT(excpt_r);
495         }
496       } // for filesystems
497     } // for _devices
498
499     if (!mountsucceeded)
500     {
501       _lastdev = -1;
502
503       if( !merr.mountOutput().empty())
504       {
505         ZYPP_THROW(MediaMountException(merr.mountError(),
506                                        _url.asString(),
507                                        mountpoint,
508                                        merr.mountOutput()));
509       }
510       else
511       {
512         ZYPP_THROW(MediaMountException("Mounting media failed",
513                                        _url.asString(), mountpoint));
514       }
515     }
516     DBG << _lastdev << " " << count << endl;
517   }
518
519
520   ///////////////////////////////////////////////////////////////////
521   //
522   //
523   //  METHOD NAME : MediaCD::releaseFrom
524   //  METHOD TYPE : PMError
525   //
526   //  DESCRIPTION : Asserted that media is attached.
527   //
528   void MediaCD::releaseFrom( const std::string & ejectDev )
529   {
530     Mount mount;
531     try
532     {
533       AttachedMedia am( attachedMedia());
534       if(am.mediaSource && am.mediaSource->iown)
535         mount.umount(am.attachPoint->path.asString());
536     }
537     catch (const Exception & excpt_r)
538     {
539       ZYPP_CAUGHT(excpt_r);
540       if (!ejectDev.empty())
541       {
542         forceRelaseAllMedia(false);
543         if(openTray( ejectDev ))
544           return;
545       }
546       ZYPP_RETHROW(excpt_r);
547     }
548
549     // eject device
550     if (!ejectDev.empty())
551     {
552       forceRelaseAllMedia(false);
553       if( !openTray( ejectDev ))
554       {
555 #if REPORT_EJECT_ERRORS
556         ZYPP_THROW(MediaNotEjectedException(ejectDev));
557 #endif
558       }
559     }
560   }
561
562   ///////////////////////////////////////////////////////////////////
563   //
564   //
565   //  METHOD NAME : MediaCD::forceEject
566   //  METHOD TYPE : void
567   //
568   // Asserted that media is not attached.
569   //
570   void MediaCD::forceEject(const std::string & ejectDev)
571   {
572     bool ejected=false;
573     if ( !isAttached()) {       // no device mounted in this instance
574
575       DeviceList detected(
576           detectDevices(_url.getScheme() == "dvd" ? true : false));
577
578       if(_devices.empty())
579       {
580         DBG << "creating on-demand device list" << endl;
581         //default is /dev/cdrom; for dvd: /dev/dvd if it exists
582         string device( "/dev/cdrom" );
583         if ( _url.getScheme() == "dvd" && PathInfo( "/dev/dvd" ).isBlk() ) {
584           device = "/dev/dvd";
585         }
586
587         PathInfo dinfo(device);
588         if( dinfo.isBlk())
589         {
590           MediaSource media("cdrom", device, dinfo.major(), dinfo.minor());
591
592           DeviceList::const_iterator d( detected.begin());
593           for( ; d != detected.end(); ++d)
594           {
595             // /dev/cdrom or /dev/dvd to the front
596             if( media.equals( *d))
597               _devices.push_front( *d);
598             else
599               _devices.push_back( *d);
600           }
601         }
602         else
603         {
604           // no /dev/cdrom or /dev/dvd link
605           _devices = detected;
606         }
607       }
608
609       DeviceList::iterator it;
610       for( it = _devices.begin(); it != _devices.end(); ++it ) {
611         MediaSourceRef media( new MediaSource( *it));
612         if (media->name != ejectDev)
613           continue;
614
615         bool        valid=false;
616         PathInfo    dinfo(media->name);
617         if( dinfo.isBlk())
618         {
619           media->maj_nr = dinfo.major();
620           media->min_nr = dinfo.minor();
621
622           DeviceList::const_iterator d( detected.begin());
623           for( ; d != detected.end(); ++d)
624           {
625             if( media->equals( *d))
626             {
627               valid = true;
628               break;
629             }
630           }
631         }
632         if( !valid)
633         {
634           DBG << "skipping invalid device: " << it->name << endl;
635           continue;
636         }
637
638         // FIXME: we have also to check if it is mounted in the system
639         AttachedMedia ret( findAttachedMedia( media));
640         if( !ret.mediaSource)
641         {
642           forceRelaseAllMedia(media, false);
643           if ( openTray( it->name ) )
644           {
645             ejected = true;
646             break; // on 1st success
647           }
648         }
649       }
650     }
651     if( !ejected)
652     {
653 #if REPORT_EJECT_ERRORS
654       ZYPP_THROW(MediaNotEjectedException());
655 #endif
656     }
657   }
658
659   ///////////////////////////////////////////////////////////////////
660   //
661   //  METHOD NAME : MediaCD::isAttached
662   //  METHOD TYPE : bool
663   //
664   //  DESCRIPTION : Override check if media is attached.
665   //
666   bool
667   MediaCD::isAttached() const
668   {
669     return checkAttached(false);
670   }
671
672   ///////////////////////////////////////////////////////////////////
673   //
674   //  METHOD NAME : MediaCD::getFile
675   //  METHOD TYPE : PMError
676   //
677   //  DESCRIPTION : Asserted that media is attached.
678   //
679   void MediaCD::getFile( const Pathname & filename ) const
680   {
681     MediaHandler::getFile( filename );
682   }
683
684   ///////////////////////////////////////////////////////////////////
685   //
686   //  METHOD NAME : MediaCD::getDir
687   //  METHOD TYPE : PMError
688   //
689   //  DESCRIPTION : Asserted that media is attached.
690   //
691   void MediaCD::getDir( const Pathname & dirname, bool recurse_r ) const
692   {
693     MediaHandler::getDir( dirname, recurse_r );
694   }
695
696   ///////////////////////////////////////////////////////////////////
697   //
698   //
699   //  METHOD NAME : MediaCD::getDirInfo
700   //  METHOD TYPE : PMError
701   //
702   //  DESCRIPTION : Asserted that media is attached and retlist is empty.
703   //
704   void MediaCD::getDirInfo( std::list<std::string> & retlist,
705                             const Pathname & dirname, bool dots ) const
706   {
707     MediaHandler::getDirInfo( retlist, dirname, dots );
708   }
709
710   ///////////////////////////////////////////////////////////////////
711   //
712   //
713   //  METHOD NAME : MediaCD::getDirInfo
714   //  METHOD TYPE : PMError
715   //
716   //  DESCRIPTION : Asserted that media is attached and retlist is empty.
717   //
718   void MediaCD::getDirInfo( filesystem::DirContent & retlist,
719                             const Pathname & dirname, bool dots ) const
720   {
721     MediaHandler::getDirInfo( retlist, dirname, dots );
722   }
723
724   bool MediaCD::getDoesFileExist( const Pathname & filename ) const
725   {
726     return MediaHandler::getDoesFileExist( filename );
727   }
728
729   bool MediaCD::hasMoreDevices()
730   {
731     if (_devices.size() == 0)
732       return false;
733     else if (_lastdev_tried < 0)
734       return true;
735
736     return (unsigned) _lastdev_tried < _devices.size() - 1;
737   }
738
739   void
740   MediaCD::getDetectedDevices(std::vector<std::string> & devices,
741                               unsigned int & index) const
742   {
743     index = 0;
744     if (!devices.empty())
745       devices.clear();
746
747     for (DeviceList::const_iterator it = _devices.begin();
748          it != _devices.end(); ++it)
749       devices.push_back(it->name);
750
751     if (_lastdev >= 0)
752       index = _lastdev;
753
754     // try to detect again if _devices are empty (maybe this method will be
755     // called before _devices get actually filled)
756     if (devices.empty())
757     {
758       DBG << "no device list so far, trying to detect" << endl;
759
760       DeviceList detected(
761         detectDevices(_url.getScheme() == "dvd" ? true : false));
762
763       for (DeviceList::const_iterator it = detected.begin();
764            it != detected.end(); ++it)
765         devices.push_back(it->name);
766
767       // don't know which one is in use in this case
768       index = 0;
769     }
770
771     MIL << "got " << devices.size() << " detected devices, current: "
772         << (index < devices.size() ? devices[index] : "<none>")
773         << "(" << index << ")" << endl;
774   }
775
776
777   } // namespace media
778 } // namespace zypp
779 // vim: set ts=8 sts=2 sw=2 ai noet: