@Override
    public void onTextAvailable(ProcessEvent event, Key outputType) {
      if (outputType == ProcessOutputTypes.STDERR) {
        LOG.warn(event.getText().trim());
      }
      if (outputType != ProcessOutputTypes.STDOUT) {
        return;
      }

      final String line = event.getText().trim();
      if (LOG.isDebugEnabled()) {
        LOG.debug(">> " + line);
      }

      if (myLastOp == null) {
        final WatcherOp watcherOp;
        try {
          watcherOp = WatcherOp.valueOf(line);
        } catch (IllegalArgumentException e) {
          LOG.error("Illegal watcher command: " + line);
          return;
        }

        if (watcherOp == WatcherOp.GIVEUP) {
          notifyOnFailure(ApplicationBundle.message("watcher.gave.up"), null);
          myIsShuttingDown = true;
        } else if (watcherOp == WatcherOp.RESET) {
          reset();
        } else {
          myLastOp = watcherOp;
        }
      } else if (myLastOp == WatcherOp.MESSAGE) {
        notifyOnFailure(line, NotificationListener.URL_OPENING_LISTENER);
        myLastOp = null;
      } else if (myLastOp == WatcherOp.REMAP || myLastOp == WatcherOp.UNWATCHEABLE) {
        if ("#".equals(line)) {
          if (myLastOp == WatcherOp.REMAP) {
            processRemap();
          } else {
            mySettingRoots.decrementAndGet();
            processUnwatchable();
          }
          myLines.clear();
          myLastOp = null;
        } else {
          myLines.add(line);
        }
      } else {
        String path = line.replace('\0', '\n'); // unescape
        processChange(path, myLastOp);
        myLastOp = null;
      }
    }
    @Override
    public void processTerminated(ProcessEvent event) {
      LOG.warn("Watcher terminated with exit code " + event.getExitCode());

      myProcessHandler = null;

      try {
        startupProcess(true);
      } catch (IOException e) {
        shutdownProcess();
        LOG.warn(
            "Watcher terminated and attempt to restart has failed. Exiting watching thread.", e);
      }
    }
    private void processChange(String path, WatcherOp op) {
      if (SystemInfo.isWindows
          && op == WatcherOp.RECDIRTY
          && path.length() == 3
          && Character.isLetter(path.charAt(0))) {
        VirtualFile root = LocalFileSystem.getInstance().findFileByPath(path);
        if (root != null) {
          myNotificationSink.notifyPathsRecursive(list(root.getPresentableUrl()));
        }
        notifyOnAnyEvent();
        return;
      }

      if (op == WatcherOp.CHANGE) {
        // collapse subsequent change file change notifications that happen once we copy large file,
        // this allows reduction of path checks at least 20% for Windows
        synchronized (myLock) {
          for (int i = 0; i < myLastChangedPaths.length; ++i) {
            int last = myLastChangedPathIndex - i - 1;
            if (last < 0) last += myLastChangedPaths.length;
            String lastChangedPath = myLastChangedPaths[last];
            if (lastChangedPath != null && lastChangedPath.equals(path)) {
              return;
            }
          }
          myLastChangedPaths[myLastChangedPathIndex++] = path;
          if (myLastChangedPathIndex == myLastChangedPaths.length) myLastChangedPathIndex = 0;
        }
      }

      int length = path.length();
      if (length > 1 && path.charAt(length - 1) == '/') path = path.substring(0, length - 1);
      boolean exactPath = op != WatcherOp.DIRTY && op != WatcherOp.RECDIRTY;
      Collection<String> paths = checkWatchable(path, exactPath, false);

      if (paths.isEmpty()) {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Not watchable, filtered: " + path);
        }
        return;
      }

      switch (op) {
        case STATS:
        case CHANGE:
          myNotificationSink.notifyDirtyPaths(paths);
          break;

        case CREATE:
        case DELETE:
          myNotificationSink.notifyPathsCreatedOrDeleted(paths);
          break;

        case DIRTY:
          myNotificationSink.notifyDirtyDirectories(paths);
          break;

        case RECDIRTY:
          myNotificationSink.notifyPathsRecursive(paths);
          break;

        default:
          LOG.error("Unexpected op: " + op);
      }

      notifyOnAnyEvent();
    }