├── .circleci └── config.yml ├── .clang-format ├── .clang-tidy ├── .github └── no-response.yml ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── SUPPORT.md ├── appveyor.yml ├── binding.gyp ├── docs ├── README.md ├── help.md ├── linux.md ├── macos.md └── windows.md ├── lib ├── binding.js ├── cli.js ├── index.js ├── logger.js ├── native-watcher-registry.js ├── native-watcher.js ├── path-watcher-manager.js ├── path-watcher.js └── registry │ ├── directory-node.js │ ├── helper.js │ ├── nonrecursive-watcher-node.js │ ├── recursive-watcher-node.js │ ├── result.js │ └── tree.js ├── package-lock.json ├── package.json ├── script ├── c++-format ├── c++-format.ps1 ├── c++-lint └── helper │ └── gen-compilation-db.js ├── src ├── binding.cpp ├── errable.cpp ├── errable.h ├── helper │ ├── common.h │ ├── common_impl.h │ ├── common_posix.cpp │ ├── common_win.cpp │ ├── libuv.cpp │ ├── libuv.h │ ├── linux │ │ ├── constants.h │ │ └── helper.h │ ├── macos │ │ ├── helper.cpp │ │ └── helper.h │ └── windows │ │ ├── constants.h │ │ ├── helper.cpp │ │ └── helper.h ├── hub.cpp ├── hub.h ├── lock.cpp ├── lock.h ├── log.cpp ├── log.h ├── message.cpp ├── message.h ├── message_buffer.cpp ├── message_buffer.h ├── nan │ ├── all_callback.cpp │ ├── all_callback.h │ ├── async_callback.cpp │ ├── async_callback.h │ ├── functional_callback.cpp │ ├── functional_callback.h │ ├── options.cpp │ └── options.h ├── polling │ ├── directory_record.cpp │ ├── directory_record.h │ ├── polled_root.cpp │ ├── polled_root.h │ ├── polling_iterator.cpp │ ├── polling_iterator.h │ ├── polling_thread.cpp │ └── polling_thread.h ├── queue.cpp ├── queue.h ├── result.h ├── status.cpp ├── status.h ├── thread.cpp ├── thread.h ├── thread_starter.cpp ├── thread_starter.h └── worker │ ├── linux │ ├── cookie_jar.cpp │ ├── cookie_jar.h │ ├── linux_worker_platform.cpp │ ├── pipe.cpp │ ├── pipe.h │ ├── side_effect.cpp │ ├── side_effect.h │ ├── watch_registry.cpp │ ├── watch_registry.h │ ├── watched_directory.cpp │ └── watched_directory.h │ ├── macos │ ├── batch_handler.cpp │ ├── batch_handler.h │ ├── flags.h │ ├── macos_worker_platform.cpp │ ├── rename_buffer.cpp │ ├── rename_buffer.h │ ├── subscription.cpp │ └── subscription.h │ ├── recent_file_cache.cpp │ ├── recent_file_cache.h │ ├── windows │ ├── subscription.cpp │ ├── subscription.h │ └── windows_worker_platform.cpp │ ├── worker_platform.h │ ├── worker_thread.cpp │ └── worker_thread.h └── test ├── configuration.test.js ├── errors.test.js ├── events ├── basic.test.js ├── kind-change.test.js ├── nonrecursive.test.js ├── parent-rename.test.js ├── rapid.test.js ├── root-rename.test.js ├── symlink.test.js ├── unicode-paths.test.js └── unpaired-rename.test.js ├── fixture └── .gitignore ├── global.js ├── helper.js ├── index.test.js ├── matcher.js ├── mocha.opts ├── native-watcher-registry.test.js ├── polling.test.js ├── unwatching.test.js └── watching.test.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | macos: 6 | xcode: 10.2.1 7 | environment: 8 | NODE_VERSION: '12' 9 | steps: 10 | - checkout 11 | - run: 12 | name: install llvm 13 | command: | 14 | brew update 15 | brew install llvm 16 | echo "export PATH=${PATH}:/usr/local/opt/llvm/bin" >> $BASH_ENV 17 | - run: 18 | name: install node.js with nvm 19 | command: | 20 | export NVM_DIR=${HOME}/.nvm 21 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash 22 | [ -s "${NVM_DIR}/nvm.sh" ] && \. "${NVM_DIR}/nvm.sh" 23 | nvm install ${NODE_VERSION} 24 | nvm alias default ${NODE_VERSION} 25 | echo "[ -s \"${NVM_DIR}/nvm.sh\" ] && . \"${NVM_DIR}/nvm.sh\"" >> $BASH_ENV 26 | - run: npm install --build-from-source 27 | - run: npm run ci:circle 28 | - store_test_results: 29 | path: test-results 30 | - store_artifacts: 31 | path: test-results 32 | - run: 33 | name: lint js 34 | command: npm run lint:js 35 | - run: 36 | name: format c++ 37 | command: npm run format && git diff --exit-code -- src/ test/ 38 | #- run: 39 | # name: lint c++ 40 | # command: npm run lint:cpp 41 | - run: 42 | name: deploy on tag 43 | command: git describe --tags --exact >/dev/null 2>&1 && npm run pre-build -- -u ${NODE_PRE_GYP_GITHUB_TOKEN} || true 44 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | AccessModifierOffset: -2 4 | AlignAfterOpenBracket: DontAlign 5 | AlignConsecutiveAssignments: false 6 | AlignConsecutiveDeclarations: false 7 | AlignEscapedNewlines: DontAlign 8 | AlignOperands: false 9 | AlignTrailingComments: false 10 | AllowAllParametersOfDeclarationOnNextLine: false 11 | AllowShortBlocksOnASingleLine: false 12 | AllowShortCaseLabelsOnASingleLine: true 13 | AllowShortFunctionsOnASingleLine: Inline 14 | AllowShortIfStatementsOnASingleLine: true 15 | AllowShortLoopsOnASingleLine: true 16 | AlwaysBreakAfterReturnType: None 17 | AlwaysBreakBeforeMultilineStrings: true 18 | AlwaysBreakTemplateDeclarations: true 19 | BinPackArguments: false 20 | BinPackParameters: false 21 | BraceWrapping: 22 | AfterClass: true 23 | AfterControlStatement: false 24 | AfterEnum: true 25 | AfterFunction: true 26 | AfterNamespace: false 27 | AfterObjCDeclaration: false 28 | AfterStruct: true 29 | AfterUnion: true 30 | BeforeCatch: false 31 | BeforeElse: false 32 | IndentBraces: false 33 | SplitEmptyFunction: false 34 | SplitEmptyRecord: false 35 | SplitEmptyNamespace: false 36 | BreakBeforeBinaryOperators: NonAssignment 37 | BreakBeforeBraces: Custom 38 | BreakBeforeInheritanceComma: false 39 | BreakBeforeTernaryOperators: true 40 | BreakConstructorInitializersBeforeComma: false 41 | BreakConstructorInitializers: AfterColon 42 | BreakAfterJavaFieldAnnotations: false 43 | BreakStringLiterals: true 44 | ColumnLimit: 120 45 | CommentPragmas: '^ IWYU pragma:' 46 | CompactNamespaces: false 47 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 48 | ConstructorInitializerIndentWidth: 2 49 | ContinuationIndentWidth: 2 50 | Cpp11BracedListStyle: true 51 | DerivePointerAlignment: false 52 | DisableFormat: false 53 | FixNamespaceComments: false 54 | ForEachMacros: [] 55 | IncludeCategories: 56 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 57 | Priority: 2 58 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 59 | Priority: 3 60 | - Regex: '.*' 61 | Priority: 1 62 | IncludeIsMainRegex: '_(impl|posix|win|mac)?$' 63 | IndentCaseLabels: true 64 | IndentWidth: 2 65 | IndentWrappedFunctionNames: false 66 | KeepEmptyLinesAtTheStartOfBlocks: false 67 | MacroBlockBegin: '' 68 | MacroBlockEnd: '' 69 | MaxEmptyLinesToKeep: 1 70 | NamespaceIndentation: None 71 | PenaltyBreakAssignment: 2 72 | PenaltyBreakBeforeFirstCallParameter: 19 73 | PenaltyBreakComment: 300 74 | PenaltyBreakFirstLessLess: 120 75 | PenaltyBreakString: 1000 76 | PenaltyExcessCharacter: 1000000 77 | PenaltyReturnTypeOnItsOwnLine: 200 78 | PointerAlignment: Right 79 | ReflowComments: true 80 | SortIncludes: true 81 | SortUsingDeclarations: true 82 | SpaceAfterCStyleCast: true 83 | SpaceAfterTemplateKeyword: true 84 | SpaceBeforeAssignmentOperators: true 85 | SpaceBeforeParens: ControlStatements 86 | SpaceInEmptyParentheses: false 87 | SpacesBeforeTrailingComments: 2 88 | SpacesInAngles: false 89 | SpacesInContainerLiterals: false 90 | SpacesInCStyleCastParentheses: false 91 | SpacesInParentheses: false 92 | SpacesInSquareBrackets: false 93 | Standard: Cpp11 94 | TabWidth: 8 95 | UseTab: Never 96 | ... 97 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | Checks: "-*,\ 3 | bugprone-*,\ 4 | cert-*,\ 5 | cppcoreguidelines-*,\ 6 | clang-diagnostic-*,\ 7 | clang-analyzer-*,\ 8 | misc-*,\ 9 | modernize-*,\ 10 | performance-*,\ 11 | readability-*,\ 12 | -bugprone-undefined-memory-manipulation,\ 13 | -clang-diagnostic-unused-command-line-argument,\ 14 | -cppcoreguidelines-pro-bounds-constant-array-index,\ 15 | -cppcoreguidelines-pro-bounds-array-to-pointer-decay,\ 16 | -cppcoreguidelines-pro-type-union-access,\ 17 | -cppcoreguidelines-pro-type-reinterpret-cast,\ 18 | -cppcoreguidelines-pro-bounds-pointer-arithmetic,\ 19 | -cppcoreguidelines-owning-memory,\ 20 | -modernize-make-unique,\ 21 | -modernize-make-shared," 22 | FormatStyle: file 23 | WarningsAsErrors: '*' 24 | AnalyzeTemporaryDtors: false 25 | CheckOptions: 26 | - key: readability-braces-around-statements.ShortStatementLines 27 | value: '1' 28 | ... 29 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an issue is closed for lack of response 4 | daysUntilClose: 28 5 | 6 | # Label requiring a response 7 | responseRequiredLabel: more-information-needed 8 | 9 | # Comment to post when closing an issue for lack of response. Set to `false` to disable. 10 | closeComment: > 11 | This issue has been automatically closed because there has been no response 12 | to our request for more information from the original author. With only the 13 | information that is currently in the issue, we don't have enough information 14 | to take action. Please reach out if you have or find the answers we need so 15 | that we can investigate further. 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | prebuilds/ 4 | bin 5 | 6 | *.log 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | - 12 5 | sudo: false 6 | dist: trusty 7 | addons: 8 | apt: 9 | packages: 10 | - clang-3.9 11 | - build-essential 12 | git: 13 | depth: 10 14 | env: 15 | global: 16 | - CC=clang 17 | - CXX=clang++ 18 | - npm_config_clang=1 19 | - secure: rBW+3CfuKKohLPy525/A1W6YezUQ7jxTGauYaDcugwa/jFJciO8xHJUJSBtT0HXL0sxYPGKCeN9uDgp1emMVOON0yYqQu3ia6aWJjrIWh1WQoMgaj67almNzVm8fDEbL17dw57fw3q1hGPmUxeFToIvVYhcTAVMVNfGBjQs5X9gpxOuGvUdd8uDhv/avVTKW5RLXAL9mznfCWeCnsEwMunr4le0FdyJvk2xxd05aj2Xw9VlYzV82e1dazSlbz3eb/QgghG10uuRF2LZyO4k7X82NBBpWml9RM3NwE7NBW1Awqakdd9bBIxmknAZwGQjEEqw2D8DDO1A/qnERhkE9KNIkpOv9wRyZUIFHE3UWZiOIhDPJifGKe5JV8EzGJ6lwePvA7jJxlQJOYXtOKzt91H9gZLIucYtOlMJHc/vo2ZzmhYRblDcQUf1+FSaFyiaC+GH1bJt/MYWYwmCkQSyp8rfKjPGdoTdZ1uyploFWIBuMFmN/X5cniMlhXddmWu8e2HUffpJch7CeFNujwqYrf8TJr7v+YxlWkqUfWIiXlRS1qHy3JsK1D+h11a+FvPLyfVdRLlKtmVmI4kqkxuU6UsVFaKYOBO2XYrXdcaAp3ZX4wdEoQdcwoE+n/72P2vQJVSStLspaK+yr52Vg5wje5EuUbIyLJDzBIqi9H5gjwmM= 20 | - secure: TzA+hIv7XxJXaDls/iH/MjFZQ6z5tNJtkaMJJY8P7L1j9gMLjtrTpsaVyAlV2fqvuHniyydyldm4RcWUiOr6+YopUMjrUzf2TBjXxyeeOfRMvR0lFcKUdroSlhu2AVGcZgYlDM2Hesn7DWoDsN+eXB+fhjrCxunnqzHNv/uYvblK00/qUC+iuTJABqD5yD2aEMEZmnjSsQEcHcCDGQkSKDeZZzhAAIpDE26yqLYKN+c9a16NrLb+8MVHzLu3fWRnjUufS4n5811ck97IP94BXeRbaROXYW79xvzbZsuFyyVoE4CxxdiVrBNYSYUEg7cBAOYBHXWv8xCAi9H0VxzsHOXlyo0V+fG8LyzdRbrvdtl+LlIUCFoMce6P+iFk68rcs6lxIxVMk2WBDo66TENupHzu5nQv4t5MFwILj/taRCIrAiG0tRGGXoAGlcWpmxZIsapseT7+h7JK051LLr0HZz5ZwpijctT/DkKtjJx+x3gbbIUzUJ65Ui7Qj3vgUZFwDCBA7Xc4tHEiWV2KIwk0FewdsBJJkkob093I+JkgjHoJQM98pVB6XrrJk6gPd2njHfowCjIJUpMkS3kC2qKE+ndNMkf1ZFQXZLDj45HZI1aW4iFx1uv5teihX1IJmtLVZL0fE3xoViWsXWlwDz5SA2VUYun5ITpxvBv5ot1gBo0= 21 | before_install: 22 | - npm install -g npm@latest greenkeeper-lockfile@1 23 | install: 24 | - npm install --build-from-source 25 | before_script: greenkeeper-lockfile-update 26 | script: 27 | - npm run ci:travis 28 | - npm run lint:js 29 | after_script: greenkeeper-lockfile-upload 30 | deploy: 31 | provider: script 32 | script: npm run pre-build -- -u ${NODE_PRE_GYP_GITHUB_TOKEN} 33 | skip_cleanup: true 34 | on: 35 | tags: true 36 | notifications: 37 | email: 38 | on_success: never 39 | on_failure: change 40 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | See https://github.com/atom/atom/blob/master/CODE_OF_CONDUCT.md 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See https://github.com/atom/atom/blob/master/CONTRIBUTING.md 2 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Prerequisites 10 | 11 | * [ ] Put an X between the brackets on this line if you have done all of the following: 12 | * Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode 13 | * Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/ 14 | * Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq 15 | * Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom 16 | * Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages 17 | 18 | ### Description 19 | 20 | [Description of the issue] 21 | 22 | ### Steps to Reproduce 23 | 24 | 1. [First Step] 25 | 2. [Second Step] 26 | 3. [and so on...] 27 | 28 | **Expected behavior:** 29 | 30 | [What you expect to happen] 31 | 32 | **Actual behavior:** 33 | 34 | [What actually happens] 35 | 36 | **Reproduces how often:** 37 | 38 | [What percentage of the time does it reproduce?] 39 | 40 | ### Versions 41 | 42 | You can get this information from copy and pasting the output of `atom --version` and `apm --version` from the command line. Also, please include the OS and what version of the OS you're running. 43 | 44 | ### Additional Information 45 | 46 | Any additional information, configuration or data that might be necessary to reproduce the issue. 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 GitHub Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Requirements 2 | 3 | * Filling out the template is required. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. 4 | * All new code requires tests to ensure against regressions 5 | 6 | ### Description of the Change 7 | 8 | 13 | 14 | ### Alternate Designs 15 | 16 | 17 | 18 | ### Benefits 19 | 20 | 21 | 22 | ### Possible Drawbacks 23 | 24 | 25 | 26 | ### Applicable Issues 27 | 28 | 29 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | See https://github.com/atom/atom/blob/master/SUPPORT.md 2 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2015 2 | 3 | cache: 4 | - '%HOMEDRIVE%%HOMEPATH%\.node-gyp' 5 | - '%APPDATA%\npm-cache' 6 | 7 | environment: 8 | nodejs_version: "12" 9 | NODE_PRE_GYP_GITHUB_TOKEN: 10 | secure: izXdqKc3Q97YCK/iHmkf5704WRhBwZXVBn2G+MX/NgyxVJPfwTkZxc8WMET/QZOh 11 | 12 | platform: 13 | - x86 14 | - x64 15 | 16 | build: off 17 | 18 | install: 19 | - ps: Install-Product node $env:nodejs_version $env:platform 20 | - npm config set msvs_version 2015 21 | - npm install --build-from-source 22 | 23 | test_script: 24 | - npm run ci:appveyor 25 | - ps: | 26 | if (($env:APPVEYOR_REPO_TAG -eq 'true') -and $env:NODE_PRE_GYP_GITHUB_TOKEN) { 27 | npm run pre-build -- -u $env:NODE_PRE_GYP_GITHUB_TOKEN 2> $null 28 | if ($LASTEXITCODE -eq 0) { 29 | $host.SetShouldExit(0) 30 | } 31 | } 32 | 33 | shallow_clone: true 34 | clone_depth: 5 35 | 36 | skip_branch_with_pr: true 37 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [{ 3 | "target_name": "watcher", 4 | "sources": [ 5 | "src/binding.cpp", 6 | "src/hub.cpp", 7 | "src/log.cpp", 8 | "src/errable.cpp", 9 | "src/queue.cpp", 10 | "src/lock.cpp", 11 | "src/message.cpp", 12 | "src/message_buffer.cpp", 13 | "src/thread_starter.cpp", 14 | "src/thread.cpp", 15 | "src/status.cpp", 16 | "src/worker/worker_thread.cpp", 17 | "src/worker/recent_file_cache.cpp", 18 | "src/polling/directory_record.cpp", 19 | "src/polling/polled_root.cpp", 20 | "src/polling/polling_iterator.cpp", 21 | "src/polling/polling_thread.cpp", 22 | "src/helper/libuv.cpp", 23 | "src/nan/async_callback.cpp", 24 | "src/nan/all_callback.cpp", 25 | "src/nan/functional_callback.cpp", 26 | "src/nan/options.cpp" 27 | ], 28 | "include_dirs": [ 29 | " /watchroot/some-path 13 | rm /watchroot/some-path 14 | mv /watchroot/a-directory /watchroot/some-path 15 | ``` 16 | 17 | An FSEventStream callback may receive only these events entries: 18 | 19 | ``` 20 | /watchroot/a-directory = 21 | kFSEventStreamEventFlagItemCreated | 22 | kFSEventStreamEventFlagItemRenamed | 23 | kFSEventStreamEventFlagItemIsDir 24 | /watchroot/some-path = 25 | kFSEventStreamEventFlagItemCreated | 26 | kFSEventStreamEventFlagItemModified | 27 | kFSEventStreamEventFlagItemRemoved | 28 | kFSEventStreamEventFlagItemRenamed | 29 | kFSEventStreamEventFlagItemIsFile | 30 | kFSEventStreamEventFlagItemIsDir 31 | ``` 32 | 33 | Depending on the timing involved and the way that events are batched, sometimes a path's event entry may be delivered multiple times, with new bits OR'd in each time: 34 | 35 | ``` 36 | /watchroot/a-directory = 37 | kFSEventStreamEventFlagItemCreated | 38 | kFSEventStreamEventFlagItemIsDir 39 | /watchroot/some-path = 40 | kFSEventStreamEventFlagItemCreated | 41 | kFSEventStreamEventFlagItemModified | 42 | kFSEventStreamEventFlagItemIsFile 43 | /watchroot/some-path = 44 | kFSEventStreamEventFlagItemCreated | 45 | kFSEventStreamEventFlagItemModified | 46 | kFSEventStreamEventFlagItemRemoved | 47 | kFSEventStreamEventFlagItemIsFile 48 | /watchroot/a-directory = 49 | kFSEventStreamEventFlagItemCreated | 50 | kFSEventStreamEventFlagItemRenamed | 51 | kFSEventStreamEventFlagItemIsDir 52 | /watchroot/some-path = 53 | kFSEventStreamEventFlagItemCreated | 54 | kFSEventStreamEventFlagItemModified | 55 | kFSEventStreamEventFlagItemRemoved | 56 | kFSEventStreamEventFlagItemRenamed | 57 | kFSEventStreamEventFlagItemIsFile | 58 | kFSEventStreamEventFlagItemIsDir 59 | ``` 60 | 61 | To disentangle this, @atom/watcher combines information from: 62 | 63 | * An `lstat()` call performed the last time an event occurred at this path, if an event has occurred at this path recently; 64 | * The bits that are set in each event; 65 | * A recent `lstat()` call that was performed during this event batch. 66 | 67 | By combining these data points, @atom/watcher heuristically deduces what actions must have occurred on the filesystem to result in the observed sequence of events. 68 | 69 | FSEvents provides no mechanism to associate the old and new sides of a rename event. It only produces an event at the old and new paths with `kFSEventStreamEventFlagItemRenamed` bit set. This bit is set regardless of whether or not the source or destination are both watched and are not guaranteed to arrive with deterministic order or timing. @atom/watcher uses a cache (storing a maximum of 4k entries) of recently observed `lstat()` results to correlate rename events by inode. If half of a rename event is unpaired after 50ms, it is emitted as a `"create"` or `"delete"` instead. 70 | 71 | ## Known platform limits 72 | 73 | After roughly 450 event streams have been created and attached, `FSEventStreamStart()` will fail by returning false. When this is detected @atom/watcher falls back to polling. 74 | -------------------------------------------------------------------------------- /docs/windows.md: -------------------------------------------------------------------------------- 1 | # Windows 2 | 3 | On Windows, @atom/watcher uses the [`ReadDirectoryChangesW()`](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365465%28v%3Dvs.85%29.aspx) call to monitor a directory for changes. ReadDirectoryChangesW is called in asynchronous mode with a completion routine. 4 | Out-of-band command signalling is handled by an asynchronous procedure call scheduled with [`QueueUserAPC()`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms684954%28v%3Dvs.85%29.aspx). 5 | 6 | ## ReadDirectoryChangesW oddities 7 | 8 | The [`FILE_NOTIFY_INFORMATION` structure](https://msdn.microsoft.com/en-us/library/windows/desktop/aa364391%28v=vs.85%29.aspx) provided to the completion routine does not indicate whether the entry is a directory or a file. @atom/watcher uses [`GetFileAttributesW`](https://msdn.microsoft.com/en-us/library/windows/desktop/aa364944%28v=vs.85%29.aspx) to identify entry kinds, but leaves them as `"unknown"` for deletions. 9 | 10 | If the filesystem does not support events and `ERROR_INVALID_FUNCTION` is returned, @atom/watcher will fall back to polling. However, depending on the filesystem, Windows may simply fail to deliver events. 11 | 12 | ## Known platform limits 13 | 14 | A Windows process can open a maximum of 16,711,680 handles, so the number of watch roots is somewhat less than that :wink: 15 | -------------------------------------------------------------------------------- /lib/binding.js: -------------------------------------------------------------------------------- 1 | const logger = require('./logger') 2 | 3 | let watcher = null 4 | function getWatcher () { 5 | if (!watcher) { 6 | try { 7 | watcher = require('../build/Release/watcher.node') 8 | } catch (err) { 9 | watcher = require('../build/Debug/watcher.node') 10 | } 11 | } 12 | return watcher 13 | } 14 | 15 | // Private: Logging mode constants 16 | const DISABLE = Symbol('disable') 17 | const STDERR = Symbol('stderr') 18 | const STDOUT = Symbol('stdout') 19 | 20 | function logOption (baseName, options, normalized) { 21 | const value = options[baseName] 22 | 23 | if (value === undefined) return 24 | 25 | if (value === DISABLE) { 26 | normalized[`${baseName}Disable`] = true 27 | return 28 | } 29 | 30 | if (value === STDERR) { 31 | normalized[`${baseName}Stderr`] = true 32 | return 33 | } 34 | 35 | if (value === STDOUT) { 36 | normalized[`${baseName}Stdout`] = true 37 | return 38 | } 39 | 40 | if (typeof value === 'string' || value instanceof String) { 41 | normalized[`${baseName}File`] = value 42 | return 43 | } 44 | 45 | throw new Error(`option ${baseName} must be DISABLE, STDERR, STDOUT, or a filename`) 46 | } 47 | 48 | function jsLogOption (value) { 49 | if (value === undefined) return 50 | 51 | if (value === DISABLE) { 52 | logger.disable() 53 | return 54 | } 55 | 56 | if (value === STDERR) { 57 | logger.toStderr() 58 | return 59 | } 60 | 61 | if (value === STDOUT) { 62 | logger.toStdout() 63 | return 64 | } 65 | 66 | if (typeof value === 'string' || value instanceof String) { 67 | logger.toFile(value) 68 | return 69 | } 70 | 71 | throw new Error('option jsLog must be DISABLE, STDERR, STDOUT, or a filename') 72 | } 73 | 74 | function configure (options) { 75 | if (!options) { 76 | return Promise.reject(new Error('configure() requires an option object')) 77 | } 78 | 79 | const normalized = {} 80 | 81 | logOption('mainLog', options, normalized) 82 | logOption('workerLog', options, normalized) 83 | logOption('pollingLog', options, normalized) 84 | jsLogOption(options.jsLog) 85 | 86 | if (options.workerCacheSize) normalized.workerCacheSize = options.workerCacheSize 87 | if (options.pollingThrottle) normalized.pollingThrottle = options.pollingThrottle 88 | if (options.pollingInterval) normalized.pollingInterval = options.pollingInterval 89 | 90 | return new Promise((resolve, reject) => { 91 | getWatcher().configure(normalized, err => (err ? reject(err) : resolve())) 92 | }) 93 | } 94 | 95 | function status () { 96 | return new Promise((resolve, reject) => { 97 | getWatcher().status((err, st) => { 98 | if (err) { reject(err) } else { resolve(st) } 99 | }) 100 | }) 101 | } 102 | 103 | function lazy (key) { 104 | return function (...args) { 105 | return getWatcher()[key](...args) 106 | } 107 | } 108 | 109 | module.exports = { 110 | watch: lazy('watch'), 111 | unwatch: lazy('unwatch'), 112 | configure, 113 | status, 114 | 115 | DISABLE, 116 | STDERR, 117 | STDOUT 118 | } 119 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path') 4 | const watcher = require('./index') 5 | 6 | function usage () { 7 | console.log('Usage: watcher [...] [options]') 8 | console.log(' -h, --help\tShow help') 9 | console.log(' -v, --verbose\tMake output more verbose') 10 | } 11 | 12 | function start (dirs, verbose) { 13 | const options = { recursive: true } 14 | 15 | const eventCallback = events => { 16 | for (const event of events) { 17 | if (event.action === 'modified' && !verbose) { 18 | return 19 | } else if (event.action === 'renamed') { 20 | console.log( 21 | `${event.action} ${event.kind}: ${event.oldPath} → ${event.path}` 22 | ) 23 | } else { 24 | console.log(`${event.action} ${event.kind}: ${event.path}`) 25 | } 26 | } 27 | } 28 | 29 | for (const dir of dirs) { 30 | watcher 31 | .watchPath(dir, options, eventCallback) 32 | .then(w => { 33 | if (verbose) { 34 | console.log('Watching', dir) 35 | } 36 | w.onDidError(err => console.error('Error:', err)) 37 | }) 38 | .catch(err => { 39 | console.error('Error:', err) 40 | }) 41 | } 42 | } 43 | 44 | function main (argv) { 45 | const dirs = [] 46 | let verbose = false 47 | 48 | argv.forEach((arg, i) => { 49 | if (i === 0) { 50 | return 51 | } 52 | if (i === 1 && path.basename(argv[0]) === 'node') { 53 | return 54 | } 55 | if (arg === '-h' || arg === '--help') { 56 | return usage() 57 | } else if (arg === '-v' || arg === '--verbose') { 58 | verbose = true 59 | } else { 60 | dirs.push(arg) 61 | } 62 | }) 63 | 64 | if (dirs.length === 0) { 65 | return usage() 66 | } 67 | 68 | start(dirs, verbose) 69 | } 70 | 71 | main(process.argv) 72 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const { PathWatcherManager } = require('./path-watcher-manager') 2 | const { configure, status, DISABLE, STDERR, STDOUT } = require('./binding') 3 | 4 | // Extended: Invoke a callback with each filesystem event that occurs beneath a specified path. 5 | // 6 | // watchPath handles the efficient re-use of operating system resources across living watchers. Watching the same path 7 | // more than once, or the child of a watched path, will re-use the existing native watcher. 8 | // 9 | // * `rootPath` {String} specifies the absolute path to the root of the filesystem content to watch. 10 | // * `options` Control the watcher's behavior. 11 | // * `eventCallback` {Function} or other callable to be called each time a batch of filesystem events is observed. 12 | // * `events` {Array} of objects that describe the events that have occurred. 13 | // * `action` {String} describing the filesystem action that occurred. One of `"created"`, `"modified"`, 14 | // `"deleted"`, or `"renamed"`. 15 | // * `kind` {String} distinguishing the type of filesystem entry that was acted upon, when available. One of 16 | // `"file"`, `"directory"`, or `"unknown"`. 17 | // * `path` {String} containing the absolute path to the filesystem entry that was acted upon. 18 | // * `oldPath` For rename events, {String} containing the filesystem entry's former absolute path. 19 | // 20 | // Returns a {Promise} that will resolve to a {PathWatcher} once it has started. Note that every {PathWatcher} 21 | // is a {Disposable}, so they can be managed by a {CompositeDisposable} if desired. 22 | // 23 | // ```js 24 | // const {watchPath} = require('@atom/watcher') 25 | // 26 | // const disposable = await watchPath('/var/log', {}, events => { 27 | // console.log(`Received batch of ${events.length} events.`) 28 | // for (const event of events) { 29 | // // "created", "modified", "deleted", "renamed" 30 | // console.log(`Event action: ${event.action}`) 31 | // 32 | // // absolute path to the filesystem entry that was touched 33 | // console.log(`Event path: ${event.path}`) 34 | // 35 | // // "file", "directory", or "unknown" 36 | // console.log(`Event kind: ${event.kind}`) 37 | // 38 | // if (event.action === 'renamed') { 39 | // console.log(`.. renamed from: ${event.oldPath}`) 40 | // } 41 | // } 42 | // }) 43 | // 44 | // // Immediately stop receiving filesystem events. If this is the last 45 | // // watcher, asynchronously release any OS resources required to 46 | // // subscribe to these events. 47 | // disposable.dispose() 48 | // ``` 49 | function watchPath (rootPath, options, eventCallback) { 50 | const watcher = PathWatcherManager.instance().createWatcher(rootPath, options, eventCallback) 51 | return watcher.getStartPromise().then(() => watcher) 52 | } 53 | 54 | // Private: Return a Promise that resolves when all {NativeWatcher} instances associated with a FileSystemManager 55 | // have stopped listening. This is useful for `afterEach()` blocks in unit tests. 56 | function stopAllWatchers () { 57 | return PathWatcherManager.instance().stopAllWatchers() 58 | } 59 | 60 | function getRegistry () { 61 | return PathWatcherManager.instance().getRegistry() 62 | } 63 | 64 | // Private: Show the currently active native watchers. 65 | function printWatchers () { 66 | return PathWatcherManager.instance().print() 67 | } 68 | 69 | module.exports = { 70 | watchPath, 71 | stopAllWatchers, 72 | getRegistry, 73 | printWatchers, 74 | configure, 75 | status, 76 | DISABLE, 77 | STDERR, 78 | STDOUT 79 | } 80 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | const util = require('util') 2 | const fs = require('fs') 3 | 4 | class StreamLogger { 5 | constructor (stream) { 6 | this.stream = stream 7 | } 8 | 9 | log (...args) { 10 | this.stream.write(util.format(...args) + '\n') 11 | } 12 | } 13 | 14 | const nullLogger = { 15 | log () {} 16 | } 17 | 18 | let activeLogger = null 19 | 20 | function disable () { 21 | activeLogger = nullLogger 22 | } 23 | 24 | function toStdout () { 25 | activeLogger = new StreamLogger(process.stdout) 26 | } 27 | 28 | function toStderr () { 29 | activeLogger = new StreamLogger(process.stderr) 30 | } 31 | 32 | function toFile (filePath) { 33 | const stream = fs.createWriteStream(filePath, { defaultEncoding: 'utf8', flags: 'a' }) 34 | activeLogger = new StreamLogger(stream) 35 | } 36 | 37 | function fromEnv (value) { 38 | if (!value) { 39 | return disable() 40 | } else if (value === 'stdout') { 41 | return toStdout() 42 | } else if (value === 'stderr') { 43 | return toStderr() 44 | } else { 45 | return toFile(value) 46 | } 47 | } 48 | 49 | function getActiveLogger () { 50 | if (activeLogger === null) { 51 | fromEnv(process.env.WATCHER_LOG_JS) 52 | } 53 | return activeLogger 54 | } 55 | 56 | module.exports = { 57 | disable, 58 | toStdout, 59 | toStderr, 60 | toFile, 61 | fromEnv, 62 | log (...args) { return getActiveLogger().log(...args) } 63 | } 64 | -------------------------------------------------------------------------------- /lib/native-watcher-registry.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { log } = require('./logger') 3 | const { Tree } = require('./registry/tree') 4 | 5 | // Private: Track the directories being monitored by native filesystem watchers. Minimize the number of native watchers 6 | // allocated to receive events for a desired set of directories by: 7 | // 8 | // 1. Subscribing to the same underlying {NativeWatcher} when watching the same directory multiple times. 9 | // 2. Subscribing to an existing {NativeWatcher} on a parent of a desired directory. 10 | // 3. Replacing multiple {NativeWatcher} instances on child directories with a single new {NativeWatcher} on the 11 | // parent. 12 | class NativeWatcherRegistry { 13 | // Private: Instantiate an empty registry. 14 | // 15 | // * `createNative` {Function} that will be called with a normalized filesystem path to create a new native 16 | // filesystem watcher. 17 | constructor (createNative) { 18 | this.tree = new Tree([], createNative) 19 | } 20 | 21 | // Private: Attach a watcher to a directory, assigning it a {NativeWatcher}. If a suitable {NativeWatcher} already 22 | // exists, it will be attached to the new {PathWatcher} with an appropriate subpath configuration. Otherwise, the 23 | // `createWatcher` callback will be invoked to create a new {NativeWatcher}, which will be registered in the tree 24 | // and attached to the watcher. 25 | // 26 | // If any pre-existing child watchers are removed as a result of this operation, {NativeWatcher.onWillReattach} will 27 | // be broadcast on each with the new parent watcher as an event payload to give child watchers a chance to attach to 28 | // the new watcher. 29 | // 30 | // * `watcher` an unattached {PathWatcher}. 31 | async attach (watcher) { 32 | log('attaching watcher %s to native registry.', watcher) 33 | const normalizedDirectory = await watcher.getNormalizedPathPromise() 34 | const pathSegments = normalizedDirectory.split(path.sep).filter(segment => segment.length > 0) 35 | 36 | log('adding watcher %s to tree.', watcher) 37 | this.tree.add(pathSegments, watcher.getOptions(), (native, nativePath, options) => { 38 | watcher.attachToNative(native, nativePath, options) 39 | }) 40 | log('watcher %s added. tree state:\n%s', watcher, this.print()) 41 | } 42 | 43 | // Private: Generate a visual representation of the currently active watchers managed by this 44 | // registry. 45 | // 46 | // Returns a {String} showing the tree structure. 47 | print () { 48 | return this.tree.print() 49 | } 50 | } 51 | 52 | module.exports = { NativeWatcherRegistry } 53 | -------------------------------------------------------------------------------- /lib/path-watcher-manager.js: -------------------------------------------------------------------------------- 1 | const { PathWatcher } = require('./path-watcher') 2 | const { NativeWatcher } = require('./native-watcher') 3 | const { NativeWatcherRegistry } = require('./native-watcher-registry') 4 | 5 | // Private: Globally tracked state used to de-duplicate related [PathWatchers]{PathWatcher}. 6 | class PathWatcherManager { 7 | // Private: Access or lazily initialize the singleton manager instance. 8 | // 9 | // Returns the one and only {PathWatcherManager}. 10 | static instance () { 11 | if (!PathWatcherManager.theManager) { 12 | PathWatcherManager.theManager = new PathWatcherManager() 13 | } 14 | return PathWatcherManager.theManager 15 | } 16 | 17 | // Private: Initialize global {PathWatcher} state. 18 | constructor () { 19 | this.live = new Set() 20 | this.nativeRegistry = new NativeWatcherRegistry( 21 | (normalizedPath, options) => { 22 | const nativeWatcher = new NativeWatcher(normalizedPath, options) 23 | 24 | this.live.add(nativeWatcher) 25 | const sub = nativeWatcher.onWillStop(() => { 26 | this.live.delete(nativeWatcher) 27 | sub.dispose() 28 | }) 29 | 30 | return nativeWatcher 31 | } 32 | ) 33 | } 34 | 35 | // Private: Access the {nativeRegistry} for introspection and diagnostics. 36 | getRegistry () { 37 | return this.nativeRegistry 38 | } 39 | 40 | // Private: Create a {PathWatcher} tied to this global state. See {watchPath} for detailed arguments. 41 | createWatcher (rootPath, options, eventCallback) { 42 | const watcher = new PathWatcher(this.nativeRegistry, rootPath, options) 43 | watcher.onDidChange(eventCallback) 44 | return watcher 45 | } 46 | 47 | // Private: Return a {String} depicting the currently active native watchers. 48 | print () { 49 | return this.nativeRegistry.print() 50 | } 51 | 52 | // Private: Stop all living watchers. 53 | // 54 | // Returns a {Promise} that resolves when all native watcher resources are disposed. 55 | stopAllWatchers () { 56 | return Promise.all( 57 | Array.from(this.live, watcher => watcher.stop(false)) 58 | ) 59 | } 60 | } 61 | 62 | module.exports = { PathWatcherManager } 63 | -------------------------------------------------------------------------------- /lib/registry/directory-node.js: -------------------------------------------------------------------------------- 1 | const { MissingResult, ChildrenResult } = require('./result') 2 | 3 | // Private: Non-leaf node in a {Tree} used by the {NativeWatcherRegistry} to cover the allocated {PathWatcher} 4 | // instances with the most efficient set of {NativeWatcher} instances possible. Each {DirectoryNode} maps to a directory 5 | // in the filesystem tree. 6 | class DirectoryNode { 7 | // Private: Construct a new, empty node representing a node with no watchers. 8 | constructor (children) { 9 | this.children = children || {} 10 | } 11 | 12 | // Private: Recursively discover any existing watchers corresponding to a path. 13 | // 14 | // * `pathSegments` filesystem path of a new {PathWatcher} already split into an Array of directory names. 15 | // 16 | // Returns: A {ParentResult} if the exact requested directory or a parent directory is being watched, a 17 | // {ChildrenResult} if one or more child paths are being watched, or a {MissingResult} if no relevant watchers 18 | // exist. 19 | lookup (pathSegments) { 20 | if (pathSegments.length === 0) { 21 | return new ChildrenResult(this.childWatchers([]), this.children) 22 | } 23 | 24 | const child = this.children[pathSegments[0]] 25 | if (child === undefined) { 26 | return new MissingResult(this) 27 | } 28 | 29 | return child.lookup(pathSegments.slice(1)) 30 | } 31 | 32 | // Private: Insert a new watcher node into the tree, creating new intermediate {DirectoryNode} instances as 33 | // needed. Existing children of the watched directory are adopted as children by a new {NonrecursiveWatcherNode} 34 | // or removed by a new {RecursiveWatcherNode}. 35 | // 36 | // * `pathSegments` filesystem path of the new {PathWatcher}, already split into an Array of directory names. 37 | // * `node` initialized {RecursiveWatcherNode} or {NonrecursiveWatcherNode} to insert. 38 | // 39 | // Returns: The root of a new tree with the new node inserted at the correct location. Callers should replace their 40 | // node references with the returned value. 41 | insert (pathSegments, node) { 42 | if (pathSegments.length === 0) { 43 | return node 44 | } 45 | 46 | const pathKey = pathSegments[0] 47 | let child = this.children[pathKey] 48 | if (child === undefined) { 49 | child = new DirectoryNode() 50 | } 51 | this.children[pathKey] = child.insert(pathSegments.slice(1), node) 52 | return this 53 | } 54 | 55 | // Private: Remove a {RecursiveWatcherNode} by its exact watched directory. 56 | // 57 | // * `pathSegments` absolute pre-split filesystem path of the node to remove. 58 | // * `createSplitNative` callback to be invoked with each child path segment {Array} if the {RecursiveWatcherNode} 59 | // is split into child watchers rather than removed outright. See {RecursiveWatcherNode.remove}. If `null`, 60 | // no child node splitting will occur. 61 | // 62 | // Returns: The root of a new tree with the node removed. Callers should replace their node references with the 63 | // returned value. 64 | remove (pathSegments, createSplitNative) { 65 | if (pathSegments.length === 0) { 66 | // Attempt to remove a path with child watchers. Do nothing. 67 | return this 68 | } 69 | 70 | const pathKey = pathSegments[0] 71 | const child = this.children[pathKey] 72 | if (child === undefined) { 73 | // Attempt to remove a path that isn't watched. Do nothing. 74 | return this 75 | } 76 | 77 | // Recurse 78 | const newChild = child.remove(pathSegments.slice(1), createSplitNative) 79 | if (newChild === null) { 80 | delete this.children[pathKey] 81 | } else { 82 | this.children[pathKey] = newChild 83 | } 84 | 85 | // Remove this node if all of its children have been removed 86 | return Object.keys(this.children).length === 0 ? null : this 87 | } 88 | 89 | // Private: Discover all node instances beneath this tree node associated with a {NativeWatcher} and the child paths 90 | // that they are watching. 91 | // 92 | // * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths. 93 | // 94 | // Returns: A possibly empty {Array} of `{node, path}` objects describing {RecursiveWatcherNode} and 95 | // {NonrecursiveWatcherNode} instances beneath this node. 96 | childWatchers (prefix) { 97 | const results = [] 98 | for (const p of Object.keys(this.children)) { 99 | results.push(...this.children[p].childWatchers(prefix.concat([p]))) 100 | } 101 | return results 102 | } 103 | 104 | // Private: Return a {String} representation of this subtree for diagnostics and testing. 105 | print (indent = 0) { 106 | let spaces = '' 107 | for (let i = 0; i < indent; i++) { 108 | spaces += ' ' 109 | } 110 | 111 | let result = '' 112 | for (const p of Object.keys(this.children)) { 113 | result += `${spaces}${p}\n${this.children[p].print(indent + 2)}` 114 | } 115 | return result 116 | } 117 | } 118 | 119 | module.exports = { DirectoryNode } 120 | -------------------------------------------------------------------------------- /lib/registry/helper.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | // Private: re-join the segments split from an absolute path to form another absolute path. 4 | function absolute (...parts) { 5 | let candidate = parts.length !== 1 ? path.join(...parts) : parts[0] 6 | if (process.platform === 'win32' && /^[A-Z]:$/.test(candidate)) candidate += '\\' 7 | return path.isAbsolute(candidate) ? candidate : path.join(path.sep, candidate) 8 | } 9 | 10 | module.exports = { absolute } 11 | -------------------------------------------------------------------------------- /lib/registry/nonrecursive-watcher-node.js: -------------------------------------------------------------------------------- 1 | const { DirectoryNode } = require('./directory-node') 2 | const { ParentResult } = require('./result') 3 | 4 | // Private: leaf or body node within a {Tree}. Represents a directory that is watched by a non-recursive 5 | // {NativeWatcher}. 6 | class NonrecursiveWatcherNode extends DirectoryNode { 7 | // Private: Allocate a new node to track a non-recursive {NativeWatcher}. 8 | // 9 | // * `nativeWatcher` An existing {NativeWatcher} instance. 10 | // * `absolutePathSegments` The absolute path to this {NativeWatcher}'s directory as an {Array} of path segments. 11 | // * `children` {Object} mapping directory entries to immediate child nodes within the {Tree}. 12 | constructor (nativeWatcher, absolutePathSegments, children) { 13 | super(children) 14 | this.absolutePathSegments = absolutePathSegments 15 | this.nativeWatcher = nativeWatcher 16 | } 17 | 18 | // Private: Reconstruct the {NativeWatcher} options used to create our watcher. 19 | // 20 | // Returns an {Object} containing settings that will replicate the {NativeWatcher} we own. 21 | getOptions () { 22 | return { recursive: false } 23 | } 24 | 25 | // Private: Determine if this node's {NativeWatcher} will deliver at least the events requested by an options 26 | // {Object}. 27 | isCompatible (options) { 28 | return options.recursive === false 29 | } 30 | 31 | // Private: Ensure that only compatible, non-recursive watchers are attached here. 32 | addChildPath (childPathSegments, options) { 33 | if (!this.isCompatible(options)) { 34 | throw new Error(`Attempt to add an incompatible child watcher to ${this}`) 35 | } 36 | 37 | if (childPathSegments.length !== 0) { 38 | throw new Error(`Attempt to adopt a child watcher on ${this}`) 39 | } 40 | } 41 | 42 | // Private: Ensure that only exactly matching watchers have been attached here. 43 | removeChildPath (childPathSegments) { 44 | if (childPathSegments.length !== 0) { 45 | throw new Error(`Attempt to remove a child watcher on ${this}`) 46 | } 47 | } 48 | 49 | // Private: Accessor for the {NativeWatcher}. 50 | getNativeWatcher () { 51 | return this.nativeWatcher 52 | } 53 | 54 | // Private: Return the absolute path watched by this {NativeWatcher} as an {Array} of directory names. 55 | getAbsolutePathSegments () { 56 | return this.absolutePathSegments 57 | } 58 | 59 | // Private: Identify how this watcher relates to a request to watch a directory tree. 60 | // 61 | // * `pathSegments` filesystem path of a new {PathWatcher} already split into an {Array} of directory names. 62 | // 63 | // Returns: A {ParentResult} referencing this node if it is an exact match, a {ParentResult} referencing an 64 | // descendent node if it is an exact match of the query or a parent of the query, a {ChildrenResult} if one or 65 | // more child paths of the request are being watched, or a {MissingResult} if no relevant watchers exist. 66 | lookup (pathSegments) { 67 | if (pathSegments.length === 0) { 68 | return new ParentResult(this, pathSegments) 69 | } 70 | 71 | return super.lookup(pathSegments) 72 | } 73 | 74 | // Private: Become a regular {DirectoryNode} if the watcher's exact path matches. 75 | // 76 | // * `pathSegments` absolute pre-split filesystem path of the node to remove. 77 | // * `createSplitNative` callback to be invoked with each child path segment {Array} if the {RecursiveWatcherNode} 78 | // is split into child watchers rather than removed outright. See {RecursiveWatcherNode.remove}. If `null`, 79 | // no child node splitting will occur. 80 | // 81 | // Returns: The root of a new tree with the node removed. Callers should replace their node references with the 82 | // returned value. 83 | remove (pathSegments, createSplitNative) { 84 | if (pathSegments.length === 0 && Object.keys(this.children).length > 0) { 85 | // Become a regular DirectoryNode with the same children. 86 | return new DirectoryNode(this.children) 87 | } 88 | 89 | return super.remove(pathSegments, createSplitNative) 90 | } 91 | 92 | // Private: Discover all node instances beneath this tree node associated with a {NativeWatcher} and the child paths 93 | // that they are watching. 94 | // 95 | // * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths. 96 | // 97 | // Returns: A possibly empty {Array} of `{node, path}` objects describing {RecursiveWatcherNode} and 98 | // {NonrecursiveWatcherNode} instances beneath this node, including this node. 99 | childWatchers (prefix) { 100 | return [{ node: this, path: prefix }, ...super.childWatchers(prefix)] 101 | } 102 | 103 | // Private: Return a {String} representation of this watcher and its descendents for diagnostics and testing. 104 | print (indent = 0) { 105 | let result = '' 106 | for (let i = 0; i < indent; i++) { 107 | result += ' ' 108 | } 109 | result += '[non-recursive watcher]\n' 110 | return result + super.print(indent) 111 | } 112 | } 113 | 114 | module.exports = { NonrecursiveWatcherNode } 115 | -------------------------------------------------------------------------------- /lib/registry/result.js: -------------------------------------------------------------------------------- 1 | // Private: A {DirectoryNode} traversal result that's returned when neither a directory, its children, nor its parents 2 | // are present in the tree. 3 | class MissingResult { 4 | // Private: Instantiate a new {MissingResult}. 5 | // 6 | // * `lastParent` the final successfully traversed {DirectoryNode}. 7 | constructor (lastParent) { 8 | this.lastParent = lastParent 9 | } 10 | 11 | // Private: Dispatch within a map of callback actions. 12 | // 13 | // * `actions` {Object} containing a `missing` key that maps to a callback to be invoked when no results were returned 14 | // by {DirectoryNode.lookup}. The callback will be called with the last parent node that was encountered during the 15 | // traversal. 16 | // 17 | // Returns: the result of the `actions` callback. 18 | when (actions) { 19 | return actions.missing(this.lastParent) 20 | } 21 | } 22 | 23 | // Private: A {DirectoryNode.lookup} traversal result that's returned when a parent or an exact match of the requested 24 | // directory is being watched by an existing {RecursiveWatcherNode}. 25 | class ParentResult { 26 | // Private: Instantiate a new {ParentResult}. 27 | // 28 | // * `parent` the {RecursiveWatcherNode} that was discovered. 29 | // * `remainingPathSegments` an {Array} of the directories that lie between the leaf node's watched directory and 30 | // the requested directory. This will be empty for exact matches. 31 | constructor (parent, remainingPathSegments) { 32 | this.parent = parent 33 | this.remainingPathSegments = remainingPathSegments 34 | } 35 | 36 | // Private: Dispatch within a map of callback actions. 37 | // 38 | // * `actions` {Object} containing a `parent` key that maps to a callback to be invoked when a parent of a requested 39 | // requested directory is returned by a {DirectoryNode.lookup} call. The callback will be called with the 40 | // {RecursiveWatcherNode} instance and an {Array} of the {String} path segments that separate the parent node 41 | // and the requested directory. 42 | // 43 | // Returns: the result of the `actions` callback. 44 | when (actions) { 45 | return actions.parent(this.parent, this.remainingPathSegments) 46 | } 47 | } 48 | 49 | // Private: A {DirectoryNode.lookup} traversal result that's returned when one or more children of the requested 50 | // directory are already being watched. 51 | class ChildrenResult { 52 | // Private: Instantiate a new {ChildrenResult}. 53 | // 54 | // * `children` {Array} containing objects with: 55 | // * `node` {RecursiveWatcherNode} or {NonrecursiveWatcherNode} instance that was discovered. 56 | // * `path` the relative path between the query and the corresponding node. 57 | // * `immediate` {Object} containing the child node map from the last node traversed. 58 | constructor (children, immediate) { 59 | this.children = children 60 | this.immediate = immediate 61 | } 62 | 63 | // Private: Dispatch within a map of callback actions. 64 | // 65 | // * `actions` {Object} containing a `children` key that maps to a callback to be invoked when a parent of a requested 66 | // requested directory is returned by a {DirectoryNode.lookup} call. The callback will be called with the 67 | // {RecursiveWatcherNode} instance. 68 | // 69 | // Returns: the result of the `actions` callback. 70 | when (actions) { 71 | return actions.children(this.children, this.immediate) 72 | } 73 | } 74 | 75 | module.exports = { MissingResult, ParentResult, ChildrenResult } 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@atom/watcher", 3 | "version": "1.3.4-0", 4 | "description": "Atom filesystem watcher", 5 | "main": "lib/index.js", 6 | "bin": "lib/cli.js", 7 | "scripts": { 8 | "install": "prebuild-install || node-gyp rebuild", 9 | "pre-build": "prebuild --all --strip --verbose", 10 | "pre-build:upload": "prebuild --upload-all", 11 | "lint": "npm run lint:js && npm run lint:cpp", 12 | "lint:js": "standard", 13 | "lint:cpp": "script/c++-lint", 14 | "format": "npm run format:js && npm run format:cpp", 15 | "format:cpp": "script/c++-format", 16 | "format:js": "standard --fix", 17 | "build:debug": "node --harmony script/helper/gen-compilation-db.js rebuild --debug", 18 | "build:atom": "electron-rebuild --version 6.1.12", 19 | "test": "mocha", 20 | "test:lldb": "lldb -- node --harmony ./node_modules/.bin/_mocha --require test/global.js --require mocha-stress --recursive", 21 | "test:gdb": "gdb --args node --harmony ./node_modules/.bin/_mocha --require test/global.js --require mocha-stress --recursive", 22 | "ci:appveyor": "npm run test -- --fgrep ^windows --invert --reporter mocha-appveyor-reporter --reporter-options appveyorBatchSize=5 --timeout 30000", 23 | "ci:circle": "npm run test -- --fgrep '^mac' --invert --reporter mocha-junit-reporter --reporter-options mochaFile=test-results/mocha/test-results.xml", 24 | "ci:travis": "npm run test -- --fgrep '^linux' --invert --reporter list", 25 | "aw:test": "npm run clean:fixture && clear && npm run build:debug && clear && npm run test", 26 | "aw:win": "npm run clean:fixture && cls && npm run build:debug && cls && npm run test", 27 | "clean:fixture": "git clean -xfd test/fixture" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/atom/watcher.git" 32 | }, 33 | "bugs": "https://github.com/atom/watcher/issues", 34 | "keywords": [ 35 | "filewatch", 36 | "watcher", 37 | "file", 38 | "inotify", 39 | "fsevents", 40 | "readdirectorychangesw" 41 | ], 42 | "author": "GitHub", 43 | "license": "MIT", 44 | "devDependencies": { 45 | "chai": "4.2.0", 46 | "chai-as-promised": "7.1.1", 47 | "electron-rebuild": "1.11.0", 48 | "eslint-plugin-import": "2.16.0", 49 | "eslint-plugin-promise": "4.0.1", 50 | "eslint-plugin-react": "7.12.4", 51 | "mocha": "6.0.2", 52 | "mocha-appveyor-reporter": "0.4.2", 53 | "mocha-junit-reporter": "1.18.0", 54 | "mocha-stress": "1.0.0", 55 | "prebuild": "8.2.1", 56 | "shell-quote": "1.6.1", 57 | "standard": "12.0.1", 58 | "temp": "0.9.0", 59 | "test-until": "1.1.1" 60 | }, 61 | "dependencies": { 62 | "event-kit": "2.5.3", 63 | "fs-extra": "7.0.1", 64 | "nan": "2.14.1", 65 | "prebuild-install": "5.3.3" 66 | }, 67 | "standard": { 68 | "globals": [ 69 | "describe", 70 | "it", 71 | "assert", 72 | "beforeEach", 73 | "afterEach", 74 | "until" 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /script/c++-format: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | cd "$(dirname $0)/.." 5 | 6 | if ! type clang-format 2>/dev/null 1>&2; then 7 | printf "clang-format must be installed for 'npm run format:cpp' to work.\n" >&2 8 | exit 1 9 | fi 10 | 11 | exec clang-format -style=file -i $(find src -type f \( -name '*.cpp' -or -name '*.h' \)) 12 | -------------------------------------------------------------------------------- /script/c++-format.ps1: -------------------------------------------------------------------------------- 1 | $srcfiles = Get-ChildItem -Path src -Include *.cpp,*.h -Recurse | %{$_.FullName} 2 | 3 | clang-format -style=file -i $srcfiles 4 | -------------------------------------------------------------------------------- /script/c++-lint: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | cd "$(dirname $0)/.." 5 | 6 | COMPILATION_DB=build/compile_commands.json 7 | 8 | if ! type clang-tidy 2>/dev/null 1>&2; then 9 | printf "clang-tidy must be installed for 'npm run lint:cpp' to work.\n" >&2 10 | exit 1 11 | fi 12 | 13 | if [ ! -f "${COMPILATION_DB}" ]; then 14 | printf "Generating compilation command database.\n" 15 | node script/helper/gen-compilation-db rebuild 16 | fi 17 | 18 | clang-tidy \ 19 | -p "${COMPILATION_DB}" \ 20 | -quiet \ 21 | "-header-filter=^\.\./src/*" \ 22 | $(find src -type f -name '*.cpp') 23 | -------------------------------------------------------------------------------- /script/helper/gen-compilation-db.js: -------------------------------------------------------------------------------- 1 | // Generate an LLVM compilation database from verbose Make output. This is a JSON file used by LLVM tools to 2 | // consistently operate on the files in a build tree. See: 3 | // https://clang.llvm.org/docs/JSONCompilationDatabase.html 4 | // for a description of the format. 5 | 6 | const path = require('path') 7 | const readline = require('readline') 8 | const { spawn, execFileSync } = require('child_process') 9 | 10 | const BASE_DIR = path.resolve(__dirname, '..', '..') 11 | const BUILD_DIR = path.resolve(BASE_DIR, 'build') 12 | const OUTPUT_FILE = path.join(BUILD_DIR, 'compile_commands.json') 13 | const NODE_GYP_BINARY = process.platform === 'win32' ? 'node-gyp.cmd' : 'node-gyp' 14 | const VERBOSE = process.env.V === '1' 15 | 16 | let fs, shell 17 | try { 18 | fs = require('fs-extra') 19 | shell = require('shell-quote') 20 | } catch (e) { 21 | execFileSync(NODE_GYP_BINARY, process.argv.slice(2), { stdio: 'inherit' }) 22 | process.exit(0) 23 | } 24 | 25 | class CompilationDatabase { 26 | constructor () { 27 | this.entries = [] 28 | } 29 | 30 | addEntryForCompilation (line) { 31 | const sourceFiles = shell.parse(line).filter(word => word.endsWith('.cpp')) 32 | if (sourceFiles.length === 0) { 33 | console.error(`No source file parsed from: ${line}`) 34 | return 35 | } 36 | if (sourceFiles.length > 1) { 37 | console.error(`Ambiguous source files parsed from: ${line}`) 38 | return 39 | } 40 | const sourceFile = sourceFiles[0] 41 | 42 | const entry = { 43 | directory: BUILD_DIR, 44 | command: line.trim(), 45 | file: path.resolve(BUILD_DIR, sourceFile) 46 | } 47 | this.entries.push(entry) 48 | return entry 49 | } 50 | 51 | write () { 52 | return fs.writeFile(OUTPUT_FILE, JSON.stringify(this.entries, null, ' ')) 53 | } 54 | } 55 | 56 | function runNodeGyp () { 57 | const db = new CompilationDatabase() 58 | 59 | return new Promise((resolve, reject) => { 60 | const nodeGyp = spawn(NODE_GYP_BINARY, process.argv.slice(2), { 61 | env: Object.assign({}, process.env, { V: '1' }), 62 | stdio: [process.stdin, 'pipe', process.stderr] 63 | }) 64 | const lineReader = readline.createInterface({ input: nodeGyp.stdout }) 65 | 66 | lineReader.on('line', line => { 67 | if (/-DNODE_GYP_MODULE_NAME=/.test(line)) { 68 | const entry = db.addEntryForCompilation(line) 69 | if (VERBOSE) { 70 | process.stdout.write(`build command [${line}]\n`) 71 | } else { 72 | const relPath = path.relative(BASE_DIR, entry.file) 73 | process.stdout.write(` building ${relPath}\n`) 74 | } 75 | } else { 76 | process.stdout.write(`${line}\n`) 77 | } 78 | }) 79 | 80 | nodeGyp.on('close', code => { 81 | if (code === 0) { 82 | resolve(db.write()) 83 | } else { 84 | const e = new Error('node-gyp failure') 85 | e.code = code 86 | reject(e) 87 | } 88 | }) 89 | }) 90 | } 91 | 92 | runNodeGyp().then( 93 | () => process.exit(0), 94 | err => { 95 | console.error(err) 96 | process.exit(err.code || 1) 97 | } 98 | ) 99 | -------------------------------------------------------------------------------- /src/errable.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "errable.h" 6 | #include "result.h" 7 | 8 | using std::move; 9 | using std::string; 10 | 11 | Result<> Errable::health_err_result() const 12 | { 13 | if (message.empty()) return ok_result(); 14 | return error_result(string(message)); 15 | } 16 | 17 | void Errable::report_errable(const Errable &component) 18 | { 19 | report_if_error(component.health_err_result()); 20 | } 21 | 22 | void Errable::report_uv_error(int err_code) 23 | { 24 | report_error(uv_strerror(err_code)); 25 | } 26 | 27 | void Errable::report_error(string &&message) 28 | { 29 | assert(!frozen); 30 | this->message = move(message); 31 | } 32 | -------------------------------------------------------------------------------- /src/errable.h: -------------------------------------------------------------------------------- 1 | #ifndef ERRABLE_H 2 | #define ERRABLE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "result.h" 10 | 11 | // Superclass for resources that can potentially fail to be constructed properly. 12 | // 13 | // While a resource is being constructed, if a required resource cannot be initialized correctly, call one of the 14 | // report_error() functions to mark it as "unhealthy". Before exiting the constructor, call freeze() to prevent further 15 | // modifications. 16 | // 17 | // External consumers of the resource can use health_err_result() to log the cause of the failure. 18 | class Errable 19 | { 20 | public: 21 | Errable() = default; 22 | 23 | virtual ~Errable() = default; 24 | 25 | bool is_healthy() const { return message.empty(); } 26 | 27 | std::string get_message() const { return message.empty() ? "ok" : message; } 28 | 29 | // Generate a Result from the current error status of this resource. If it has entered an error state, 30 | // an errored Result will be created with its error message. Otherwise, an ok Result will be returned. 31 | Result<> health_err_result() const; 32 | 33 | Errable(const Errable &) = delete; 34 | Errable(Errable &&) = delete; 35 | Errable &operator=(const Errable &) = delete; 36 | Errable &operator=(Errable &&) = delete; 37 | 38 | protected: 39 | void report_errable(const Errable &component); 40 | 41 | void report_uv_error(int err_code); 42 | 43 | void report_error(std::string &&message); 44 | 45 | template 46 | void report_if_error(const Result &result) 47 | { 48 | assert(!frozen); 49 | 50 | if (result.is_ok()) return; 51 | report_error(std::string(result.get_error())); 52 | } 53 | 54 | void freeze() { frozen = true; } 55 | 56 | private: 57 | bool frozen{false}; 58 | std::string message; 59 | }; 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /src/helper/common.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMON_H 2 | #define COMMON_H 3 | 4 | #include 5 | 6 | std::string path_join(const std::string &left, const std::string &right); 7 | 8 | std::wstring wpath_join(const std::wstring &left, const std::wstring &right); 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /src/helper/common_impl.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMON_IMPL_H 2 | #define COMMON_IMPL_H 3 | 4 | #include 5 | 6 | using std::string; 7 | using std::wstring; 8 | 9 | template 10 | Str _path_join_impl(const Str &left, const Str &right, const typename Str::value_type &sep) 11 | { 12 | Str joined(left); 13 | 14 | if (left.back() != sep && right.front() != sep) { 15 | joined.reserve(left.size() + right.size() + 1); 16 | joined += sep; 17 | } else { 18 | joined.reserve(left.size() + right.size()); 19 | } 20 | 21 | joined += right; 22 | 23 | return joined; 24 | } 25 | 26 | string path_join(const string &left, const string &right) // NOLINT 27 | { 28 | return _path_join_impl(left, right, DIRECTORY_SEPARATOR); 29 | } 30 | 31 | wstring wpath_join(const wstring &left, const wstring &right) // NOLINT 32 | { 33 | return _path_join_impl(left, right, W_DIRECTORY_SEPARATOR); 34 | } 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /src/helper/common_posix.cpp: -------------------------------------------------------------------------------- 1 | #include "common.h" 2 | #include "linux/constants.h" 3 | 4 | #include "common_impl.h" 5 | -------------------------------------------------------------------------------- /src/helper/common_win.cpp: -------------------------------------------------------------------------------- 1 | #include "common.h" 2 | #include "windows/constants.h" 3 | 4 | #include "common_impl.h" 5 | -------------------------------------------------------------------------------- /src/helper/libuv.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "libuv.h" 4 | #include 5 | #include 6 | #include 7 | 8 | using std::dec; 9 | using std::hex; 10 | using std::ostream; 11 | 12 | ostream &operator<<(ostream &out, const uv_stat_t &stat) 13 | { 14 | out << "[ino=" << stat.st_ino << " size=" << stat.st_size << " mode=" << hex << stat.st_mode << dec << " ("; 15 | if ((stat.st_mode & S_IFDIR) == S_IFDIR) out << " DIR"; 16 | if ((stat.st_mode & S_IFREG) == S_IFREG) out << " REG"; 17 | if ((stat.st_mode & S_IFLNK) == S_IFLNK) out << " LNK"; 18 | out << " ) atim=" << stat.st_atim << " mtim=" << stat.st_mtim << " birthtim=" << stat.st_birthtim << "]"; 19 | return out; 20 | } 21 | 22 | ostream &operator<<(ostream &out, const FSReq &r) 23 | { 24 | if (r.req.result < 0) { 25 | return out << "[" << uv_strerror(static_cast(r.req.result)) << "]"; 26 | } 27 | 28 | return out << r.req.statbuf; 29 | } 30 | -------------------------------------------------------------------------------- /src/helper/libuv.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBUV_H 2 | #define LIBUV_H 3 | 4 | #include 5 | #include 6 | 7 | #include "../message.h" 8 | 9 | struct FSReq 10 | { 11 | uv_fs_t req{}; 12 | 13 | FSReq() = default; 14 | FSReq(const FSReq &) = delete; 15 | FSReq(FSReq &&) = delete; 16 | ~FSReq() { uv_fs_req_cleanup(&req); } 17 | 18 | FSReq &operator=(const FSReq &) = delete; 19 | FSReq &operator=(FSReq &&) = delete; 20 | }; 21 | 22 | inline std::ostream &operator<<(std::ostream &out, const uv_timespec_t &ts) 23 | { 24 | return out << ts.tv_sec << "s " << ts.tv_nsec << "ns"; 25 | } 26 | 27 | std::ostream &operator<<(std::ostream &out, const uv_stat_t &stat); 28 | 29 | std::ostream &operator<<(std::ostream &out, const FSReq &r); 30 | 31 | inline bool ts_not_equal(const uv_timespec_t &left, const uv_timespec_t &right) 32 | { 33 | return left.tv_sec != right.tv_sec || left.tv_nsec != right.tv_nsec; 34 | } 35 | 36 | inline EntryKind kind_from_stat(const uv_stat_t &st) 37 | { 38 | if ((st.st_mode & S_IFLNK) == S_IFLNK) return KIND_SYMLINK; 39 | if ((st.st_mode & S_IFDIR) == S_IFDIR) return KIND_DIRECTORY; 40 | if ((st.st_mode & S_IFREG) == S_IFREG) return KIND_FILE; 41 | return KIND_UNKNOWN; 42 | } 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /src/helper/linux/constants.h: -------------------------------------------------------------------------------- 1 | #ifndef CONSTANTS_H 2 | #define CONSTANTS_H 3 | 4 | const char DIRECTORY_SEPARATOR = '/'; 5 | 6 | const wchar_t W_DIRECTORY_SEPARATOR = L'/'; 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /src/helper/linux/helper.h: -------------------------------------------------------------------------------- 1 | #ifndef HELPER_H 2 | #define HELPER_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include "../../result.h" 11 | 12 | inline int _strerror_result(char *buffer, char *&out, int r) 13 | { 14 | // XSI strerror_r 15 | out = buffer; 16 | return r; 17 | } 18 | 19 | inline int _strerror_result(char * /*buffer*/, char *&out, char *r) 20 | { 21 | // GNU strerror_r 22 | out = r; 23 | return 0; 24 | } 25 | 26 | template 27 | Result errno_result(const std::string &prefix); 28 | 29 | template 30 | Result errno_result(const std::string &prefix, int errnum) 31 | { 32 | const size_t BUFSIZE = 1024; 33 | char buffer[BUFSIZE]; 34 | char *msg = buffer; 35 | 36 | // Use a function overloading trick to work with either strerror_r variant. 37 | // See https://linux.die.net/man/3/strerror_r for the different signatures. 38 | int result = _strerror_result(buffer, msg, strerror_r(errnum, buffer, BUFSIZE)); 39 | if (result == EINVAL) { 40 | strncpy(msg, "Not a valid error number", BUFSIZE); 41 | } else if (result == ERANGE) { 42 | strncpy(msg, "Insuffient buffer size for error message", BUFSIZE); 43 | } else if (result < 0) { 44 | return errno_result(prefix); 45 | } 46 | 47 | std::ostringstream out; 48 | out << prefix << " (" << errnum << ") " << msg; 49 | return Result::make_error(out.str()); 50 | } 51 | 52 | template 53 | Result errno_result(const std::string &prefix) 54 | { 55 | return errno_result(prefix, errno); 56 | } 57 | 58 | #endif 59 | -------------------------------------------------------------------------------- /src/helper/macos/helper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "../../log.h" 7 | #include "helper.h" 8 | 9 | void SourceFnRegistry::callback(void *info) 10 | { 11 | auto it_ptr = static_cast(info); 12 | auto it = *it_ptr; 13 | FnRegistryAction action = it->fn(); 14 | if (action == FN_DISPOSE) { 15 | it->registry->fns.erase(it); 16 | delete it_ptr; 17 | } 18 | } 19 | 20 | void TimerFnRegistry::callback(CFRunLoopTimerRef timer, void *info) 21 | { 22 | auto it_ptr = reinterpret_cast(info); 23 | auto it = *it_ptr; 24 | FnRegistryAction action = it->fn(timer); 25 | if (action == FN_DISPOSE) { 26 | it->registry->fns.erase(it); 27 | delete it_ptr; 28 | } 29 | } 30 | 31 | void EventStreamFnRegistry::callback(ConstFSEventStreamRef ref, 32 | void *info, 33 | size_t num_events, 34 | void *event_paths, 35 | const FSEventStreamEventFlags *event_flags, 36 | const FSEventStreamEventId *event_ids) 37 | { 38 | auto it_ptr = reinterpret_cast(info); 39 | auto it = *it_ptr; 40 | FnRegistryAction action = it->fn(ref, num_events, event_paths, event_flags, event_ids); 41 | if (action == FN_DISPOSE) { 42 | it->registry->fns.erase(it); 43 | delete it_ptr; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/helper/macos/helper.h: -------------------------------------------------------------------------------- 1 | #ifndef CFREF_H 2 | #define CFREF_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | template 10 | class RefHolder 11 | { 12 | public: 13 | RefHolder() : ref{nullptr} {}; 14 | 15 | explicit RefHolder(T ref) : ref{ref} {}; 16 | 17 | RefHolder(RefHolder &&original) noexcept : ref{original.ref} { original.ref = nullptr; }; 18 | 19 | ~RefHolder() { clear(); } 20 | 21 | void set_from_create(T ref) 22 | { 23 | assert(this->ref == nullptr); 24 | this->ref = ref; 25 | } 26 | 27 | T get() 28 | { 29 | assert(this->ref != nullptr); 30 | return ref; 31 | } 32 | 33 | bool empty() { return ref == nullptr; } 34 | 35 | bool ok() { return ref != nullptr; } 36 | 37 | void set_from_get(T ref) 38 | { 39 | if (ref != nullptr) CFRetain(ref); 40 | set_from_create(ref); 41 | } 42 | 43 | void clear() 44 | { 45 | if (ref != nullptr) CFRelease(ref); 46 | } 47 | 48 | RefHolder(const RefHolder &) = delete; 49 | RefHolder &operator=(const RefHolder &) = delete; 50 | RefHolder &operator=(RefHolder &&) = delete; 51 | 52 | protected: 53 | T ref; 54 | }; 55 | 56 | // Specialize RefHolder for FSEventStreamRef to use different retain and release functions 57 | 58 | template <> 59 | inline void RefHolder::set_from_get(FSEventStreamRef ref) 60 | { 61 | if (ref != nullptr) FSEventStreamRetain(ref); 62 | set_from_create(ref); 63 | } 64 | 65 | template <> 66 | inline void RefHolder::clear() 67 | { 68 | if (ref != nullptr) FSEventStreamRelease(ref); 69 | } 70 | 71 | enum FnRegistryAction 72 | { 73 | FN_KEEP, 74 | FN_DISPOSE 75 | }; 76 | 77 | using SourceFn = std::function; 78 | 79 | using TimerFn = std::function; 80 | 81 | using EventStreamFn = std::function; 86 | 87 | template 88 | class FnRegistry 89 | { 90 | public: 91 | FnRegistry() = default; 92 | 93 | virtual ~FnRegistry() = default; 94 | 95 | FnRegistry(const FnRegistry &) = delete; 96 | FnRegistry(FnRegistry &&) = delete; 97 | FnRegistry &operator=(const FnRegistry &) = delete; 98 | FnRegistry &operator=(FnRegistry &&) = delete; 99 | 100 | protected: 101 | struct Entry 102 | { 103 | Entry(FnType &&fn, This *registry) : fn(std::move(fn)), registry{registry} {} 104 | 105 | ~Entry() = default; 106 | 107 | FnType fn; 108 | This *registry; 109 | 110 | Entry(const Entry &) = delete; 111 | Entry(Entry &&) = delete; 112 | Entry &operator=(const Entry &) = delete; 113 | Entry &operator=(Entry &&) = delete; 114 | }; 115 | 116 | std::list fns; 117 | 118 | using Iter = typename std::list::const_iterator; 119 | 120 | public: 121 | std::unique_ptr create_info(FnType &&fn); 122 | }; 123 | 124 | class SourceFnRegistry : public FnRegistry 125 | { 126 | public: 127 | static void callback(void *info); 128 | 129 | SourceFnRegistry() = default; 130 | 131 | ~SourceFnRegistry() override = default; 132 | 133 | SourceFnRegistry(const SourceFnRegistry &) = delete; 134 | SourceFnRegistry(SourceFnRegistry &&) = delete; 135 | SourceFnRegistry &operator=(const SourceFnRegistry &) = delete; 136 | SourceFnRegistry &operator=(SourceFnRegistry &&) = delete; 137 | }; 138 | 139 | class TimerFnRegistry : public FnRegistry 140 | { 141 | public: 142 | static void callback(CFRunLoopTimerRef timer, void *info); 143 | 144 | TimerFnRegistry() = default; 145 | 146 | ~TimerFnRegistry() override = default; 147 | 148 | TimerFnRegistry(const TimerFnRegistry &) = delete; 149 | TimerFnRegistry(TimerFnRegistry &&) = delete; 150 | TimerFnRegistry &operator=(const TimerFnRegistry &) = delete; 151 | TimerFnRegistry &operator=(TimerFnRegistry &&) = delete; 152 | }; 153 | 154 | class EventStreamFnRegistry : public FnRegistry 155 | { 156 | public: 157 | static void callback(ConstFSEventStreamRef ref, 158 | void *info, 159 | size_t num_events, 160 | void *event_paths, 161 | const FSEventStreamEventFlags *event_flags, 162 | const FSEventStreamEventId *event_ids); 163 | 164 | EventStreamFnRegistry() = default; 165 | 166 | ~EventStreamFnRegistry() override = default; 167 | 168 | EventStreamFnRegistry(const EventStreamFnRegistry &) = delete; 169 | EventStreamFnRegistry(EventStreamFnRegistry &&) = delete; 170 | EventStreamFnRegistry &operator=(const EventStreamFnRegistry &) = delete; 171 | EventStreamFnRegistry &operator=(EventStreamFnRegistry &&) = delete; 172 | }; 173 | 174 | template 175 | std::unique_ptr::Iter> FnRegistry::create_info(FnType &&fn) 176 | { 177 | fns.emplace_front(std::move(fn), static_cast(this)); 178 | return std::unique_ptr(new Iter(fns.cbegin())); 179 | } 180 | 181 | #endif 182 | -------------------------------------------------------------------------------- /src/helper/windows/constants.h: -------------------------------------------------------------------------------- 1 | #ifndef CONSTANTS_H 2 | #define CONSTANTS_H 3 | 4 | const char DIRECTORY_SEPARATOR = '\\'; 5 | 6 | const wchar_t W_DIRECTORY_SEPARATOR = L'\\'; 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /src/helper/windows/helper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "helper.h" 6 | 7 | using std::string; 8 | using std::unique_ptr; 9 | using std::wstring; 10 | 11 | Result to_utf8(const wstring &in) 12 | { 13 | size_t len = WideCharToMultiByte(CP_UTF8, // code page 14 | 0, // flags 15 | in.data(), // source string 16 | in.size(), // source string length 17 | nullptr, // destination string, null to measure 18 | 0, // destination string length 19 | nullptr, // default char 20 | nullptr // used default char 21 | ); 22 | if (!len) { 23 | return windows_error_result("Unable to measure string as UTF-8"); 24 | } 25 | 26 | unique_ptr payload(new char[len]); 27 | size_t copied = WideCharToMultiByte(CP_UTF8, // code page 28 | 0, // flags 29 | in.data(), // source string 30 | in.size(), // source string length 31 | payload.get(), // destination string 32 | len, // destination string length 33 | nullptr, // default char 34 | nullptr // used default char 35 | ); 36 | if (!copied) { 37 | return windows_error_result("Unable to convert string to UTF-8"); 38 | } 39 | 40 | return ok_result(string(payload.get(), len)); 41 | } 42 | 43 | Result to_wchar(const string &in) 44 | { 45 | size_t wlen = MultiByteToWideChar(CP_UTF8, // code page 46 | 0, // flags 47 | in.data(), // source string data 48 | in.size(), // source string length (null-terminated) 49 | 0, // output buffer 50 | 0 // output buffer size 51 | ); 52 | if (wlen == 0) { 53 | return windows_error_result("Unable to measure string as wide string"); 54 | } 55 | 56 | unique_ptr payload(new WCHAR[wlen]); 57 | size_t conv_success = MultiByteToWideChar(CP_UTF8, // code page 58 | 0, // flags, 59 | in.data(), // source string data 60 | in.size(), // source string length (null-terminated) 61 | payload.get(), // output buffer 62 | wlen // output buffer size (in bytes) 63 | ); 64 | if (!conv_success) { 65 | return windows_error_result("Unable to convert string to wide string"); 66 | } 67 | 68 | return ok_result(wstring(payload.get(), wlen)); 69 | } 70 | 71 | Result to_long_path_try(const wstring &short_path, size_t bufsize, bool retry) 72 | { 73 | unique_ptr longpath_data(new wchar_t[bufsize]); 74 | DWORD longpath_length = GetLongPathNameW(short_path.c_str(), longpath_data.get(), bufsize); 75 | 76 | if (longpath_length == 0) { 77 | DWORD longpath_err = GetLastError(); 78 | if (longpath_err != ERROR_FILE_NOT_FOUND && longpath_err != ERROR_PATH_NOT_FOUND 79 | && longpath_err != ERROR_ACCESS_DENIED) { 80 | return windows_error_result("Unable to convert to long path", longpath_err); 81 | } 82 | return ok_result(wstring(short_path)); 83 | } 84 | 85 | if (longpath_length > bufsize) { 86 | longpath_data.reset(nullptr); 87 | if (retry) { 88 | return to_long_path_try(short_path, longpath_length, false); 89 | } 90 | 91 | return ok_result(wstring(short_path)); 92 | } 93 | 94 | return ok_result(wstring(longpath_data.get(), longpath_length)); 95 | } 96 | 97 | Result to_long_path(const wstring &short_path) 98 | { 99 | return to_long_path_try(short_path, short_path.size() + 1, true); 100 | } 101 | -------------------------------------------------------------------------------- /src/helper/windows/helper.h: -------------------------------------------------------------------------------- 1 | #ifndef HELPER_H 2 | #define HELPER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "../../result.h" 9 | 10 | // Convert a wide-character string to a utf8 string. 11 | Result to_utf8(const std::wstring &in); 12 | 13 | // Convert a utf8 string to a wide-character string. 14 | Result to_wchar(const std::string &in); 15 | 16 | // Convert an 8.3 short path to a long path. 17 | Result to_long_path(const std::wstring &short_path); 18 | 19 | template 20 | Result windows_error_result(const std::string &prefix) 21 | { 22 | return windows_error_result(prefix, GetLastError()); 23 | } 24 | 25 | template 26 | Result windows_error_result(const std::string &prefix, DWORD error_code) 27 | { 28 | LPVOID msg_buffer; 29 | 30 | FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, 31 | NULL, // source 32 | error_code, // message ID 33 | MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // language ID 34 | (LPSTR) &msg_buffer, // output buffer 35 | 0, // size 36 | NULL // arguments 37 | ); 38 | 39 | std::string msg_str(static_cast(msg_buffer)); 40 | // Remove the pesky CRLF and punctuation 41 | if (msg_str.size() > 3) { 42 | msg_str.erase(msg_str.size() - 3, 3); 43 | } 44 | 45 | std::ostringstream msg; 46 | msg << prefix << " (" << error_code << ") " << msg_str; 47 | LocalFree(msg_buffer); 48 | 49 | return Result::make_error(msg.str()); 50 | } 51 | 52 | #endif 53 | -------------------------------------------------------------------------------- /src/lock.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "lock.h" 4 | 5 | Lock::Lock(uv_mutex_t &mutex) : mutex{mutex} 6 | { 7 | uv_mutex_lock(&mutex); 8 | } 9 | 10 | Lock::~Lock() 11 | { 12 | uv_mutex_unlock(&mutex); 13 | } 14 | 15 | ReadLock::ReadLock(uv_rwlock_t &rwlock) : rwlock{rwlock} 16 | { 17 | uv_rwlock_rdlock(&rwlock); 18 | } 19 | 20 | ReadLock::~ReadLock() 21 | { 22 | uv_rwlock_rdunlock(&rwlock); 23 | } 24 | 25 | WriteLock::WriteLock(uv_rwlock_t &rwlock) : rwlock{rwlock} 26 | { 27 | uv_rwlock_wrlock(&rwlock); 28 | } 29 | 30 | WriteLock::~WriteLock() 31 | { 32 | uv_rwlock_wrunlock(&rwlock); 33 | } 34 | -------------------------------------------------------------------------------- /src/lock.h: -------------------------------------------------------------------------------- 1 | #ifndef LOCK_H 2 | #define LOCK_H 3 | 4 | #include 5 | 6 | // Hold a UV mutex for the lifetime of the Lock instance. 7 | // RAII FTW 8 | class Lock 9 | { 10 | public: 11 | Lock(uv_mutex_t &mutex); 12 | Lock(const Lock &) = delete; 13 | Lock(Lock &&) = delete; 14 | ~Lock(); 15 | 16 | Lock &operator=(const Lock &) = delete; 17 | Lock &operator=(Lock &&) = delete; 18 | 19 | private: 20 | uv_mutex_t &mutex; 21 | }; 22 | 23 | // Hold a read lock on a uv_rwlock_t for the lifetime of this instance. 24 | class ReadLock 25 | { 26 | public: 27 | ReadLock(uv_rwlock_t &rwlock); 28 | ReadLock(const ReadLock &) = delete; 29 | ReadLock(ReadLock &&) = delete; 30 | ~ReadLock(); 31 | 32 | ReadLock &operator=(const ReadLock &) = delete; 33 | ReadLock &operator=(ReadLock &&) = delete; 34 | 35 | private: 36 | uv_rwlock_t &rwlock; 37 | }; 38 | 39 | // Hold a write lock on a uv_rwlock_t for the lifetime of this instance. 40 | class WriteLock 41 | { 42 | public: 43 | WriteLock(uv_rwlock_t &rwlock); 44 | WriteLock(const WriteLock &) = delete; 45 | WriteLock(WriteLock &&) = delete; 46 | ~WriteLock(); 47 | 48 | WriteLock &operator=(const WriteLock &) = delete; 49 | WriteLock &operator=(WriteLock &&) = delete; 50 | 51 | private: 52 | uv_rwlock_t &rwlock; 53 | }; 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /src/log.h: -------------------------------------------------------------------------------- 1 | #ifndef LOG_H 2 | #define LOG_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class Logger 10 | { 11 | public: 12 | static Logger *current(); 13 | 14 | static std::string to_file(const char *filename); 15 | 16 | static std::string to_stderr(); 17 | 18 | static std::string to_stdout(); 19 | 20 | static std::string disable(); 21 | 22 | static std::string from_env(const char *varname); 23 | 24 | Logger() = default; 25 | 26 | virtual ~Logger() = default; 27 | 28 | virtual Logger *prefix(const char *file, int line) = 0; 29 | 30 | virtual std::ostream &stream() = 0; 31 | 32 | virtual std::string get_error() const { return ""; } 33 | 34 | Logger(const Logger &) = delete; 35 | Logger(Logger &&) = delete; 36 | Logger &operator=(const Logger &) = delete; 37 | Logger &operator=(Logger &&) = delete; 38 | }; 39 | 40 | std::string plural(long quantity, const std::string &singular_form, const std::string &plural_form); 41 | 42 | std::string plural(long quantity, const std::string &singular_form); 43 | 44 | #define LOGGER (Logger::current()->prefix(__FILE__, __LINE__)->stream()) 45 | 46 | class Timer 47 | { 48 | public: 49 | Timer(); 50 | 51 | ~Timer() = default; 52 | 53 | void stop(); 54 | 55 | std::string format_duration() const; 56 | 57 | Timer(const Timer &) = delete; 58 | Timer(Timer &&) = delete; 59 | Timer &operator=(const Timer &) = delete; 60 | Timer &operator=(Timer &&) = delete; 61 | 62 | private: 63 | size_t measure_duration() const 64 | { 65 | return std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); 66 | } 67 | 68 | std::chrono::time_point start; 69 | 70 | size_t duration; 71 | }; 72 | 73 | inline std::ostream &operator<<(std::ostream &out, const Timer &t) 74 | { 75 | return out << t.format_duration(); 76 | } 77 | 78 | #endif 79 | -------------------------------------------------------------------------------- /src/message_buffer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "log.h" 7 | #include "message.h" 8 | #include "message_buffer.h" 9 | 10 | using std::endl; 11 | using std::move; 12 | using std::string; 13 | 14 | void MessageBuffer::created(ChannelID channel_id, std::string &&path, const EntryKind &kind) 15 | { 16 | Message message(FileSystemPayload::created(channel_id, move(path), kind)); 17 | LOGGER << "Emitting filesystem message " << message << endl; 18 | messages.push_back(move(message)); 19 | } 20 | 21 | void MessageBuffer::modified(ChannelID channel_id, std::string &&path, const EntryKind &kind) 22 | { 23 | Message message(FileSystemPayload::modified(channel_id, move(path), kind)); 24 | LOGGER << "Emitting filesystem message " << message << endl; 25 | messages.push_back(move(message)); 26 | } 27 | 28 | void MessageBuffer::deleted(ChannelID channel_id, std::string &&path, const EntryKind &kind) 29 | { 30 | Message message(FileSystemPayload::deleted(channel_id, move(path), kind)); 31 | LOGGER << "Emitting filesystem message " << message << endl; 32 | messages.push_back(move(message)); 33 | } 34 | 35 | void MessageBuffer::renamed(ChannelID channel_id, std::string &&old_path, std::string &&path, const EntryKind &kind) 36 | { 37 | Message message(FileSystemPayload::renamed(channel_id, move(old_path), move(path), kind)); 38 | LOGGER << "Emitting filesystem message " << message << endl; 39 | messages.push_back(move(message)); 40 | } 41 | 42 | void MessageBuffer::ack(CommandID command_id, ChannelID channel_id, bool success, string &&msg) 43 | { 44 | Message message(AckPayload(command_id, channel_id, success, move(msg))); 45 | LOGGER << "Emitting ack message " << message << endl; 46 | messages.push_back(move(message)); 47 | } 48 | 49 | void MessageBuffer::error(ChannelID channel_id, string &&message, bool fatal) 50 | { 51 | Message m(ErrorPayload(channel_id, move(message), fatal)); 52 | LOGGER << "Emitting error message " << m << endl; 53 | messages.push_back(move(m)); 54 | } 55 | 56 | ChannelMessageBuffer::ChannelMessageBuffer(MessageBuffer &buffer, ChannelID channel_id) : 57 | channel_id{channel_id}, 58 | buffer{buffer} { 59 | // 60 | }; 61 | -------------------------------------------------------------------------------- /src/message_buffer.h: -------------------------------------------------------------------------------- 1 | #ifndef MESSAGE_BUFFER_H 2 | #define MESSAGE_BUFFER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "message.h" 9 | 10 | class MessageBuffer 11 | { 12 | public: 13 | MessageBuffer() = default; 14 | 15 | ~MessageBuffer() = default; 16 | 17 | using iter = std::vector::iterator; 18 | 19 | void created(ChannelID channel_id, std::string &&path, const EntryKind &kind); 20 | 21 | void modified(ChannelID channel_id, std::string &&path, const EntryKind &kind); 22 | 23 | void deleted(ChannelID channel_id, std::string &&path, const EntryKind &kind); 24 | 25 | void renamed(ChannelID channel_id, std::string &&old_path, std::string &&path, const EntryKind &kind); 26 | 27 | void ack(CommandID command_id, ChannelID channel_id, bool success, std::string &&msg); 28 | 29 | void error(ChannelID channel_id, std::string &&message, bool fatal); 30 | 31 | void reserve(size_t capacity) { messages.reserve(capacity); } 32 | 33 | void add(Message &&message) { messages.emplace_back(std::move(message)); } 34 | 35 | MessageBuffer::iter begin() { return messages.begin(); } 36 | 37 | MessageBuffer::iter end() { return messages.end(); } 38 | 39 | size_t size() { return messages.size(); } 40 | 41 | bool empty() { return messages.empty(); } 42 | 43 | MessageBuffer(const MessageBuffer &) = delete; 44 | MessageBuffer(MessageBuffer &&) = delete; 45 | MessageBuffer &operator=(const MessageBuffer &) = delete; 46 | MessageBuffer &operator=(MessageBuffer &&) = delete; 47 | 48 | private: 49 | std::vector messages; 50 | }; 51 | 52 | class ChannelMessageBuffer 53 | { 54 | public: 55 | ChannelMessageBuffer(MessageBuffer &buffer, ChannelID channel_id); 56 | ChannelMessageBuffer(const ChannelMessageBuffer &) = delete; 57 | ChannelMessageBuffer(ChannelMessageBuffer &&) = delete; 58 | ~ChannelMessageBuffer() = default; 59 | 60 | ChannelMessageBuffer &operator=(const ChannelMessageBuffer &) = delete; 61 | ChannelMessageBuffer &operator=(ChannelMessageBuffer &&) = delete; 62 | 63 | void created(std::string &&path, const EntryKind &kind) { buffer.created(channel_id, std::move(path), kind); } 64 | 65 | void modified(std::string &&path, const EntryKind &kind) { buffer.modified(channel_id, std::move(path), kind); } 66 | 67 | void deleted(std::string &&path, const EntryKind &kind) { buffer.deleted(channel_id, std::move(path), kind); } 68 | 69 | void renamed(std::string &&old_path, std::string &&path, const EntryKind &kind) 70 | { 71 | buffer.renamed(channel_id, std::move(old_path), std::move(path), kind); 72 | } 73 | 74 | void ack(CommandID command_id, bool success, std::string &&msg) 75 | { 76 | buffer.ack(command_id, channel_id, success, std::move(msg)); 77 | } 78 | 79 | void error(std::string &&message, bool fatal) { buffer.error(channel_id, std::move(message), fatal); } 80 | 81 | void reserve(size_t capacity) { buffer.reserve(capacity); } 82 | 83 | MessageBuffer::iter begin() { return buffer.begin(); } 84 | 85 | MessageBuffer::iter end() { return buffer.end(); } 86 | 87 | size_t size() { return buffer.size(); } 88 | 89 | bool empty() { return buffer.empty(); } 90 | 91 | ChannelID get_channel_id() { return channel_id; } 92 | 93 | private: 94 | ChannelID channel_id; 95 | MessageBuffer &buffer; 96 | }; 97 | 98 | #endif 99 | -------------------------------------------------------------------------------- /src/nan/all_callback.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "all_callback.h" 9 | #include "async_callback.h" 10 | #include "functional_callback.h" 11 | 12 | using Nan::FunctionCallback; 13 | using Nan::FunctionCallbackInfo; 14 | using Nan::HandleScope; 15 | using std::bind; 16 | using std::list; 17 | using std::move; 18 | using std::shared_ptr; 19 | using std::unique_ptr; 20 | using std::placeholders::_1; 21 | using v8::Array; 22 | using v8::Local; 23 | using v8::Value; 24 | 25 | list> AllCallback::retained; 26 | 27 | shared_ptr AllCallback::create(unique_ptr &&done) 28 | { 29 | shared_ptr created(new AllCallback(move(done))); 30 | retained.emplace_front(created); 31 | retained.front()->me = retained.begin(); 32 | return retained.front(); 33 | } 34 | 35 | AllCallback::AllCallback(unique_ptr &&done) : 36 | done(move(done)), 37 | fired{false}, 38 | total{0}, 39 | remaining{0}, 40 | error(Nan::Undefined()), 41 | results(Nan::New(0)), 42 | me{retained.end()} 43 | { 44 | // 45 | } 46 | 47 | unique_ptr AllCallback::create_callback(const char *async_name) 48 | { 49 | size_t index = total; 50 | functions.emplace_front(bind(&AllCallback::callback_complete, this, index, _1)); 51 | 52 | total++; 53 | remaining++; 54 | 55 | return fn_callback(async_name, functions.front()); 56 | } 57 | 58 | void AllCallback::set_result(Result<> &&r) 59 | { 60 | if (r.is_ok()) return; 61 | 62 | if (Nan::New(error)->IsUndefined()) { 63 | HandleScope scope; 64 | Local l_error = Nan::Error(r.get_error().c_str()); 65 | 66 | error.Reset(l_error); 67 | } 68 | } 69 | 70 | void AllCallback::fire_if_empty(bool sync) 71 | { 72 | if (remaining > 0) return; 73 | if (fired) return; 74 | fired = true; 75 | 76 | HandleScope scope; 77 | Local l_error = Nan::New(error); 78 | Local l_results = Nan::New(results); 79 | 80 | Local argv[] = {l_error, l_results}; 81 | if (sync) { 82 | done->SyncCall(2, argv); 83 | } else { 84 | done->Call(2, argv); 85 | } 86 | 87 | retained.erase(me); 88 | } 89 | 90 | void AllCallback::callback_complete(size_t callback_index, const FunctionCallbackInfo &info) 91 | { 92 | Local err = info[0]; 93 | 94 | if (!err->IsNull() && !err->IsUndefined()) { 95 | if (Nan::New(error)->IsUndefined()) { 96 | error.Reset(err); 97 | } 98 | } 99 | 100 | Local rest = Nan::New(info.Length() - 1); 101 | for (int i = 1; i < info.Length(); i++) { 102 | Nan::Set(rest, i - 1, info[i]); 103 | } 104 | 105 | Local l_results = Nan::New(results); 106 | Nan::Set(l_results, callback_index, rest); 107 | 108 | remaining--; 109 | 110 | fire_if_empty(false); 111 | } 112 | -------------------------------------------------------------------------------- /src/nan/all_callback.h: -------------------------------------------------------------------------------- 1 | #ifndef ALL_CALLBACK_H 2 | #define ALL_CALLBACK_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "../result.h" 12 | #include "async_callback.h" 13 | #include "functional_callback.h" 14 | 15 | class AllCallback 16 | { 17 | public: 18 | static std::shared_ptr create(std::unique_ptr &&done); 19 | 20 | ~AllCallback() = default; 21 | 22 | std::unique_ptr create_callback(const char *async_name); 23 | 24 | void set_result(Result<> &&r); 25 | 26 | void fire_if_empty(bool sync); 27 | 28 | AllCallback(const AllCallback &) = delete; 29 | AllCallback(AllCallback &&) = delete; 30 | AllCallback &operator=(const AllCallback &) = delete; 31 | AllCallback &operator=(AllCallback &&) = delete; 32 | 33 | private: 34 | explicit AllCallback(std::unique_ptr &&done); 35 | 36 | void callback_complete(size_t callback_index, const Nan::FunctionCallbackInfo &info); 37 | 38 | std::unique_ptr done; 39 | bool fired; 40 | size_t total; 41 | size_t remaining; 42 | 43 | std::forward_list functions; 44 | 45 | Nan::Persistent error; 46 | Nan::Persistent results; 47 | 48 | std::list>::iterator me; 49 | 50 | static std::list> retained; 51 | }; 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /src/nan/async_callback.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "async_callback.h" 5 | 6 | using Nan::AsyncResource; 7 | using Nan::HandleScope; 8 | using Nan::New; 9 | using v8::Function; 10 | using v8::Local; 11 | using v8::MaybeLocal; 12 | using v8::Object; 13 | using v8::Value; 14 | 15 | AsyncCallback::AsyncCallback(const char *name, Local fn) : AsyncResource(name), fn(fn) 16 | { 17 | // 18 | } 19 | 20 | AsyncCallback::~AsyncCallback() 21 | { 22 | fn.Reset(); 23 | } 24 | 25 | MaybeLocal AsyncCallback::Call(int argc, Local *argv) 26 | { 27 | HandleScope scope; 28 | Local localFn = New(fn); 29 | Local target = New(); 30 | 31 | return runInAsyncScope(target, localFn, argc, argv); 32 | } 33 | 34 | MaybeLocal AsyncCallback::SyncCall(int argc, Local *argv) 35 | { 36 | HandleScope scope; 37 | Local localFn = New(fn); 38 | Local target = New(); 39 | 40 | return Nan::Call(localFn, target, argc, argv); 41 | } 42 | -------------------------------------------------------------------------------- /src/nan/async_callback.h: -------------------------------------------------------------------------------- 1 | #ifndef ASYNC_CALLBACK_H 2 | #define ASYNC_CALLBACK_H 3 | 4 | #include 5 | #include 6 | 7 | // Wrap a v8::Function with an AsyncResource so that it fires async_hooks correctly. 8 | class AsyncCallback : public Nan::AsyncResource 9 | { 10 | public: 11 | AsyncCallback(const char *name, v8::Local fn); 12 | ~AsyncCallback(); 13 | 14 | v8::MaybeLocal Call(int argc, v8::Local *argv); 15 | 16 | v8::MaybeLocal SyncCall(int argc, v8::Local *argv); 17 | 18 | AsyncCallback(const AsyncCallback &) = delete; 19 | AsyncCallback(AsyncCallback &&) = delete; 20 | AsyncCallback &operator=(const AsyncCallback &) = delete; 21 | AsyncCallback &operator=(AsyncCallback &&original) = delete; 22 | 23 | private: 24 | Nan::Persistent fn; 25 | }; 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /src/nan/functional_callback.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "async_callback.h" 7 | #include "functional_callback.h" 8 | 9 | using Nan::FunctionCallback; 10 | using Nan::FunctionCallbackInfo; 11 | using std::unique_ptr; 12 | using v8::ArrayBuffer; 13 | using v8::Function; 14 | using v8::Isolate; 15 | using v8::Local; 16 | using v8::Value; 17 | using Contents = v8::ArrayBuffer::Contents; 18 | 19 | void _noop_callback_helper(const FunctionCallbackInfo & /*info*/) 20 | { 21 | // Do nothing 22 | } 23 | 24 | void _fn_callback_helper(const FunctionCallbackInfo &info) 25 | { 26 | Local cb_array = info.Data().As(); 27 | Contents cb_contents = cb_array->GetContents(); 28 | 29 | auto *payload = static_cast(cb_contents.Data()); 30 | assert(cb_contents.ByteLength() == sizeof(FnCallback *)); 31 | 32 | auto *fn = reinterpret_cast(*payload); 33 | 34 | delete payload; 35 | (*fn)(info); 36 | } 37 | 38 | unique_ptr fn_callback(const char *async_name, FnCallback &fn) 39 | { 40 | Nan::HandleScope scope; 41 | 42 | auto *payload = new intptr_t(reinterpret_cast(&fn)); 43 | 44 | Local fn_addr = 45 | ArrayBuffer::New(Isolate::GetCurrent(), static_cast(payload), sizeof(FnCallback *)); 46 | Local wrapper = Nan::New(_fn_callback_helper, fn_addr); 47 | return unique_ptr(new AsyncCallback(async_name, wrapper)); 48 | } 49 | 50 | unique_ptr noop_callback() 51 | { 52 | Nan::HandleScope scope; 53 | 54 | Local wrapper = Nan::New(_noop_callback_helper); 55 | return unique_ptr(new AsyncCallback("@atom/watcher:noop", wrapper)); 56 | } 57 | -------------------------------------------------------------------------------- /src/nan/functional_callback.h: -------------------------------------------------------------------------------- 1 | #ifndef FUNCTIONAL_CALLBACK_H 2 | #define FUNCTIONAL_CALLBACK_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "async_callback.h" 10 | 11 | using FnCallback = std::function &)>; 12 | 13 | std::unique_ptr fn_callback(const char *async_name, FnCallback &fn); 14 | 15 | std::unique_ptr noop_callback(); 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /src/nan/options.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "options.h" 7 | 8 | using Nan::Maybe; 9 | using Nan::MaybeLocal; 10 | using std::ostringstream; 11 | using std::string; 12 | using v8::Local; 13 | using v8::Object; 14 | using v8::String; 15 | using v8::Value; 16 | 17 | bool get_string_option(Local &options, const char *key_name, string &out) 18 | { 19 | Nan::HandleScope scope; 20 | const Local key = Nan::New(key_name).ToLocalChecked(); 21 | 22 | MaybeLocal as_maybe_value = Nan::Get(options, key); 23 | if (as_maybe_value.IsEmpty()) { 24 | return true; 25 | } 26 | Local as_value = as_maybe_value.ToLocalChecked(); 27 | if (as_value->IsUndefined()) { 28 | return true; 29 | } 30 | 31 | if (!as_value->IsString()) { 32 | ostringstream message; 33 | message << "option " << key_name << " must be a String"; 34 | Nan::ThrowError(message.str().c_str()); 35 | return false; 36 | } 37 | 38 | Nan::Utf8String as_string(as_value); 39 | 40 | if (*as_string == nullptr) { 41 | ostringstream message; 42 | message << "option " << key_name << " must be a valid UTF-8 String"; 43 | Nan::ThrowError(message.str().c_str()); 44 | return false; 45 | } 46 | 47 | out.assign(*as_string, as_string.length()); 48 | return true; 49 | } 50 | 51 | bool get_bool_option(Local &options, const char *key_name, bool &out) 52 | { 53 | Nan::HandleScope scope; 54 | const Local key = Nan::New(key_name).ToLocalChecked(); 55 | 56 | MaybeLocal as_maybe_value = Nan::Get(options, key); 57 | if (as_maybe_value.IsEmpty()) { 58 | return true; 59 | } 60 | Local as_value = as_maybe_value.ToLocalChecked(); 61 | if (as_value->IsUndefined()) { 62 | return true; 63 | } 64 | 65 | if (!as_value->IsBoolean()) { 66 | ostringstream message; 67 | message << "configure() option " << key_name << " must be a Boolean"; 68 | Nan::ThrowError(message.str().c_str()); 69 | return false; 70 | } 71 | 72 | out = as_value->IsTrue(); 73 | return true; 74 | } 75 | 76 | bool get_uint_option(v8::Local &options, const char *key_name, uint_fast32_t &out) 77 | { 78 | Nan::HandleScope scope; 79 | const Local key = Nan::New(key_name).ToLocalChecked(); 80 | 81 | MaybeLocal as_maybe_value = Nan::Get(options, key); 82 | if (as_maybe_value.IsEmpty()) { 83 | return true; 84 | } 85 | Local as_value = as_maybe_value.ToLocalChecked(); 86 | if (as_value->IsUndefined()) { 87 | return true; 88 | } 89 | 90 | if (!as_value->IsUint32()) { 91 | ostringstream message; 92 | message << "configure() option " << key_name << " must be a non-negative integer"; 93 | Nan::ThrowError(message.str().c_str()); 94 | return false; 95 | } 96 | 97 | Maybe as_maybe_uint = Nan::To(as_value); 98 | if (as_maybe_uint.IsNothing()) { 99 | ostringstream message; 100 | message << "configure() option " << key_name << " must be a non-negative integer"; 101 | Nan::ThrowError(message.str().c_str()); 102 | return false; 103 | } 104 | 105 | out = as_maybe_uint.FromJust(); 106 | return true; 107 | } 108 | -------------------------------------------------------------------------------- /src/nan/options.h: -------------------------------------------------------------------------------- 1 | #ifndef OPTIONS_H 2 | #define OPTIONS_H 3 | 4 | #include 5 | #include 6 | 7 | bool get_string_option(v8::Local &options, const char *key_name, std::string &out); 8 | 9 | bool get_bool_option(v8::Local &options, const char *key_name, bool &out); 10 | 11 | bool get_uint_option(v8::Local &options, const char *key_name, uint_fast32_t &out); 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /src/polling/directory_record.h: -------------------------------------------------------------------------------- 1 | #ifndef DIRECTORY_RECORD_H 2 | #define DIRECTORY_RECORD_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "../message.h" 11 | 12 | class BoundPollingIterator; 13 | 14 | // Remembered stat() results from the previous time a polling cycle visited a subdirectory of a `PolledRoot`. Contains 15 | // a recursive substructure that mirrors the last-known state of the filesystem tree. 16 | class DirectoryRecord 17 | { 18 | public: 19 | // Create a new, unpopulated directory with no parent. `prefix` should be a fully qualified path to the directory 20 | // tree. 21 | // 22 | // The record begins in an unpopulated state, which means that the first scan will discover existing entries, but 23 | // not emit any events. 24 | DirectoryRecord(std::string &&prefix); 25 | 26 | DirectoryRecord(const DirectoryRecord &) = delete; 27 | DirectoryRecord(DirectoryRecord &&) = delete; 28 | ~DirectoryRecord() = default; 29 | DirectoryRecord &operator=(const DirectoryRecord &) = delete; 30 | DirectoryRecord &operator=(DirectoryRecord &&) = delete; 31 | 32 | // Access the full path of this directory by walking up the `DirectoryRecord` tree. 33 | // 34 | // This is reasonably expensive on deep filesystems, so you should probably cache it somewhere. 35 | std::string path() const; 36 | 37 | // Perform a `scandir()` on this directories. If populated, emit deletion events for any entries that were found here 38 | // before but are now missing. Store the discovered entries within `it` as part of the iteration state. 39 | void scan(BoundPollingIterator *it); 40 | 41 | // Perform a single `lstat()` on an entry within this directory. If the DirectoryRecord is populated and the entry 42 | // has been created, deleted, or modified since the previous `DirectoryRecord::entry()` call, emit the appropriate 43 | // events into the `it`'s buffer. 44 | void entry(BoundPollingIterator *it, 45 | const std::string &entry_name, 46 | const std::string &entry_path, 47 | EntryKind scan_kind); 48 | 49 | // Note that this `DirectoryResult` has had an initial `scan()` and set of `entry()` calls completed. Subsequent 50 | // calls should emit actual events. 51 | void mark_populated() { populated = true; } 52 | 53 | // Return true if all `DirectoryResults` beneath this one have been populated by an initial scan. 54 | bool all_populated() const; 55 | 56 | // Recursively count the number of stat entries tracked beneath this directory, including this directory itself, as 57 | // of the last scan. 58 | size_t count_entries() const; 59 | 60 | private: 61 | // Construct a `DirectoryRecord` for a child entry. 62 | DirectoryRecord(DirectoryRecord *parent, std::string &&name); 63 | 64 | // Use an iterator to emit deletion, creation, or modification events. 65 | void entry_deleted(BoundPollingIterator *it, const std::string &entry_path, EntryKind kind); 66 | void entry_created(BoundPollingIterator *it, const std::string &entry_path, EntryKind kind); 67 | void entry_modified(BoundPollingIterator *it, const std::string &entry_path, EntryKind kind); 68 | 69 | // The parent directory. May be `null` at the root `DirectoryRecord` of a subtree. 70 | DirectoryRecord *parent; 71 | 72 | // If `parent` is null, this contains the full directory prefix. Otherwise, it contains only the entry name of this 73 | // directory in its parent. 74 | std::string name; 75 | 76 | // Recursive subdirectory records. 77 | std::map> subdirectories; 78 | 79 | // Recorded stat results from previous scans. Includes stat results for *all* entries within the directory that are 80 | // not `.` or `..`. 81 | std::map entries; 82 | 83 | // If true, a complete pass has already filled `entries` and `subdirectories` with initial stat results to compare 84 | // against. Otherwise, we have nothing to compare against, so we shouldn't emit anything. 85 | bool populated; 86 | 87 | // If true, this directory was present and scannable the last time it was encountered in the polling cycle. Used to 88 | // prevent duplicate deletion events for missing directories. 89 | bool was_present; 90 | 91 | // For great logging. 92 | friend std::ostream &operator<<(std::ostream &out, const DirectoryRecord &record) 93 | { 94 | out << "DirectoryRecord{" << record.name << " entries=" << record.entries.size() 95 | << " subdirectories=" << record.subdirectories.size(); 96 | if (record.populated) out << " populated"; 97 | return out << "}"; 98 | } 99 | }; 100 | 101 | #endif 102 | -------------------------------------------------------------------------------- /src/polling/polled_root.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../message.h" 5 | #include "../message_buffer.h" 6 | #include "directory_record.h" 7 | #include "polled_root.h" 8 | 9 | using std::move; 10 | using std::string; 11 | 12 | PolledRoot::PolledRoot(string &&root_path, ChannelID channel_id, bool recursive) : 13 | root(new DirectoryRecord(move(root_path))), channel_id{channel_id}, iterator(root, recursive), all_populated{false} 14 | { 15 | // 16 | } 17 | 18 | size_t PolledRoot::advance(MessageBuffer &buffer, size_t throttle_allocation) 19 | { 20 | ChannelMessageBuffer channel_buffer(buffer, channel_id); 21 | BoundPollingIterator bound_iterator(iterator, channel_buffer); 22 | 23 | size_t progress = bound_iterator.advance(throttle_allocation); 24 | 25 | if (!all_populated && root->all_populated()) { 26 | all_populated = true; 27 | } 28 | 29 | return progress; 30 | } 31 | 32 | size_t PolledRoot::count_entries() const 33 | { 34 | return root->count_entries(); 35 | } 36 | -------------------------------------------------------------------------------- /src/polling/polled_root.h: -------------------------------------------------------------------------------- 1 | #ifndef POLLED_ROOT_H 2 | #define POLLED_ROOT_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "../message.h" 9 | #include "directory_record.h" 10 | #include "polling_iterator.h" 11 | 12 | // Single root directory monitored by the `PollingThread`. 13 | class PolledRoot 14 | { 15 | public: 16 | // Begin watching a new root directory. Events produced by changes observed within this subtree should be 17 | // sent to `channel_id`. 18 | // 19 | // The newly constructed root does *not* contain any initial scan information, to avoid CPU usage spikes when 20 | // watching large directory trees. The subtree's records will be populated on the first scan. 21 | PolledRoot(std::string &&root_path, ChannelID channel_id, bool recursive); 22 | 23 | ~PolledRoot() = default; 24 | 25 | // Perform at most `throttle_allocation` operations, accumulating any changes into a provided `buffer` for batch 26 | // delivery. Return the number of operations actually performed. 27 | // 28 | // Iteration state is persisted within a `PollingIterator`, so subsequent calls to `PolledRoot::advance()` will pick 29 | // up where this call left off. When a complete scan is performed, the iteration will stop and the iterator will be 30 | // left ready to begin again at the root directory next time. 31 | size_t advance(MessageBuffer &buffer, size_t throttle_allocation); 32 | 33 | // Return `true` once the first complete scan has been completed by calls to `PolledRoot::advance()`. 34 | bool is_all_populated() { return all_populated; } 35 | 36 | // Count the number of filesystem entries that are covered by this polling thread. 37 | size_t count_entries() const; 38 | 39 | PolledRoot(const PolledRoot &) = delete; 40 | PolledRoot(PolledRoot &&) = delete; 41 | PolledRoot &operator=(const PolledRoot &) = delete; 42 | PolledRoot &operator=(PolledRoot &&) = delete; 43 | 44 | private: 45 | // Recursive data structure used to remember the last stat results from the entire filesystem subhierarchy. 46 | std::shared_ptr root; 47 | 48 | // Events produced by changes within this root should by targetted for this channel. 49 | ChannelID channel_id; 50 | 51 | // Persistent iteration state. 52 | PollingIterator iterator; 53 | 54 | // Becomes `true` when the first full subtree scan has completed. 55 | bool all_populated; 56 | 57 | // Diagnostics and logging are your friend. 58 | friend std::ostream &operator<<(std::ostream &out, const PolledRoot &root) 59 | { 60 | return out << "PolledRoot{root=" << root.root->path() << " channel=" << root.channel_id << "}"; 61 | } 62 | }; 63 | 64 | #endif 65 | -------------------------------------------------------------------------------- /src/polling/polling_iterator.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "../helper/common.h" 7 | #include "../message_buffer.h" 8 | #include "directory_record.h" 9 | #include "polling_iterator.h" 10 | 11 | using std::shared_ptr; 12 | using std::string; 13 | 14 | PollingIterator::PollingIterator(const shared_ptr &root, bool recursive) : 15 | root(root), recursive{recursive}, current(root), current_path(root->path()), phase{PollingIterator::SCAN} 16 | { 17 | // 18 | } 19 | 20 | BoundPollingIterator::BoundPollingIterator(PollingIterator &iterator, ChannelMessageBuffer &buffer) : 21 | buffer{buffer}, iterator{iterator} 22 | { 23 | // 24 | } 25 | 26 | size_t BoundPollingIterator::advance(size_t throttle_allocation) 27 | { 28 | size_t total = throttle_allocation > 0 ? throttle_allocation : 1; 29 | size_t count = 0; 30 | 31 | while (count < total) { 32 | if (iterator.phase == PollingIterator::SCAN) { 33 | advance_scan(); 34 | } else if (iterator.phase == PollingIterator::ENTRIES) { 35 | advance_entry(); 36 | } else if (iterator.phase == PollingIterator::RESET) { 37 | break; 38 | } 39 | count++; 40 | } 41 | 42 | if (iterator.phase == PollingIterator::RESET) { 43 | iterator.current = iterator.root; 44 | iterator.current_path = iterator.current->path(); 45 | iterator.phase = PollingIterator::SCAN; 46 | } 47 | 48 | return count; 49 | } 50 | 51 | void BoundPollingIterator::advance_scan() 52 | { 53 | iterator.current->scan(this); 54 | 55 | iterator.current_entry = iterator.entries.begin(); 56 | iterator.phase = PollingIterator::ENTRIES; 57 | } 58 | 59 | void BoundPollingIterator::advance_entry() 60 | { 61 | if (iterator.current_entry != iterator.entries.end()) { 62 | string &entry_name = iterator.current_entry->first; 63 | EntryKind kind = iterator.current_entry->second; 64 | 65 | iterator.current->entry(this, entry_name, path_join(iterator.current_path, entry_name), kind); 66 | iterator.current_entry++; 67 | } 68 | 69 | if (iterator.current_entry != iterator.entries.end()) { 70 | // Remain in ENTRIES phase 71 | return; 72 | } 73 | 74 | iterator.current->mark_populated(); 75 | iterator.entries.clear(); 76 | iterator.current_entry = iterator.entries.end(); 77 | 78 | if (iterator.directories.empty()) { 79 | iterator.phase = PollingIterator::RESET; 80 | return; 81 | } 82 | 83 | // Advance to the next directory in the queue 84 | iterator.current = iterator.directories.front(); 85 | iterator.current_path = iterator.current->path(); 86 | iterator.directories.pop(); 87 | iterator.phase = PollingIterator::SCAN; 88 | } 89 | -------------------------------------------------------------------------------- /src/polling/polling_thread.h: -------------------------------------------------------------------------------- 1 | #ifndef POLLING_THREAD_H 2 | #define POLLING_THREAD_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "../result.h" 12 | #include "../status.h" 13 | #include "../thread.h" 14 | #include "polled_root.h" 15 | 16 | const std::chrono::milliseconds DEFAULT_POLL_INTERVAL = std::chrono::milliseconds(100); 17 | const uint_fast32_t DEFAULT_POLL_THROTTLE = 1000; 18 | 19 | // The PollingThread observes filesystem changes by repeatedly calling scandir() and lstat() on registered root 20 | // directories. It runs automatically when a `COMMAND_ADD` message is sent to it, and stops automatically when a 21 | // `COMMAND_REMOVE` message removes the last polled root. 22 | // 23 | // It has a configurable "throttle" which roughly corresponds to the number of filesystem calls performed within each 24 | // polling cycle. The throttle is distributed among polled roots so that small directories won't be starved by large 25 | // ones. 26 | class PollingThread : public Thread 27 | { 28 | public: 29 | explicit PollingThread(uv_async_t *main_callback); 30 | PollingThread(const PollingThread &) = delete; 31 | PollingThread(PollingThread &&) = delete; 32 | ~PollingThread() override = default; 33 | 34 | PollingThread &operator=(const PollingThread &) = delete; 35 | PollingThread &operator=(PollingThread &&) = delete; 36 | 37 | private: 38 | Result<> body() override; 39 | 40 | // Perform pre-command initialization. 41 | Result<> init() override; 42 | 43 | // Perform a single polling cycle. 44 | Result<> cycle(); 45 | 46 | // Wake up when a `COMMAND_ADD` message is received while stopped. 47 | Result handle_offline_command(const CommandPayload *command) override; 48 | 49 | Result handle_add_command(const CommandPayload *command) override; 50 | 51 | Result handle_remove_command(const CommandPayload *command) override; 52 | 53 | // Configure the sleep interval. 54 | Result handle_polling_interval_command(const CommandPayload *command) override; 55 | 56 | // Configure the number of system calls to perform during each `cycle()`. 57 | Result handle_polling_throttle_command(const CommandPayload *command) override; 58 | 59 | // Respond to a request for collecting status. 60 | Result handle_status_command(const CommandPayload *command) override; 61 | 62 | std::chrono::milliseconds poll_interval; 63 | uint_fast32_t poll_throttle; 64 | 65 | std::multimap roots; 66 | 67 | using PendingSplit = std::pair; 68 | std::map pending_splits; 69 | }; 70 | 71 | #endif 72 | -------------------------------------------------------------------------------- /src/queue.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "lock.h" 8 | #include "message.h" 9 | #include "queue.h" 10 | #include "result.h" 11 | 12 | using std::move; 13 | using std::string; 14 | using std::unique_ptr; 15 | using std::vector; 16 | 17 | Queue::Queue() : active{new vector} 18 | { 19 | int err; 20 | 21 | err = uv_mutex_init(&mutex); 22 | if (err != 0) { 23 | report_uv_error(err); 24 | } 25 | freeze(); 26 | } 27 | 28 | Queue::~Queue() 29 | { 30 | uv_mutex_destroy(&mutex); 31 | } 32 | 33 | void Queue::enqueue(Message &&message) 34 | { 35 | Lock lock(mutex); 36 | active->push_back(move(message)); 37 | } 38 | 39 | unique_ptr> Queue::accept_all() 40 | { 41 | Lock lock(mutex); 42 | 43 | if (active->empty()) { 44 | unique_ptr> n; 45 | return n; 46 | } 47 | 48 | unique_ptr> consumed = move(active); 49 | active.reset(new vector); 50 | return consumed; 51 | } 52 | 53 | size_t Queue::size() 54 | { 55 | Lock lock(mutex); 56 | return active->size(); 57 | } 58 | -------------------------------------------------------------------------------- /src/queue.h: -------------------------------------------------------------------------------- 1 | #ifndef QUEUE_H 2 | #define QUEUE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "errable.h" 13 | #include "lock.h" 14 | #include "message.h" 15 | #include "result.h" 16 | 17 | // Primary channel of communication between threads. 18 | // 19 | // The producing thread accumulates a sequence of Messages to be handled through repeated 20 | // calls to .enqueue_all(). The consumer processes a chunk of Messages by calling 21 | // .accept_all(). 22 | class Queue : public Errable 23 | { 24 | public: 25 | Queue(); 26 | 27 | ~Queue() override; 28 | 29 | // Atomically enqueue a single Message. 30 | void enqueue(Message &&message); 31 | 32 | // Atomically enqueue a collection of Messages from a source STL container type between 33 | // the iterators [begin, end). 34 | template 35 | void enqueue_all(InputIt begin, InputIt end) 36 | { 37 | Lock lock(mutex); 38 | std::move(begin, end, std::back_inserter(*active)); 39 | } 40 | 41 | // Atomically consume the current contents of the queue, emptying it. 42 | // 43 | // Returns a result containing unique_ptr to the vector of Messages, nullptr if no Messages were 44 | // present, or an error if the Queue is unhealthy. 45 | std::unique_ptr> accept_all(); 46 | 47 | // Atomically report the number of items waiting on the queue. 48 | size_t size(); 49 | 50 | Queue(const Queue &) = delete; 51 | Queue(Queue &&) = delete; 52 | Queue &operator=(const Queue &) = delete; 53 | Queue &operator=(Queue &&) = delete; 54 | 55 | private: 56 | uv_mutex_t mutex{}; 57 | std::unique_ptr> active; 58 | }; 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /src/status.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "log.h" 5 | #include "status.h" 6 | 7 | using std::endl; 8 | using std::ostream; 9 | 10 | void Status::assimilate_worker_status(const Status &other) 11 | { 12 | worker_thread_state = other.worker_thread_state; 13 | worker_thread_ok = other.worker_thread_ok; 14 | worker_in_size = other.worker_in_size; 15 | worker_in_ok = other.worker_in_ok; 16 | worker_out_size = other.worker_out_size; 17 | worker_out_ok = other.worker_out_ok; 18 | 19 | worker_subscription_count = other.worker_subscription_count; 20 | #ifdef PLATFORM_MACOS 21 | worker_rename_buffer_size = other.worker_rename_buffer_size; 22 | worker_recent_file_cache_size = other.worker_recent_file_cache_size; 23 | #endif 24 | #ifdef PLATFORM_LINUX 25 | worker_watch_descriptor_count = other.worker_watch_descriptor_count; 26 | worker_channel_count = other.worker_channel_count; 27 | worker_cookie_jar_size = other.worker_cookie_jar_size; 28 | #endif 29 | 30 | worker_received = true; 31 | } 32 | 33 | void Status::assimilate_polling_status(const Status &other) 34 | { 35 | polling_thread_state = other.polling_thread_state; 36 | polling_thread_ok = other.polling_thread_ok; 37 | polling_in_size = other.polling_in_size; 38 | polling_in_ok = other.polling_in_ok; 39 | polling_out_size = other.polling_out_size; 40 | polling_out_ok = other.polling_out_ok; 41 | 42 | polling_root_count = other.polling_root_count; 43 | polling_entry_count = other.polling_entry_count; 44 | 45 | polling_received = true; 46 | } 47 | 48 | ostream &operator<<(ostream &out, const Status &status) 49 | { 50 | out << "WATCHER STATUS SUMMARY\n" 51 | << "* main thread:\n" 52 | << " - " << plural(status.pending_callback_count, "pending callback") << "\n" 53 | << " - " << plural(status.channel_callback_count, "channel callback") << "\n" 54 | << "* worker thread:\n" 55 | << " - state: " << status.worker_thread_state << "\n" 56 | << " - health: " << status.worker_thread_ok << "\n" 57 | << " - in queue health: " << status.worker_in_ok << "\n" 58 | << " - " << plural(status.worker_in_size, "in queue message") << "\n" 59 | << " - out queue health: " << status.worker_out_ok << "\n" 60 | << " - " << plural(status.worker_out_size, "out queue message") << "* polling thread\n" 61 | << " - " << plural(status.worker_subscription_count, "subscription") << endl; 62 | #ifdef PLATFORM_MACOS 63 | out << " - " << plural(status.worker_rename_buffer_size, "rename buffer entry", "rename buffer entries") << "\n" 64 | << " - " << plural(status.worker_recent_file_cache_size, "recent cache entry", "recent cache entries") << "\n"; 65 | #endif 66 | #ifdef PLATFORM_LINUX 67 | out << " - " << plural(status.worker_watch_descriptor_count, "active watch descriptor") << "\n" 68 | << " - " << plural(status.worker_channel_count, "channel") << "\n" 69 | << " - " << plural(status.worker_cookie_jar_size, "cookies") << "\n"; 70 | #endif 71 | out << "* polling thread\n" 72 | << " - state: " << status.polling_thread_state << "\n" 73 | << " - health: " << status.polling_thread_ok << "\n" 74 | << " - in queue health: " << status.worker_in_ok << "\n" 75 | << " - " << plural(status.polling_in_size, "in queue message") << "\n" 76 | << " - out queue health: " << status.worker_out_ok << "\n" 77 | << " - " << plural(status.polling_out_size, "out queue message") << "\n" 78 | << " - " << plural(status.polling_root_count, "polled root") << "\n" 79 | << " - " << plural(status.polling_entry_count, "polled entry", "polled entries") << "\n" 80 | << endl; 81 | return out; 82 | } 83 | -------------------------------------------------------------------------------- /src/status.h: -------------------------------------------------------------------------------- 1 | #ifndef STATUS_H 2 | #define STATUS_H 3 | 4 | #include 5 | #include 6 | 7 | // Summarize the module's health. This includes information like the health of all Errable resources and the sizes of 8 | // internal queues and buffers. 9 | class Status 10 | { 11 | public: 12 | // Main thread 13 | size_t pending_callback_count{0}; 14 | size_t channel_callback_count{0}; 15 | 16 | // Worker thread 17 | std::string worker_thread_state{}; 18 | std::string worker_thread_ok{}; 19 | size_t worker_in_size{0}; 20 | std::string worker_in_ok{}; 21 | size_t worker_out_size{0}; 22 | std::string worker_out_ok{}; 23 | 24 | size_t worker_subscription_count{0}; 25 | #ifdef PLATFORM_MACOS 26 | size_t worker_rename_buffer_size{0}; 27 | size_t worker_recent_file_cache_size{0}; 28 | #endif 29 | #ifdef PLATFORM_LINUX 30 | size_t worker_watch_descriptor_count{0}; 31 | size_t worker_channel_count{0}; 32 | size_t worker_cookie_jar_size{0}; 33 | #endif 34 | 35 | // Polling thread 36 | std::string polling_thread_state{}; 37 | std::string polling_thread_ok{}; 38 | size_t polling_in_size{0}; 39 | std::string polling_in_ok{}; 40 | size_t polling_out_size{0}; 41 | std::string polling_out_ok{}; 42 | 43 | size_t polling_root_count{0}; 44 | size_t polling_entry_count{0}; 45 | 46 | bool worker_received{false}; 47 | bool polling_received{false}; 48 | 49 | void assimilate_worker_status(const Status &other); 50 | 51 | void assimilate_polling_status(const Status &other); 52 | 53 | bool complete() { return worker_received && polling_received; } 54 | }; 55 | 56 | std::ostream &operator<<(std::ostream &out, const Status &status); 57 | 58 | #endif 59 | -------------------------------------------------------------------------------- /src/thread_starter.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "message.h" 7 | #include "thread_starter.h" 8 | 9 | using std::string; 10 | using std::unique_ptr; 11 | using std::vector; 12 | 13 | vector ThreadStarter::get_messages() 14 | { 15 | vector results; 16 | if (logging) { 17 | results.emplace_back(wrap_command(logging)); 18 | } 19 | return results; 20 | } 21 | 22 | void ThreadStarter::set_command(unique_ptr &dest, const CommandPayload *src) 23 | { 24 | dest.reset(new CommandPayload(*src)); 25 | } 26 | 27 | Message ThreadStarter::wrap_command(unique_ptr &src) 28 | { 29 | return Message(CommandPayload(*src)); 30 | } 31 | -------------------------------------------------------------------------------- /src/thread_starter.h: -------------------------------------------------------------------------------- 1 | #ifndef THREAD_STARTER_H 2 | #define THREAD_STARTER_H 3 | 4 | #include 5 | #include 6 | 7 | #include "message.h" 8 | 9 | class ThreadStarter 10 | { 11 | public: 12 | ThreadStarter() = default; 13 | ThreadStarter(const ThreadStarter &) = delete; 14 | ThreadStarter(ThreadStarter &&) = delete; 15 | ~ThreadStarter() = default; 16 | 17 | ThreadStarter &operator=(const ThreadStarter &) = delete; 18 | ThreadStarter &operator=(ThreadStarter &&) = delete; 19 | 20 | std::vector get_messages(); 21 | 22 | void set_logging(const CommandPayload *payload) { set_command(logging, payload); } 23 | 24 | protected: 25 | void set_command(std::unique_ptr &dest, const CommandPayload *src); 26 | 27 | Message wrap_command(std::unique_ptr &src); 28 | 29 | private: 30 | std::unique_ptr logging; 31 | }; 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /src/worker/linux/cookie_jar.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "../../message.h" 9 | #include "../../message_buffer.h" 10 | #include "../recent_file_cache.h" 11 | #include "cookie_jar.h" 12 | 13 | using std::move; 14 | using std::string; 15 | using std::unique_ptr; 16 | 17 | Cookie::Cookie(ChannelID channel_id, std::string &&from_path, EntryKind kind) noexcept : 18 | channel_id{channel_id}, from_path(move(from_path)), kind{kind} 19 | { 20 | // 21 | } 22 | 23 | Cookie::Cookie(Cookie &&other) noexcept : 24 | channel_id{other.channel_id}, from_path(move(other.from_path)), kind{other.kind} 25 | { 26 | // 27 | } 28 | 29 | void CookieBatch::moved_from(MessageBuffer &messages, 30 | ChannelID channel_id, 31 | uint32_t cookie, 32 | string &&old_path, 33 | EntryKind kind) 34 | { 35 | auto existing = from_paths.find(cookie); 36 | if (existing != from_paths.end()) { 37 | // Duplicate IN_MOVED_FROM cookie. 38 | // Resolve the old one as a deletion. 39 | Cookie dup(move(existing->second)); 40 | messages.deleted(dup.get_channel_id(), dup.move_from_path(), dup.get_kind()); 41 | from_paths.erase(existing); 42 | } 43 | 44 | Cookie c(channel_id, move(old_path), kind); 45 | from_paths.emplace(cookie, move(c)); 46 | } 47 | 48 | unique_ptr CookieBatch::yoink(uint32_t cookie) 49 | { 50 | auto from = from_paths.find(cookie); 51 | if (from == from_paths.end()) { 52 | return unique_ptr(nullptr); 53 | } 54 | 55 | unique_ptr c(new Cookie(move(from->second))); 56 | from_paths.erase(from); 57 | return c; 58 | } 59 | 60 | void CookieBatch::flush(MessageBuffer &messages, RecentFileCache &cache) 61 | { 62 | for (auto &pair : from_paths) { 63 | Cookie dup(move(pair.second)); 64 | cache.evict(dup.get_from_path()); 65 | messages.deleted(dup.get_channel_id(), dup.move_from_path(), dup.get_kind()); 66 | } 67 | from_paths.clear(); 68 | } 69 | 70 | CookieJar::CookieJar(unsigned int max_batches) : batches(max_batches) 71 | { 72 | // 73 | } 74 | 75 | void CookieJar::moved_from(MessageBuffer &messages, 76 | ChannelID channel_id, 77 | uint32_t cookie, 78 | std::string &&old_path, 79 | EntryKind kind) 80 | { 81 | if (batches.empty()) return; 82 | 83 | batches.back().moved_from(messages, channel_id, cookie, move(old_path), kind); 84 | } 85 | 86 | void CookieJar::moved_to(MessageBuffer &messages, 87 | ChannelID channel_id, 88 | uint32_t cookie, 89 | std::string &&new_path, 90 | EntryKind kind) 91 | { 92 | unique_ptr from; 93 | for (auto &batch : batches) { 94 | unique_ptr found = batch.yoink(cookie); 95 | if (found) { 96 | if (from) { 97 | // Multiple IN_MOVED_FROM results. 98 | // Report deletions for all but the most recent. 99 | messages.deleted(from->get_channel_id(), from->move_from_path(), from->get_kind()); 100 | } 101 | 102 | from = move(found); 103 | } 104 | } 105 | 106 | if (!from) { 107 | // Unmatched IN_MOVED_TO. 108 | // Resolve it as a creation. 109 | messages.created(channel_id, move(new_path), kind); 110 | return; 111 | } 112 | 113 | if (from->get_channel_id() != channel_id || kinds_are_different(from->get_kind(), kind)) { 114 | // Existing IN_MOVED_FROM with this cookie does not match. 115 | // Resolve it as a deletion/creation pair. 116 | messages.deleted(from->get_channel_id(), from->move_from_path(), from->get_kind()); 117 | messages.created(channel_id, move(new_path), kind); 118 | return; 119 | } 120 | 121 | messages.renamed(channel_id, from->move_from_path(), move(new_path), kind); 122 | } 123 | 124 | void CookieJar::flush_oldest_batch(MessageBuffer &messages, RecentFileCache &cache) 125 | { 126 | if (batches.empty()) return; 127 | 128 | batches.front().flush(messages, cache); 129 | batches.pop_front(); 130 | batches.emplace_back(); 131 | } 132 | -------------------------------------------------------------------------------- /src/worker/linux/cookie_jar.h: -------------------------------------------------------------------------------- 1 | #ifndef COOKIE_JAR 2 | #define COOKIE_JAR 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "../../message.h" 12 | #include "../../message_buffer.h" 13 | #include "../recent_file_cache.h" 14 | 15 | // Remember a path that was observed in an IN_MOVED_FROM inotify event until its corresponding IN_MOVED_TO event 16 | // is observed, or until it times out. 17 | class Cookie 18 | { 19 | public: 20 | Cookie(ChannelID channel_id, std::string &&from_path, EntryKind kind); 21 | Cookie(Cookie &&other) noexcept; 22 | ~Cookie() = default; 23 | 24 | const ChannelID &get_channel_id() const { return channel_id; } 25 | 26 | // Access the absolute path from this event. 27 | const std::string &get_from_path() { return from_path; } 28 | 29 | // Take possession of the absolute path from this event. 30 | std::string move_from_path() { return std::string(std::move(from_path)); } 31 | 32 | const EntryKind &get_kind() { return kind; } 33 | 34 | Cookie(const Cookie &other) = delete; 35 | Cookie &operator=(Cookie &&cookie) = delete; 36 | Cookie &operator=(const Cookie &other) = delete; 37 | 38 | private: 39 | const ChannelID channel_id; 40 | std::string from_path; 41 | const EntryKind kind; 42 | }; 43 | 44 | // Collection of Cookies observed from rename events within a single cycle of inotify events. Batches are used to 45 | // age off rename events that haven't been matched after a fixed number of inotify deliveries. 46 | class CookieBatch 47 | { 48 | public: 49 | CookieBatch() = default; 50 | ~CookieBatch() = default; 51 | 52 | // Insert a new Cookie to eventually match an IN_MOVED_FROM event. If an existing Cookie already exists for this 53 | // cookie value, immediately age the old Cookie off and buffer a deletion event. 54 | void moved_from(MessageBuffer &messages, 55 | ChannelID channel_id, 56 | uint32_t cookie, 57 | std::string &&old_path, 58 | EntryKind kind); 59 | 60 | // Remove a Cookie from this batch that has the specified cookie value. Return nullptr instead if no such cookie 61 | // exists. 62 | std::unique_ptr yoink(uint32_t cookie); 63 | 64 | // Age off all Cookies within this batch by buffering them as deletion events. Evict them from the cache. 65 | void flush(MessageBuffer &messages, RecentFileCache &cache); 66 | 67 | bool empty() const { return from_paths.empty(); } 68 | 69 | CookieBatch(const CookieBatch &) = delete; 70 | CookieBatch(CookieBatch &&) = delete; 71 | CookieBatch &operator=(const CookieBatch &) = delete; 72 | CookieBatch &operator=(CookieBatch &&) = delete; 73 | 74 | private: 75 | std::map from_paths; 76 | }; 77 | 78 | // Associate IN_MOVED_FROM and IN_MOVED_TO events from inotify received within a configurable number of consecutive 79 | // notification cycles. The CookieJar contains a fixed number of CookieBatches that contain unmatched IN_MOVED_FROM 80 | // events collected within a single notification cycle. As more notifications arrive or read() calls time out, events 81 | // that remain unmatched are aged off and emitted as deletion events. 82 | class CookieJar 83 | { 84 | public: 85 | // Construct a CookieJar capable of correlating rename events across `max_batches` consecutive inotify event cycles. 86 | // Specifying a higher number of batches improves the watcher's ability to match rename events that occur at 87 | // high rates, at the cost of increasing memory usage and the latency of delete events delivered when an entry 88 | // is renamed outside of a watched directory. If `max_batches` is 0, _no_ rename correlation will be done at 89 | // all; all renames will be emitted as create/delete pairs instead. If `max_batches` is 1, only rename events 90 | // that arrive within the same notification cycle will be caught, but deletion events will be delivered with 91 | // no additional latency. The default of 2 correlates rename events across consecutive notification cycles but 92 | // no further. 93 | explicit CookieJar(unsigned int max_batches = 2); 94 | ~CookieJar() = default; 95 | 96 | // Observe an IN_MOVED_FROM event by adding a Cookie to the freshest CookieBatch. 97 | void moved_from(MessageBuffer &messages, 98 | ChannelID channel_id, 99 | uint32_t cookie, 100 | std::string &&old_path, 101 | EntryKind kind); 102 | 103 | // Observe an IN_MOVED_TO event. Search the current CookieBatches for a recent IN_MOVED_FROM event with a matching 104 | // `cookie` value. If no match is found, emit a creation event for the entry. If a match is found but the channel 105 | // or entry kind don't match, emit a delete/create event pair for the old and new entries. Otherwise, emit the 106 | // successfully correlated rename event. 107 | void moved_to(MessageBuffer &messages, ChannelID channel_id, uint32_t cookie, std::string &&new_path, EntryKind kind); 108 | 109 | // Buffer deletion events for any Cookies that have not been matched within `max_batches` CookieBatches. Add a 110 | // fresh CookieBatch to capture the next cycle of rename events. 111 | void flush_oldest_batch(MessageBuffer &messages, RecentFileCache &cache); 112 | 113 | CookieJar(const CookieJar &other) = delete; 114 | CookieJar(CookieJar &&other) = delete; 115 | CookieJar &operator=(const CookieJar &other) = delete; 116 | CookieJar &operator=(CookieJar &&other) = delete; 117 | 118 | private: 119 | std::deque batches; 120 | }; 121 | 122 | #endif 123 | -------------------------------------------------------------------------------- /src/worker/linux/linux_worker_platform.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "../../helper/linux/helper.h" 8 | #include "../../log.h" 9 | #include "../../message.h" 10 | #include "../../result.h" 11 | #include "../recent_file_cache.h" 12 | #include "../worker_platform.h" 13 | #include "../worker_thread.h" 14 | #include "cookie_jar.h" 15 | #include "pipe.h" 16 | #include "side_effect.h" 17 | #include "watch_registry.h" 18 | 19 | using std::endl; 20 | using std::ostream; 21 | using std::string; 22 | using std::unique_ptr; 23 | using std::vector; 24 | 25 | const size_t DEFAULT_CACHE_SIZE = 4096; 26 | 27 | // In milliseconds 28 | const int RENAME_TIMEOUT = 500; 29 | 30 | // Platform-specific worker implementation for Linux systems. 31 | class LinuxWorkerPlatform : public WorkerPlatform 32 | { 33 | public: 34 | LinuxWorkerPlatform(WorkerThread *thread) : WorkerPlatform(thread), cache{DEFAULT_CACHE_SIZE} 35 | { 36 | report_errable(pipe); 37 | report_errable(registry); 38 | freeze(); 39 | }; 40 | 41 | // Inform the listen() loop that one or more commands are waiting from the main thread. 42 | Result<> wake() override { return pipe.signal(); } 43 | 44 | // Main event loop. Use poll(2) to wait on I/O from either the Pipe or inotify events. 45 | Result<> listen() override 46 | { 47 | pollfd to_poll[2]; 48 | to_poll[0].fd = pipe.get_read_fd(); 49 | to_poll[0].events = POLLIN; 50 | to_poll[0].revents = 0; 51 | to_poll[1].fd = registry.get_read_fd(); 52 | to_poll[1].events = POLLIN; 53 | to_poll[1].revents = 0; 54 | 55 | while (true) { 56 | int result = poll(to_poll, 2, RENAME_TIMEOUT); 57 | 58 | if (result < 0) { 59 | return errno_result<>("Unable to poll"); 60 | } 61 | 62 | if (result == 0) { 63 | // Poll timeout. Cycle the CookieJar. 64 | MessageBuffer messages; 65 | jar.flush_oldest_batch(messages, cache); 66 | 67 | if (!messages.empty()) { 68 | LOGGER << "Flushing " << plural(messages.size(), "unpaired rename") << "." << endl; 69 | Result<> er = emit_all(messages.begin(), messages.end()); 70 | if (er.is_error()) return er; 71 | } 72 | 73 | continue; 74 | } 75 | 76 | if ((to_poll[0].revents & (POLLIN | POLLERR)) != 0u) { 77 | Result<> cr = pipe.consume(); 78 | if (cr.is_error()) return cr; 79 | 80 | Result<> hr = handle_commands(); 81 | if (hr.is_error()) return hr; 82 | } 83 | 84 | if ((to_poll[1].revents & (POLLIN | POLLERR)) != 0u) { 85 | MessageBuffer messages; 86 | 87 | Result<> cr = registry.consume(messages, jar, cache); 88 | if (cr.is_error()) LOGGER << cr << endl; 89 | 90 | if (!messages.empty()) { 91 | Result<> er = emit_all(messages.begin(), messages.end()); 92 | if (er.is_error()) return er; 93 | } 94 | } 95 | } 96 | 97 | return error_result("Polling loop exited unexpectedly"); 98 | } 99 | 100 | // Recursively watch a directory tree. 101 | Result handle_add_command(CommandID /*command*/, 102 | ChannelID channel, 103 | const string &root_path, 104 | bool recursive) override 105 | { 106 | Timer t; 107 | vector poll; 108 | 109 | ostream &logline = LOGGER << "Adding watcher for path " << root_path; 110 | if (!recursive) { 111 | logline << " (non-recursively)"; 112 | } 113 | logline << " at channel " << channel << "." << endl; 114 | 115 | Result<> r = registry.add(channel, string(root_path), recursive, poll); 116 | if (r.is_error()) return r.propagate(); 117 | 118 | if (!poll.empty()) { 119 | vector poll_messages; 120 | poll_messages.reserve(poll.size()); 121 | 122 | for (string &poll_root : poll) { 123 | poll_messages.emplace_back( 124 | CommandPayloadBuilder::add(channel, move(poll_root), recursive, poll.size()).build()); 125 | } 126 | 127 | t.stop(); 128 | LOGGER << "Watcher for path " << root_path << " and " << plural(poll.size(), "polled watch root") << " added in " 129 | << t << "." << endl; 130 | return emit_all(poll_messages.begin(), poll_messages.end()).propagate(false); 131 | } 132 | 133 | t.stop(); 134 | LOGGER << "Watcher for path " << root_path << " added in " << t << "." << endl; 135 | return ok_result(true); 136 | } 137 | 138 | // Unwatch a directory tree. 139 | Result handle_remove_command(CommandID /*command*/, ChannelID channel) override 140 | { 141 | return registry.remove(channel).propagate(true); 142 | } 143 | 144 | private: 145 | Pipe pipe; 146 | WatchRegistry registry; 147 | CookieJar jar; 148 | RecentFileCache cache; 149 | }; 150 | 151 | unique_ptr WorkerPlatform::for_worker(WorkerThread *thread) 152 | { 153 | return unique_ptr(new LinuxWorkerPlatform(thread)); 154 | } 155 | -------------------------------------------------------------------------------- /src/worker/linux/pipe.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "../../errable.h" 7 | #include "../../helper/linux/helper.h" 8 | #include "../../result.h" 9 | #include "pipe.h" 10 | 11 | using std::move; 12 | using std::string; 13 | 14 | const char WAKE = '!'; 15 | 16 | Pipe::Pipe() : read_fd{0}, write_fd{0} 17 | { 18 | int fds[2] = {0, 0}; 19 | int err = pipe2(fds, O_CLOEXEC | O_NONBLOCK); 20 | if (err == -1) { 21 | report_if_error<>(errno_result<>("Unable to open pipe")); 22 | freeze(); 23 | return; 24 | } 25 | 26 | read_fd = fds[0]; 27 | write_fd = fds[1]; 28 | freeze(); 29 | } 30 | 31 | Pipe::~Pipe() 32 | { 33 | close(read_fd); 34 | close(write_fd); 35 | } 36 | 37 | Result<> Pipe::signal() 38 | { 39 | ssize_t result = write(write_fd, &WAKE, sizeof(char)); 40 | if (result == -1) { 41 | int write_errno = errno; 42 | 43 | if (write_errno == EAGAIN || write_errno == EWOULDBLOCK) { 44 | // If the kernel buffer is full, that means there's already a pending signal. 45 | return ok_result(); 46 | } 47 | 48 | return errno_result<>("Unable to write a byte to the pipe", write_errno); 49 | } 50 | if (result == 0) { 51 | return error_result("No bytes written to pipe"); 52 | } 53 | 54 | return ok_result(); 55 | } 56 | 57 | Result<> Pipe::consume() 58 | { 59 | const size_t BUFSIZE = 256; 60 | char buf[BUFSIZE]; 61 | ssize_t result = 0; 62 | 63 | do { 64 | result = read(read_fd, &buf, BUFSIZE); 65 | } while (result > 0); 66 | 67 | if (result < 0) { 68 | int read_errno = errno; 69 | 70 | if (read_errno == EAGAIN || read_errno == EWOULDBLOCK) { 71 | // Nothing left to read. 72 | return ok_result(); 73 | } 74 | 75 | return errno_result<>("Unable to read from pipe", read_errno); 76 | } 77 | 78 | return ok_result(); 79 | } 80 | -------------------------------------------------------------------------------- /src/worker/linux/pipe.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPE_H 2 | #define PIPE_H 3 | 4 | #include "../../errable.h" 5 | #include "../../result.h" 6 | 7 | // RAII wrapper for a Linux pipe created with pipe(2). We don't care about the actual data transmitted. 8 | class Pipe : public Errable 9 | { 10 | public: 11 | // Construct a new Pipe identified in Result<> errors with a specified name. 12 | Pipe(); 13 | 14 | // Deallocate and close() the underlying pipe file descriptor. 15 | ~Pipe() override; 16 | 17 | // Write a byte to the pipe to inform readers that data is available. 18 | Result<> signal(); 19 | 20 | // Read and discard all data waiting on the pipe to prepare for a new signal. 21 | Result<> consume(); 22 | 23 | // Access the file descriptor that should be polled for data. 24 | int get_read_fd() const { return read_fd; } 25 | 26 | Pipe(const Pipe &) = delete; 27 | Pipe(Pipe &&) = delete; 28 | Pipe &operator=(const Pipe &) = delete; 29 | Pipe &operator=(Pipe &&) = delete; 30 | 31 | private: 32 | int read_fd; 33 | int write_fd; 34 | }; 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /src/worker/linux/side_effect.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "../../message.h" 7 | #include "../../message_buffer.h" 8 | #include "../../result.h" 9 | #include "side_effect.h" 10 | #include "watch_registry.h" 11 | 12 | using std::move; 13 | using std::shared_ptr; 14 | using std::string; 15 | using std::vector; 16 | 17 | void SideEffect::track_subdirectory(string subdir, ChannelID channel_id) 18 | { 19 | subdirectories.emplace_back(move(subdir), channel_id); 20 | } 21 | 22 | void SideEffect::enact_in(const shared_ptr &parent, WatchRegistry *registry, MessageBuffer &messages) 23 | { 24 | for (ChannelID channel_id : removed_roots) { 25 | Result<> r = registry->remove(channel_id); 26 | if (r.is_error()) messages.error(channel_id, string(r.get_error()), false); 27 | } 28 | 29 | for (Subdirectory &subdir : subdirectories) { 30 | if (removed_roots.find(subdir.channel_id) != removed_roots.end()) { 31 | continue; 32 | } 33 | 34 | vector poll_roots; 35 | Result<> r = registry->add(subdir.channel_id, parent, subdir.basename, true, poll_roots); 36 | if (r.is_error()) messages.error(subdir.channel_id, string(r.get_error()), false); 37 | 38 | for (string &poll_root : poll_roots) { 39 | messages.add(Message(CommandPayloadBuilder::add(subdir.channel_id, move(poll_root), true, 1).build())); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/worker/linux/side_effect.h: -------------------------------------------------------------------------------- 1 | #ifndef SIDE_EFFECT_H 2 | #define SIDE_EFFECT_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "../../message.h" 11 | #include "../../result.h" 12 | 13 | // Forward declaration for pointer access. 14 | class WatchRegistry; 15 | 16 | class MessageBuffer; 17 | 18 | class WatchedDirectory; 19 | 20 | // Record additional actions that should be triggered by inotify events received in the course of a single notification 21 | // cycle. 22 | class SideEffect 23 | { 24 | public: 25 | SideEffect() = default; 26 | ~SideEffect() = default; 27 | 28 | // Recursively watch a newly created subdirectory. 29 | void track_subdirectory(std::string subdir, ChannelID channel_id); 30 | 31 | // Unsubscribe from a channel after this event has been handled. 32 | void remove_channel(ChannelID channel_id) { removed_roots.insert(channel_id); } 33 | 34 | // Perform all enqueued actions. 35 | void enact_in(const std::shared_ptr &parent, WatchRegistry *registry, MessageBuffer &messages); 36 | 37 | SideEffect(const SideEffect &other) = delete; 38 | SideEffect(SideEffect &&other) = delete; 39 | SideEffect &operator=(const SideEffect &other) = delete; 40 | SideEffect &operator=(SideEffect &&other) = delete; 41 | 42 | private: 43 | struct Subdirectory 44 | { 45 | Subdirectory(std::string &&basename, ChannelID channel_id) : basename(std::move(basename)), channel_id{channel_id} 46 | { 47 | // 48 | } 49 | 50 | Subdirectory(Subdirectory &&original) : basename{std::move(original.basename)}, channel_id{original.channel_id} 51 | { 52 | // 53 | } 54 | 55 | ~Subdirectory() = default; 56 | 57 | std::string basename; 58 | ChannelID channel_id; 59 | 60 | Subdirectory(const Subdirectory &) = delete; 61 | Subdirectory &operator=(const Subdirectory &) = delete; 62 | Subdirectory &operator=(Subdirectory &&) = delete; 63 | }; 64 | 65 | std::vector subdirectories; 66 | 67 | std::set removed_roots; 68 | }; 69 | 70 | #endif 71 | -------------------------------------------------------------------------------- /src/worker/linux/watch_registry.h: -------------------------------------------------------------------------------- 1 | #ifndef WATCHER_REGISTRY_H 2 | #define WATCHER_REGISTRY_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "../../errable.h" 11 | #include "../../message_buffer.h" 12 | #include "../../result.h" 13 | #include "../recent_file_cache.h" 14 | #include "cookie_jar.h" 15 | #include "side_effect.h" 16 | #include "watched_directory.h" 17 | 18 | // Manage the set of open inotify watch descriptors. 19 | class WatchRegistry : public Errable 20 | { 21 | public: 22 | // Initialize inotify. Enter an error state if inotify initialization fails. 23 | WatchRegistry(); 24 | 25 | // Stop inotify and release all kernel resources associated with it. 26 | ~WatchRegistry() override; 27 | 28 | // Begin watching a root path. If `recursive` is `true`, recursively watch all subdirectories as well. If inotify 29 | // watch descriptors are exhausted before the entire directory tree can be watched, the unsuccessfully watched roots 30 | // will be accumulated into the `poll` vector. 31 | // 32 | // `root` must name a directory if `recursive` is `true`. 33 | Result<> add(ChannelID channel_id, const std::string &root, bool recursive, std::vector &poll) 34 | { 35 | return add(channel_id, nullptr, root, recursive, poll); 36 | } 37 | 38 | // Begin watching path beneath an existing WatchedDirectory. If `recursive` is `true`, recursively watch all 39 | // subdirectories as well. If inotify watch descriptors are exhausted before the entire directory tree can be watched, 40 | // the unsuccessfully watched roots will be accumulated into the `poll` vector. 41 | // 42 | // `root` must name a directory if `recursive` is `true`. 43 | Result<> add(ChannelID channel_id, 44 | const std::shared_ptr &parent, 45 | const std::string &name, 46 | bool recursive, 47 | std::vector &poll); 48 | 49 | // Uninstall inotify watchers used to deliver events on a specified channel. 50 | Result<> remove(ChannelID channel_id); 51 | 52 | // Interpret all inotify events created since the previous call to consume(), until the 53 | // read() call would block. Buffer messages corresponding to each inotify event. Use the 54 | // CookieJar to match pairs of rename events across event batches and the RecentFileCache to 55 | // identify symlinks without doing a stat for every event. 56 | Result<> consume(MessageBuffer &messages, CookieJar &jar, RecentFileCache &cache); 57 | 58 | // Return the file descriptor that should be polled to wake up when inotify events are 59 | // available. 60 | int get_read_fd() { return inotify_fd; } 61 | 62 | WatchRegistry(const WatchRegistry &) = delete; 63 | WatchRegistry(WatchRegistry &&) = delete; 64 | WatchRegistry &operator=(const WatchRegistry &) = delete; 65 | WatchRegistry &operator=(WatchRegistry &&) = delete; 66 | 67 | private: 68 | int inotify_fd; 69 | std::unordered_multimap> by_wd; 70 | std::unordered_multimap> by_channel; 71 | }; 72 | 73 | #endif 74 | -------------------------------------------------------------------------------- /src/worker/linux/watched_directory.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "../../message.h" 8 | #include "../../message_buffer.h" 9 | #include "../../result.h" 10 | #include "../recent_file_cache.h" 11 | #include "cookie_jar.h" 12 | #include "side_effect.h" 13 | #include "watched_directory.h" 14 | 15 | using std::move; 16 | using std::ostringstream; 17 | using std::shared_ptr; 18 | using std::string; 19 | 20 | WatchedDirectory::WatchedDirectory(int wd, 21 | ChannelID channel_id, 22 | shared_ptr parent, 23 | string &&name, 24 | bool recursive) : 25 | wd{wd}, channel_id{channel_id}, parent{parent}, name{move(name)}, recursive{recursive} 26 | { 27 | // 28 | } 29 | 30 | Result<> WatchedDirectory::accept_event(MessageBuffer &buffer, 31 | CookieJar &jar, 32 | SideEffect &side, 33 | RecentFileCache &cache, 34 | const inotify_event &event) 35 | { 36 | string basename{event.name}; 37 | string path = absolute_event_path(event); 38 | 39 | bool dir_hint = (event.mask & IN_ISDIR) == IN_ISDIR; 40 | 41 | // Read or refresh the cached lstat() entry primarily to determine if this entry is a symlink or not. 42 | shared_ptr stat = cache.former_at_path(path, !dir_hint, dir_hint, false); 43 | if (stat->is_absent()) { 44 | stat = cache.current_at_path(path, !dir_hint, dir_hint, false); 45 | cache.apply(); 46 | } 47 | EntryKind kind = stat->get_entry_kind(); 48 | 49 | if ((event.mask & IN_CREATE) == IN_CREATE) { 50 | // create entry inside directory 51 | if (kind == KIND_DIRECTORY && recursive) { 52 | side.track_subdirectory(basename, channel_id); 53 | } 54 | buffer.created(channel_id, move(path), kind); 55 | return ok_result(); 56 | } 57 | 58 | if ((event.mask & IN_DELETE) == IN_DELETE) { 59 | // delete entry inside directory 60 | cache.evict(path); 61 | buffer.deleted(channel_id, move(path), kind); 62 | return ok_result(); 63 | } 64 | 65 | if ((event.mask & (IN_MODIFY | IN_ATTRIB)) != 0u) { 66 | // modify entry inside directory or attribute change for directory or entry inside directory 67 | buffer.modified(channel_id, move(path), kind); 68 | return ok_result(); 69 | } 70 | 71 | if ((event.mask & (IN_DELETE_SELF | IN_UNMOUNT)) != 0u) { 72 | if (is_root()) { 73 | side.remove_channel(channel_id); 74 | cache.evict(get_absolute_path()); 75 | buffer.deleted(channel_id, get_absolute_path(), KIND_DIRECTORY); 76 | } 77 | return ok_result(); 78 | } 79 | 80 | if ((event.mask & IN_MOVE_SELF) == IN_MOVE_SELF) { 81 | // directory itself was renamed 82 | if (is_root()) { 83 | side.remove_channel(channel_id); 84 | cache.evict(get_absolute_path()); 85 | buffer.deleted(channel_id, get_absolute_path(), KIND_DIRECTORY); 86 | } 87 | return ok_result(); 88 | } 89 | 90 | if ((event.mask & IN_MOVED_FROM) == IN_MOVED_FROM) { 91 | // rename source for directory or entry inside directory 92 | cache.evict(path); 93 | jar.moved_from(buffer, channel_id, event.cookie, move(path), kind); 94 | return ok_result(); 95 | } 96 | 97 | if ((event.mask & IN_MOVED_TO) == IN_MOVED_TO) { 98 | // rename destination for directory or entry inside directory 99 | if (kind == KIND_DIRECTORY && recursive) { 100 | side.track_subdirectory(basename, channel_id); 101 | } 102 | jar.moved_to(buffer, channel_id, event.cookie, move(path), kind); 103 | return ok_result(); 104 | } 105 | 106 | // IN_IGNORED 107 | 108 | return ok_result(); 109 | } 110 | 111 | string WatchedDirectory::get_absolute_path() 112 | { 113 | ostringstream stream; 114 | build_absolute_path(stream); 115 | return stream.str(); 116 | } 117 | 118 | void WatchedDirectory::build_absolute_path(ostringstream &stream) 119 | { 120 | if (parent) { 121 | parent->build_absolute_path(stream); 122 | stream << "/"; 123 | } 124 | stream << name; 125 | } 126 | 127 | string WatchedDirectory::absolute_event_path(const inotify_event &event) 128 | { 129 | ostringstream stream; 130 | build_absolute_path(stream); 131 | if (event.len > 0) { 132 | stream << "/" << event.name; 133 | } 134 | return stream.str(); 135 | } 136 | -------------------------------------------------------------------------------- /src/worker/linux/watched_directory.h: -------------------------------------------------------------------------------- 1 | #ifndef WATCHED_DIRECTORY 2 | #define WATCHED_DIRECTORY 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "../../message_buffer.h" 11 | #include "../../result.h" 12 | #include "../recent_file_cache.h" 13 | #include "cookie_jar.h" 14 | #include "side_effect.h" 15 | 16 | // Associate resources used to watch inotify events that are delivered with a single watch descriptor. 17 | class WatchedDirectory 18 | { 19 | public: 20 | WatchedDirectory(int wd, 21 | ChannelID channel_id, 22 | std::shared_ptr parent, 23 | std::string &&name, 24 | bool recursive); 25 | 26 | ~WatchedDirectory() = default; 27 | 28 | // Interpret a single inotify event. Buffer messages, store or resolve rename Cookies from the CookieJar, and 29 | // enqueue SideEffects based on the event's mask. 30 | Result<> accept_event(MessageBuffer &buffer, 31 | CookieJar &jar, 32 | SideEffect &side, 33 | RecentFileCache &cache, 34 | const inotify_event &event); 35 | 36 | // A parent WatchedDirectory reported that this directory was renamed. Update our internal state immediately so 37 | // that events on child paths will be reported with the correct path. 38 | void was_renamed(const std::shared_ptr &new_parent, const std::string &new_name) 39 | { 40 | parent = new_parent; 41 | name = new_name; 42 | } 43 | 44 | // Access the Channel ID this WatchedDirectory will broadcast on. 45 | ChannelID get_channel_id() { return channel_id; } 46 | 47 | // Access the watch descriptor that corresponds to this directory. 48 | int get_descriptor() { return wd; } 49 | 50 | // Return true if this directory is the root of a recursively watched subtree. 51 | bool is_root() { return parent == nullptr; } 52 | 53 | // Return the full absolute path to this directory. 54 | std::string get_absolute_path(); 55 | 56 | WatchedDirectory(const WatchedDirectory &other) = delete; 57 | WatchedDirectory(WatchedDirectory &&other) = delete; 58 | WatchedDirectory &operator=(const WatchedDirectory &other) = delete; 59 | WatchedDirectory &operator=(WatchedDirectory &&other) = delete; 60 | 61 | private: 62 | void build_absolute_path(std::ostringstream &stream); 63 | 64 | // Translate the relative path within an inotify event into an absolute path within this directory. 65 | std::string absolute_event_path(const inotify_event &event); 66 | 67 | int wd; 68 | ChannelID channel_id; 69 | std::shared_ptr parent; 70 | std::string name; 71 | bool recursive; 72 | }; 73 | 74 | #endif 75 | -------------------------------------------------------------------------------- /src/worker/macos/batch_handler.h: -------------------------------------------------------------------------------- 1 | #ifndef EVENT_HANDLER_H 2 | #define EVENT_HANDLER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "../../message.h" 9 | #include "../../message_buffer.h" 10 | #include "../recent_file_cache.h" 11 | #include "flags.h" 12 | #include "rename_buffer.h" 13 | 14 | class BatchHandler; 15 | 16 | class Event 17 | { 18 | public: 19 | Event(BatchHandler &batch, std::string &&event_path, FSEventStreamEventFlags flags); 20 | 21 | Event(Event &&original) noexcept; 22 | 23 | ~Event() = default; 24 | 25 | bool flag_created() { return (flags & CREATE_FLAGS) != 0; } 26 | 27 | bool flag_deleted() { return (flags & DELETED_FLAGS) != 0; } 28 | 29 | bool flag_modified() { return (flags & MODIFY_FLAGS) != 0; } 30 | 31 | bool flag_renamed() { return (flags & RENAME_FLAGS) != 0; } 32 | 33 | bool flag_file() { return (flags & IS_FILE) != 0; } 34 | 35 | bool flag_directory() { return (flags & IS_DIRECTORY) != 0; } 36 | 37 | bool flag_symlink() { return (flags & IS_SYMLINK) != 0; } 38 | 39 | bool is_recursive(); 40 | 41 | const std::string &root_path(); 42 | 43 | ChannelMessageBuffer &message_buffer(); 44 | 45 | RecentFileCache &cache(); 46 | 47 | RenameBuffer &rename_buffer(); 48 | 49 | const std::string &get_event_path() { return event_path; } 50 | 51 | const std::string &get_stat_path() { return updated_event_path.empty() ? event_path : updated_event_path; } 52 | 53 | const std::shared_ptr &get_former() { return former; } 54 | 55 | const std::shared_ptr &get_current() { return current; } 56 | 57 | bool update_for_rename(const std::string &from_dir_path, const std::string &to_dir_path); 58 | 59 | bool needs_updated_info() { return !current || (get_stat_path() != current->get_path()); }; 60 | 61 | Event(const Event &) = delete; 62 | Event &operator=(const Event &) = delete; 63 | Event &operator=(Event &&) = delete; 64 | 65 | private: 66 | bool skip_recursive_event(); 67 | 68 | void collect_info(); 69 | 70 | bool should_defer(); 71 | 72 | void report(); 73 | 74 | bool emit_if_unambiguous(); 75 | 76 | bool emit_if_rename(); 77 | 78 | bool emit_if_absent(); 79 | 80 | bool emit_if_present(); 81 | 82 | BatchHandler &handler; 83 | std::string event_path; 84 | std::string updated_event_path; 85 | FSEventStreamEventFlags flags; 86 | 87 | std::shared_ptr former; 88 | std::shared_ptr current; 89 | 90 | friend class BatchHandler; 91 | }; 92 | 93 | class BatchHandler 94 | { 95 | public: 96 | BatchHandler(ChannelMessageBuffer &message_buffer, 97 | RecentFileCache &cache, 98 | RenameBuffer &rename_buffer, 99 | bool recursive, 100 | const std::string &root_path); 101 | 102 | ~BatchHandler() = default; 103 | 104 | void event(std::string &&event_path, FSEventStreamEventFlags flags); 105 | 106 | bool update_for_rename(const std::string &from_dir_path, const std::string &to_dir_path); 107 | 108 | void handle_deferred(); 109 | 110 | BatchHandler(const BatchHandler &) = delete; 111 | BatchHandler(BatchHandler &&) = delete; 112 | BatchHandler &operator=(const BatchHandler &) = delete; 113 | BatchHandler &operator=(BatchHandler &&) = delete; 114 | 115 | private: 116 | RecentFileCache &cache; 117 | ChannelMessageBuffer &message_buffer; 118 | RenameBuffer &rename_buffer; 119 | bool recursive; 120 | const std::string &root_path; 121 | std::list deferred; 122 | 123 | friend class Event; 124 | }; 125 | 126 | // Inline event methods that need the full BatchHandler type. 127 | 128 | inline bool Event::is_recursive() 129 | { 130 | return handler.recursive; 131 | } 132 | 133 | inline const std::string &Event::root_path() 134 | { 135 | return handler.root_path; 136 | } 137 | 138 | inline ChannelMessageBuffer &Event::message_buffer() 139 | { 140 | return handler.message_buffer; 141 | } 142 | 143 | inline RecentFileCache &Event::cache() 144 | { 145 | return handler.cache; 146 | } 147 | 148 | inline RenameBuffer &Event::rename_buffer() 149 | { 150 | return handler.rename_buffer; 151 | } 152 | 153 | #endif 154 | -------------------------------------------------------------------------------- /src/worker/macos/flags.h: -------------------------------------------------------------------------------- 1 | #ifndef FLAGS_H 2 | #define FLAGS_H 3 | 4 | #include 5 | 6 | const CFAbsoluteTime LATENCY = 0; 7 | 8 | const FSEventStreamEventFlags CREATE_FLAGS = kFSEventStreamEventFlagItemCreated; 9 | 10 | const FSEventStreamEventFlags DELETED_FLAGS = kFSEventStreamEventFlagItemRemoved; 11 | 12 | const FSEventStreamEventFlags MODIFY_FLAGS = kFSEventStreamEventFlagItemInodeMetaMod 13 | | kFSEventStreamEventFlagItemFinderInfoMod | kFSEventStreamEventFlagItemChangeOwner 14 | | kFSEventStreamEventFlagItemXattrMod | kFSEventStreamEventFlagItemModified; 15 | 16 | const FSEventStreamEventFlags RENAME_FLAGS = kFSEventStreamEventFlagItemRenamed; 17 | 18 | const FSEventStreamEventFlags IS_FILE = kFSEventStreamEventFlagItemIsFile; 19 | 20 | const FSEventStreamEventFlags IS_DIRECTORY = kFSEventStreamEventFlagItemIsDir; 21 | 22 | const FSEventStreamEventFlags IS_SYMLINK = kFSEventStreamEventFlagItemIsSymlink; 23 | 24 | const CFTimeInterval RENAME_TIMEOUT = 0.05; 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /src/worker/macos/rename_buffer.h: -------------------------------------------------------------------------------- 1 | #ifndef RENAME_BUFFER_H 2 | #define RENAME_BUFFER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "../../message.h" 12 | #include "../../message_buffer.h" 13 | #include "../recent_file_cache.h" 14 | 15 | // Forward declaration to be able to accept an Event argument 16 | class Event; 17 | 18 | // Forward declaration to be able to accept a BatchHandler argument 19 | class BatchHandler; 20 | 21 | // Filesystem entry that was flagged as participating in a rename by a received filesystem event. 22 | class RenameBufferEntry 23 | { 24 | public: 25 | RenameBufferEntry(RenameBufferEntry &&original) noexcept; 26 | 27 | ~RenameBufferEntry() = default; 28 | 29 | RenameBufferEntry(const RenameBufferEntry &) = delete; 30 | RenameBufferEntry &operator=(const RenameBufferEntry &) = delete; 31 | RenameBufferEntry &operator=(RenameBufferEntry &&) = delete; 32 | 33 | private: 34 | RenameBufferEntry(std::shared_ptr entry, std::string event_path, bool current); 35 | 36 | std::shared_ptr entry; 37 | std::string event_path; 38 | bool current; 39 | size_t age; 40 | 41 | friend class RenameBuffer; 42 | }; 43 | 44 | class RenameBuffer 45 | { 46 | public: 47 | // Create an empty buffer. 48 | RenameBuffer() = default; 49 | 50 | ~RenameBuffer() = default; 51 | 52 | using Key = ino_t; 53 | 54 | // Observe a rename event for a filesystem event. Deduce the matching side of the rename, if possible, 55 | // based on the previous and currently observed state of the entry at that path. Return "true" if the event 56 | // is consumed, or "false" if it should be treated as something other than a rename. 57 | bool observe_event(Event &event, BatchHandler &batch); 58 | 59 | // Enqueue creation and removal events for any buffer entries that have remained unpaired through two consecutive 60 | // event batches. 61 | // 62 | // Return the collection of unpaired Keys that were created during this run. 63 | std::shared_ptr> flush_unmatched(ChannelMessageBuffer &message_buffer, RecentFileCache &cache); 64 | 65 | // Enqueue creation and removal events for buffer entries that map to any of the listed keys. Return the collection 66 | // of unpaired Keys that were aged, but not processed, during this run. 67 | std::shared_ptr> flush_unmatched(ChannelMessageBuffer &message_buffer, 68 | RecentFileCache &cache, 69 | const std::shared_ptr> &keys); 70 | 71 | size_t size() { return observed_by_inode.size(); } 72 | 73 | RenameBuffer(const RenameBuffer &) = delete; 74 | RenameBuffer(RenameBuffer &&) = delete; 75 | RenameBuffer &operator=(const RenameBuffer &) = delete; 76 | RenameBuffer &operator=(RenameBuffer &&) = delete; 77 | 78 | private: 79 | bool observe_present_entry(Event &event, 80 | BatchHandler &batch, 81 | const std::shared_ptr &present, 82 | bool current); 83 | 84 | bool observe_absent(Event &event, BatchHandler &batch, const std::shared_ptr &absent); 85 | 86 | std::unordered_map observed_by_inode; 87 | }; 88 | 89 | #endif 90 | -------------------------------------------------------------------------------- /src/worker/macos/subscription.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../../helper/macos/helper.h" 5 | #include "../../message.h" 6 | #include "subscription.h" 7 | 8 | using std::move; 9 | using std::string; 10 | 11 | Subscription::Subscription(ChannelID channel_id, 12 | bool recursive, 13 | string &&root, 14 | RefHolder &&event_stream) : 15 | channel_id{channel_id}, root{move(root)}, recursive{recursive}, event_stream{move(event_stream)} 16 | { 17 | // 18 | } 19 | 20 | Subscription::Subscription(Subscription &&original) noexcept : 21 | channel_id{original.channel_id}, 22 | root{move(original.root)}, 23 | recursive{original.recursive}, 24 | event_stream{move(original.event_stream)} 25 | { 26 | // 27 | } 28 | 29 | Subscription::~Subscription() 30 | { 31 | if (event_stream.ok()) { 32 | FSEventStreamStop(event_stream.get()); 33 | FSEventStreamInvalidate(event_stream.get()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/worker/macos/subscription.h: -------------------------------------------------------------------------------- 1 | #ifndef SUBSCRIPTION_H 2 | #define SUBSCRIPTION_H 3 | 4 | #include "../../helper/macos/helper.h" 5 | #include "../../message.h" 6 | #include 7 | #include 8 | 9 | class Subscription 10 | { 11 | public: 12 | Subscription(ChannelID channel_id, bool recursive, std::string &&root, RefHolder &&event_stream); 13 | 14 | Subscription(Subscription &&original) noexcept; 15 | 16 | ~Subscription(); 17 | 18 | const ChannelID &get_channel_id() { return channel_id; } 19 | 20 | const std::string &get_root() { return root; } 21 | 22 | const bool &get_recursive() { return recursive; } 23 | 24 | const RefHolder &get_event_stream() { return event_stream; } 25 | 26 | Subscription(const Subscription &) = delete; 27 | Subscription &operator=(const Subscription &) = delete; 28 | Subscription &operator=(Subscription &&) = delete; 29 | 30 | private: 31 | ChannelID channel_id; 32 | std::string root; 33 | bool recursive; 34 | RefHolder event_stream; 35 | }; 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /src/worker/recent_file_cache.h: -------------------------------------------------------------------------------- 1 | #ifndef RECENT_FILE_CACHE_H 2 | #define RECENT_FILE_CACHE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "../helper/libuv.h" 14 | #include "../message.h" 15 | 16 | class StatResult 17 | { 18 | public: 19 | static std::shared_ptr at(std::string &&path, bool file_hint, bool directory_hint, bool symlink_hint); 20 | 21 | virtual ~StatResult() = default; 22 | 23 | virtual bool is_present() const = 0; 24 | 25 | bool is_absent() const { return !is_present(); } 26 | 27 | virtual bool has_changed_from(const StatResult &other) const; 28 | 29 | virtual bool could_be_rename_of(const StatResult &other) const; 30 | 31 | bool update_for_rename(const std::string &from_dir_path, const std::string &to_dir_path); 32 | 33 | const std::string &get_path() const; 34 | 35 | EntryKind get_entry_kind() const; 36 | 37 | virtual std::string to_string(bool verbose = false) const = 0; 38 | 39 | StatResult(const StatResult &) = delete; 40 | StatResult(StatResult &&) = delete; 41 | StatResult &operator=(const StatResult &) = delete; 42 | StatResult &operator=(StatResult &&) = delete; 43 | 44 | protected: 45 | StatResult(std::string &&path, EntryKind entry_kind) : path{std::move(path)}, entry_kind{entry_kind} {}; 46 | 47 | private: 48 | std::string path; 49 | EntryKind entry_kind; 50 | }; 51 | 52 | std::ostream &operator<<(std::ostream &out, const StatResult &result); 53 | 54 | class PresentEntry : public StatResult 55 | { 56 | public: 57 | PresentEntry(std::string &&path, EntryKind entry_kind, uint64_t inode, uint64_t size); 58 | 59 | ~PresentEntry() override = default; 60 | 61 | bool is_present() const override; 62 | 63 | bool has_changed_from(const StatResult &other) const override; 64 | 65 | bool could_be_rename_of(const StatResult &other) const override; 66 | 67 | uint64_t get_inode() const; 68 | 69 | uint64_t get_size() const; 70 | 71 | const std::chrono::time_point &get_last_seen() const; 72 | 73 | std::string to_string(bool verbose = false) const override; 74 | 75 | PresentEntry(const PresentEntry &) = delete; 76 | PresentEntry(PresentEntry &&) = delete; 77 | PresentEntry &operator=(const PresentEntry &) = delete; 78 | PresentEntry &operator=(PresentEntry &&) = delete; 79 | 80 | private: 81 | uint64_t inode; 82 | uint64_t size; 83 | std::chrono::time_point last_seen; 84 | }; 85 | 86 | class AbsentEntry : public StatResult 87 | { 88 | public: 89 | AbsentEntry(std::string &&path, EntryKind entry_kind) : StatResult(std::move(path), entry_kind){}; 90 | 91 | ~AbsentEntry() override = default; 92 | 93 | bool is_present() const override; 94 | 95 | bool has_changed_from(const StatResult &other) const override; 96 | 97 | bool could_be_rename_of(const StatResult &other) const override; 98 | 99 | std::string to_string(bool verbose = false) const override; 100 | 101 | AbsentEntry(const AbsentEntry &) = delete; 102 | AbsentEntry(AbsentEntry &&) = delete; 103 | AbsentEntry &operator=(const AbsentEntry &) = delete; 104 | AbsentEntry &operator=(AbsentEntry &&) = delete; 105 | }; 106 | 107 | class RecentFileCache 108 | { 109 | public: 110 | explicit RecentFileCache(size_t maximum_size); 111 | 112 | ~RecentFileCache() = default; 113 | 114 | std::shared_ptr current_at_path(const std::string &path, 115 | bool file_hint, 116 | bool directory_hint, 117 | bool symlink_hint); 118 | 119 | std::shared_ptr former_at_path(const std::string &path, 120 | bool file_hint, 121 | bool directory_hint, 122 | bool symlink_hint); 123 | 124 | void evict(const std::string &path); 125 | 126 | void evict(const std::shared_ptr &entry); 127 | 128 | void update_for_rename(const std::string &from_dir_path, const std::string &to_dir_path); 129 | 130 | void apply(); 131 | 132 | void prune(); 133 | 134 | void prepopulate(const std::string &root, size_t max, bool recursive); 135 | 136 | void resize(size_t maximum_size); 137 | 138 | size_t size() { return by_path.size(); } 139 | 140 | RecentFileCache(const RecentFileCache &) = delete; 141 | RecentFileCache(RecentFileCache &&) = delete; 142 | RecentFileCache &operator=(const RecentFileCache &) = delete; 143 | RecentFileCache &operator=(RecentFileCache &&) = delete; 144 | 145 | private: 146 | size_t prepopulate_helper(const std::string &root, size_t max, bool recursive); 147 | 148 | size_t maximum_size; 149 | 150 | std::map> pending; 151 | 152 | std::unordered_map> by_path; 153 | 154 | std::multimap, std::shared_ptr> by_timestamp; 155 | }; 156 | 157 | #endif 158 | -------------------------------------------------------------------------------- /src/worker/windows/subscription.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "../../helper/windows/helper.h" 8 | #include "../../log.h" 9 | #include "../../result.h" 10 | #include "subscription.h" 11 | 12 | using std::endl; 13 | using std::ostream; 14 | using std::ostringstream; 15 | using std::string; 16 | using std::wostringstream; 17 | using std::wstring; 18 | 19 | const DWORD DEFAULT_BUFFER_SIZE = 128 * 1024; 20 | const DWORD NETWORK_BUFFER_SIZE = 64 * 1024; 21 | 22 | Subscription::Subscription(ChannelID channel, 23 | HANDLE root, 24 | const wstring &path, 25 | bool recursive, 26 | WindowsWorkerPlatform *platform) : 27 | command{0}, 28 | channel{channel}, 29 | platform{platform}, 30 | path{path}, 31 | root{root}, 32 | terminating{false}, 33 | recursive{recursive}, 34 | buffer_size{DEFAULT_BUFFER_SIZE}, 35 | buffer{new BYTE[buffer_size]}, 36 | written{new BYTE[buffer_size]}, 37 | old_path_seen{false} 38 | { 39 | ZeroMemory(&overlapped, sizeof(OVERLAPPED)); 40 | overlapped.hEvent = this; 41 | } 42 | 43 | Subscription::~Subscription() 44 | { 45 | CloseHandle(root); 46 | } 47 | 48 | Result Subscription::schedule(LPOVERLAPPED_COMPLETION_ROUTINE fn) 49 | { 50 | if (terminating) { 51 | LOGGER << "Declining to schedule a new change callback for channel " << channel 52 | << " because the subscription is terminating." << endl; 53 | return ok_result(true); 54 | } 55 | 56 | ostream &logline = LOGGER << "Scheduling the next change callback for channel " << channel; 57 | if (!recursive) logline << " (non-recursively)"; 58 | logline << "." << endl; 59 | 60 | int success = ReadDirectoryChangesW(root, // root directory handle 61 | buffer.get(), // result buffer 62 | buffer_size, // result buffer size 63 | recursive, // recursive 64 | FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SIZE 65 | | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_LAST_ACCESS | FILE_NOTIFY_CHANGE_CREATION 66 | | FILE_NOTIFY_CHANGE_SECURITY, // change flags 67 | NULL, // bytes returned 68 | &overlapped, // overlapped 69 | fn // completion routine 70 | ); 71 | 72 | if (!success) { 73 | DWORD last_error = GetLastError(); 74 | if (last_error == ERROR_INVALID_FUNCTION) { 75 | // Filesystem does not support change events. 76 | return ok_result(false); 77 | } 78 | 79 | return windows_error_result("Unable to subscribe to filesystem events", last_error); 80 | } 81 | 82 | return ok_result(true); 83 | } 84 | 85 | Result<> Subscription::use_network_size() 86 | { 87 | if (buffer_size <= NETWORK_BUFFER_SIZE) { 88 | ostringstream out("Buffer size of "); 89 | out << buffer_size << " is already lower than the network buffer size " << NETWORK_BUFFER_SIZE; 90 | return error_result(out.str()); 91 | } 92 | 93 | buffer_size = NETWORK_BUFFER_SIZE; 94 | buffer.reset(new BYTE[buffer_size]); 95 | written.reset(new BYTE[buffer_size]); 96 | 97 | return ok_result(); 98 | } 99 | 100 | BYTE *Subscription::get_written(DWORD written_size) 101 | { 102 | memcpy(written.get(), buffer.get(), written_size); 103 | return written.get(); 104 | } 105 | 106 | Result Subscription::get_root_path() 107 | { 108 | return to_utf8(path); 109 | } 110 | 111 | wstring Subscription::make_absolute(const wstring &sub_path) 112 | { 113 | wostringstream out; 114 | 115 | out << path; 116 | if (path.back() != L'\\' && sub_path.front() != L'\\') { 117 | out << L'\\'; 118 | } 119 | out << sub_path; 120 | 121 | return out.str(); 122 | } 123 | 124 | Result<> Subscription::stop(const CommandID cmd) 125 | { 126 | if (terminating) return ok_result(); 127 | 128 | bool success = CancelIo(root); 129 | if (!success) return windows_error_result<>("Unable to cancel pending I/O"); 130 | 131 | terminating = true; 132 | command = cmd; 133 | 134 | return ok_result(); 135 | } 136 | -------------------------------------------------------------------------------- /src/worker/windows/subscription.h: -------------------------------------------------------------------------------- 1 | #ifndef SUBSCRIPTION_H 2 | #define SUBSCRIPTION_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "../../message.h" 10 | #include "../../result.h" 11 | 12 | class WindowsWorkerPlatform; 13 | 14 | class Subscription 15 | { 16 | public: 17 | Subscription(ChannelID channel, 18 | HANDLE root, 19 | const std::wstring &path, 20 | bool recursive, 21 | WindowsWorkerPlatform *platform); 22 | 23 | ~Subscription(); 24 | 25 | Result schedule(LPOVERLAPPED_COMPLETION_ROUTINE fn); 26 | 27 | Result<> use_network_size(); 28 | 29 | BYTE *get_written(DWORD written_size); 30 | 31 | Result get_root_path(); 32 | 33 | std::wstring make_absolute(const std::wstring &sub_path); 34 | 35 | Result<> stop(const CommandID command); 36 | 37 | const CommandID &get_command_id() const { return command; } 38 | 39 | const ChannelID &get_channel() const { return channel; } 40 | 41 | WindowsWorkerPlatform *get_platform() const { return platform; } 42 | 43 | const bool &is_recursive() const { return recursive; } 44 | 45 | const bool &is_terminating() const { return terminating; } 46 | 47 | void remember_old_path(std::string &&old_path, EntryKind kind) 48 | { 49 | this->old_path = std::move(old_path); 50 | this->old_path_kind = kind; 51 | this->old_path_seen = true; 52 | } 53 | 54 | void clear_old_path() 55 | { 56 | old_path.clear(); 57 | old_path_kind = KIND_UNKNOWN; 58 | old_path_seen = false; 59 | } 60 | 61 | const std::string &get_old_path() const { return old_path; } 62 | 63 | const EntryKind &get_old_path_kind() const { return old_path_kind; } 64 | 65 | const bool &was_old_path_seen() const { return old_path_seen; } 66 | 67 | private: 68 | CommandID command; 69 | ChannelID channel; 70 | WindowsWorkerPlatform *platform; 71 | 72 | std::wstring path; 73 | HANDLE root; 74 | OVERLAPPED overlapped; 75 | bool recursive; 76 | bool terminating; 77 | 78 | DWORD buffer_size; 79 | std::unique_ptr buffer; 80 | std::unique_ptr written; 81 | 82 | std::string old_path; 83 | EntryKind old_path_kind; 84 | bool old_path_seen; 85 | }; 86 | 87 | #endif 88 | -------------------------------------------------------------------------------- /src/worker/worker_platform.h: -------------------------------------------------------------------------------- 1 | #ifndef WORKER_PLATFORM_H 2 | #define WORKER_PLATFORM_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "../errable.h" 9 | #include "../message.h" 10 | #include "../result.h" 11 | #include "../status.h" 12 | #include "worker_thread.h" 13 | 14 | class WorkerPlatform : public Errable 15 | { 16 | public: 17 | static std::unique_ptr for_worker(WorkerThread *thread); 18 | 19 | ~WorkerPlatform() override = default; 20 | 21 | virtual Result<> wake() = 0; 22 | 23 | virtual Result<> init() { return ok_result(); } 24 | 25 | virtual Result<> listen() = 0; 26 | 27 | virtual Result handle_add_command(CommandID command, 28 | ChannelID channel, 29 | const std::string &root_path, 30 | bool recursive) = 0; 31 | 32 | virtual Result handle_remove_command(CommandID command, ChannelID channel) = 0; 33 | 34 | virtual void handle_cache_size_command(size_t /*cache_size*/) {} 35 | 36 | virtual void populate_status(Status & /*status*/) {} 37 | 38 | Result<> handle_commands() { return thread->handle_commands().propagate_as_void(); } 39 | 40 | WorkerPlatform(const WorkerPlatform &) = delete; 41 | WorkerPlatform(WorkerPlatform &&) = delete; 42 | WorkerPlatform &operator=(const WorkerPlatform &) = delete; 43 | WorkerPlatform &operator=(WorkerPlatform &&) = delete; 44 | 45 | protected: 46 | WorkerPlatform(WorkerThread *thread) : thread{thread} 47 | { 48 | // 49 | } 50 | 51 | Result<> emit(Message &&message) { return thread->emit(std::move(message)); } 52 | 53 | template 54 | Result<> emit_all(InputIt begin, InputIt end) 55 | { 56 | return thread->emit_all(begin, end); 57 | } 58 | 59 | WorkerThread *thread{}; 60 | }; 61 | 62 | #endif 63 | -------------------------------------------------------------------------------- /src/worker/worker_thread.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "../log.h" 7 | #include "../message.h" 8 | #include "../queue.h" 9 | #include "../result.h" 10 | #include "../status.h" 11 | #include "worker_platform.h" 12 | #include "worker_thread.h" 13 | 14 | using std::string; 15 | using std::unique_ptr; 16 | 17 | WorkerThread::WorkerThread(uv_async_t *main_callback) : 18 | Thread("worker thread", main_callback), platform{WorkerPlatform::for_worker(this)} 19 | { 20 | report_errable(*platform); 21 | freeze(); 22 | } 23 | 24 | // Definition must be here to see the full definition of WorkerPlatform. 25 | WorkerThread::~WorkerThread() = default; 26 | 27 | Result<> WorkerThread::wake() 28 | { 29 | return platform->wake(); 30 | } 31 | 32 | Result<> WorkerThread::init() 33 | { 34 | Logger::from_env("WATCHER_LOG_WORKER"); 35 | 36 | return platform->init(); 37 | } 38 | 39 | Result<> WorkerThread::body() 40 | { 41 | return platform->listen(); 42 | } 43 | 44 | Result WorkerThread::handle_add_command(const CommandPayload *payload) 45 | { 46 | Result r = platform->handle_add_command( 47 | payload->get_id(), payload->get_channel_id(), payload->get_root(), payload->get_recursive()); 48 | return r.is_ok() ? r.propagate(r.get_value() ? ACK : NOTHING) : r.propagate(); 49 | } 50 | 51 | Result WorkerThread::handle_remove_command(const CommandPayload *payload) 52 | { 53 | Result r = platform->handle_remove_command(payload->get_id(), payload->get_channel_id()); 54 | return r.propagate(r.get_value() ? ACK : NOTHING); 55 | } 56 | 57 | Result WorkerThread::handle_cache_size_command(const CommandPayload *payload) 58 | { 59 | platform->handle_cache_size_command(payload->get_arg()); 60 | return ok_result(ACK); 61 | } 62 | 63 | Result WorkerThread::handle_status_command(const CommandPayload *payload) 64 | { 65 | unique_ptr status{new Status()}; 66 | 67 | status->worker_thread_state = state_name(); 68 | status->worker_thread_ok = get_message(); 69 | status->worker_in_size = get_in_queue_size(); 70 | status->worker_in_ok = get_in_queue_error(); 71 | status->worker_out_size = get_out_queue_size(); 72 | status->worker_out_ok = get_out_queue_error(); 73 | 74 | platform->populate_status(*status); 75 | 76 | Result<> r = emit(Message(StatusPayload(payload->get_request_id(), move(status)))); 77 | return r.propagate(NOTHING); 78 | } 79 | -------------------------------------------------------------------------------- /src/worker/worker_thread.h: -------------------------------------------------------------------------------- 1 | #ifndef WORKER_THREAD_H 2 | #define WORKER_THREAD_H 3 | 4 | #include 5 | #include 6 | 7 | #include "../message.h" 8 | #include "../queue.h" 9 | #include "../result.h" 10 | #include "../status.h" 11 | #include "../thread.h" 12 | 13 | class WorkerPlatform; 14 | 15 | class WorkerThread : public Thread 16 | { 17 | public: 18 | explicit WorkerThread(uv_async_t *main_callback); 19 | ~WorkerThread() override; 20 | 21 | WorkerThread(const WorkerThread &) = delete; 22 | WorkerThread(WorkerThread &&) = delete; 23 | WorkerThread &operator=(const WorkerThread &) = delete; 24 | WorkerThread &operator=(WorkerThread &&) = delete; 25 | 26 | private: 27 | Result<> wake() override; 28 | 29 | Result<> init() override; 30 | 31 | Result<> body() override; 32 | 33 | Result handle_add_command(const CommandPayload *payload) override; 34 | 35 | Result handle_remove_command(const CommandPayload *payload) override; 36 | 37 | Result handle_cache_size_command(const CommandPayload *payload) override; 38 | 39 | Result handle_status_command(const CommandPayload *payload) override; 40 | 41 | std::unique_ptr platform; 42 | 43 | friend WorkerPlatform; 44 | }; 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /test/configuration.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-dev mocha */ 2 | const fs = require('fs-extra') 3 | 4 | const { configure } = require('../lib/binding') 5 | const { Fixture } = require('./helper') 6 | 7 | describe('configuration', function () { 8 | let fixture, badPath 9 | 10 | beforeEach(async function () { 11 | fixture = new Fixture() 12 | await fixture.before() 13 | 14 | badPath = fixture.fixturePath('bad', 'path.log') 15 | }) 16 | 17 | afterEach(async function () { 18 | await fixture.after(this.currentTest) 19 | }) 20 | 21 | it('validates its arguments', async function () { 22 | await assert.isRejected(configure(), /requires an option object/) 23 | }) 24 | 25 | it('configures the main thread logger', async function () { 26 | await configure({ mainLog: fixture.mainLogFile }) 27 | 28 | const contents = await fs.readFile(fixture.mainLogFile) 29 | assert.match(contents, /FileLogger opened/) 30 | }) 31 | 32 | it('configures the worker thread logger', async function () { 33 | await configure({ workerLog: fixture.workerLogFile }) 34 | 35 | const contents = await fs.readFile(fixture.workerLogFile) 36 | assert.match(contents, /FileLogger opened/) 37 | }) 38 | 39 | it('fails if the main log file cannot be written', async function () { 40 | await assert.isRejected(configure({ mainLog: badPath }), /No such file or directory/) 41 | }) 42 | 43 | it('fails if the worker log file cannot be written', async function () { 44 | await assert.isRejected(configure({ workerLog: badPath }), /No such file or directory/) 45 | }) 46 | 47 | describe('for the polling thread', function () { 48 | describe("while it's stopped", function () { 49 | it('configures the logger', async function () { 50 | await configure({ pollingLog: fixture.pollingLogFile }) 51 | 52 | assert.isFalse(await fs.pathExists(fixture.pollingLogFile)) 53 | 54 | await fixture.watch([], { poll: true }, () => {}) 55 | 56 | const contents = await fs.readFile(fixture.pollingLogFile) 57 | assert.match(contents, /FileLogger opened/) 58 | }) 59 | 60 | it('defers the check for a valid polling log file', async function () { 61 | await configure({ pollingLog: badPath }) 62 | }) 63 | }) 64 | 65 | describe("after it's started", function () { 66 | it('configures the logger', async function () { 67 | await fixture.watch([], { poll: true }, () => {}) 68 | 69 | await configure({ pollingLog: fixture.pollingLogFile }) 70 | 71 | const contents = await fs.readFile(fixture.pollingLogFile) 72 | assert.match(contents, /FileLogger opened/) 73 | }) 74 | 75 | it('fails if the polling log file cannot be written', async function () { 76 | await fixture.watch([], { poll: true }, () => {}) 77 | 78 | await assert.isRejected(configure({ pollingLog: badPath }), /No such file or directory/) 79 | }) 80 | }) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /test/errors.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-dev mocha */ 2 | 3 | const { Fixture } = require('./helper') 4 | const { EventMatcher } = require('./matcher') 5 | 6 | describe('error reporting', function () { 7 | let fixture, matcher 8 | 9 | beforeEach(async function () { 10 | fixture = new Fixture() 11 | 12 | await fixture.before() 13 | await fixture.log() 14 | 15 | matcher = new EventMatcher(fixture) 16 | }) 17 | 18 | afterEach(async function () { 19 | await fixture.after(this.currentTest) 20 | }) 21 | 22 | it('rejects the promise if the path does not exist', async function () { 23 | await assert.isRejected(matcher.watch(['nope'], {})) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/events/nonrecursive.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | 3 | const { Fixture } = require('../helper') 4 | const { EventMatcher } = require('../matcher'); 5 | 6 | [false, true].forEach(poll => { 7 | describe(`nonrecursive watching with poll = ${poll}`, function () { 8 | let fixture, matcher 9 | 10 | beforeEach(async function () { 11 | fixture = new Fixture() 12 | await fixture.before() 13 | await fixture.log() 14 | 15 | await Promise.all([ 16 | fs.mkdir(fixture.watchPath('subdir-0')), 17 | fs.mkdir(fixture.watchPath('subdir-1')) 18 | ]) 19 | 20 | matcher = new EventMatcher(fixture) 21 | await matcher.watch([], { poll, recursive: false }) 22 | }) 23 | 24 | afterEach(async function () { 25 | await fixture.after(this.currentTest) 26 | }) 27 | 28 | it('receives events for entries directly within its root', async function () { 29 | const filePath = fixture.watchPath('file0.txt') 30 | const dirPath = fixture.watchPath('subdir-2') 31 | 32 | await Promise.all([ 33 | fs.writeFile(filePath, 'yes\n'), 34 | fs.mkdir(dirPath) 35 | ]) 36 | 37 | await until('creation events arrive', matcher.allEvents( 38 | { action: 'created', kind: 'file', path: filePath }, 39 | { action: 'created', kind: 'directory', path: dirPath } 40 | )) 41 | 42 | await Promise.all([ 43 | fs.unlink(filePath), 44 | fs.rmdir(dirPath) 45 | ]) 46 | 47 | await until('deletion events arrive', matcher.allEvents( 48 | { action: 'deleted', path: filePath }, 49 | { action: 'deleted', path: dirPath } 50 | )) 51 | }) 52 | 53 | it('ignores events for entries within a subdirectory', async function () { 54 | const flagFile = fixture.watchPath('file0.txt') 55 | const file1Path = fixture.watchPath('subdir-0', 'file1.txt') 56 | const file2Path = fixture.watchPath('subdir-0', 'file2.txt') 57 | const file3Path = fixture.watchPath('subdir-1', 'file3.txt') 58 | 59 | await fs.writeFile(file1Path, 'nope\n') 60 | await fs.writeFile(file2Path, 'nope\n') 61 | await fs.writeFile(file3Path, 'nope\n') 62 | await fs.writeFile(flagFile, 'uh huh\n') 63 | 64 | await until('creation event arrives', matcher.allEvents( 65 | { action: 'created', kind: 'file', path: flagFile } 66 | )) 67 | assert.isTrue(matcher.noEvents( 68 | { path: file1Path }, 69 | { path: file2Path }, 70 | { path: file3Path } 71 | )) 72 | }) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /test/events/parent-rename.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | 3 | const { Fixture } = require('../helper') 4 | const { EventMatcher } = require('../matcher') 5 | 6 | // These cases interfere with the caches on MacOS, but other platforms should handle them correctly as well. 7 | describe('when a parent directory is renamed', function () { 8 | let fixture, matcher 9 | let originalParentDir, originalFile 10 | let finalParentDir, finalFile 11 | 12 | beforeEach(async function () { 13 | fixture = new Fixture() 14 | await fixture.before() 15 | await fixture.log() 16 | 17 | originalParentDir = fixture.watchPath('parent-0') 18 | originalFile = fixture.watchPath('parent-0', 'file.txt') 19 | finalParentDir = fixture.watchPath('parent-1') 20 | finalFile = fixture.watchPath('parent-1', 'file.txt') 21 | 22 | await fs.mkdir(originalParentDir) 23 | await fs.writeFile(originalFile, 'contents\n') 24 | 25 | matcher = new EventMatcher(fixture) 26 | await matcher.watch([], {}) 27 | }) 28 | 29 | afterEach(async function () { 30 | await fixture.after(this.currentTest) 31 | }) 32 | 33 | it('tracks the file rename across event batches', async function () { 34 | const changedFile = fixture.watchPath('parent-1', 'file-1.txt') 35 | 36 | await fs.rename(originalParentDir, finalParentDir) 37 | await until('the rename event arrives', matcher.allEvents( 38 | { action: 'renamed', kind: 'directory', oldPath: originalParentDir, path: finalParentDir } 39 | )) 40 | 41 | await fs.rename(finalFile, changedFile) 42 | 43 | await until('the rename event arrives', matcher.allEvents( 44 | { action: 'renamed', kind: 'file', oldPath: finalFile, path: changedFile } 45 | )) 46 | }) 47 | 48 | it('tracks the file rename within the same event batch', async function () { 49 | const changedFile = fixture.watchPath('parent-1', 'file-1.txt') 50 | 51 | await fs.rename(originalParentDir, finalParentDir) 52 | await fs.rename(finalFile, changedFile) 53 | 54 | await until('the rename events arrive', matcher.allEvents( 55 | { action: 'renamed', kind: 'directory', oldPath: originalParentDir, path: finalParentDir }, 56 | { action: 'renamed', kind: 'file', oldPath: finalFile, path: changedFile } 57 | )) 58 | }) 59 | 60 | it('tracks the file rename when the file is renamed first', async function () { 61 | const changedFile = fixture.watchPath('parent-0', 'file-1.txt') 62 | 63 | await fs.rename(originalFile, changedFile) 64 | await fs.rename(originalParentDir, finalParentDir) 65 | 66 | await until('the rename events arrive', matcher.allEvents( 67 | { action: 'renamed', kind: 'file', oldPath: originalFile, path: changedFile }, 68 | { action: 'renamed', kind: 'directory', oldPath: originalParentDir, path: finalParentDir } 69 | )) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /test/events/root-rename.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | 3 | const { Fixture } = require('../helper') 4 | const { EventMatcher } = require('../matcher'); 5 | 6 | [false, true].forEach(poll => { 7 | describe(`renaming the root with poll = ${poll}`, function () { 8 | let fixture, matcher 9 | 10 | beforeEach(async function () { 11 | fixture = new Fixture() 12 | await fixture.before() 13 | await fixture.log() 14 | 15 | matcher = new EventMatcher(fixture) 16 | await matcher.watch([], { poll }) 17 | }) 18 | 19 | afterEach(async function () { 20 | await fixture.after(this.currentTest) 21 | }) 22 | 23 | it('emits a deletion event for the root move itself ^windows', async function () { 24 | const oldRoot = fixture.watchPath() 25 | const newRoot = fixture.fixturePath('new-root') 26 | 27 | await fs.rename(oldRoot, newRoot) 28 | 29 | await until('deletion event arrives', matcher.allEvents( 30 | { action: 'deleted', kind: 'directory', path: oldRoot } 31 | )) 32 | }) 33 | 34 | it('does not emit events within the new root ^windows', async function () { 35 | const oldRoot = fixture.watchPath() 36 | const oldFile = fixture.watchPath('some-file.txt') 37 | const newRoot = fixture.fixturePath('new-root') 38 | const newFile = fixture.fixturePath('new-root', 'some-file.txt') 39 | 40 | await fs.rename(oldRoot, newRoot) 41 | await fs.appendFile(newFile, 'changed\n') 42 | 43 | await until('deletion event arrives', matcher.allEvents( 44 | { action: 'deleted', kind: 'directory', path: oldRoot } 45 | )) 46 | 47 | assert.isTrue(matcher.noEvents( 48 | { path: oldFile }, 49 | { path: newFile } 50 | )) 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /test/events/symlink.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | 3 | const { Fixture } = require('../helper') 4 | const { EventMatcher } = require('../matcher') 5 | 6 | describe('watching beneath symlinked directories', function () { 7 | let fixture 8 | 9 | beforeEach(async function () { 10 | fixture = new Fixture() 11 | await fixture.before() 12 | await fixture.log() 13 | }) 14 | 15 | afterEach(async function () { 16 | await fixture.after(this.currentTest) 17 | }) 18 | 19 | it('reports paths consistently with the argument to watchPath', async function () { 20 | const realSubdir = fixture.watchPath('realdir') 21 | const realFile = fixture.watchPath('realdir', 'file.txt') 22 | 23 | const symlinkSubdir = fixture.watchPath('linkdir') 24 | const symlinkFile = fixture.watchPath('linkdir', 'file.txt') 25 | 26 | await fs.mkdirs(realSubdir) 27 | await fs.symlink(realSubdir, symlinkSubdir) 28 | 29 | const symlinkMatcher = new EventMatcher(fixture) 30 | const sw = await symlinkMatcher.watch(['linkdir'], {}) 31 | 32 | const realMatcher = new EventMatcher(fixture) 33 | const rw = await realMatcher.watch(['realdir'], {}) 34 | 35 | assert.strictEqual(sw.native, rw.native) 36 | 37 | await fs.writeFile(realFile, 'contents\n') 38 | 39 | await Promise.all([ 40 | until('symlink event arrives', symlinkMatcher.allEvents({ action: 'created', kind: 'file', path: symlinkFile })), 41 | until('real path event arrives', realMatcher.allEvents({ action: 'created', kind: 'file', path: realFile })) 42 | ]) 43 | }) 44 | 45 | it("doesn't die horribly on a symlink loop", async function () { 46 | const parentDir = fixture.watchPath('parent') 47 | const subDir = fixture.watchPath('parent', 'child') 48 | const linkName = fixture.watchPath('parent', 'child', 'turtles') 49 | const fileName = fixture.watchPath('parent', 'file.txt') 50 | 51 | await fs.mkdirs(subDir) 52 | await fs.symlink(parentDir, linkName) 53 | 54 | const matcher = new EventMatcher(fixture) 55 | await matcher.watch([''], {}) 56 | 57 | await fs.writeFile(fileName, 'contents\n') 58 | 59 | await until('creation event arrives', matcher.allEvents( 60 | { action: 'created', kind: 'file', path: fileName } 61 | )) 62 | }) 63 | 64 | it("doesn't die horribly on a broken symlink", async function () { 65 | const targetName = fixture.watchPath('target.txt') 66 | const linkName = fixture.watchPath('symlink.txt') 67 | const fileName = fixture.watchPath('file.txt') 68 | 69 | await fs.writeFile(targetName, 'original\n') 70 | await fs.symlink(targetName, linkName) 71 | await fs.unlink(targetName) 72 | 73 | const matcher = new EventMatcher(fixture) 74 | await matcher.watch([''], {}) 75 | 76 | await fs.writeFile(fileName, 'stuff\n') 77 | 78 | await until('creation event arrives', matcher.allEvents( 79 | { action: 'created', kind: 'file', path: fileName } 80 | )) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /test/events/unicode-paths.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | 3 | const { Fixture } = require('../helper') 4 | const { EventMatcher } = require('../matcher') 5 | 6 | describe('paths with extended utf8 characters', function () { 7 | let fixture 8 | 9 | beforeEach(async function () { 10 | fixture = new Fixture() 11 | await fixture.before() 12 | await fixture.log() 13 | }) 14 | 15 | afterEach(async function () { 16 | await fixture.after(this.currentTest) 17 | }) 18 | 19 | it('creates watches and reports event paths', async function () { 20 | // Thanks, http://www.i18nguy.com/unicode/supplementary-test.html 21 | // I sure hope these don't mean anything really obscene! 22 | const rootDir = fixture.watchPath('𠜎') 23 | const fileName = fixture.watchPath('𠜎', '𤓓') 24 | 25 | await fs.mkdirs(rootDir) 26 | 27 | const matcher = new EventMatcher(fixture) 28 | await matcher.watch(['𠜎'], {}) 29 | 30 | await fs.writeFile(fileName, 'wat\n') 31 | 32 | await until('creation event arrives', matcher.allEvents( 33 | { action: 'created', kind: 'file', path: fileName } 34 | )) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /test/events/unpaired-rename.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | 3 | const { Fixture } = require('../helper') 4 | const { EventMatcher } = require('../matcher'); 5 | 6 | [false, true].forEach(poll => { 7 | describe(`unpaired rename events with poll = ${poll}`, function () { 8 | let fixture, matcher 9 | 10 | beforeEach(async function () { 11 | fixture = new Fixture() 12 | await fixture.before() 13 | await fixture.log() 14 | 15 | matcher = new EventMatcher(fixture) 16 | await matcher.watch([], { poll }) 17 | }) 18 | 19 | afterEach(async function () { 20 | await fixture.after(this.currentTest) 21 | }) 22 | 23 | it('when a file is renamed from outside of the watch root in', async function () { 24 | const outsideFile = fixture.fixturePath('file.txt') 25 | const insideFile = fixture.watchPath('file.txt') 26 | 27 | await fs.writeFile(outsideFile, 'contents') 28 | await fs.rename(outsideFile, insideFile) 29 | 30 | await until('the creation event arrives', matcher.allEvents( 31 | { action: 'created', kind: 'file', path: insideFile } 32 | )) 33 | }) 34 | 35 | it('when a file is renamed from inside of the watch root out', async function () { 36 | const outsideFile = fixture.fixturePath('file.txt') 37 | const insideFile = fixture.watchPath('file.txt') 38 | 39 | await fs.writeFile(insideFile, 'contents') 40 | await until('the creation event arrives', matcher.allEvents( 41 | { action: 'created', kind: 'file', path: insideFile } 42 | )) 43 | 44 | await fs.rename(insideFile, outsideFile) 45 | await until('the deletion event arrives', matcher.allEvents( 46 | { action: 'deleted', kind: 'file', path: insideFile } 47 | )) 48 | }) 49 | 50 | it('when a file is renamed out of, then back into, the watch root', async function () { 51 | const outsideFile = fixture.fixturePath('file.txt') 52 | const insideFile = fixture.watchPath('file.txt') 53 | 54 | await fs.writeFile(insideFile, 'contents') 55 | 56 | await until('the creation event arrives', matcher.allEvents( 57 | { action: 'created', kind: 'file', path: insideFile } 58 | )) 59 | matcher.reset() 60 | 61 | await fs.rename(insideFile, outsideFile) 62 | await until('the deletion event arrives', matcher.allEvents( 63 | { action: 'deleted', kind: 'file', path: insideFile } 64 | )) 65 | matcher.reset() 66 | 67 | await fs.rename(outsideFile, insideFile) 68 | await until('the re-creation event arrives', matcher.allEvents( 69 | { action: 'created', kind: 'file', path: insideFile } 70 | )) 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /test/fixture/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /test/global.js: -------------------------------------------------------------------------------- 1 | // Global Mocha helpers 2 | 3 | const chai = require('chai') 4 | const chaiAsPromised = require('chai-as-promised') 5 | 6 | chai.use(chaiAsPromised) 7 | 8 | global.assert = chai.assert 9 | 10 | global.until = require('test-until') 11 | 12 | if (process.env.APPVEYOR === 'True') { 13 | until.setDefaultTimeout(20000) 14 | } 15 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | // Shared helper functions 2 | 3 | const path = require('path') 4 | const fs = require('fs-extra') 5 | const { CompositeDisposable } = require('event-kit') 6 | 7 | const { watchPath, configure, DISABLE } = require('../lib') 8 | 9 | class Fixture { 10 | constructor () { 11 | this.watchers = [] 12 | this.subs = new CompositeDisposable() 13 | this.createWatcher = watchPath 14 | } 15 | 16 | async before () { 17 | const rootDir = path.join(__dirname, 'fixture') 18 | this.fixtureDir = await fs.mkdtemp(path.join(rootDir, 'watched-')) 19 | this.watchDir = path.join(this.fixtureDir, 'root') 20 | 21 | this.mainLogFile = path.join(this.fixtureDir, 'logs', 'main.test.log') 22 | this.workerLogFile = path.join(this.fixtureDir, 'logs', 'worker.test.log') 23 | this.pollingLogFile = path.join(this.fixtureDir, 'logs', 'polling.test.log') 24 | 25 | await Promise.all([ 26 | fs.mkdirs(this.watchDir), 27 | fs.mkdirs(path.join(this.fixtureDir, 'logs')) 28 | ]) 29 | return Promise.all([ 30 | [this.mainLogFile, this.workerLogFile, this.pollingLogFile].map(fname => { 31 | fs.unlink(fname, { encoding: 'utf8' }).catch(() => '') 32 | }) 33 | ]) 34 | } 35 | 36 | log () { 37 | return configure({ 38 | mainLog: this.mainLogFile, 39 | workerLog: this.workerLogFile, 40 | pollingLog: this.pollingLogFile 41 | }) 42 | } 43 | 44 | fixturePath (...subPath) { 45 | return path.join(this.fixtureDir, ...subPath) 46 | } 47 | 48 | watchPath (...subPath) { 49 | return path.join(this.watchDir, ...subPath) 50 | } 51 | 52 | async watch (subPath, options, callback) { 53 | const watchRoot = this.watchPath(...subPath) 54 | const watcher = await watchPath(watchRoot, options, events => callback(null, events)) 55 | this.subs.add(watcher.onDidError(err => callback(err))) 56 | 57 | this.watchers.push(watcher) 58 | return watcher 59 | } 60 | 61 | async after (currentTest) { 62 | this.subs.dispose() 63 | this.subs = new CompositeDisposable() 64 | 65 | const natives = new Set(this.watchers.map(watcher => watcher.getNativeWatcher()).filter(Boolean)) 66 | await Promise.all(Array.from(natives, native => native.stop(false))) 67 | 68 | if (process.platform === 'win32') { 69 | await configure({ mainLog: DISABLE, workerLog: DISABLE, pollingLog: DISABLE }) 70 | } 71 | 72 | if (currentTest.state === 'failed' || process.env.VERBOSE) { 73 | const [mainLog, workerLog, pollingLog] = await Promise.all( 74 | [this.mainLogFile, this.workerLogFile, this.pollingLogFile].map(fname => { 75 | return fs.readFile(fname, { encoding: 'utf8' }).catch(() => '') 76 | }) 77 | ) 78 | 79 | console.log(`>>> main log ${this.mainLogFile}:\n${mainLog}\n<<<\n`) 80 | console.log(`>>> worker log ${this.workerLogFile}:\n${workerLog}\n<<<\n`) 81 | console.log(`>>> polling log ${this.pollingLogFile}:\n${pollingLog}\n<<<\n`) 82 | } 83 | 84 | await fs.remove(this.fixtureDir, { maxBusyTries: 1 }) 85 | .catch(err => console.warn('Unable to delete fixture directory', err)) 86 | } 87 | } 88 | 89 | module.exports = { Fixture } 90 | -------------------------------------------------------------------------------- /test/matcher.js: -------------------------------------------------------------------------------- 1 | function specMatches (spec, event) { 2 | return (spec.action === undefined || event.action === spec.action) && 3 | (spec.kind === undefined || event.kind === spec.kind) && 4 | (spec.path === undefined || event.path === spec.path) && 5 | (spec.oldPath === undefined || event.oldPath === spec.oldPath) 6 | } 7 | 8 | class EventMatcher { 9 | constructor (fixture) { 10 | this.fixture = fixture 11 | 12 | this.reset() 13 | } 14 | 15 | watch (...args) { 16 | return this.fixture.watch(...args, (err, events) => { 17 | this.errors.push(err) 18 | this.events.push(...events) 19 | 20 | if (process.env.VERBOSE) { 21 | console.log(events) 22 | } 23 | }) 24 | } 25 | 26 | allEvents (...specs) { 27 | const remaining = new Set(specs) 28 | 29 | return () => { 30 | for (const event of this.events) { 31 | for (const spec of remaining) { 32 | if (specMatches(spec, event)) { 33 | remaining.delete(spec) 34 | } 35 | } 36 | } 37 | 38 | return remaining.size === 0 39 | } 40 | } 41 | 42 | orderedEvents (...specs) { 43 | return () => { 44 | let specIndex = 0 45 | 46 | for (const event of this.events) { 47 | if (specs[specIndex] && specMatches(specs[specIndex], event)) { 48 | specIndex++ 49 | } 50 | } 51 | 52 | return specIndex >= specs.length 53 | } 54 | } 55 | 56 | noEvents (...specs) { 57 | return this.events.every(event => { 58 | return specs.every(spec => !specMatches(spec, event)) 59 | }) 60 | } 61 | 62 | reset () { 63 | this.errors = [] 64 | this.events = [] 65 | } 66 | } 67 | 68 | module.exports = { EventMatcher } 69 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require test/global.js 2 | --require mocha-stress 3 | --recursive 4 | --harmony 5 | --exit 6 | -------------------------------------------------------------------------------- /test/polling.test.js: -------------------------------------------------------------------------------- 1 | const { status } = require('../lib/binding') 2 | const { Fixture } = require('./helper') 3 | 4 | describe('polling', function () { 5 | let fixture 6 | 7 | beforeEach(async function () { 8 | fixture = new Fixture() 9 | await fixture.before() 10 | await fixture.log() 11 | }) 12 | 13 | afterEach(async function () { 14 | await fixture.after(this.currentTest) 15 | }) 16 | 17 | describe('thread state', function () { 18 | it('does not run the polling thread while no paths are being polled', async function () { 19 | const s = await status() 20 | assert.equal(s.pollingThreadState, 'stopped') 21 | }) 22 | 23 | it('runs the polling thread when polling a directory for changes', async function () { 24 | const watcher = await fixture.watch([], { poll: true }, () => {}) 25 | const s = await status() 26 | assert.equal(s.pollingThreadState, 'running') 27 | 28 | await watcher.getNativeWatcher().stop(false) 29 | await until(async () => (await status()).pollingThreadState === 'stopped') 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/unwatching.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const { Fixture } = require('./helper') 3 | 4 | describe('unwatching a directory', function () { 5 | let fixture 6 | 7 | beforeEach(async function () { 8 | fixture = new Fixture() 9 | await fixture.before() 10 | await fixture.log() 11 | }) 12 | 13 | afterEach(async function () { 14 | await fixture.after(this.currentTest) 15 | }) 16 | 17 | it('unwatches a previously watched directory', async function () { 18 | let error = null 19 | const events = [] 20 | 21 | const watcher = await fixture.watch([], {}, (err, es) => { 22 | error = err 23 | events.push(...es) 24 | }) 25 | 26 | const filePath = fixture.watchPath('file.txt') 27 | await fs.writeFile(filePath, 'original') 28 | 29 | await until('the event arrives', () => events.some(event => event.path === filePath)) 30 | assert.isNull(error) 31 | 32 | await watcher.getNativeWatcher().stop(false) 33 | const eventCount = events.length 34 | 35 | await fs.writeFile(filePath, 'the modification') 36 | 37 | // Give the modification event a chance to arrive. 38 | // Not perfect, but adequate. 39 | await new Promise(resolve => setTimeout(resolve, 100)) 40 | 41 | assert.lengthOf(events, eventCount) 42 | }) 43 | 44 | it('is a no-op if the directory is not being watched', async function () { 45 | let error = null 46 | const watcher = await fixture.watch([], {}, err => (error = err)) 47 | assert.isNull(error) 48 | 49 | const native = watcher.getNativeWatcher() 50 | await native.stop(false) 51 | assert.isNull(error) 52 | assert.isNull(watcher.getNativeWatcher()) 53 | 54 | await native.stop(false) 55 | assert.isNull(error) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /test/watching.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const { Fixture } = require('./helper') 3 | const { EventMatcher } = require('./matcher') 4 | 5 | describe('watching a directory', function () { 6 | let fixture 7 | 8 | beforeEach(async function () { 9 | fixture = new Fixture() 10 | await fixture.before() 11 | await fixture.log() 12 | }) 13 | 14 | afterEach(async function () { 15 | await fixture.after(this.currentTest) 16 | }) 17 | 18 | it('begins receiving events within that directory', async function () { 19 | const matcher = new EventMatcher(fixture) 20 | await matcher.watch([], {}) 21 | 22 | const filePath = fixture.watchPath('file.txt') 23 | await fs.writeFile(filePath, 'indeed') 24 | 25 | await until('an event arrives', matcher.allEvents({ path: filePath })) 26 | }) 27 | 28 | it('can watch multiple directories at once and dispatch events appropriately', async function () { 29 | const watchDirA = fixture.watchPath('dir_a') 30 | const watchDirB = fixture.watchPath('dir_b') 31 | await Promise.all( 32 | [watchDirA, watchDirB].map(subdir => fs.mkdir(subdir)) 33 | ) 34 | 35 | const matcherA = new EventMatcher(fixture) 36 | await matcherA.watch(['dir_a'], {}) 37 | 38 | const matcherB = new EventMatcher(fixture) 39 | await matcherB.watch(['dir_b'], {}) 40 | 41 | const fileA = fixture.watchPath('dir_a', 'a.txt') 42 | await fs.writeFile(fileA, 'file a') 43 | await until('watcher A picks up its event', matcherA.allEvents({ path: fileA })) 44 | 45 | const fileB = fixture.watchPath('dir_b', 'b.txt') 46 | await fs.writeFile(fileB, 'file b') 47 | await until('watcher B picks up its event', matcherB.allEvents({ path: fileB })) 48 | 49 | // Assert that the streams weren't crossed 50 | assert.isTrue(matcherA.noEvents({ path: fileB })) 51 | assert.isTrue(matcherB.noEvents({ path: fileA })) 52 | }) 53 | 54 | it('watches subdirectories recursively', async function () { 55 | const subdir0 = fixture.watchPath('subdir0') 56 | const subdir1 = fixture.watchPath('subdir1') 57 | await Promise.all( 58 | [subdir0, subdir1].map(subdir => fs.mkdir(subdir)) 59 | ) 60 | 61 | const matcher = new EventMatcher(fixture) 62 | await matcher.watch([], {}) 63 | 64 | const rootFile = fixture.watchPath('root.txt') 65 | await fs.writeFile(rootFile, 'root') 66 | 67 | const file0 = fixture.watchPath('subdir0', '0.txt') 68 | await fs.writeFile(file0, 'file 0') 69 | 70 | const file1 = fixture.watchPath('subdir1', '1.txt') 71 | await fs.writeFile(file1, 'file 1') 72 | 73 | await until('all three events arrive', matcher.allEvents( 74 | { path: rootFile }, 75 | { path: file0 }, 76 | { path: file1 } 77 | )) 78 | }) 79 | 80 | it('watches newly created subdirectories', async function () { 81 | const matcher = new EventMatcher(fixture) 82 | await matcher.watch([], {}) 83 | 84 | const subdir = fixture.watchPath('subdir') 85 | const file0 = fixture.watchPath('subdir', 'file-0.txt') 86 | 87 | await fs.mkdir(subdir) 88 | await until('the subdirectory creation event arrives', matcher.allEvents({ path: subdir })) 89 | 90 | await fs.writeFile(file0, 'file 0') 91 | await until('the modification event arrives', matcher.allEvents({ path: file0 })) 92 | }) 93 | 94 | it('watches directories renamed within a watch root', async function () { 95 | const externalDir = fixture.fixturePath('outside') 96 | const externalSubdir = fixture.fixturePath('outside', 'directory') 97 | const externalFile = fixture.fixturePath('outside', 'directory', 'file.txt') 98 | 99 | const internalDir = fixture.watchPath('inside') 100 | const internalFile = fixture.watchPath('inside', 'directory', 'file.txt') 101 | 102 | await fs.mkdirs(externalSubdir) 103 | await fs.writeFile(externalFile, 'contents') 104 | 105 | const matcher = new EventMatcher(fixture) 106 | await matcher.watch([], {}) 107 | 108 | await fs.rename(externalDir, internalDir) 109 | await until('creation event arrives', matcher.allEvents({ path: internalDir })) 110 | 111 | await fs.writeFile(internalFile, 'changed') 112 | 113 | await until('modification event arrives', matcher.allEvents({ path: internalFile })) 114 | }) 115 | 116 | it('can watch a directory nested within an already-watched directory', async function () { 117 | const rootFile = fixture.watchPath('root-file.txt') 118 | const subDir = fixture.watchPath('subdir') 119 | const subFile = fixture.watchPath('subdir', 'sub-file.txt') 120 | 121 | await fs.mkdir(subDir) 122 | await fs.writeFile(rootFile, 'root\n') 123 | await fs.writeFile(subFile, 'sub\n') 124 | 125 | const parent = new EventMatcher(fixture) 126 | await parent.watch([], {}) 127 | 128 | const child = new EventMatcher(fixture) 129 | const w = await child.watch(['subdir'], {}) 130 | 131 | await fs.appendFile(rootFile, 'change 0\n') 132 | await fs.appendFile(subFile, 'change 0\n') 133 | 134 | await until('parent events arrive', parent.allEvents( 135 | { path: rootFile }, 136 | { path: subFile } 137 | )) 138 | await until('child events arrive', parent.allEvents( 139 | { path: subFile } 140 | )) 141 | 142 | w.dispose() 143 | parent.reset() 144 | 145 | await fs.appendFile(rootFile, 'change 1\n') 146 | await fs.appendFile(subFile, 'change 1\n') 147 | 148 | await until('parent events arrive', parent.allEvents( 149 | { path: rootFile }, 150 | { path: subFile } 151 | )) 152 | }) 153 | }) 154 | --------------------------------------------------------------------------------