1 #include "RemovableDriveManager.hpp"
2 #include "libslic3r/Platform.hpp"
3 #include <libslic3r/libslic3r.h>
4
5 #include <boost/nowide/convert.hpp>
6 #include <boost/log/trivial.hpp>
7
8 #if _WIN32
9 #include <windows.h>
10 #include <tchar.h>
11 #include <winioctl.h>
12 #include <shlwapi.h>
13
14 #include <Dbt.h>
15
16 #else
17 // unix, linux & OSX includes
18 #include <errno.h>
19 #include <sys/mount.h>
20 #include <sys/stat.h>
21 #include <glob.h>
22 #include <pwd.h>
23 #include <boost/filesystem.hpp>
24 #include <boost/system/error_code.hpp>
25 #include <boost/filesystem/convenience.hpp>
26 #include <boost/process.hpp>
27 #endif
28
29 namespace Slic3r {
30 namespace GUI {
31
32 wxDEFINE_EVENT(EVT_REMOVABLE_DRIVE_EJECTED, RemovableDriveEjectEvent);
33 wxDEFINE_EVENT(EVT_REMOVABLE_DRIVES_CHANGED, RemovableDrivesChangedEvent);
34
35 #if _WIN32
search_for_removable_drives() const36 std::vector<DriveData> RemovableDriveManager::search_for_removable_drives() const
37 {
38 // Get logical drives flags by letter in alphabetical order.
39 DWORD drives_mask = ::GetLogicalDrives();
40
41 // Allocate the buffers before the loop.
42 std::wstring volume_name;
43 std::wstring file_system_name;
44 // Iterate the Windows drives from 'C' to 'Z'
45 std::vector<DriveData> current_drives;
46 // Skip A and B drives.
47 drives_mask >>= 2;
48 for (char drive = 'C'; drive <= 'Z'; ++ drive, drives_mask >>= 1)
49 if (drives_mask & 1) {
50 std::string path { drive, ':' };
51 UINT drive_type = ::GetDriveTypeA(path.c_str());
52 // DRIVE_REMOVABLE on W are sd cards and usb thumbnails (not usb harddrives)
53 if (drive_type == DRIVE_REMOVABLE) {
54 // get name of drive
55 std::wstring wpath = boost::nowide::widen(path);
56 volume_name.resize(MAX_PATH + 1);
57 file_system_name.resize(MAX_PATH + 1);
58 BOOL error = ::GetVolumeInformationW(wpath.c_str(), volume_name.data(), sizeof(volume_name), nullptr, nullptr, nullptr, file_system_name.data(), sizeof(file_system_name));
59 if (error != 0) {
60 volume_name.erase(volume_name.begin() + wcslen(volume_name.c_str()), volume_name.end());
61 if (! file_system_name.empty()) {
62 ULARGE_INTEGER free_space;
63 ::GetDiskFreeSpaceExW(wpath.c_str(), &free_space, nullptr, nullptr);
64 if (free_space.QuadPart > 0) {
65 path += "\\";
66 current_drives.emplace_back(DriveData{ boost::nowide::narrow(volume_name), path });
67 }
68 }
69 }
70 }
71 }
72 return current_drives;
73 }
74
75 // Called from UI therefore it blocks the UI thread.
76 // It also blocks updates at the worker thread.
77 // Win32 implementation.
eject_drive()78 void RemovableDriveManager::eject_drive()
79 {
80 if (m_last_save_path.empty())
81 return;
82
83 #ifndef REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
84 this->update();
85 #endif // REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
86 BOOST_LOG_TRIVIAL(info) << "Ejecting started";
87 std::scoped_lock<std::mutex> lock(m_drives_mutex);
88 auto it_drive_data = this->find_last_save_path_drive_data();
89 if (it_drive_data != m_current_drives.end()) {
90 // get handle to device
91 std::string mpath = "\\\\.\\" + m_last_save_path;
92 mpath = mpath.substr(0, mpath.size() - 1);
93 HANDLE handle = CreateFileW(boost::nowide::widen(mpath).c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
94 if (handle == INVALID_HANDLE_VALUE) {
95 BOOST_LOG_TRIVIAL(error) << "Ejecting " << mpath << " failed (handle == INVALID_HANDLE_VALUE): " << GetLastError();
96 assert(m_callback_evt_handler);
97 if (m_callback_evt_handler)
98 wxPostEvent(m_callback_evt_handler, RemovableDriveEjectEvent(EVT_REMOVABLE_DRIVE_EJECTED, std::pair<DriveData, bool>(*it_drive_data, false)));
99 return;
100 }
101 DWORD deviceControlRetVal(0);
102 //these 3 commands should eject device safely but they dont, the device does disappear from file explorer but the "device was safely remove" notification doesnt trigger.
103 //sd cards does trigger WM_DEVICECHANGE messege, usb drives dont
104 BOOL e1 = DeviceIoControl(handle, FSCTL_LOCK_VOLUME, nullptr, 0, nullptr, 0, &deviceControlRetVal, nullptr);
105 BOOST_LOG_TRIVIAL(debug) << "FSCTL_LOCK_VOLUME " << e1 << " ; " << deviceControlRetVal << " ; " << GetLastError();
106 BOOL e2 = DeviceIoControl(handle, FSCTL_DISMOUNT_VOLUME, nullptr, 0, nullptr, 0, &deviceControlRetVal, nullptr);
107 BOOST_LOG_TRIVIAL(debug) << "FSCTL_DISMOUNT_VOLUME " << e2 << " ; " << deviceControlRetVal << " ; " << GetLastError();
108 // some implemenatations also calls IOCTL_STORAGE_MEDIA_REMOVAL here but it returns error to me
109 BOOL error = DeviceIoControl(handle, IOCTL_STORAGE_EJECT_MEDIA, nullptr, 0, nullptr, 0, &deviceControlRetVal, nullptr);
110 if (error == 0) {
111 CloseHandle(handle);
112 BOOST_LOG_TRIVIAL(error) << "Ejecting " << mpath << " failed (IOCTL_STORAGE_EJECT_MEDIA)" << deviceControlRetVal << " " << GetLastError();
113 assert(m_callback_evt_handler);
114 if (m_callback_evt_handler)
115 wxPostEvent(m_callback_evt_handler, RemovableDriveEjectEvent(EVT_REMOVABLE_DRIVE_EJECTED, std::pair<DriveData, bool>(*it_drive_data, false)));
116 return;
117 }
118 CloseHandle(handle);
119 BOOST_LOG_TRIVIAL(info) << "Ejecting finished";
120 assert(m_callback_evt_handler);
121 if (m_callback_evt_handler)
122 wxPostEvent(m_callback_evt_handler, RemovableDriveEjectEvent(EVT_REMOVABLE_DRIVE_EJECTED, std::pair< DriveData, bool >(std::move(*it_drive_data), true)));
123 m_current_drives.erase(it_drive_data);
124 }
125 }
126
get_removable_drive_path(const std::string & path)127 std::string RemovableDriveManager::get_removable_drive_path(const std::string &path)
128 {
129 #ifndef REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
130 this->update();
131 #endif // REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
132
133 std::scoped_lock<std::mutex> lock(m_drives_mutex);
134 if (m_current_drives.empty())
135 return std::string();
136 std::size_t found = path.find_last_of("\\");
137 std::string new_path = path.substr(0, found);
138 int letter = PathGetDriveNumberW(boost::nowide::widen(new_path).c_str());
139 for (const DriveData &drive_data : m_current_drives) {
140 char drive = drive_data.path[0];
141 if (drive == 'A' + letter)
142 return path;
143 }
144 return m_current_drives.front().path;
145 }
146
get_removable_drive_from_path(const std::string & path)147 std::string RemovableDriveManager::get_removable_drive_from_path(const std::string& path)
148 {
149 std::scoped_lock<std::mutex> lock(m_drives_mutex);
150 std::size_t found = path.find_last_of("\\");
151 std::string new_path = path.substr(0, found);
152 int letter = PathGetDriveNumberW(boost::nowide::widen(new_path).c_str());
153 for (const DriveData &drive_data : m_current_drives) {
154 assert(! drive_data.path.empty());
155 if (drive_data.path.front() == 'A' + letter)
156 return drive_data.path;
157 }
158 return std::string();
159 }
160
161 // Called by Win32 Volume arrived / detached callback.
volumes_changed()162 void RemovableDriveManager::volumes_changed()
163 {
164 if (m_initialized) {
165 // Signal the worker thread to wake up and enumerate removable drives.
166 m_wakeup = true;
167 m_thread_stop_condition.notify_all();
168 }
169 }
170
171 #else
172
173 namespace search_for_drives_internal
174 {
compare_filesystem_id(const std::string & path_a,const std::string & path_b)175 static bool compare_filesystem_id(const std::string &path_a, const std::string &path_b)
176 {
177 struct stat buf;
178 stat(path_a.c_str() ,&buf);
179 dev_t id_a = buf.st_dev;
180 stat(path_b.c_str() ,&buf);
181 dev_t id_b = buf.st_dev;
182 return id_a == id_b;
183 }
184
inspect_file(const std::string & path,const std::string & parent_path,std::vector<DriveData> & out)185 void inspect_file(const std::string &path, const std::string &parent_path, std::vector<DriveData> &out)
186 {
187 //confirms if the file is removable drive and adds it to vector
188
189 if (
190 #ifdef __linux__
191 // Chromium mounts removable drives in a way that produces the same device ID.
192 platform_flavor() == PlatformFlavor::LinuxOnChromium ||
193 #endif
194 // If not same file system - could be removable drive.
195 ! compare_filesystem_id(path, parent_path)) {
196 //free space
197 boost::system::error_code ec;
198 boost::filesystem::space_info si = boost::filesystem::space(path, ec);
199 if (!ec && si.available != 0) {
200 //user id
201 struct stat buf;
202 stat(path.c_str(), &buf);
203 uid_t uid = buf.st_uid;
204 std::string username(std::getenv("USER"));
205 struct passwd *pw = getpwuid(uid);
206 if (pw != 0 && pw->pw_name == username)
207 out.emplace_back(DriveData{ boost::filesystem::basename(boost::filesystem::path(path)), path });
208 }
209 }
210 }
211
search_path(const std::string & path,const std::string & parent_path,std::vector<DriveData> & out)212 static void search_path(const std::string &path, const std::string &parent_path, std::vector<DriveData> &out)
213 {
214 glob_t globbuf;
215 globbuf.gl_offs = 2;
216 int error = glob(path.c_str(), GLOB_TILDE, NULL, &globbuf);
217 if (error == 0) {
218 for (size_t i = 0; i < globbuf.gl_pathc; ++ i)
219 inspect_file(globbuf.gl_pathv[i], parent_path, out);
220 } else {
221 //if error - path probably doesnt exists so function just exits
222 //std::cout<<"glob error "<< error<< "\n";
223 }
224 globfree(&globbuf);
225 }
226 }
227
search_for_removable_drives() const228 std::vector<DriveData> RemovableDriveManager::search_for_removable_drives() const
229 {
230 std::vector<DriveData> current_drives;
231
232 #if __APPLE__
233
234 this->list_devices(current_drives);
235
236 #else
237
238 if (platform_flavor() == PlatformFlavor::LinuxOnChromium) {
239 // ChromeOS specific: search /mnt/chromeos/removable/* folder
240 search_for_drives_internal::search_path("/mnt/chromeos/removable/*", "/mnt/chromeos/removable", current_drives);
241 } else {
242 //search /media/* folder
243 search_for_drives_internal::search_path("/media/*", "/media", current_drives);
244
245 //search_path("/Volumes/*", "/Volumes");
246 std::string path(std::getenv("USER"));
247 std::string pp(path);
248
249 //search /media/USERNAME/* folder
250 pp = "/media/"+pp;
251 path = "/media/" + path + "/*";
252 search_for_drives_internal::search_path(path, pp, current_drives);
253
254 //search /run/media/USERNAME/* folder
255 path = "/run" + path;
256 pp = "/run"+pp;
257 search_for_drives_internal::search_path(path, pp, current_drives);
258 }
259
260 #endif
261
262 return current_drives;
263 }
264
265 // Called from UI therefore it blocks the UI thread.
266 // It also blocks updates at the worker thread.
267 // Unix & OSX implementation.
eject_drive()268 void RemovableDriveManager::eject_drive()
269 {
270 if (m_last_save_path.empty())
271 return;
272
273 #ifndef REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
274 this->update();
275 #endif // REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
276 #if __APPLE__
277 // If eject is still pending on the eject thread, wait until it finishes.
278 //FIXME while waiting for the eject thread to finish, the main thread is not pumping Cocoa messages, which may lead
279 // to blocking by the diskutil tool for a couple (up to 10) seconds. This is likely not critical, as the eject normally
280 // finishes quickly.
281 this->eject_thread_finish();
282 #endif
283
284 BOOST_LOG_TRIVIAL(info) << "Ejecting started";
285
286 DriveData drive_data;
287 {
288 std::scoped_lock<std::mutex> lock(m_drives_mutex);
289 auto it_drive_data = this->find_last_save_path_drive_data();
290 if (it_drive_data == m_current_drives.end())
291 return;
292 drive_data = *it_drive_data;
293 }
294
295 std::string correct_path(m_last_save_path);
296 #if __APPLE__
297 // On Apple, run the eject asynchronously on a worker thread, see the discussion at GH issue #4844.
298 m_eject_thread = new boost::thread([this, correct_path, drive_data]()
299 #endif
300 {
301 //std::cout<<"Ejecting "<<(*it).name<<" from "<< correct_path<<"\n";
302 // there is no usable command in c++ so terminal command is used instead
303 // but neither triggers "succesful safe removal messege"
304
305 BOOST_LOG_TRIVIAL(info) << "Ejecting started";
306 boost::process::ipstream istd_err;
307 boost::process::child child(
308 #if __APPLE__
309 boost::process::search_path("diskutil"), "eject", correct_path.c_str(), (boost::process::std_out & boost::process::std_err) > istd_err);
310 //Another option how to eject at mac. Currently not working.
311 //used insted of system() command;
312 //this->eject_device(correct_path);
313 #else
314 boost::process::search_path("umount"), correct_path.c_str(), (boost::process::std_out & boost::process::std_err) > istd_err);
315 #endif
316 std::string line;
317 while (child.running() && std::getline(istd_err, line)) {
318 BOOST_LOG_TRIVIAL(trace) << line;
319 }
320 // wait for command to finnish (blocks ui thread)
321 std::error_code ec;
322 child.wait(ec);
323 bool success = false;
324 if (ec) {
325 // The wait call can fail, as it did in https://github.com/prusa3d/PrusaSlicer/issues/5507
326 // It can happen even in cases where the eject is sucessful, but better report it as failed.
327 // We did not find a way to reliably retrieve the exit code of the process.
328 BOOST_LOG_TRIVIAL(error) << "boost::process::child::wait() failed during Ejection. State of Ejection is unknown. Error code: " << ec.value();
329 } else {
330 int err = child.exit_code();
331 if (err) {
332 BOOST_LOG_TRIVIAL(error) << "Ejecting failed. Exit code: " << err;
333 } else {
334 BOOST_LOG_TRIVIAL(info) << "Ejecting finished";
335 success = true;
336 }
337 }
338 assert(m_callback_evt_handler);
339 if (m_callback_evt_handler)
340 wxPostEvent(m_callback_evt_handler, RemovableDriveEjectEvent(EVT_REMOVABLE_DRIVE_EJECTED, std::pair<DriveData, bool>(drive_data, success)));
341 if (success) {
342 // Remove the drive_data from m_current drives, searching by value, not by pointer, as m_current_drives may get modified during
343 // asynchronous execution on m_eject_thread.
344 std::scoped_lock<std::mutex> lock(m_drives_mutex);
345 auto it = std::find(m_current_drives.begin(), m_current_drives.end(), drive_data);
346 if (it != m_current_drives.end())
347 m_current_drives.erase(it);
348 }
349 }
350 #if __APPLE__
351 );
352 #endif // __APPLE__
353 }
354
get_removable_drive_path(const std::string & path)355 std::string RemovableDriveManager::get_removable_drive_path(const std::string &path)
356 {
357 #ifndef REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
358 this->update();
359 #endif // REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
360
361 std::size_t found = path.find_last_of("/");
362 std::string new_path = found == path.size() - 1 ? path.substr(0, found) : path;
363
364 std::scoped_lock<std::mutex> lock(m_drives_mutex);
365 for (const DriveData &data : m_current_drives)
366 if (search_for_drives_internal::compare_filesystem_id(new_path, data.path))
367 return path;
368 return m_current_drives.empty() ? std::string() : m_current_drives.front().path;
369 }
370
get_removable_drive_from_path(const std::string & path)371 std::string RemovableDriveManager::get_removable_drive_from_path(const std::string& path)
372 {
373 std::size_t found = path.find_last_of("/");
374 std::string new_path = found == path.size() - 1 ? path.substr(0, found) : path;
375 // trim the filename
376 found = new_path.find_last_of("/");
377 new_path = new_path.substr(0, found);
378
379 // check if same filesystem
380 std::scoped_lock<std::mutex> lock(m_drives_mutex);
381 for (const DriveData &drive_data : m_current_drives)
382 if (search_for_drives_internal::compare_filesystem_id(new_path, drive_data.path))
383 return drive_data.path;
384 return std::string();
385 }
386 #endif
387
init(wxEvtHandler * callback_evt_handler)388 void RemovableDriveManager::init(wxEvtHandler *callback_evt_handler)
389 {
390 assert(! m_initialized);
391 assert(m_callback_evt_handler == nullptr);
392
393 if (m_initialized)
394 return;
395
396 m_initialized = true;
397 m_callback_evt_handler = callback_evt_handler;
398
399 #if __APPLE__
400 this->register_window_osx();
401 #endif
402
403 #ifdef REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
404 this->update();
405 #else // REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
406 // Don't call update() manually, as the UI triggered APIs call this->update() anyways.
407 m_thread = boost::thread((boost::bind(&RemovableDriveManager::thread_proc, this)));
408 #endif // REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
409 }
410
shutdown()411 void RemovableDriveManager::shutdown()
412 {
413 #if __APPLE__
414 // If eject is still pending on the eject thread, wait until it finishes.
415 //FIXME while waiting for the eject thread to finish, the main thread is not pumping Cocoa messages, which may lead
416 // to blocking by the diskutil tool for a couple (up to 10) seconds. This is likely not critical, as the eject normally
417 // finishes quickly.
418 this->eject_thread_finish();
419 #endif
420
421 #ifndef REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
422 if (m_thread.joinable()) {
423 // Stop the worker thread, if running.
424 {
425 // Notify the worker thread to cancel wait on detection polling.
426 std::lock_guard<std::mutex> lck(m_thread_stop_mutex);
427 m_stop = true;
428 }
429 m_thread_stop_condition.notify_all();
430 // Wait for the worker thread to stop.
431 m_thread.join();
432 m_stop = false;
433 }
434 #endif // REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
435
436 m_initialized = false;
437 m_callback_evt_handler = nullptr;
438 }
439
set_and_verify_last_save_path(const std::string & path)440 bool RemovableDriveManager::set_and_verify_last_save_path(const std::string &path)
441 {
442 #ifndef REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
443 this->update();
444 #endif // REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
445 m_last_save_path = this->get_removable_drive_from_path(path);
446 m_exporting_finished = false;
447 return ! m_last_save_path.empty();
448 }
449
status()450 RemovableDriveManager::RemovableDrivesStatus RemovableDriveManager::status()
451 {
452
453 RemovableDriveManager::RemovableDrivesStatus out;
454 {
455 std::scoped_lock<std::mutex> lock(m_drives_mutex);
456 out.has_eject =
457 // Cannot control eject on Chromium.
458 platform_flavor() != PlatformFlavor::LinuxOnChromium &&
459 this->find_last_save_path_drive_data() != m_current_drives.end();
460 out.has_removable_drives = ! m_current_drives.empty();
461 }
462 if (! out.has_eject)
463 m_last_save_path.clear();
464 out.has_eject = out.has_eject && m_exporting_finished;
465 return out;
466 }
467
468 // Update is called from thread_proc() and from most of the public methods on demand.
update()469 void RemovableDriveManager::update()
470 {
471 std::unique_lock<std::mutex> inside_update_lock(m_inside_update_mutex, std::defer_lock);
472 #ifdef _WIN32
473 // All wake up calls up to now are now consumed when the drive enumeration starts.
474 m_wakeup = false;
475 #endif // _WIN32
476 if (inside_update_lock.try_lock()) {
477 // Got the lock without waiting. That means, the update was not running.
478 // Run the update.
479 std::vector<DriveData> current_drives = this->search_for_removable_drives();
480 // Post update events.
481 std::scoped_lock<std::mutex> lock(m_drives_mutex);
482 std::sort(current_drives.begin(), current_drives.end());
483 if (current_drives != m_current_drives) {
484 assert(m_callback_evt_handler);
485 if (m_callback_evt_handler)
486 wxPostEvent(m_callback_evt_handler, RemovableDrivesChangedEvent(EVT_REMOVABLE_DRIVES_CHANGED));
487 }
488 m_current_drives = std::move(current_drives);
489 } else {
490 // Acquiring the m_iniside_update lock failed, therefore another update is running.
491 // Just block until the other instance of update() finishes.
492 inside_update_lock.lock();
493 }
494 }
495
496 #ifndef REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
thread_proc()497 void RemovableDriveManager::thread_proc()
498 {
499 // Signal the worker thread to update initially.
500 #ifdef _WIN32
501 m_wakeup = true;
502 #endif // _WIN32
503
504 for (;;) {
505 // Wait for 2 seconds before running the disk enumeration.
506 // Cancellable.
507 {
508 std::unique_lock<std::mutex> lck(m_thread_stop_mutex);
509 #ifdef _WIN32
510 // Reacting to updates by WM_DEVICECHANGE and WM_USER_MEDIACHANGED
511 m_thread_stop_condition.wait(lck, [this]{ return m_stop || m_wakeup; });
512 #else
513 m_thread_stop_condition.wait_for(lck, std::chrono::seconds(2), [this]{ return m_stop; });
514 #endif
515 }
516 if (m_stop)
517 // Stop the worker thread.
518 break;
519 // Update m_current drives and send out update events.
520 this->update();
521 }
522 }
523 #endif // REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS
524
find_last_save_path_drive_data() const525 std::vector<DriveData>::const_iterator RemovableDriveManager::find_last_save_path_drive_data() const
526 {
527 return Slic3r::binary_find_by_predicate(m_current_drives.begin(), m_current_drives.end(),
528 [this](const DriveData &data){ return data.path < m_last_save_path; },
529 [this](const DriveData &data){ return data.path == m_last_save_path; });
530 }
531
532 #if __APPLE__
eject_thread_finish()533 void RemovableDriveManager::eject_thread_finish()
534 {
535 if (m_eject_thread) {
536 m_eject_thread->join();
537 delete m_eject_thread;
538 m_eject_thread = nullptr;
539 }
540 }
541 #endif // __APPLE__
542
543 }} // namespace Slic3r::GUI
544