PageRenderTime 65ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/src/cinder/FileWatcher.cpp

https://github.com/afrancois/Cinder
C++ | 451 lines | 300 code | 91 blank | 60 comment | 53 complexity | 6a87d85419771c43639874162d918a65 MD5 | raw file
  1. /*
  2. Copyright (c) 2017, The Cinder Project, All rights reserved.
  3. This code is intended for use with the Cinder C++ library: http://libcinder.org
  4. Redistribution and use in source and binary forms, with or without modification, are permitted provided that
  5. the following conditions are met:
  6. * Redistributions of source code must retain the above copyright notice, this list of conditions and
  7. the following disclaimer.
  8. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
  9. the following disclaimer in the documentation and/or other materials provided with the distribution.
  10. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
  11. WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
  12. PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
  13. ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
  14. TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  15. HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  16. NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  17. POSSIBILITY OF SUCH DAMAGE.
  18. */
  19. #include "cinder/FileWatcher.h"
  20. #include "cinder/app/AppBase.h"
  21. #include "cinder/Log.h"
  22. #include "cinder/Utilities.h"
  23. //#define LOG_UPDATE( stream ) CI_LOG_I( stream )
  24. #define LOG_UPDATE( stream ) ( (void)( 0 ) )
  25. using namespace ci;
  26. using namespace std;
  27. namespace cinder {
  28. //! Base class for Watch types, which are returned from FileWatcher::load() and watch()
  29. class Watch : public std::enable_shared_from_this<Watch>, private Noncopyable {
  30. public:
  31. Watch( const std::vector<fs::path> &filePaths, bool needsCallback );
  32. signals::Connection connect( const function<void ( const WatchEvent& )> &callback ) { return mSignalChanged.connect( callback ); }
  33. //! Checks if the asset file is up-to-date. Also may discard the Watch if there are no more connected slots.
  34. void checkCurrent();
  35. //! Remove any watches for \a filePath. If it is the last file associated with this Watch, discard
  36. void unwatch( const fs::path &filePath );
  37. //! Emit the signal callback.
  38. void emitCallback();
  39. //! Enables or disables a Watch
  40. void setEnabled( bool enable, const fs::path &filePath );
  41. //! Marks the Watch as needing its callback to be emitted on the main thread.
  42. void setNeedsCallback( bool b ) { mNeedsCallback = b; }
  43. //! Returns whether the Watch needs its callback emitted on the main thread.
  44. bool needsCallback() const { return mNeedsCallback; }
  45. //! Marks the Watch as discarded, will be destroyed the next update loop.
  46. void markDiscarded() { mDiscarded = true; }
  47. //! Returns whether the Watch is discarded and should be destroyed.
  48. bool isDiscarded() const { return mDiscarded; }
  49. class WatchItem {
  50. public:
  51. WatchItem( const fs::path& path, const fs::file_time_type& timeStamp, bool enabled )
  52. : mFilePath( path ), mTimeStamp( timeStamp ), mEnabled( enabled ), mErrors( 0 )
  53. {}
  54. fs::path mFilePath;
  55. fs::file_time_type mTimeStamp;
  56. bool mEnabled;
  57. int8_t mErrors;
  58. };
  59. const std::vector<WatchItem>& getItems() const { return mWatchItems; }
  60. private:
  61. bool mDiscarded = false;
  62. bool mEnabled = true;
  63. bool mNeedsCallback = false;
  64. std::vector<WatchItem> mWatchItems;
  65. std::vector<fs::path> mModifiedFilePaths;
  66. signals::Signal<void ( const WatchEvent& )> mSignalChanged;
  67. };
  68. namespace {
  69. fs::path findFullFilePath( const fs::path &filePath )
  70. {
  71. if( filePath.empty() )
  72. throw FileWatcherException( "empty path" );
  73. if( filePath.is_absolute() && fs::exists( filePath ) )
  74. return filePath;
  75. auto resolvedAssetPath = app::getAssetPath( filePath );
  76. if( ! fs::exists( resolvedAssetPath ) )
  77. throw FileWatcherException( "could not resolve file path: " + filePath.string() );
  78. return resolvedAssetPath;
  79. }
  80. // Used from the debugger.
  81. void debugPrintWatches( const std::list<std::unique_ptr<Watch>>&watchList )
  82. {
  83. int i = 0;
  84. string str;
  85. for( const auto &watch : watchList ) {
  86. string needsCallback = watch->needsCallback() ? "true" : "false";
  87. string discarded = watch->isDiscarded() ? "true" : "false";
  88. const auto &items = watch->getItems();
  89. string filePathStr = items.front().mFilePath.string();
  90. if( items.size() > 1 )
  91. filePathStr += " ...(" + to_string( items.size() ) + " files)";
  92. str += "[" + to_string( i ) + "] needs callback: " + needsCallback + ", discarded: " + discarded + ", file: " + filePathStr + "\n";
  93. i++;
  94. }
  95. app::console() << str << std::endl;
  96. }
  97. // Used for elapsed seconds without app context
  98. double getElapsedSeconds()
  99. {
  100. static auto start = std::chrono::high_resolution_clock::now();
  101. auto current = std::chrono::high_resolution_clock::now();
  102. std::chrono::duration<double> elapsed = current - start;
  103. return elapsed.count();
  104. }
  105. } // anonymous namespace
  106. // ----------------------------------------------------------------------------------------------------
  107. // Watch
  108. // ----------------------------------------------------------------------------------------------------
  109. Watch::Watch( const vector<fs::path> &filePaths, bool needsCallback )
  110. {
  111. mWatchItems.reserve( filePaths.size() );
  112. for( const auto &fp : filePaths ) {
  113. auto fullPath = findFullFilePath( fp );
  114. mWatchItems.push_back( { fullPath, fs::last_write_time( fullPath ), true } );
  115. }
  116. if( needsCallback ) {
  117. // mark all files as modified, using the full path we just resolved.
  118. for( const auto &item : mWatchItems )
  119. mModifiedFilePaths.push_back( item.mFilePath );
  120. setNeedsCallback( true );
  121. }
  122. }
  123. void Watch::checkCurrent()
  124. {
  125. // Discard when there are no more connected slots
  126. if( mSignalChanged.getNumSlots() == 0 ) {
  127. markDiscarded();
  128. return;
  129. }
  130. mModifiedFilePaths.clear();
  131. for( auto &item : mWatchItems ) {
  132. try {
  133. if( item.mEnabled && fs::exists( item.mFilePath ) ) {
  134. auto timeLastWrite = fs::last_write_time( item.mFilePath );
  135. if( item.mTimeStamp < timeLastWrite ) {
  136. item.mTimeStamp = timeLastWrite;
  137. mModifiedFilePaths.emplace_back( item.mFilePath );
  138. setNeedsCallback( true );
  139. }
  140. }
  141. item.mErrors = 0;
  142. }
  143. catch( fs::filesystem_error & ) {
  144. /*
  145. ++item.mErrors;
  146. if( item.mErrors > 3 ) {
  147. // TODO check to see what's wrong with the file. Perhaps deleted?
  148. }
  149. */
  150. }
  151. }
  152. }
  153. void Watch::unwatch( const fs::path &filePath )
  154. {
  155. mWatchItems.erase( remove_if( mWatchItems.begin(), mWatchItems.end(),
  156. [&filePath]( const WatchItem &item ) {
  157. return item.mFilePath == filePath;
  158. } ),
  159. mWatchItems.end() );
  160. if( mWatchItems.empty() )
  161. markDiscarded();
  162. }
  163. void Watch::setEnabled( bool enable, const fs::path &filePath )
  164. {
  165. for( auto &item : mWatchItems ) {
  166. if( item.mFilePath == filePath ) {
  167. item.mEnabled = enable;
  168. // update the timestamp so that any modifications while
  169. // the watch was disabled don't trigger a callback
  170. item.mTimeStamp = fs::last_write_time( item.mFilePath );
  171. }
  172. }
  173. }
  174. void Watch::emitCallback()
  175. {
  176. WatchEvent event( mModifiedFilePaths );
  177. mSignalChanged.emit( event );
  178. setNeedsCallback( false );
  179. }
  180. // ----------------------------------------------------------------------------------------------------
  181. // FileWatcher
  182. // ----------------------------------------------------------------------------------------------------
  183. // static
  184. FileWatcher& FileWatcher::instance()
  185. {
  186. static FileWatcher sInstance;
  187. return sInstance;
  188. }
  189. FileWatcher::FileWatcher()
  190. {
  191. }
  192. FileWatcher::~FileWatcher()
  193. {
  194. stopWatchPolling();
  195. }
  196. void FileWatcher::setWatchingEnabled( bool enable )
  197. {
  198. if( mWatchingEnabled == enable )
  199. return;
  200. mWatchingEnabled = enable;
  201. if( enable )
  202. configureWatchPolling();
  203. else
  204. stopWatchPolling();
  205. }
  206. void FileWatcher::setConnectToAppUpdateEnabled( bool enable )
  207. {
  208. if( mConnectToAppUpdateEnabled == enable )
  209. return;
  210. mConnectToAppUpdateEnabled = enable;
  211. if( ! mConnectToAppUpdateEnabled && mConnectionAppUpdate.isConnected() )
  212. mConnectionAppUpdate.disconnect();
  213. if( mConnectToAppUpdateEnabled && ! mConnectionAppUpdate.isConnected() && app::AppBase::get() )
  214. connectAppUpdate();
  215. }
  216. signals::Connection FileWatcher::watch( const fs::path &filePath, const function<void ( const WatchEvent& )> &callback )
  217. {
  218. vector<fs::path> filePaths = { filePath };
  219. return watch( filePaths, Options(), callback );
  220. }
  221. signals::Connection FileWatcher::watch( const fs::path &filePath, const Options &options, const function<void( const WatchEvent& )> &callback )
  222. {
  223. vector<fs::path> filePaths = { filePath };
  224. return watch( filePaths, options, callback );
  225. }
  226. signals::Connection FileWatcher::watch( const vector<fs::path> &filePaths, const function<void ( const WatchEvent& )> &callback )
  227. {
  228. return watch( filePaths, Options(), callback );
  229. }
  230. signals::Connection FileWatcher::watch( const vector<fs::path> &filePaths, const Options &options, const function<void ( const WatchEvent& )> &callback )
  231. {
  232. auto watch = new Watch( filePaths, options.mCallOnWatch );
  233. auto conn = watch->connect( callback );
  234. lock_guard<recursive_mutex> lock( mMutex );
  235. mWatchList.emplace_back( watch );
  236. if( options.mCallOnWatch )
  237. watch->emitCallback();
  238. configureWatchPolling();
  239. return conn;
  240. }
  241. void FileWatcher::unwatch( const fs::path &filePath )
  242. {
  243. auto fullPath = findFullFilePath( filePath );
  244. lock_guard<recursive_mutex> lock( mMutex );
  245. for( auto it = mWatchList.begin(); it != mWatchList.end(); /* */ ) {
  246. const auto &watch = *it;
  247. watch->unwatch( fullPath );
  248. if( watch->isDiscarded() ) {
  249. it = mWatchList.erase( it );
  250. continue;
  251. }
  252. ++it;
  253. }
  254. }
  255. void FileWatcher::unwatch( const vector<fs::path> &filePaths )
  256. {
  257. for( const auto &filePath : filePaths ) {
  258. unwatch( filePath );
  259. }
  260. }
  261. void FileWatcher::enable( const fs::path &filePath )
  262. {
  263. auto fullPath = findFullFilePath( filePath );
  264. lock_guard<recursive_mutex> lock( mMutex );
  265. for( auto &watch : mWatchList ) {
  266. watch->setEnabled( true, fullPath );
  267. }
  268. }
  269. void FileWatcher::disable( const fs::path &filePath )
  270. {
  271. auto fullPath = findFullFilePath( filePath );
  272. lock_guard<recursive_mutex> lock( mMutex );
  273. for( auto &watch : mWatchList ) {
  274. watch->setEnabled( false, fullPath );
  275. }
  276. }
  277. void FileWatcher::connectAppUpdate()
  278. {
  279. mConnectionAppUpdate = app::AppBase::get()->getSignalUpdate().connect( bind( &FileWatcher::update, this ) );
  280. }
  281. void FileWatcher::configureWatchPolling()
  282. {
  283. if( mConnectToAppUpdateEnabled && ! mConnectionAppUpdate.isConnected() && app::AppBase::get() )
  284. connectAppUpdate();
  285. if( ! mThread.joinable() ) {
  286. mThreadShouldQuit = false;
  287. mThread = thread( std::bind( &FileWatcher::threadEntry, this ) );
  288. }
  289. }
  290. void FileWatcher::stopWatchPolling()
  291. {
  292. mConnectionAppUpdate.disconnect();
  293. mThreadShouldQuit = true;
  294. if( mThread.joinable() ) {
  295. mThread.join();
  296. }
  297. }
  298. void FileWatcher::threadEntry()
  299. {
  300. setThreadName( "cinder::FileWatcher" );
  301. while( ! mThreadShouldQuit ) {
  302. LOG_UPDATE( "epoch seconds: " << getElapsedSeconds() );
  303. // scope the lock outside of the sleep
  304. {
  305. lock_guard<recursive_mutex> lock( mMutex );
  306. LOG_UPDATE( "\t - updating watches, elapsed seconds: " << getElapsedSeconds() );
  307. for( auto it = mWatchList.begin(); it != mWatchList.end(); /* */ ) {
  308. const auto &watch = *it;
  309. // erase discarded
  310. if( watch->isDiscarded() ) {
  311. it = mWatchList.erase( it );
  312. continue;
  313. }
  314. // check if Watch's target has been modified and needs a callback, if not already marked.
  315. if( ! watch->needsCallback() ) {
  316. watch->checkCurrent();
  317. // If the Watch needs a callback, move it to the front of the list
  318. if( watch->needsCallback() && it != mWatchList.begin() ) {
  319. mWatchList.splice( mWatchList.begin(), mWatchList, it );
  320. }
  321. }
  322. ++it;
  323. }
  324. }
  325. this_thread::sleep_for( chrono::duration<double>( mThreadUpdateInterval ) );
  326. }
  327. }
  328. void FileWatcher::update()
  329. {
  330. LOG_UPDATE( "elapsed seconds: " << getElapsedSeconds() );
  331. // Note: if threadEntry sleeps less than an app frame, it can cause multiple updates() to be skipped.
  332. // try-lock so we don't block the main thread, if we fail to acquire the mutex then we skip this update
  333. unique_lock<recursive_mutex> lock( mMutex, std::try_to_lock );
  334. if( ! lock.owns_lock() ) {
  335. return;
  336. }
  337. LOG_UPDATE( "\t - checking watches, elapsed seconds: " << getElapsedSeconds() );
  338. // Watches are sorted so that all that need a callback are in the beginning.
  339. // So break when we hit the first one that doesn't need a callback
  340. for( const auto &watch : mWatchList ) {
  341. if( ! watch->needsCallback() )
  342. break;
  343. watch->emitCallback();
  344. }
  345. }
  346. const size_t FileWatcher::getNumWatchedFiles() const
  347. {
  348. lock_guard<recursive_mutex> lock(mMutex);
  349. size_t result = 0;
  350. for( const auto &w : mWatchList ) {
  351. result += w->getItems().size();
  352. }
  353. return result;
  354. }
  355. } // namespace cinder