Branch data Line data Source code
1 : : #pragma once
2 : :
3 : : /*
4 : : MIT License
5 : :
6 : : Copyright (c) 2014-2024 Stephane Cuillerdier (aka aiekick)
7 : :
8 : : Permission is hereby granted, free of charge, to any person obtaining a copy
9 : : of this software and associated documentation files (the "Software"), to deal
10 : : in the Software without restriction, including without limitation the rights
11 : : to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 : : copies of the Software, and to permit persons to whom the Software is
13 : : furnished to do so, subject to the following conditions:
14 : :
15 : : The above copyright notice and this permission notice shall be included in all
16 : : copies or substantial portions of the Software.
17 : :
18 : : THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 : : IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 : : FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 : : AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 : : LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 : : OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 : : SOFTWARE.
25 : : */
26 : :
27 : : // ezFile is part of the ezLibs project : https://github.com/aiekick/ezLibs.git
28 : :
29 : : #include <map>
30 : : #include <set>
31 : : #include <array>
32 : : #include <regex>
33 : : #include <mutex>
34 : : #include <ctime>
35 : : #include <string>
36 : : #include <vector>
37 : : #include <thread>
38 : : #include <atomic>
39 : : #include <memory>
40 : : #include <sstream>
41 : : #include <fstream>
42 : : #include <cstdint>
43 : : #include <iterator>
44 : : #include <iostream>
45 : : #include <functional>
46 : : #include <unordered_map>
47 : :
48 : : #include "ezOS.hpp"
49 : : #include "ezApp.hpp"
50 : : #include "ezStr.hpp"
51 : : #include "ezLog.hpp"
52 : : #include <sys/stat.h>
53 : :
54 : : #ifdef WINDOWS_OS
55 : : #ifndef EZ_FILE_SLASH_TYPE
56 : : #define EZ_FILE_SLASH_TYPE "\\"
57 : : #endif // EZ_FILE_SLASH_TYPE
58 : : #include <Windows.h>
59 : : #include <shellapi.h> // ShellExecute
60 : : #define stat _stat
61 : : #else // UNIX_OS
62 : : #include <unistd.h> // rmdir
63 : : #ifndef EZ_FILE_SLASH_TYPE
64 : 54 : #define EZ_FILE_SLASH_TYPE "/"
65 : : #endif // EZ_FILE_SLASH_TYPE
66 : : #ifdef APPLE_OS
67 : : #include <CoreServices/CoreServices.h>
68 : : #else
69 : : #include <sys/inotify.h>
70 : : #endif
71 : : #endif
72 : :
73 : : namespace ez {
74 : : namespace file {
75 : :
76 : 10 : inline std::string loadFileToString(const std::string &vFilePathName, bool vVerbose = true) {
77 : 10 : std::string ret;
78 : 10 : std::ifstream docFile(vFilePathName, std::ios::in);
79 [ + - ]: 10 : if (docFile.is_open()) {
80 : 10 : std::stringstream strStream;
81 : 10 : strStream << docFile.rdbuf(); // read the file
82 : 10 : ret = strStream.str();
83 : 10 : ez::str::replaceString(ret, "\r\n", "\n");
84 : 10 : ez::str::replaceString(ret, "\r", "\n");
85 : 10 : docFile.close();
86 : 10 : } else {
87 [ # # ]: 0 : if (vVerbose) {
88 : 0 : #ifdef EZ_TOOLS_LOG
89 : 0 : LogVarError("File \"%s\" Not Found !\n", vFilePathName.c_str());
90 : 0 : #endif
91 : 0 : }
92 : 0 : }
93 : 10 : return ret;
94 : 10 : }
95 : :
96 : 4 : inline bool saveStringToFile(const std::string &vDatas, const std::string &vFilePathName, bool vAddTimeStamp = false) {
97 : 4 : std::string fpn = vFilePathName;
98 [ + - ]: 4 : if (!fpn.empty()) {
99 [ - + ]: 4 : if (vAddTimeStamp) {
100 : 0 : auto dot_p = fpn.find_last_of('.');
101 : 0 : time_t epoch = std::time(nullptr);
102 [ # # ]: 0 : if (dot_p != std::string::npos) {
103 : 0 : fpn = fpn.substr(0, dot_p) + ez::str::toStr("_%llu", epoch) + fpn.substr(dot_p);
104 : 0 : } else {
105 : 0 : fpn += ez::str::toStr("_%llu", epoch);
106 : 0 : }
107 : 0 : }
108 : 4 : std::ofstream configFileWriter(fpn, std::ios::out);
109 [ + - ]: 4 : if (!configFileWriter.bad()) {
110 : 4 : configFileWriter << vDatas;
111 : 4 : configFileWriter.close();
112 : 4 : return true;
113 : 4 : }
114 : 4 : }
115 : 0 : return false;
116 : 4 : }
117 : :
118 : 1 : inline std::vector<uint8_t> loadFileToBin(const std::string &vFilePathName) {
119 : 1 : std::vector<uint8_t> ret;
120 : 1 : std::ifstream file(vFilePathName, std::ios::binary | std::ios::ate);
121 [ + - ]: 1 : if (file.is_open()) {
122 : 1 : std::streamsize size = file.tellg(); // taille du fichier
123 [ - + ]: 1 : if (size <= 0) {
124 : 0 : return ret;
125 : 0 : }
126 : 1 : ret.resize(static_cast<size_t>(size));
127 : 1 : file.seekg(0, std::ios::beg);
128 [ - + ]: 1 : if (!file.read(reinterpret_cast<char *>(&ret[0]), size)) {
129 : 0 : ret.clear();
130 : 0 : }
131 : 1 : }
132 : 1 : return ret;
133 : 1 : }
134 : :
135 : 1 : inline bool saveBinToFile(const std::vector<uint8_t> &vDatas, const std::string &vFilePathName, bool vAddTimeStamp = false) {
136 : 1 : std::string fpn = vFilePathName;
137 [ + - ]: 1 : if (!fpn.empty()) {
138 [ - + ]: 1 : if (vAddTimeStamp) {
139 : 0 : auto dot_p = fpn.find_last_of('.');
140 : 0 : time_t epoch = std::time(nullptr);
141 [ # # ]: 0 : if (dot_p != std::string::npos) {
142 : 0 : fpn = fpn.substr(0, dot_p) + ez::str::toStr("_%llu", epoch) + fpn.substr(dot_p);
143 : 0 : } else {
144 : 0 : fpn += ez::str::toStr("_%llu", epoch);
145 : 0 : }
146 : 0 : }
147 : 1 : std::ofstream out(fpn, std::ios::binary | std::ios::trunc);
148 [ + - ]: 1 : if (!out.bad()) {
149 : 1 : out.write(reinterpret_cast<const char *>(vDatas.data()), vDatas.size());
150 : 1 : out.close();
151 : 1 : return true;
152 : 1 : }
153 : 1 : }
154 : 0 :
155 : 0 : return false;
156 : 1 : }
157 : :
158 : : /* correct file path between os and different slash type between window and unix */
159 : 23 : inline std::string correctSlashTypeForFilePathName(const std::string &vFilePathName) {
160 : 23 : std::string res = vFilePathName;
161 : 23 : ez::str::replaceString(res, "\\", EZ_FILE_SLASH_TYPE);
162 : 23 : ez::str::replaceString(res, "/", EZ_FILE_SLASH_TYPE);
163 : 23 : return res;
164 : 23 : }
165 : :
166 : : struct PathInfos {
167 : : std::string path;
168 : : std::string name;
169 : : std::string ext;
170 : : bool isOk = false;
171 : :
172 : 1 : PathInfos() { isOk = false; }
173 : :
174 : 0 : PathInfos(const std::string &vPath, const std::string &vName, const std::string &vExt) {
175 : 0 : isOk = true;
176 : 0 : path = vPath;
177 : 0 : name = vName;
178 : 0 : ext = vExt;
179 : 0 :
180 : 0 : if (ext.empty()) {
181 : 0 : const size_t lastPoint = name.find_last_of('.');
182 : 0 : if (lastPoint != std::string::npos) {
183 : 0 : ext = name.substr(lastPoint + 1);
184 : 0 : name = name.substr(0, lastPoint);
185 : 0 : }
186 : 0 : }
187 : 0 : }
188 : :
189 : 0 : std::string GetFPNE() const { return GetFPNE_WithPathNameExt(path, name, ext); }
190 : :
191 : 0 : std::string GetFPNE_WithPathNameExt(std::string vPath, const std::string &vName, const std::string &vExt) const {
192 : 0 : if (vPath[0] == EZ_FILE_SLASH_TYPE[0]) {
193 : 0 : #ifdef WINDOWS_OS
194 : 0 : // if it happening on window this seem that this path msut be a relative path but with an error
195 : 0 : vPath = vPath.substr(1); // bad formated path go relative
196 : 0 : #endif
197 : 0 : } else {
198 : 0 : #ifdef UNIX_OS
199 : 0 : vPath = "/" + vPath; // make it absolute
200 : 0 : #endif
201 : 0 : }
202 : 0 : std::string ext = vExt;
203 : 0 : if (!ext.empty()) {
204 : 0 : ext = '.' + ext;
205 : 0 : }
206 : 0 :
207 : 0 : if (vPath.empty()) {
208 : 0 : return vName + ext;
209 : 0 : }
210 : 0 :
211 : 0 : return vPath + EZ_FILE_SLASH_TYPE + vName + ext;
212 : 0 : }
213 : :
214 : 0 : std::string GetFPNE_WithPath(const std::string &vPath) const { return GetFPNE_WithPathNameExt(vPath, name, ext); }
215 : :
216 : 0 : std::string GetFPNE_WithPathName(const std::string &vPath, const std::string &vName) const { return GetFPNE_WithPathNameExt(vPath, vName, ext); }
217 : :
218 : 0 : std::string GetFPNE_WithPathExt(const std::string &vPath, const std::string &vExt) { return GetFPNE_WithPathNameExt(vPath, name, vExt); }
219 : :
220 : 0 : std::string GetFPNE_WithName(const std::string &vName) const { return GetFPNE_WithPathNameExt(path, vName, ext); }
221 : :
222 : 0 : std::string GetFPNE_WithNameExt(const std::string &vName, const std::string &vExt) const { return GetFPNE_WithPathNameExt(path, vName, vExt); }
223 : :
224 : 0 : std::string GetFPNE_WithExt(const std::string &vExt) const { return GetFPNE_WithPathNameExt(path, name, vExt); }
225 : : };
226 : :
227 : 1 : inline PathInfos parsePathFileName(const std::string &vPathFileName) {
228 : 1 : PathInfos res;
229 [ + - ]: 1 : if (!vPathFileName.empty()) {
230 : 1 : const std::string pfn = correctSlashTypeForFilePathName(vPathFileName);
231 [ + - ]: 1 : if (!pfn.empty()) {
232 : 1 : const size_t lastSlash = pfn.find_last_of(EZ_FILE_SLASH_TYPE);
233 [ + - ]: 1 : if (lastSlash != std::string::npos) {
234 : 1 : res.name = pfn.substr(lastSlash + 1);
235 : 1 : res.path = pfn.substr(0, lastSlash);
236 : 1 : res.isOk = true;
237 : 1 : }
238 : 1 : const size_t lastPoint = pfn.find_last_of('.');
239 [ + - ]: 1 : if (lastPoint != std::string::npos) {
240 [ - + ]: 1 : if (!res.isOk) {
241 : 0 : res.name = pfn;
242 : 0 : res.isOk = true;
243 : 0 : }
244 : 1 : res.ext = pfn.substr(lastPoint + 1);
245 : 1 : ez::str::replaceString(res.name, "." + res.ext, "");
246 : 1 : }
247 [ - + ]: 1 : if (!res.isOk) {
248 : 0 : res.name = pfn;
249 : 0 : res.isOk = true;
250 : 0 : }
251 : 1 : }
252 : 1 : }
253 : 1 : return res;
254 : 1 : }
255 : 1 : inline std::string simplifyFilePath(const std::string &vFilePath) {
256 : 1 : std::string path = correctSlashTypeForFilePathName(vFilePath);
257 : 1 : std::vector<std::string> parts;
258 : 1 : std::stringstream ss(path);
259 : 1 : std::string item;
260 : 0 :
261 [ + + ]: 5 : while (std::getline(ss, item, EZ_FILE_SLASH_TYPE[0])) {
262 [ - + ][ - + ]: 4 : if (item == "" || item == ".")
263 : 0 : continue;
264 [ + + ]: 4 : if (item == "..") {
265 [ + - ]: 1 : if (!parts.empty())
266 : 1 : parts.pop_back();
267 : 0 : // sinon on est au-dessus de root => on ignore
268 : 3 : } else {
269 : 3 : parts.push_back(item);
270 : 3 : }
271 : 4 : }
272 : 0 :
273 : 1 : std::string result;
274 [ + + ]: 3 : for (size_t i = 0; i < parts.size(); ++i) {
275 : 2 : result += parts[i];
276 [ + + ]: 2 : if (i < parts.size() - 1)
277 : 1 : result += EZ_FILE_SLASH_TYPE;
278 : 2 : }
279 : 1 : return result;
280 : 1 : }
281 : :
282 : 1 : inline std::string composePath(const std::string &vPath, const std::string &vFileName, const std::string &vExt) {
283 : 1 : const auto path = correctSlashTypeForFilePathName(vPath);
284 : 1 : std::string res = path;
285 [ + - ]: 1 : if (!vFileName.empty()) {
286 [ + - ]: 1 : if (!path.empty()) {
287 : 1 : res += EZ_FILE_SLASH_TYPE;
288 : 1 : }
289 : 1 : res += vFileName;
290 [ + - ]: 1 : if (!vExt.empty()) {
291 : 1 : res += "." + vExt;
292 : 1 : }
293 : 1 : }
294 : 1 : return res;
295 : 1 : }
296 : :
297 : 5 : inline bool isFileExist(const std::string &name) {
298 : 5 : auto fileToOpen = correctSlashTypeForFilePathName(name);
299 : 5 : ez::str::replaceString(fileToOpen, "\"", "");
300 : 5 : ez::str::replaceString(fileToOpen, "\n", "");
301 : 5 : ez::str::replaceString(fileToOpen, "\r", "");
302 : 5 : std::ifstream docFile(fileToOpen, std::ios::in);
303 [ + + ]: 5 : if (docFile.is_open()) {
304 : 4 : docFile.close();
305 : 4 : return true;
306 : 4 : }
307 : 1 : return false;
308 : 5 : }
309 : :
310 : 8 : inline bool isDirectoryExist(const std::string &name) {
311 [ + - ]: 8 : if (!name.empty()) {
312 : 8 : const std::string dir = correctSlashTypeForFilePathName(name);
313 : 8 : struct stat sb;
314 [ + + ]: 8 : if (stat(dir.c_str(), &sb) == 0) {
315 : 4 : return (sb.st_mode & S_IFDIR);
316 : 4 : }
317 : 8 : }
318 : 4 : return false;
319 : 8 : }
320 : :
321 : 3 : inline bool destroyFile(const std::string &vFilePathName) {
322 [ + - ]: 3 : if (!vFilePathName.empty()) {
323 : 3 : const auto filePathName = correctSlashTypeForFilePathName(vFilePathName);
324 [ + + ]: 3 : if (isFileExist(filePathName)) {
325 [ + - ]: 2 : if (remove(filePathName.c_str()) == 0) {
326 : 2 : return true;
327 : 2 : }
328 : 2 : }
329 : 3 : }
330 : 1 : return false;
331 : 3 : }
332 : :
333 : 3 : inline bool destroyDir(const std::string &vPath) {
334 [ + - ]: 3 : if (!vPath.empty()) {
335 [ + + ]: 3 : if (isDirectoryExist(vPath)) {
336 : : #ifdef WINDOWS_OS
337 : : return (RemoveDirectoryA(vPath.c_str()) != 0);
338 : : #elif defined(UNIX_OS)
339 : : return (rmdir(vPath.c_str()) == 0);
340 : 2 : #endif
341 : 2 : }
342 : 3 : }
343 : 1 : return false;
344 : 3 : }
345 : :
346 : 3 : inline bool createDirectoryIfNotExist(const std::string &name) {
347 : 3 : bool res = false;
348 [ + - ]: 3 : if (!name.empty()) {
349 : 3 : const auto filePathName = correctSlashTypeForFilePathName(name);
350 [ + + ]: 3 : if (!isDirectoryExist(filePathName)) {
351 : 2 : res = true;
352 : : #ifdef WINDOWS_OS
353 : : if (CreateDirectory(filePathName.c_str(), nullptr) == 0) {
354 : : res = true;
355 : : }
356 : : #elif defined(UNIX_OS)
357 : : auto cmd = ez::str::toStr("mkdir -p %s", filePathName.c_str());
358 : 2 : const int dir_err = std::system(cmd.c_str());
359 [ - + ]: 2 : if (dir_err == -1) {
360 : 0 : LogVarError("Error creating directory %s", filePathName.c_str());
361 : 0 : res = false;
362 : 0 : }
363 : 2 : #endif
364 : 2 : }
365 : 3 : }
366 : 3 : return res;
367 : 3 : }
368 : :
369 : 0 : inline bool createPathIfNotExist(const std::string &vPath) {
370 : 0 : bool res = false;
371 : 0 : if (!vPath.empty()) {
372 : 0 : auto path = correctSlashTypeForFilePathName(vPath);
373 : 0 : if (!isDirectoryExist(path)) {
374 : 0 : res = true;
375 : 0 : ez::str::replaceString(path, "/", "|");
376 : 0 : ez::str::replaceString(path, "\\", "|");
377 : 0 : auto arr = ez::str::splitStringToVector(path, "|");
378 : 0 : std::string fullPath;
379 : 0 : for (auto it = arr.begin(); it != arr.end(); ++it) {
380 : 0 : fullPath += *it;
381 : 0 : res &= createDirectoryIfNotExist(fullPath);
382 : 0 : fullPath += EZ_FILE_SLASH_TYPE;
383 : 0 : }
384 : 0 : }
385 : 0 : }
386 : 0 : return res;
387 : 0 : }
388 : :
389 : : // will open the file is the associated app
390 : 0 : inline void openFile(const std::string &vFile) {
391 : 0 : const auto file = correctSlashTypeForFilePathName(vFile);
392 : 0 : #if defined(WINDOWS_OS)
393 : 0 : auto *result = ShellExecute(nullptr, "", file.c_str(), nullptr, nullptr, SW_SHOW);
394 : 0 : if (result < (HINSTANCE)32) { //-V112
395 : 0 : // try to open an editor
396 : 0 : result = ShellExecute(nullptr, "edit", file.c_str(), nullptr, nullptr, SW_SHOW);
397 : 0 : if (result == (HINSTANCE)SE_ERR_ASSOCINCOMPLETE || result == (HINSTANCE)SE_ERR_NOASSOC) {
398 : 0 : // open associating dialog
399 : 0 : const std::string sCmdOpenWith = "shell32.dll,OpenAs_RunDLL \"" + file + "\"";
400 : 0 : result = ShellExecute(nullptr, "", "rundll32.exe", sCmdOpenWith.c_str(), nullptr, SW_NORMAL);
401 : 0 : }
402 : 0 : if (result < (HINSTANCE)32) { // open in explorer //-V112
403 : 0 : const std::string sCmdExplorer = "/select,\"" + file + "\"";
404 : 0 : ShellExecute(
405 : 0 : nullptr, "", "explorer.exe", sCmdExplorer.c_str(), nullptr, SW_NORMAL); // ce serait peut etre mieu d'utilsier la commande system comme dans SelectFile
406 : 0 : }
407 : 0 : }
408 : 0 : #elif defined(LINUX_OS)
409 : 0 : int pid = fork();
410 : 0 : if (pid == 0) {
411 : 0 : execl("/usr/bin/xdg-open", "xdg-open", file.c_str(), (char *)0);
412 : 0 : }
413 : 0 : #elif defined(APPLE_OS)
414 : 0 : std::string cmd = "open " + file;
415 : 0 : std::system(cmd.c_str());
416 : 0 : #endif
417 : 0 : }
418 : :
419 : : // will open the url in the related browser
420 : 0 : inline void openUrl(const std::string &vUrl) {
421 : 0 : const auto url = correctSlashTypeForFilePathName(vUrl);
422 : 0 : #ifdef WINDOWS_OS
423 : 0 : ShellExecute(nullptr, nullptr, url.c_str(), nullptr, nullptr, SW_SHOW);
424 : 0 : #elif defined(LINUX_OS)
425 : 0 : auto cmd = ez::str::toStr("<mybrowser> %s", url.c_str());
426 : 0 : std::system(cmd.c_str());
427 : 0 : #elif defined(APPLE_OS)
428 : 0 : // std::string sCmdOpenWith = "open -a Firefox " + vUrl;
429 : 0 : std::string cmd = "open " + url;
430 : 0 : std::system(cmd.c_str());
431 : 0 : #endif
432 : 0 : }
433 : :
434 : : // will open the current file explorer and will select the file
435 : 0 : inline void selectFile(const std::string &vFileToSelect) {
436 : 0 : const auto fileToSelect = correctSlashTypeForFilePathName(vFileToSelect);
437 : 0 : #ifdef WINDOWS_OS
438 : 0 : if (!fileToSelect.empty()) {
439 : 0 : const std::string sCmdOpenWith = "explorer /select," + fileToSelect;
440 : 0 : std::system(sCmdOpenWith.c_str());
441 : 0 : }
442 : 0 : #elif defined(LINUX_OS)
443 : 0 : // todo : is there a similar cmd on linux ?
444 : 0 : assert(nullptr);
445 : 0 : #elif defined(APPLE_OS)
446 : 0 : if (!fileToSelect.empty()) {
447 : 0 : std::string cmd = "open -R " + fileToSelect;
448 : 0 : std::system(cmd.c_str());
449 : 0 : }
450 : 0 : #endif
451 : 0 : }
452 : :
453 : 0 : inline std::vector<std::string> getDrives() {
454 : 0 : std::vector<std::string> res;
455 : 0 : #ifdef WINDOWS_OS
456 : 0 : const DWORD mydrives = 2048;
457 : 0 : char lpBuffer[2048 + 1];
458 : 0 : const DWORD countChars = GetLogicalDriveStrings(mydrives, lpBuffer);
459 : 0 : if (countChars > 0) {
460 : 0 : std::string var = std::string(lpBuffer, (size_t)countChars);
461 : 0 : ez::str::replaceString(var, "\\", "");
462 : 0 : res = ez::str::splitStringToVector(var, "\0");
463 : 0 : }
464 : 0 : #elif defined(UNIX_OS)
465 : 0 : assert(nullptr);
466 : 0 : #endif
467 : 0 : return res;
468 : 0 : }
469 : :
470 : : class Watcher {
471 : : public:
472 : : struct PathResult {
473 : : // path are relative to rootDir
474 : : std::string rootPath; // the watched dir
475 : : std::string oldPath; // before rneaming
476 : : std::string newPath; // after renaming, creation, deletion or modification
477 : : enum class ModifType { // type of change
478 : : NONE = 0,
479 : : MODIFICATION,
480 : : CREATION,
481 : : DELETION,
482 : : RENAMED
483 : : } modifType = ModifType::NONE;
484 : 0 : void clear() { *this = {}; }
485 : : // Needed for std::set, defines strict weak ordering
486 : 0 : bool operator<(const PathResult &other) const {
487 [ # # ]: 0 : if (newPath != other.newPath) {
488 : 0 : return newPath < other.newPath;
489 : 0 : }
490 : 0 : return modifType < other.modifType;
491 : 0 : }
492 : : };
493 : : using Callback = std::function<void(const std::set<PathResult> &)>;
494 : :
495 : : private:
496 : : std::string m_appPath;
497 : : Callback m_callback;
498 : : std::thread m_thread;
499 : : std::atomic<bool> m_running{false};
500 : :
501 : : class Pattern;
502 : : using PatternPtr = std::shared_ptr<Pattern>;
503 : : using PatternWeak = std::weak_ptr<Pattern>;
504 : :
505 : : class Pattern {
506 : : public:
507 : : enum class PatternType { //
508 : : PATH = 0,
509 : : GLOB, // glob is pattern with wildcard : ex toto_*_tata_*_.txt
510 : : REGEX
511 : : };
512 : : enum class PhysicalType { //
513 : : DIR = 0, // directory watcher
514 : : FILE // file watcher
515 : : };
516 : :
517 : : static std::shared_ptr<Pattern> sCreatePath( //
518 : : const std::string &vPath,
519 : : PatternType vPatternType,
520 : 2 : PhysicalType vPhysicalType) {
521 [ + + ]: 2 : if (vPath.empty()) {
522 : 1 : return nullptr;
523 : 1 : }
524 : 1 : auto pRet = std::make_shared<Pattern>();
525 : 1 : pRet->m_path = vPath;
526 : 1 : pRet->m_patternType = vPatternType;
527 : 1 : pRet->m_physicalType = vPhysicalType;
528 : 1 : return pRet;
529 : 2 : }
530 : : static std::shared_ptr<Pattern> sCreatePathFile( //
531 : : const std::string &vRootPath,
532 : : const std::string &vFileNameExt,
533 : : PatternType vPatternType,
534 : 0 : PhysicalType vPhysicalType) {
535 : 0 : if (vRootPath.empty() || vFileNameExt.empty()) {
536 : 0 : return nullptr;
537 : 0 : }
538 : 0 : auto pRet = std::make_shared<Pattern>();
539 : 0 : pRet->m_path = vRootPath;
540 : 0 : pRet->m_fileNameExt = vFileNameExt;
541 : 0 : pRet->m_patternType = vPatternType;
542 : 0 : pRet->m_physicalType = vPhysicalType;
543 : 0 : return pRet;
544 : 0 : }
545 : :
546 : : private:
547 : : std::string m_path;
548 : : std::string m_fileNameExt;
549 : : PatternType m_patternType = PatternType::PATH;
550 : : PhysicalType m_physicalType = PhysicalType::DIR;
551 : :
552 : : public:
553 : 1 : const std::string &getPath() const { return m_path; }
554 : 0 : const std::string &getFileNameExt() const { return m_fileNameExt; }
555 : 0 : PatternType getPatternType() const { return m_patternType; }
556 : 4 : PhysicalType getPhysicalType() const { return m_physicalType; }
557 : :
558 : 0 : bool isPatternMatch(const std::string &vPath) const {
559 [ # # ]: 0 : switch (m_patternType) {
560 [ # # ]: 0 : case PatternType::PATH: {
561 [ # # ]: 0 : return (!m_fileNameExt.empty()) ? (m_fileNameExt == vPath) : (m_path == vPath);
562 : 0 : }
563 [ # # ]: 0 : case PatternType::GLOB: {
564 : 0 : return !ez::str::searchForPatternWithWildcards(vPath, m_fileNameExt).empty();
565 : 0 : }
566 [ # # ]: 0 : case PatternType::REGEX: {
567 : 0 : try {
568 : 0 : std::regex pattern(m_fileNameExt);
569 : 0 : return std::regex_match(vPath, pattern);
570 : 0 : } catch (...) {
571 : 0 : return false;
572 : 0 : }
573 : 0 : }
574 : 0 : }
575 : 0 : return false;
576 : 0 : }
577 : : };
578 : :
579 : : std::mutex m_patternsMutex;
580 : : std::vector<PatternPtr> m_watchPatterns;
581 : :
582 : : class IBackend {
583 : : public:
584 : 2 : virtual ~IBackend() = default;
585 : : virtual bool onStart(Watcher &owner) = 0;
586 : : virtual void onStop(Watcher &owner) = 0;
587 : : virtual bool addPattern(Watcher &owner, const PatternPtr &pattern) = 0;
588 : : virtual void poll(Watcher &owner, std::set<PathResult> &out) = 0;
589 : : };
590 : :
591 : : std::unique_ptr<IBackend> m_backend;
592 : :
593 : 4 : static void m_logPathResult(const PathResult& vPathResult) {
594 : : #ifdef _DEBUG
595 : : const char *mode = " ";
596 : : switch (vPathResult.modifType) {
597 : : case PathResult::ModifType::CREATION: mode = "CREATION"; break;
598 : : case PathResult::ModifType::DELETION: mode = "DELETION"; break;
599 : : case PathResult::ModifType::MODIFICATION: mode = "MODIFICATION"; break;
600 : : case PathResult::ModifType::RENAMED: mode = "RENAMED"; break;
601 : : default: break;
602 : : }
603 : : LogVarLightInfo("Event : RP(%s) OP(%s) NP(%s) MODE(%s)", vPathResult.rootPath.c_str(), vPathResult.oldPath.c_str(), vPathResult.newPath.c_str(), mode);
604 : : #else
605 : 4 : (void)vPathResult;
606 : 4 : #endif
607 : 4 : }
608 : :
609 : : static void m_emitIfMatch( //
610 : : const std::string &rootKey,
611 : : const std::vector<PatternWeak> &relatedPatterns,
612 : : const std::string &relNewName,
613 : : PathResult &pr,
614 : 4 : std::set<PathResult> &out) {
615 : 4 : pr.rootPath = rootKey;
616 [ + + ]: 4 : for (const auto &pw : relatedPatterns) {
617 [ + - ]: 4 : if (auto p = pw.lock()) {
618 [ + - ]: 4 : if (p->getPhysicalType() == Pattern::PhysicalType::DIR) {
619 : 4 : out.emplace(pr);
620 : 4 : m_logPathResult(pr);
621 : 4 : } else {
622 [ # # ][ # # ]: 0 : if (!relNewName.empty() && p->isPatternMatch(relNewName)) {
623 : 0 : out.emplace(pr);
624 : 0 : m_logPathResult(pr);
625 : 0 : }
626 : 0 : }
627 : 4 : }
628 : 4 : }
629 : 4 : }
630 : :
631 : 2 : std::string m_getAppPath() { return ez::App().getAppPath(); }
632 : 1 : std::string m_removeAppPath(const std::string &vPath) {
633 : 1 : size_t pos = vPath.find(m_appPath);
634 [ - + ]: 1 : if (pos != std::string::npos) {
635 : 0 : return vPath.substr(pos + m_appPath.size());
636 : 0 : }
637 : 1 : return vPath;
638 : 1 : }
639 : :
640 : : public:
641 : 2 : Watcher() : m_appPath(m_getAppPath()) {}
642 : 2 : ~Watcher() { stop(); }
643 : :
644 : 2 : void setCallback(Callback vCallback) { m_callback = vCallback; }
645 : :
646 : : // watch a directory. can be absolute or relative the to the app
647 : : // no widlcards or regex are supported for the directory
648 : 2 : bool watchDirectory(const std::string &vPath) { return m_registerPattern(Pattern::sCreatePath(vPath, Pattern::PatternType::PATH, Pattern::PhysicalType::DIR)); }
649 : :
650 : : // will watch from a path a filename.
651 : : // vRootPath : no widlcards or regex are supported
652 : : // vFileNameExt : can contain wildcards. regex patterns is authorized but must start with a '(' and end with a ')'
653 : 0 : bool watchFile(const std::string &vRootPath, const std::string &vFileNameExt) {
654 : 0 : if (vRootPath.empty() || vFileNameExt.empty()) {
655 : 0 : return false;
656 : 0 : }
657 : 0 : Pattern::PatternType type = Pattern::PatternType::PATH;
658 : 0 : if (vFileNameExt.front() == '(' && vFileNameExt.back() == ')') {
659 : 0 : type = Pattern::PatternType::REGEX;
660 : 0 : } else if (vFileNameExt.find('*') != std::string::npos) {
661 : 0 : type = Pattern::PatternType::GLOB;
662 : 0 : }
663 : 0 : return m_registerPattern(Pattern::sCreatePathFile(vRootPath, vFileNameExt, type, Pattern::PhysicalType::FILE));
664 : 0 : }
665 : :
666 : 4 : bool start() {
667 [ + + ][ + + ]: 4 : if (m_running || m_callback == nullptr) {
668 : 2 : return false;
669 : 2 : }
670 : 2 : m_backend = m_createBackend();
671 [ - + ]: 2 : if (!m_backend) {
672 : 0 : return false;
673 : 0 : }
674 [ - + ]: 2 : if (!m_backend->onStart(*this)) {
675 : 0 : m_backend.reset();
676 : 0 : return false;
677 : 0 : }
678 : 2 : {
679 : 2 : std::lock_guard<std::mutex> lock(m_patternsMutex);
680 [ - + ]: 2 : for (const auto &p : m_watchPatterns) {
681 : 0 : (void)m_backend->addPattern(*this, p);
682 : 0 : }
683 : 2 : }
684 : 2 : m_running = true;
685 : 2 : m_thread = std::thread(&Watcher::m_threadLoop, this);
686 : 2 : return true;
687 : 2 : }
688 : :
689 : 5 : bool stop() {
690 [ + + ]: 5 : if (!m_running) {
691 : 3 : return false;
692 : 3 : }
693 : 2 : m_running = false;
694 [ + - ]: 2 : if (m_thread.joinable()) {
695 : 2 : m_thread.join();
696 : 2 : }
697 [ + - ]: 2 : if (m_backend) {
698 : 2 : m_backend->onStop(*this);
699 : 2 : m_backend.reset();
700 : 2 : }
701 : 2 : return true;
702 : 5 : }
703 : :
704 : : private:
705 : 2 : bool m_registerPattern(const PatternPtr &pat) {
706 [ + + ]: 2 : if (!pat) {
707 : 1 : return false;
708 : 1 : }
709 : 1 : {
710 : 1 : std::lock_guard<std::mutex> lock(m_patternsMutex);
711 : 1 : m_watchPatterns.push_back(pat);
712 : 1 : }
713 [ + - ]: 1 : if (m_backend) {
714 : 1 : return m_backend->addPattern(*this, pat);
715 : 1 : }
716 : 0 : return true;
717 : 1 : }
718 : :
719 : 2 : void m_threadLoop() {
720 : 2 : std::set<PathResult> files;
721 [ + + ]: 17 : while (m_running) {
722 : 15 : m_backend->poll(*this, files);
723 [ + + ]: 15 : if (!files.empty()) {
724 : 4 : m_callback(files);
725 : 4 : files.clear();
726 : 4 : }
727 : 15 : std::this_thread::sleep_for(std::chrono::milliseconds(10));
728 : 15 : }
729 : 2 : }
730 : :
731 : : std::unique_ptr<IBackend> m_createBackend();
732 : :
733 : : // =============================== Backend Windows ===============================
734 : : #if defined(WINDOWS_OS)
735 : : #ifndef WIN32_LEAN_AND_MEAN
736 : : #define WIN32_LEAN_AND_MEAN
737 : : #endif
738 : : #include <Windows.h>
739 : : class BackendWindows : public IBackend {
740 : : public:
741 : : typedef std::wstring PathType;
742 : : struct WatchHandle {
743 : : HANDLE hDir{};
744 : : PathType path;
745 : : std::vector<PatternWeak> relatedPatterns;
746 : : HANDLE hEvent{};
747 : : OVERLAPPED ov{};
748 : : std::array<uint8_t, 4096> buffer{};
749 : : bool inFlight{false};
750 : : };
751 : :
752 : : private:
753 : : std::mutex m_mtx;
754 : : std::unordered_map<std::string, WatchHandle> m_watchHandles;
755 : : std::vector<WatchHandle *> m_handles;
756 : : std::atomic<bool> m_isDirty{false};
757 : :
758 : : public:
759 : :
760 : : bool onStart(Watcher &owner) override {
761 : : (void)owner;
762 : : m_isDirty = true;
763 : : return true;
764 : : }
765 : :
766 : : bool addPattern(Watcher &owner, const PatternPtr &pattern) override {
767 : : if (!pattern) {
768 : : return false;
769 : : }
770 : : const std::string rootKey = owner.m_removeAppPath(pattern->getPath());
771 : : std::lock_guard<std::mutex> lock(m_mtx);
772 : : auto it = m_watchHandles.find(rootKey);
773 : : if (it != m_watchHandles.end()) {
774 : : it->second.relatedPatterns.push_back(pattern);
775 : : } else {
776 : : WatchHandle hnd;
777 : : hnd.path = ez::str::utf8Decode(rootKey);
778 : : hnd.hDir = CreateFileW(
779 : : hnd.path.c_str(),
780 : : FILE_LIST_DIRECTORY,
781 : : FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
782 : : NULL,
783 : : OPEN_EXISTING,
784 : : FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
785 : : NULL);
786 : : if (hnd.hDir == INVALID_HANDLE_VALUE) {
787 : : LogVarError("Err : Unable to open directory : %s", rootKey.c_str());
788 : : return false;
789 : : }
790 : : hnd.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
791 : : hnd.ov.hEvent = hnd.hEvent;
792 : : ZeroMemory(&hnd.ov, sizeof(hnd.ov));
793 : : hnd.inFlight = false;
794 : : hnd.relatedPatterns.push_back(pattern);
795 : : m_watchHandles[rootKey] = hnd;
796 : : auto &ref = m_watchHandles[rootKey];
797 : : m_postRead(ref); // *** Arm immediately to avoid missing the first changes ***
798 : : }
799 : : m_isDirty = true;
800 : : return true;
801 : : }
802 : :
803 : : void onStop(Watcher &owner) override {
804 : : (void)owner;
805 : : std::lock_guard<std::mutex> lock(m_mtx);
806 : : for (auto &kv : m_watchHandles) {
807 : : auto &h = kv.second;
808 : : if (h.inFlight) {
809 : : CancelIoEx(h.hDir, &h.ov); // safe if no I/O too
810 : : }
811 : : if (h.hEvent) {
812 : : CloseHandle(h.hEvent);
813 : : h.hEvent = nullptr;
814 : : }
815 : : if (h.hDir && h.hDir != INVALID_HANDLE_VALUE) {
816 : : CloseHandle(h.hDir);
817 : : h.hDir = INVALID_HANDLE_VALUE;
818 : : }
819 : : }
820 : : m_watchHandles.clear();
821 : : m_handles.clear();
822 : : }
823 : :
824 : : // ---- Your exact functions (content preserved, only owner passed when needed) ----
825 : : void completePathResult(PathResult &voResult, const FILE_NOTIFY_INFORMATION *vpNotify, Watcher &owner) {
826 : : const auto chFile = owner.m_removeAppPath(ez::str::utf8Encode(std::wstring(vpNotify->FileName, vpNotify->FileNameLength / sizeof(WCHAR))));
827 : : const char *mode = " ";
828 : : switch (vpNotify->Action) {
829 : : case FILE_ACTION_ADDED: {
830 : : voResult.modifType = PathResult::ModifType::CREATION;
831 : : voResult.newPath = chFile;
832 : : } break;
833 : : case FILE_ACTION_REMOVED: {
834 : : voResult.modifType = PathResult::ModifType::DELETION;
835 : : voResult.newPath = chFile;
836 : : } break;
837 : : case FILE_ACTION_MODIFIED: {
838 : : voResult.modifType = PathResult::ModifType::MODIFICATION;
839 : : voResult.newPath = chFile;
840 : : } break;
841 : : case FILE_ACTION_RENAMED_OLD_NAME: {
842 : : voResult.modifType = PathResult::ModifType::RENAMED;
843 : : voResult.oldPath = chFile;
844 : : } break;
845 : : case FILE_ACTION_RENAMED_NEW_NAME: {
846 : : voResult.modifType = PathResult::ModifType::RENAMED;
847 : : voResult.newPath = chFile;
848 : : } break;
849 : : }
850 : : }
851 : :
852 : : bool m_postRead(WatchHandle &hnd) {
853 : : if (hnd.inFlight) {
854 : : return true;
855 : : }
856 : :
857 : : // Reset event & OVERLAPPED before issuing a new read
858 : : ResetEvent(hnd.hEvent);
859 : : ZeroMemory(&hnd.ov, sizeof(hnd.ov));
860 : : hnd.ov.hEvent = hnd.hEvent;
861 : :
862 : : const DWORD kMask = FILE_NOTIFY_CHANGE_FILE_NAME | // file create/delete/rename in root dir
863 : : FILE_NOTIFY_CHANGE_LAST_WRITE; // file content modifications
864 : :
865 : : // In OVERLAPPED mode, lpBytesReturned MUST be nullptr
866 : : BOOL ok = ReadDirectoryChangesW(
867 : : hnd.hDir,
868 : : hnd.buffer.data(),
869 : : static_cast<DWORD>(hnd.buffer.size()),
870 : : FALSE, // do NOT watch subtree
871 : : kMask,
872 : : nullptr, // must be null for OVERLAPPED
873 : : &hnd.ov,
874 : : nullptr);
875 : :
876 : : if (!ok) {
877 : : DWORD err = GetLastError();
878 : : if (err != ERROR_IO_PENDING) {
879 : : LogVarWarning("ReadDirectoryChangesW(OVERLAPPED) failed: %u", err);
880 : : return false;
881 : : }
882 : : }
883 : :
884 : : hnd.inFlight = true;
885 : : return true;
886 : : }
887 : :
888 : : // In class Watcher::BackendWindows (private)
889 : : void m_pollOneHandle(WatchHandle *vpHandle, std::set<PathResult> &voFiles, Watcher &owner) {
890 : : // Non-blocking poll of a single handle; parse results and re-arm the async read.
891 : : if (vpHandle == nullptr) {
892 : : return;
893 : : }
894 : :
895 : : // 2) Poll event (non-blocking)
896 : : DWORD wr = WaitForSingleObject(vpHandle->hEvent, 0);
897 : : #ifdef _DEBUG
898 : : switch (wr) {
899 : : case WAIT_ABANDONED: {
900 : : LogVarLightInfo("WAIT_ABANDONED");
901 : : } break;
902 : : case WAIT_OBJECT_0: {
903 : : // LogVarLightInfo("WAIT_OBJECT_0");
904 : : } break;
905 : : case WAIT_TIMEOUT: {
906 : : // LogVarLightInfo("WAIT_TIMEOUT");
907 : : } break;
908 : : case WAIT_FAILED: {
909 : : LogVarLightInfo("WAIT_FAILED");
910 : : } break;
911 : : }
912 : : #endif
913 : : if (wr != WAIT_OBJECT_0) {
914 : : // Nothing ready for this handle
915 : : return;
916 : : }
917 : :
918 : : // I/O finished
919 : : DWORD bytes = 0;
920 : : if (GetOverlappedResult(vpHandle->hDir, &vpHandle->ov, &bytes, FALSE)) {
921 : : // Parse buffer [0..bytes)
922 : : DWORD offset = 0;
923 : : PathResult pr{};
924 : : pr.clear();
925 : : pr.rootPath = ez::str::utf8Encode(vpHandle->path);
926 : :
927 : : while (offset < bytes) {
928 : : const auto *pNotify = reinterpret_cast<const FILE_NOTIFY_INFORMATION *>(vpHandle->buffer.data() + offset);
929 : : completePathResult(pr, pNotify, owner);
930 : : // Only emit when we have a valid newPath (pairs OLD/NEW may come split)
931 : : if (!pr.newPath.empty()) {
932 : : // snapshot patterns under lock to avoid concurrent modification
933 : : std::vector<PatternWeak> relatedCopy;
934 : : {
935 : : std::lock_guard<std::mutex> lock(m_mtx);
936 : : relatedCopy = vpHandle->relatedPatterns;
937 : : }
938 : : for (const auto &patW : relatedCopy) {
939 : : if (auto pPat = patW.lock()) {
940 : : if (pPat->getPhysicalType() == Pattern::PhysicalType::DIR) {
941 : : voFiles.emplace(pr);
942 : : m_logPathResult(pr);
943 : : } else {
944 : : if (pPat->isPatternMatch(pr.newPath)) {
945 : : voFiles.emplace(pr);
946 : : m_logPathResult(pr);
947 : : }
948 : : }
949 : : }
950 : : }
951 : : }
952 : : if (pNotify->NextEntryOffset == 0) {
953 : : break;
954 : : }
955 : : offset += pNotify->NextEntryOffset;
956 : : }
957 : : } else {
958 : : LogVarWarning("GetOverlappedResult failed: %u", GetLastError());
959 : : }
960 : :
961 : : // Mark as not in-flight and immediately re-arm
962 : : vpHandle->inFlight = false;
963 : : (void)m_postRead(*vpHandle);
964 : : }
965 : :
966 : : void poll(Watcher &owner, std::set<PathResult> &out) override {
967 : : if (m_isDirty.exchange(false)) {
968 : : std::lock_guard<std::mutex> lock(m_mtx);
969 : : m_handles.clear();
970 : : m_handles.reserve(m_watchHandles.size());
971 : : for (auto &kv : m_watchHandles) {
972 : : m_handles.push_back(&kv.second); // store pointer
973 : : }
974 : : }
975 : : for (auto *hnd : m_handles) {
976 : : if (!hnd->inFlight) {
977 : : m_postRead(*hnd);
978 : : }
979 : : m_pollOneHandle(hnd, out, owner);
980 : : }
981 : : }
982 : : };
983 : : #endif // WINDOWS_OS
984 : :
985 : : // =============================== Backend Linux ===============================
986 : : #if defined(LINUX_OS)
987 : : #include <sys/inotify.h>
988 : : #include <unistd.h>
989 : : #include <limits.h>
990 : : #include <errno.h>
991 : :
992 : : class BackendLinux : public IBackend {
993 : : public:
994 : : struct WatchHandle {
995 : : int wd{-1};
996 : : std::string rootKey;
997 : : std::vector<PatternWeak> relatedPatterns;
998 : : };
999 : :
1000 : 2 : bool onStart(Watcher &owner) override {
1001 : 2 : (void)owner;
1002 : 2 : m_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
1003 [ - + ]: 2 : if (m_fd < 0) {
1004 : 0 : LogVarError("Error: Unable to initialize inotify.");
1005 : 0 : return false;
1006 : 0 : }
1007 : 2 : return true;
1008 : 2 : }
1009 : :
1010 : 2 : void onStop(Watcher &owner) override {
1011 : 2 : (void)owner;
1012 [ + + ]: 2 : for (auto &kv : m_handles) {
1013 [ + - ]: 1 : if (kv.second.wd >= 0) {
1014 : 1 : inotify_rm_watch(m_fd, kv.second.wd);
1015 : 1 : kv.second.wd = -1;
1016 : 1 : }
1017 : 1 : }
1018 [ + - ]: 2 : if (m_fd >= 0) {
1019 : 2 : close(m_fd);
1020 : 2 : m_fd = -1;
1021 : 2 : }
1022 : 2 : m_wdToRoot.clear();
1023 : 2 : m_pendingRename.clear();
1024 : 2 : m_handles.clear();
1025 : 2 : m_buf.clear();
1026 : 2 : }
1027 : :
1028 : 1 : bool addPattern(Watcher &owner, const PatternPtr &pattern) override {
1029 [ - + ]: 1 : if (!pattern) {
1030 : 0 : return false;
1031 : 0 : }
1032 : 1 : const std::string rootKey = owner.m_removeAppPath(pattern->getPath());
1033 : 1 : auto it = m_handles.find(rootKey);
1034 [ - + ]: 1 : if (it != m_handles.end()) {
1035 : 0 : it->second.relatedPatterns.push_back(pattern);
1036 : 1 : } else {
1037 : 1 : const uint32_t mask = IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVED_FROM | IN_MOVED_TO;
1038 : 1 : int wd = inotify_add_watch(m_fd, rootKey.c_str(), mask);
1039 [ - + ]: 1 : if (wd < 0) {
1040 : 0 : LogVarError("Error: inotify_add_watch failed on %s (errno %d)", rootKey.c_str(), errno);
1041 : 0 : return false;
1042 : 0 : }
1043 : 1 : WatchHandle hnd;
1044 : 1 : hnd.wd = wd;
1045 : 1 : hnd.rootKey = rootKey;
1046 : 1 : hnd.relatedPatterns.push_back(pattern);
1047 : 1 : m_handles[rootKey] = hnd;
1048 : 1 : m_wdToRoot[wd] = rootKey;
1049 : 1 : }
1050 : 1 : return true;
1051 : 1 : }
1052 : :
1053 : 15 : void poll(Watcher &owner, std::set<PathResult> &out) override {
1054 : 15 : (void)owner;
1055 [ - + ]: 15 : if (m_fd < 0) {
1056 : 0 : return;
1057 : 0 : }
1058 [ + + ]: 15 : if (m_buf.size() < 64 * 1024) {
1059 : 1 : m_buf.resize(64 * 1024);
1060 : 1 : }
1061 : :
1062 : 15 : ssize_t n = read(m_fd, m_buf.data(), m_buf.size());
1063 [ + + ]: 15 : if (n <= 0) {
1064 : 11 : return;
1065 : 11 : }
1066 : :
1067 : 4 : ssize_t off = 0;
1068 [ + + ]: 9 : while (off < n) {
1069 : 5 : const inotify_event *ev = reinterpret_cast<const inotify_event *>(m_buf.data() + off);
1070 : :
1071 : 5 : auto itRoot = m_wdToRoot.find(ev->wd);
1072 [ + - ]: 5 : if (itRoot != m_wdToRoot.end()) {
1073 : 5 : const std::string &rootKey = itRoot->second;
1074 : 5 : auto itH = m_handles.find(rootKey);
1075 [ + - ]: 5 : if (itH != m_handles.end()) {
1076 : 5 : const bool isDir = (ev->mask & IN_ISDIR) != 0;
1077 [ + - ][ + - ]: 5 : if (!isDir && ev->len > 0) {
1078 : 5 : const std::string rel(ev->name);
1079 : 5 : PathResult pr{};
1080 [ + + ]: 5 : if (ev->mask & IN_CREATE) {
1081 : 1 : pr.modifType = PathResult::ModifType::CREATION;
1082 : 1 : pr.newPath = rel;
1083 : 1 : m_emitIfMatch(rootKey, itH->second.relatedPatterns, rel, pr, out);
1084 [ + + ]: 4 : } else if (ev->mask & IN_DELETE) {
1085 : 1 : pr.modifType = PathResult::ModifType::DELETION;
1086 : 1 : pr.newPath = rel;
1087 : 1 : m_emitIfMatch(rootKey, itH->second.relatedPatterns, rel, pr, out);
1088 [ + + ]: 3 : } else if (ev->mask & IN_MODIFY) {
1089 : 1 : pr.modifType = PathResult::ModifType::MODIFICATION;
1090 : 1 : pr.newPath = rel;
1091 : 1 : m_emitIfMatch(rootKey, itH->second.relatedPatterns, rel, pr, out);
1092 [ + + ]: 2 : } else if (ev->mask & IN_MOVED_FROM) {
1093 : 1 : m_pendingRename[ev->cookie] = std::make_pair(ev->wd, rel);
1094 [ + - ]: 1 : } else if (ev->mask & IN_MOVED_TO) {
1095 : 1 : auto itCookie = m_pendingRename.find(ev->cookie);
1096 [ + - ]: 1 : if (itCookie != m_pendingRename.end()) {
1097 [ + - ]: 1 : if (itCookie->second.first == ev->wd) {
1098 : 1 : pr.modifType = PathResult::ModifType::RENAMED;
1099 : 1 : pr.oldPath = itCookie->second.second;
1100 : 1 : pr.newPath = rel;
1101 : 1 : m_emitIfMatch(rootKey, itH->second.relatedPatterns, rel, pr, out);
1102 : 1 : } else {
1103 : 0 : pr.modifType = PathResult::ModifType::CREATION;
1104 : 0 : pr.newPath = rel;
1105 : 0 : m_emitIfMatch(rootKey, itH->second.relatedPatterns, rel, pr, out);
1106 : 0 : }
1107 : 1 : m_pendingRename.erase(itCookie);
1108 : 1 : } else {
1109 : 0 : pr.modifType = PathResult::ModifType::CREATION;
1110 : 0 : pr.newPath = rel;
1111 : 0 : m_emitIfMatch(rootKey, itH->second.relatedPatterns, rel, pr, out);
1112 : 0 : }
1113 : 1 : }
1114 : 5 : }
1115 : 5 : }
1116 : 5 : }
1117 : :
1118 : 5 : off += sizeof(inotify_event) + ev->len;
1119 : 5 : }
1120 : 4 : }
1121 : :
1122 : : private:
1123 : : int m_fd{-1};
1124 : : std::unordered_map<std::string, WatchHandle> m_handles;
1125 : : std::unordered_map<int, std::string> m_wdToRoot;
1126 : : std::unordered_map<uint32_t, std::pair<int, std::string>> m_pendingRename;
1127 : : std::vector<uint8_t> m_buf;
1128 : : };
1129 : : #endif // LINUX_OS
1130 : :
1131 : : // =============================== Backend macOS ===============================
1132 : : #if defined(APPLE_OS)
1133 : : class BackendMac : public IBackend {
1134 : : public:
1135 : : #include <CoreServices/CoreServices.h>
1136 : :
1137 : : struct WatchHandle {
1138 : : std::string rootKey;
1139 : : std::vector<PatternWeak> relatedPatterns;
1140 : : };
1141 : :
1142 : : bool onStart(Watcher &owner) override {
1143 : : (void)owner;
1144 : : m_dirty = true;
1145 : : return true;
1146 : : }
1147 : :
1148 : : void onStop(Watcher &owner) override {
1149 : : (void)owner;
1150 : : if (m_stream) {
1151 : : FSEventStreamStop(m_stream);
1152 : : FSEventStreamInvalidate(m_stream);
1153 : : FSEventStreamRelease(m_stream);
1154 : : m_stream = nullptr;
1155 : : }
1156 : : std::lock_guard<std::mutex> lock(m_qmtx);
1157 : : m_queue.clear();
1158 : : m_handles.clear();
1159 : : }
1160 : :
1161 : : bool addPattern(Watcher &owner, const PatternPtr &pattern) override {
1162 : : if (!pattern) {
1163 : : return false;
1164 : : }
1165 : : const std::string rootKey = owner.m_removeAppPath(pattern->getPath());
1166 : : auto it = m_handles.find(rootKey);
1167 : : if (it == m_handles.end()) {
1168 : : WatchHandle h;
1169 : : h.rootKey = rootKey;
1170 : : h.relatedPatterns.push_back(pattern);
1171 : : m_handles[rootKey] = std::move(h);
1172 : : m_dirty = true;
1173 : : } else {
1174 : : it->second.relatedPatterns.push_back(pattern);
1175 : : }
1176 : : return true;
1177 : : }
1178 : :
1179 : : void poll(Watcher &owner, std::set<PathResult> &out) override {
1180 : : (void)owner;
1181 : :
1182 : : if (m_dirty) {
1183 : : if (m_stream) {
1184 : : FSEventStreamStop(m_stream);
1185 : : FSEventStreamInvalidate(m_stream);
1186 : : FSEventStreamRelease(m_stream);
1187 : : m_stream = nullptr;
1188 : : }
1189 : :
1190 : : CFMutableArrayRef paths = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
1191 : : for (const auto &kv : m_handles) {
1192 : : if (!kv.first.empty()) {
1193 : : CFStringRef s = CFStringCreateWithCString(NULL, kv.first.c_str(), kCFStringEncodingUTF8);
1194 : : CFArrayAddValue(paths, s);
1195 : : CFRelease(s);
1196 : : }
1197 : : }
1198 : :
1199 : : if (CFArrayGetCount(paths) > 0) {
1200 : : FSEventStreamContext ctx{};
1201 : : ctx.info = this;
1202 : : const FSEventStreamCreateFlags flags = kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagNoDefer;
1203 : :
1204 : : m_stream = FSEventStreamCreate(NULL, &BackendMac::sCallback, &ctx, paths, kFSEventStreamEventIdSinceNow, 0.1, flags);
1205 : : if (m_stream) {
1206 : : FSEventStreamScheduleWithRunLoop(m_stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
1207 : : FSEventStreamStart(m_stream);
1208 : : }
1209 : : }
1210 : : CFRelease(paths);
1211 : : m_dirty = false;
1212 : : }
1213 : :
1214 : : if (m_stream) {
1215 : : CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, true);
1216 : : }
1217 : :
1218 : : {
1219 : : std::lock_guard<std::mutex> lock(m_qmtx);
1220 : : for (const PathResult &pr : m_queue) {
1221 : : out.emplace(pr);
1222 : : }
1223 : : m_queue.clear();
1224 : : }
1225 : : }
1226 : :
1227 : : private:
1228 : : static void
1229 : : sCallback(ConstFSEventStreamRef, void *userData, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags *eventFlags, const FSEventStreamEventId *) {
1230 : : BackendMac *self = static_cast<BackendMac *>(userData);
1231 : : if (!self) {
1232 : : return;
1233 : : }
1234 : :
1235 : : char **paths = static_cast<char **>(eventPaths);
1236 : :
1237 : : // Snapshot roots for matching and pattern list
1238 : : std::vector<std::pair<std::string, std::vector<PatternWeak>>> roots;
1239 : : roots.reserve(self->m_handles.size());
1240 : : for (const auto &kv : self->m_handles) {
1241 : : roots.emplace_back(kv.first, kv.second.relatedPatterns);
1242 : : }
1243 : :
1244 : : std::vector<PathResult> local;
1245 : : local.reserve(numEvents);
1246 : :
1247 : : for (size_t i = 0; i < numEvents; ++i) {
1248 : : const std::string changed = paths[i];
1249 : : const auto flags = eventFlags[i];
1250 : :
1251 : : // Only files
1252 : : if ((flags & kFSEventStreamEventFlagItemIsFile) == 0) {
1253 : : continue;
1254 : : }
1255 : :
1256 : : // Find root; produce relative path
1257 : : for (const auto &r : roots) {
1258 : : const std::string &rootKey = r.first;
1259 : : if (changed.size() <= rootKey.size()) {
1260 : : continue;
1261 : : }
1262 : : if (changed.compare(0, rootKey.size(), rootKey) != 0) {
1263 : : continue;
1264 : : }
1265 : : const char sep = changed[rootKey.size()];
1266 : : if (sep != '/' && sep != '\\') {
1267 : : continue;
1268 : : }
1269 : :
1270 : : const std::string rel = changed.substr(rootKey.size() + 1);
1271 : :
1272 : : PathResult pr{};
1273 : : pr.rootPath = rootKey;
1274 : : if (flags & kFSEventStreamEventFlagItemCreated) {
1275 : : pr.modifType = PathResult::ModifType::CREATION;
1276 : : pr.newPath = rel;
1277 : : } else if (flags & kFSEventStreamEventFlagItemRemoved) {
1278 : : pr.modifType = PathResult::ModifType::DELETION;
1279 : : pr.newPath = rel;
1280 : : } else if (flags & kFSEventStreamEventFlagItemRenamed) {
1281 : : pr.modifType = PathResult::ModifType::RENAMED;
1282 : : pr.newPath = rel; // oldPath not available with FSEvents
1283 : : } else if (
1284 : : flags &
1285 : : (kFSEventStreamEventFlagItemModified | kFSEventStreamEventFlagItemInodeMetaMod | kFSEventStreamEventFlagItemFinderInfoMod |
1286 : : kFSEventStreamEventFlagItemChangeOwner | kFSEventStreamEventFlagItemXattrMod)) {
1287 : : pr.modifType = PathResult::ModifType::MODIFICATION;
1288 : : pr.newPath = rel;
1289 : : } else {
1290 : : continue;
1291 : : }
1292 : :
1293 : : // Pattern filtering (DIR accepts all; FILE filters on newPath)
1294 : : for (const auto &pw : r.second) {
1295 : : if (auto p = pw.lock()) {
1296 : : if (p->getPhysicalType() == Pattern::PhysicalType::DIR) {
1297 : : local.push_back(pr);
1298 : : } else if (!pr.newPath.empty() && p->isPatternMatch(pr.newPath, false)) {
1299 : : local.push_back(pr);
1300 : : }
1301 : : }
1302 : : }
1303 : : }
1304 : : }
1305 : :
1306 : : if (!local.empty()) {
1307 : : std::lock_guard<std::mutex> lock(self->m_qmtx);
1308 : : for (const auto &pr : local) {
1309 : : self->m_queue.push_back(pr);
1310 : : }
1311 : : }
1312 : : }
1313 : :
1314 : : FSEventStreamRef m_stream{nullptr};
1315 : : bool m_dirty{false};
1316 : : std::unordered_map<std::string, WatchHandle> m_handles;
1317 : :
1318 : : std::mutex m_qmtx;
1319 : : std::vector<PathResult> m_queue;
1320 : : };
1321 : : #endif // APPLE_OS
1322 : :
1323 : : }; // class Watcher
1324 : :
1325 : : // =============================== Backend factory ===============================
1326 : 2 : inline std::unique_ptr<Watcher::IBackend> Watcher::m_createBackend() {
1327 : : #if defined(WINDOWS_OS)
1328 : : return std::unique_ptr<IBackend>(new BackendWindows());
1329 : : #elif defined(LINUX_OS)
1330 : : return std::unique_ptr<IBackend>(new BackendLinux());
1331 : : #elif defined(APPLE_OS)
1332 : : return std::unique_ptr<IBackend>(new BackendMac());
1333 : : #else
1334 : : return nullptr;
1335 : : #endif
1336 : 2 : }
1337 : :
1338 : :
1339 : : } // namespace file
1340 : : } // namespace ez
|