Stroika Library 3.0d16
 
Loading...
Searching...
No Matches
MountedFilesystem.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2025. All rights reserved
3 */
4#include "Stroika/Foundation/StroikaPreComp.h"
5
6#if qStroika_Foundation_Common_Platform_Linux
7#include <fcntl.h>
8#include <poll.h>
9#include <unistd.h>
10#elif qStroika_Foundation_Common_Platform_MacOS
11#include <fstab.h>
12#elif qStroika_Foundation_Common_Platform_Windows
13#include <Windows.h>
14#include <winioctl.h>
15#endif
16
27
28#include "FileInputStream.h"
29#include "FileSystem.h"
30
31#include "MountedFilesystem.h"
32
33using std::byte;
34
35using namespace Stroika::Foundation;
38using namespace Stroika::Foundation::IO;
40
41// Comment this in to turn on aggressive noisy DbgTrace in this module
42//#define USE_NOISY_TRACE_IN_THIS_MODULE_ 1
43
44#if qStroika_Foundation_Common_Platform_Linux
45namespace {
46 // This is quirky, and only works for Linux, and /proc/mounts
47 struct Watcher_Proc_Mounts_ {
48 int mfd;
49 Watcher_Proc_Mounts_ (const filesystem::path& fn)
50 : mfd{::open (fn.c_str (), O_RDONLY, 0)}
51 {
52 }
53 ~Watcher_Proc_Mounts_ ()
54 {
55 ::close (mfd);
56 }
57 bool IsNewAvail () const
58 {
59 // according to http://stackoverflow.com/questions/5070801/monitoring-mount-point-changes-via-proc-mounts
60 // have to use poll with POLLPRI | POLLERR flags
61 //
62 // and from https://linux.die.net/man/5/proc
63 // /proc/[pid]/mounts (since Linux 2.4.19)...
64 // Since kernel version 2.6.15, this file is pollable: after opening the file for reading, a change in this file
65 // (i.e., a file system mount or unmount) causes select(2) to mark the file descriptor as readable, and poll(2)
66 // and epoll_wait(2) mark the file as having an error condition.
67 struct pollfd pfd;
68 int rv;
69 pfd.fd = mfd;
70 pfd.events = POLLERR | POLLPRI;
71 pfd.revents = 0;
72 if ((rv = ::poll (&pfd, 1, 0)) >= 0) {
73 if (pfd.revents & POLLERR) {
74 return true;
75 }
76 }
77 return false;
78 }
79 };
80}
81#endif
82
83#if qStroika_Foundation_Common_Platform_MacOS
84namespace {
85 // this also works on Linux, but is a horrible API
87 {
88 // @todo - note - this only appears to capture 'fixed disks' - not network mounts, and and virtual mount points like /dev/
89 KeyedCollection<MountedFilesystemType, filesystem::path> results{[] (const MountedFilesystemType& e) { return e.fMountedOn; }};
90 static mutex sMutex_; // this API (getfsent) is NOT threadsafe, but we can at least make our use re-entrant
91 [[maybe_unused]] lock_guard critSec{sMutex_};
92 [[maybe_unused]] auto&& cleanup = Execution::Finally ([&] () noexcept { ::endfsent (); });
93 while (fstab* fs = ::getfsent ()) {
94 results += MountedFilesystemType{fs->fs_file, Containers::Set<filesystem::path>{fs->fs_spec}, String::FromNarrowSDKString (fs->fs_vfstype)};
95 }
96 return results;
97 }
98}
99#endif
100
101namespace {
102 /*
103 * Something like this is used on many unix systems.
104 */
106 {
107 /*
108 * I haven't found this clearly documented yet, but it appears that a filesystem can be over-mounted.
109 * See https://www.kernel.org/doc/Documentation/filesystems/ramfs-rootfs-initramfs.txt
110 *
111 * So the last one with a given mount point in the file wins.
112 *
113 * EXAMPLE OUTPUT (from ubuntu on WSL):
114 * cat /proc/mounts
115 * /dev/sdb / ext4 rw,relatime,discard,errors=remount-ro,data=ordered 0 0
116 * tmpfs /mnt/wsl tmpfs rw,relatime 0 0
117 * tools /init 9p ro,dirsync,relatime,aname=tools;fmask=022,loose,access=client,trans=fd,rfd=6,wfd=6 0 0
118 * none /dev devtmpfs rw,nosuid,relatime,size=6474068k,nr_inodes=1618517,mode=755 0 0
119 * sysfs /sys sysfs rw,nosuid,nodev,noexec,noatime 0 0
120 * ...
121 */
122 KeyedCollection<MountedFilesystemType, filesystem::path> results{[] (const MountedFilesystemType& e) { return e.fMountedOn; }};
124 for (Sequence<String> line : reader.ReadMatrix (readStream)) {
125#if USE_NOISY_TRACE_IN_THIS_MODULE_
126 DbgTrace ("in IO::FileSystem::{}::ReadMountInfo_MTabLikeFile_ linesize={}, line[0]={}"_f, line.size (), line.empty () ? ""_k : line[0]);
127#endif
128 //
129 // https://www.centos.org/docs/5/html/5.2/Deployment_Guide/s2-proc-mounts.html
130 //
131 // 1 - device name
132 // 2 - mounted on
133 // 3 - fstype
134 //
135 if (line.size () >= 3) {
136 String devName = line[0];
137 // procfs/mounts often contains symbolic links to device files
138 // e.g. /dev/disk/by-uuid/e1d70192-1bb0-461d-b89f-b054e45bfa00
139 if (devName.StartsWith ("/"sv)) {
140 IgnoreExceptionsExceptThreadAbortForCall (devName = String{filesystem::canonical (devName.As<filesystem::path> ())});
141 }
142 filesystem::path mountedAt = line[1].As<filesystem::path> ();
143 String fstype = line[2];
144 static const String kNone_{L"none"sv};
145 results.Add (MountedFilesystemType{
146 mountedAt, devName == kNone_ ? Set<filesystem::path>{} : Set<filesystem::path>{devName.As<filesystem::path> ()}, fstype}); // special name none often used when there is no name
147 }
148 }
149 return results;
150 }
151}
152#if qStroika_Foundation_Common_Platform_Linux
153namespace {
155 {
156 // Note - /procfs files always unseekable
157 static const filesystem::path kUseFile2List_{"/proc/mounts"};
158 static const Watcher_Proc_Mounts_ sWatcher_{kUseFile2List_};
160 [] (const MountedFilesystemType& e) { return e.fMountedOn; }};
161 static bool sFirstTime_{true};
162 if (sFirstTime_ or sWatcher_.IsNewAvail ()) {
163 sLastResult_ = ReadMountInfo_MTabLikeFile_ (FileInputStream::New (kUseFile2List_, IO::FileSystem::FileInputStream::eNotSeekable));
164 sFirstTime_ = false;
165 }
166 return sLastResult_;
167 }
168}
169#endif
170#if qStroika_Foundation_Common_Platform_Linux
171namespace {
172 Collection<MountedFilesystemType> ReadMountInfo_ETC_MTAB_ ()
173 {
174 // Note - /procfs files always unseekable and this is sklink to /procfs
175 static const filesystem::path kUseFile2List_{"/etc/mtab"};
176 return ReadMountInfo_MTabLikeFile_ (FileInputStream::New (kUseFile2List_, IO::FileSystem::FileInputStream::eNotSeekable));
177 }
178}
179#endif
180#if qStroika_Foundation_Common_Platform_Windows
181namespace {
182 using DynamicDiskIDType_ = filesystem::path;
183 DynamicDiskIDType_ GetPhysNameForDriveNumber_ (unsigned int i)
184 {
185 // This format is NOT super well documented, and was mostly derived from reading the remarks section
186 // of https://msdn.microsoft.com/en-us/library/windows/desktop/aa363216%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
187 // (DeviceIoControl function)
188 return "\\\\.\\PhysicalDrive{}"_f(i).As<filesystem::path> ();
189 }
190 DISABLE_COMPILER_MSC_WARNING_START (6262) // stack usage OK
191 optional<Set<DynamicDiskIDType_>> GetDisksForVolume_ (String volumeName)
192 {
193 wchar_t volPathsBuf[10 * 1024]; // intentionally uninitialized since we don't use it if GetVolumePathNamesForVolumeNameW () returns error, and its an OUT only parameter
194 DWORD retLen = 0;
196 DWORD x = ::GetVolumePathNamesForVolumeNameW (get<0> (volumeName.c_str (&buf)), volPathsBuf,
197 static_cast<DWORD> (Memory::NEltsOf (volPathsBuf)), &retLen);
198 if (x == 0) {
199 return {}; // missing - no known - not empty - answer
200 }
201 else if (retLen <= 1) {
203 }
204 Assert (1 <= Characters::CString::Length (volPathsBuf) and Characters::CString::Length (volPathsBuf) < Memory::NEltsOf (volPathsBuf));
205 volumeName = L"\\\\.\\" + String::FromSDKString (volPathsBuf).SubString (0, -1);
206
207 // @todo - rewrite this - must somehow otherwise callocate this to be large enuf (dynamic alloc) - if we want more disk exents, but not sure when that happens...
208 VOLUME_DISK_EXTENTS volumeDiskExtents;
209 {
210 /*
211 * For reasons I don't understand (maybe a hit at http://superuser.com/questions/733687/give-regular-user-permission-to-access-physical-drive-on-windows)
212 * this only works with admin privileges
213 */
214 HANDLE hHandle = ::CreateFileW (volumeName.As<wstring> ().c_str (), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
215 OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
216 if (hHandle == INVALID_HANDLE_VALUE) {
217 return {};
218 }
219 DWORD dwBytesReturned = 0;
220 BOOL bResult = ::DeviceIoControl (hHandle, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, nullptr, 0, &volumeDiskExtents,
221 sizeof (volumeDiskExtents), &dwBytesReturned, NULL);
222 ::CloseHandle (hHandle);
223 if (not bResult) {
224 return {};
225 }
226 }
228 for (DWORD n = 0; n < volumeDiskExtents.NumberOfDiskExtents; ++n) {
229 result.Add (GetPhysNameForDriveNumber_ (volumeDiskExtents.Extents[n].DiskNumber));
230 }
231 return result;
232 }
233 DISABLE_COMPILER_MSC_WARNING_END (6262)
234
235 DISABLE_COMPILER_MSC_WARNING_START (6262) // stack usage OK
236 Containers::KeyedCollection<MountedFilesystemType, filesystem::path> GetMountedFilesystems_Windows_ ()
237 {
239 [] (const MountedFilesystemType& e) { return e.fMountedOn; }};
240 TCHAR volumeNameBuf[1024]; // intentionally uninitialized since OUT parameter and not used unless FindFirstVolume success
241
242 HANDLE hVol = INVALID_HANDLE_VALUE;
243 [[maybe_unused]] auto&& cleanup = Execution::Finally ([&] () noexcept {
244 if (hVol != INVALID_HANDLE_VALUE) {
245 ::CloseHandle (hVol);
246 }
247 });
248
249 for (hVol = ::FindFirstVolume (volumeNameBuf, static_cast<DWORD> (Memory::NEltsOf (volumeNameBuf))); hVol != INVALID_HANDLE_VALUE;) {
250 DWORD lpMaximumComponentLength;
251 DWORD dwSysFlags;
252 TCHAR fileSysNameBuf[1024];
253 if (::GetVolumeInformation (volumeNameBuf, nullptr, static_cast<DWORD> (Memory::NEltsOf (volumeNameBuf)), nullptr, &lpMaximumComponentLength,
254 &dwSysFlags, fileSysNameBuf, static_cast<DWORD> (Memory::NEltsOf (fileSysNameBuf)))) {
256 v.fFileSystemType = String::FromSDKString (fileSysNameBuf);
257 v.fVolumeID = String::FromSDKString (volumeNameBuf);
258 v.fDevicePaths = GetDisksForVolume_ (volumeNameBuf);
259
260 TCHAR volPathsBuf[10 * 1024]; // intentionally uninitialized
261 DWORD retLen = 0;
262 DWORD x = ::GetVolumePathNamesForVolumeName (volumeNameBuf, volPathsBuf, static_cast<DWORD> (Memory::NEltsOf (volPathsBuf)), &retLen);
263 if (x == 0) {
264 DbgTrace ("Ignoring error getting paths (volume='{}')"_f, String::FromSDKString (volumeNameBuf));
265 }
266 else if (volPathsBuf[0] == 0) {
267 // Ignore - unmounted!
268 DbgTrace ("Ignoring unmounted filesystem (volume='{}')"_f, String::FromSDKString (volumeNameBuf));
269 }
270 else {
271 for (const TCHAR* NameIdx = volPathsBuf; NameIdx[0] != L'\0'; NameIdx += Characters::CString::Length (NameIdx) + 1) {
272 v.fMountedOn = filesystem::path{NameIdx};
273 results.Add (v);
274 }
275 }
276 }
277
278 // find next
279 if (not::FindNextVolume (hVol, volumeNameBuf, static_cast<DWORD> (Memory::NEltsOf (volumeNameBuf)))) {
280 ::FindVolumeClose (hVol);
281 hVol = INVALID_HANDLE_VALUE;
282 }
283 }
284 return results;
285 }
286 DISABLE_COMPILER_MSC_WARNING_END (6262)
287}
288#endif
289
290/*
291 ********************************************************************************
292 ******************** IO::FileSystem::MountedFilesystemType *********************
293 ********************************************************************************
294 */
296{
297 StringBuilder sb;
298 sb << "{"sv;
299 sb << "Mounted-On: "sv << fMountedOn;
300 if (fDevicePaths) {
301 sb << ", Device-Paths: "sv << *fDevicePaths;
302 }
303 if (fFileSystemType) {
304 sb << ", FileSystem-Type: '"sv << *fFileSystemType << "'"sv;
305 }
306 if (fVolumeID) {
307 sb << ", Volume-ID: '"sv << *fVolumeID;
308 }
309 sb << "}"sv;
310 return sb;
311}
312
313/*
314 ********************************************************************************
315 ******************** IO::FileSystem::GetMountedFilesystems *********************
316 ********************************************************************************
317 */
319{
320#if qStroika_Foundation_Common_Platform_Linux
321 return ReadMountInfo_FromProcFSMounts_ ();
322#elif qStroika_Foundation_Common_Platform_MacOS
323 return ReadMountInfo_getfsent_ ();
324#elif qStroika_Foundation_Common_Platform_Windows
325 return GetMountedFilesystems_Windows_ ();
326#else
327 // @todo - maybe a start on macos would be to walk the directory /Volumes
329 return {};
330#endif
331}
#define WeakAssertNotImplemented()
Definition Assertions.h:483
#define DbgTrace
Definition Trace.h:309
Similar to String, but intended to more efficiently construct a String. Mutable type (String is large...
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
nonvirtual bool StartsWith(const Character &c, CompareOptions co=eWithCase) const
Definition String.cpp:1059
A Collection<T> is a container to manage an un-ordered collection of items, without equality defined ...
Definition Collection.h:102
a cross between Mapping<KEY, T> and Collection<T> and Set<T>
A generalization of a vector: a container whose elements are keyed by the natural numbers.
Definition Sequence.h:187
Set<T> is a container of T, where once an item is added, additionally adds () do nothing.
Definition Set.h:105
nonvirtual void Add(ArgByValueType< value_type > item)
Definition Set.inl:138
This COULD be easily used to read CSV files, or tab-delimited files, for example.
Wrap any object with Synchronized<> and it can be used similarly to the base type,...
Logically halfway between std::array and std::vector; Smart 'direct memory array' - which when needed...
InputStream<>::Ptr is Smart pointer (with abstract Rep) class defining the interface to reading from ...
nonvirtual CONTAINER_OF_T As(CONTAINER_OF_T_CONSTRUCTOR_ARGS... args) const
auto Finally(FUNCTION &&f) -> Private_::FinallySentry< FUNCTION >
Definition Finally.inl:31
Containers::KeyedCollection< MountedFilesystemType, filesystem::path > GetMountedFilesystems()
optional< Containers::Set< filesystem::path > > fDevicePaths