"ERRnomem"
"ERRnosuchshare"
;; See /usr/include/samba-4.0/core/ntstatus.h.
- ;; Windows 4.0 (Windows NT), Windows 5.0 (Windows 2000),
- ;; Windows 5.1 (Windows XP), Windows 5.2 (Windows Server 2003),
- ;; Windows 6.0 (Windows Vista), Windows 6.1 (Windows 7),
- ;; Windows 6.3 (Windows Server 2012, Windows 10).
+ ;; <https://learn.microsoft.com/en-us/windows/win32/sysinfo/operating-system-version>
+ ;; Tested with Windows NT, Windows 2000, Windows XP, Windows
+ ;; Server 2003, Windows Vista, Windows 7, Windows Server 2012,
+ ;; Windows 10, Windows 11.
"NT_STATUS_ACCESS_DENIED"
"NT_STATUS_ACCOUNT_LOCKED_OUT"
"NT_STATUS_BAD_NETWORK_NAME"
(file-name-nondirectory . tramp-handle-file-name-nondirectory)
;; `file-name-sans-versions' performed by default handler.
(file-newer-than-file-p . tramp-handle-file-newer-than-file-p)
- (file-notify-add-watch . tramp-handle-file-notify-add-watch)
+ (file-notify-add-watch . tramp-smb-handle-file-notify-add-watch)
(file-notify-rm-watch . tramp-handle-file-notify-rm-watch)
(file-notify-valid-p . tramp-handle-file-notify-valid-p)
(file-ownership-preserved-p . ignore)
(tramp-error v 'file-error "%s `%s'" (match-string 0) directory)))
;; "rmdir" does not report an error. So we check ourselves.
- (when (file-exists-p directory)
- (tramp-error v 'file-error "`%s' not removed" directory)))))
+ ;; Deletion of a watched directory could be pending.
+ (when (and (not (tramp-directory-watched directory))
+ (file-exists-p directory))
+ (tramp-error v 'file-error "`%s' not removed" directory)))))
(defun tramp-smb-handle-delete-file (filename &optional trash)
"Like `delete-file' for Tramp files."
(tramp-error
v 'file-error "Cannot make local copy of file `%s'" filename)))))
+;; The "notify" command has been added to smbclient 4.3.0.
+(defun tramp-smb-handle-file-notify-add-watch (file-name flags _callback)
+ "Like `file-notify-add-watch' for Tramp files."
+ (setq file-name (expand-file-name file-name))
+ (with-parsed-tramp-file-name file-name nil
+ (let ((default-directory (file-name-directory file-name))
+ (command (format "notify %s" (tramp-smb-shell-quote-localname v)))
+ (events
+ (cond
+ ((memq 'change flags)
+ '(added removed modified renamed-from renamed-to))
+ ((memq 'attribute-change flags) '(modified))))
+ p)
+ ;; Start process.
+ (with-tramp-saved-connection-properties
+ v '(" process-name" " process-buffer")
+ ;; Set the new process properties.
+ (tramp-set-connection-property
+ v " process-name" (tramp-get-unique-process-name "smb-notify"))
+ (tramp-set-connection-property
+ v " process-buffer" (generate-new-buffer " *smb-notify*"))
+ (tramp-flush-connection-property v " process-exit-status")
+ (tramp-smb-send-command v command 'nooutput)
+ (setq p (tramp-get-connection-process v))
+ ;; Return the process object as watch-descriptor.
+ (if (not (processp p))
+ (tramp-error
+ v 'file-notify-error
+ "`%s' failed to start on remote host" command)
+ ;; Needed for process filter.
+ (process-put p 'tramp-events events)
+ (process-put p 'tramp-watch-name localname)
+ (set-process-filter p #'tramp-smb-notify-process-filter)
+ (set-process-sentinel p #'tramp-file-notify-process-sentinel)
+ (tramp-post-process-creation p v)
+ ;; There might be an error if the monitor is not supported.
+ ;; Give the filter a chance to read the output.
+ (while (tramp-accept-process-output p))
+ (unless (process-live-p p)
+ (tramp-error
+ p 'file-notify-error "Monitoring not supported for `%s'" file-name))
+ ;; Set "file-monitor" property. The existence of the "ADMIN$"
+ ;; share is an indication for a remote MS Windows host.
+ (tramp-set-connection-property
+ p "file-monitor"
+ (if (member
+ "ADMIN$" (directory-files (tramp-make-tramp-file-name v "/")))
+ 'SMBWindows 'SMBSamba))
+ p)))))
+
+;; FileChangeNotify subsystem was added to Smaba 4.3.0.
+;; <https://www.samba.org/samba/history/samba-4.3.0.html>
+(defun tramp-smb-notify-process-filter (proc string)
+ "Read output from \"notify\" and add corresponding `file-notify' events."
+ (let ((events (process-get proc 'tramp-events)))
+ (tramp-message proc 6 "%S\n%s" proc string)
+ (dolist (line (split-string string (rx (+ (any "\r\n"))) 'omit))
+ (catch 'next
+ ;; Watched directory is removed.
+ (when (string-match-p "NT_STATUS_DELETE_PENDING" line)
+ (setq line (concat "0002 " (process-get proc 'tramp-watch-name))))
+ ;; Stopped.
+ (when (string-match-p tramp-smb-prompt line)
+ (throw 'next 'next))
+
+ ;; Check, whether there is a problem.
+ (unless (string-match
+ (rx bol (group (+ digit))
+ (+ blank) (group (+ (not (any "\r\n")))))
+ line)
+ (tramp-error proc 'file-notify-error line))
+
+ ;; See libsmbclient.h.
+ ;; #define SMBC_NOTIFY_ACTION_ADDED 1
+ ;; #define SMBC_NOTIFY_ACTION_REMOVED 2
+ ;; #define SMBC_NOTIFY_ACTION_MODIFIED 3
+ ;; #define SMBC_NOTIFY_ACTION_OLD_NAME 4
+ ;; #define SMBC_NOTIFY_ACTION_NEW_NAME 5
+ ;; #define SMBC_NOTIFY_ACTION_ADDED_STREAM 6
+ ;; #define SMBC_NOTIFY_ACTION_REMOVED_STREAM 7
+ ;; #define SMBC_NOTIFY_ACTION_MODIFIED_STREAM 8
+ (let ((object
+ (list
+ proc
+ (pcase (string-to-number (match-string 1 line))
+ (1 '(added))
+ (2 '(removed))
+ (3 '(modified))
+ (4 '(renamed-from))
+ (5 '(renamed-to))
+ ;; Ignore stream events.
+ (_ (throw 'next 'next)))
+ (string-replace "\\" "/" (match-string 2 line)))))
+ ;; Add an Emacs event now.
+ ;; `insert-special-event' exists since Emacs 31.
+ (when (member (caadr object) events)
+ (tramp-compat-funcall
+ (if (fboundp 'insert-special-event)
+ 'insert-special-event
+ (lookup-key special-event-map [file-notify]))
+ `(file-notify ,object file-notify-callback))))))))
+
;; This function should return "foo/" for directories and "bar" for
;; files.
(defun tramp-smb-handle-file-name-all-completions (filename directory)
;; Connection functions.
-(defun tramp-smb-send-command (vec command)
+(defun tramp-smb-send-command (vec command &optional nooutput)
"Send the COMMAND to connection VEC.
-Returns nil if there has been an error message from smbclient."
+Returns nil if there has been an error message from smbclient. The
+function waits for output unless NOOUTPUT is set."
(tramp-smb-maybe-open-connection vec)
(tramp-message vec 6 "%s" command)
(tramp-send-string vec command)
- (tramp-smb-wait-for-output vec))
+ (unless nooutput (tramp-smb-wait-for-output vec)))
(defun tramp-smb-maybe-open-connection (vec &optional argument)
"Maybe open a connection to HOST, log in as USER, using `tramp-smb-program'.
(while (not (search-forward-regexp tramp-smb-prompt nil t))
(while (tramp-accept-process-output p))
(goto-char (point-min)))
- (tramp-message vec 6 "\n%s" (buffer-string))
+ (tramp-message vec 6 "%S\n%s" p (buffer-string))
;; Remove prompt.
(goto-char (point-min))
;;
;; * Keep a permanent connection process for `process-file'.
+;; * Implement "scopy" (since Samba 4.3.0).
+
;;; tramp-smb.el ends here
;; of a respective command. The first command found is used. In
;; order to use a dedicated one, the environment variable
;; $REMOTE_FILE_NOTIFY_LIBRARY shall be set, possible values are
-;; "inotifywait", "gio-monitor" and "gvfs-monitor-dir".
+;; "inotifywait", "gio-monitor", "gvfs-monitor-dir", and "smb-notify".
;; Local file-notify libraries are auto-detected during Emacs
;; configuration. This can be changed with a respective configuration
;; Filter suppressed remote file-notify libraries.
(when (stringp (getenv "REMOTE_FILE_NOTIFY_LIBRARY"))
- (dolist (lib '("inotifywait" "gio-monitor" "gvfs-monitor-dir"))
+ (dolist (lib '("inotifywait" "gio-monitor" "gvfs-monitor-dir" "smb-notify"))
(unless (string-equal (getenv "REMOTE_FILE_NOTIFY_LIBRARY") lib)
(add-to-list 'tramp-connection-properties `(nil ,lib nil)))))
TIMEOUT is the maximum time to wait for, in seconds."
`(with-timeout (,timeout (ignore))
(while (null ,until)
+ (when file-notify-debug
+ (message "file-notify--test-wait-for-events received: %s"
+ (file-notify--test-event-actions)))
(file-notify--test-wait-event))))
(defun file-notify--test-no-descriptors ()
(ignore-errors
(when (file-remote-p temporary-file-directory)
(tramp-cleanup-connection
- (tramp-dissect-file-name temporary-file-directory) nil 'keep-password)))
+ (tramp-dissect-file-name temporary-file-directory) t 'keep-password)))
(when (hash-table-p file-notify-descriptors)
(clrhash file-notify-descriptors))
file-notify--test-events nil
file-notify--test-monitors nil))
-(setq file-notify-debug nil
+(setq auth-source-cache-expiry nil
+ auth-source-save-behavior nil
+ file-notify-debug nil
password-cache-expiry nil
- ;; tramp-verbose (if (getenv "EMACS_EMBA_CI") 10 0)
+ remote-file-name-inhibit-cache nil
+ tramp-allow-unsafe-temporary-files t
+ tramp-cache-read-persistent-data t ;; For auth-sources.
tramp-verbose 0
;; When the remote user id is 0, Tramp refuses unsafe temporary files.
tramp-allow-unsafe-temporary-files
;; We cache the result, because after `file-notify-rm-watch',
;; `gfile-monitor-name' does not return a proper result anymore.
;; But we still need this information. So far, we know the monitors
- ;; GFamFileMonitor (gfilenotify on cygwin), GFamDirectoryMonitor
- ;; (gfilenotify on Solaris), GInotifyFileMonitor (gfilenotify and
- ;; gio on GNU/Linux), GKqueueFileMonitor (gfilenotify and gio on
- ;; FreeBSD) and GPollFileMonitor (gio on cygwin).
+ ;; - GFamFileMonitor (gfilenotify on cygwin)
+ ;; - GFamDirectoryMonitor (gfilenotify on Solaris)
+ ;; - GInotifyFileMonitor (gfilenotify and gio on GNU/Linux)
+ ;; - GKqueueFileMonitor (gfilenotify and gio on FreeBSD)
+ ;; - GPollFileMonitor (gio on cygwin)
+ ;; - SMBSamba (smb-notify on Samba server)
+ ;; - SMBWindows (smb-notify on MS Windows).
(when file-notify--test-desc
(or (alist-get file-notify--test-desc file-notify--test-monitors)
- (when (member (file-notify--test-library) '("gfilenotify" "gio"))
+ (when (member
+ (file-notify--test-library) '("gfilenotify" "gio" "smb-notify"))
(add-to-list
'file-notify--test-monitors
(cons file-notify--test-desc
;; `file-notify--test-desc' is the connection process.
(progn
(while (not (tramp-connection-property-p
- file-notify--test-desc "gio-file-monitor"))
+ file-notify--test-desc "file-monitor"))
(accept-process-output file-notify--test-desc 0))
(tramp-get-connection-property
- file-notify--test-desc "gio-file-monitor" nil))
+ file-notify--test-desc "file-monitor" nil))
(and (functionp 'gfile-monitor-name)
(gfile-monitor-name file-notify--test-desc)))))
;; If we don't know the monitor, there are good chances the
;; Needs further investigation.
(skip-when (string-equal (file-notify--test-library) "gio"))
(tramp-cleanup-connection
- (tramp-dissect-file-name temporary-file-directory) nil 'keep-password)
+ (tramp-dissect-file-name temporary-file-directory) t 'keep-password)
+ (file-notify-rm-all-watches)
(funcall (ert-test-body ert-test)))))
(ert-deftest file-notify-test00-availability ()
(unless (stringp file-notify--test-tmpdir)
(setq file-notify--test-tmpdir
(expand-file-name
- (make-temp-name "file-notify-test") temporary-file-directory)))
+ (make-temp-name "file-notify-test-parent") temporary-file-directory)))
(unless (file-directory-p file-notify--test-tmpdir)
(make-directory file-notify--test-tmpdir))
(expand-file-name
(result
(ert-run-test (make-ert-test :body 'file-notify--test-event-test))))
;; Do not add lock files, this would confuse the checks.
- (unless (string-match
+ (unless (string-match-p
(regexp-quote ".#")
(file-notify--test-event-file file-notify--test-event))
(when file-notify-debug
(defun file-notify--test-with-actions-check (actions)
"Check whether received actions match one of the ACTIONS alternatives."
+ (when file-notify-debug
+ (message "file-notify--test-with-actions-check"))
(let (result)
(dolist (elt actions result)
(setq result
(not (input-pending-p)))
(setq file-notify--test-events nil
file-notify--test-results nil)
+ (when file-notify-debug
+ (message "file-notify--test-with-actions expected: %s" actions))
,@body
(file-notify--test-wait-for-events
;; More actions need more time. Use some fudge factor.
(* (ceiling max-length 100) (file-notify--test-timeout))
- (= max-length (length file-notify--test-events)))
+ (or (= max-length (length file-notify--test-events))
+ (memq 'stopped (file-notify--test-event-actions))))
;; Check the result sequence just to make sure that all actions
;; are as expected.
(dolist (result file-notify--test-results)
'(change) #'file-notify--test-event-handler)))
(file-notify--test-with-actions
(cond
+ ;; SMBSamba reports three `changed' events.
+ ((eq (file-notify--test-monitor) 'SMBSamba)
+ '(created changed changed changed deleted stopped))
;; GFam{File,Directory}Monitor, GKqueueFileMonitor and
;; GPollFileMonitor do not report the `changed' event.
((memq (file-notify--test-monitor)
'(change) #'file-notify--test-event-handler)))
(file-notify--test-with-actions
(cond
+ ;; SMBSamba reports four `changed' events.
+ ((eq (file-notify--test-monitor) 'SMBSamba)
+ '(changed changed changed changed deleted stopped))
;; GFam{File,Directory}Monitor and GPollFileMonitor do
;; not detect the `changed' event reliably.
((memq (file-notify--test-monitor)
;; events for the watched directory.
((string-equal (file-notify--test-library) "w32notify")
'(created changed deleted))
+ ;; SMBSamba reports three `changed' events.
+ ((eq (file-notify--test-monitor) 'SMBSamba)
+ '(created changed changed changed deleted deleted stopped))
;; On emba, `deleted' and `stopped' events of the
;; directory are not detected.
((getenv "EMACS_EMBA_CI")
'(created changed created changed
changed changed changed
deleted deleted))
+ ;; SMBSamba reports three `changed' events.
+ ((eq (file-notify--test-monitor) 'SMBSamba)
+ '(created changed changed changed created changed changed changed
+ deleted deleted deleted stopped))
;; There are three `deleted' events, for two files and
;; for the directory. Except for
;; GFam{File,Directory}Monitor, GPollFileMonitor and
;; events for the watched directory.
((string-equal (file-notify--test-library) "w32notify")
'(created changed renamed deleted))
+ ;; SMBSamba reports three `changed' events.
+ ((eq (file-notify--test-monitor) 'SMBSamba)
+ '(created changed changed changed
+ renamed changed changed deleted deleted stopped))
;; On emba, `deleted' and `stopped' events of the
;; directory are not detected.
((getenv "EMACS_EMBA_CI")
((string-equal (file-notify--test-library) "w32notify")
'((changed changed)
(changed changed changed changed)))
+ ;; SMBWindows does not distinguish between `changed' and
+ ;; `attribute-changed'.
+ ((eq (file-notify--test-monitor) 'SMBWindows)
+ '(changed changed))
+ ;; SMBSamba does not distinguish between `changed' and
+ ;; `attribute-changed'.
+ ((eq (file-notify--test-monitor) 'SMBSamba)
+ '(changed changed changed changed))
;; GFam{File,Directory}Monitor, GKqueueFileMonitor and
;; GPollFileMonitor do not report the `attribute-changed'
;; event.
(timeout (if (file-remote-p temporary-file-directory)
60 ; FIXME: can this be shortened?
(* auto-revert-interval 2.5)))
+ (text-quoting-style 'grave)
buf)
(auto-revert-set-timer)
(unwind-protect
;; Check, that the buffer has been reverted.
(file-notify--test-wait-for-events
timeout
- (string-match
- (format-message "Reverting buffer `%s'." (buffer-name buf))
+ (string-match-p
+ (rx bol "Reverting buffer `"
+ (literal (buffer-name buf)) "'" eol)
captured-messages))
- (should (string-match "another text" (buffer-string)))))
+ (should (string-match-p "another text" (buffer-string)))))
;; Stop file notification. Autorevert shall still work via polling.
(file-notify-rm-watch auto-revert-notify-watch-descriptor)
;; Check, that the buffer has been reverted.
(file-notify--test-wait-for-events
timeout
- (string-match
- (format-message "Reverting buffer `%s'." (buffer-name buf))
+ (string-match-p
+ (rx bol "Reverting buffer `"
+ (literal (buffer-name buf)) "'" eol)
captured-messages))
- (should (string-match "foo bla" (buffer-string)))))
+ (should (string-match-p "foo bla" (buffer-string)))))
;; Stop autorevert, in order to cleanup descriptor.
(auto-revert-mode -1))
(should (file-notify-valid-p file-notify--test-desc))
(file-notify--test-with-actions
(cond
+ ;; SMBSamba reports three `changed' events.
+ ((eq (file-notify--test-monitor) 'SMBSamba)
+ '(changed changed changed changed deleted stopped))
;; GFam{File,Directory}Monitor do not
;; detect the `changed' event reliably.
((memq (file-notify--test-monitor)
"another text" nil file-notify--test-tmpfile nil 'no-message)
(file-notify--test-wait-event)
(delete-file file-notify--test-tmpfile))
+ (file-notify--test-wait-event)
;; After deleting the file, the descriptor is not valid anymore.
(should-not (file-notify-valid-p file-notify--test-desc))
(file-notify-rm-watch file-notify--test-desc)
;; events for the watched directory.
((string-equal (file-notify--test-library) "w32notify")
'(created changed deleted))
+ ;; SMBSamba reports three `changed' events.
+ ((eq (file-notify--test-monitor) 'SMBSamba)
+ '(created changed changed changed deleted deleted stopped))
;; There are two `deleted' events, for the file and for
;; the directory. Except for
;; GFam{File,Directory}Monitor, GPollFileMonitor and
(push (expand-file-name (format "y%d" i)) target-file-list))
(push (expand-file-name (format "y%d" i)) source-file-list)
(push (expand-file-name (format "x%d" i)) target-file-list)))
- (file-notify--test-with-actions (make-list (+ n n) 'created)
+ (file-notify--test-with-actions
+ (cond
+ ;; SMBSamba fires both `created' and `changed' events.
+ ((eq (file-notify--test-monitor) 'SMBSamba)
+ (let (r)
+ (dotimes (_i (+ n n) r)
+ (setq r (append '(created changed) r)))))
+ (t (make-list (+ n n) 'created)))
(let ((source-file-list source-file-list)
(target-file-list target-file-list))
(while (and source-file-list target-file-list)
;; w32notify fires both `deleted' and `renamed' events.
((string-equal (file-notify--test-library) "w32notify")
(let (r)
- (dotimes (_i n)
- (setq r (append '(deleted renamed) r)))
- r))
- ;; GFam{File,Directory}Monitor and GPollFileMonitor fire
+ (dotimes (_i n r)
+ (setq r (append '(deleted renamed) r)))))
+ ;; SMBWindows fires both `changed' and `deleted' events.
+ ((eq (file-notify--test-monitor) 'SMBWindows)
+ (let (r)
+ (dotimes (_i n r)
+ (setq r (append '(changed deleted) r)))))
+ ;; SMBSamba fires both `changed' and `deleted' events.
+ ((eq (file-notify--test-monitor) 'SMBSamba)
+ (let (r)
+ (dotimes (_i n r)
+ (setq r (append '(changed changed deleted) r)))))
+ ;; GFam{File,Directory}Monitor and GPollFileMonitor fire
;; `changed' and `deleted' events, sometimes in random
;; order.
((memq (file-notify--test-monitor)
'(GFamFileMonitor GFamDirectoryMonitor GPollFileMonitor))
(let (r)
- (dotimes (_i n)
- (setq r (append '(changed deleted) r)))
- (cons :random r)))
+ (dotimes (_i n (cons :random r))
+ (setq r (append '(changed deleted) r)))))
(t (make-list n 'renamed)))
(let ((source-file-list source-file-list)
(target-file-list target-file-list))
(should (file-notify-valid-p file-notify--test-desc))
(file-notify--test-with-actions
(cond
+ ;; SMBSamba reports four `changed' events.
+ ((eq (file-notify--test-monitor) 'SMBSamba)
+ '(changed changed changed changed))
;; GKqueueFileMonitor does not report the `changed' event.
((eq (file-notify--test-monitor) 'GKqueueFileMonitor) '())
;; There could be one or two `changed' events.
(should (file-notify-valid-p file-notify--test-desc))
(file-notify--test-with-actions
(cond
+ ;; SMBWindows reports two `changed' events.
+ ((eq (file-notify--test-monitor) 'SMBWindows)
+ '(changed changed))
+ ;; SMBSamba reports four `changed' events.
+ ((eq (file-notify--test-monitor) 'SMBSamba)
+ '(changed changed changed changed))
;; GFam{File,Directory}Monitor and GPollFileMonitor
;; report only the `changed' event.
((memq (file-notify--test-monitor)
(file-notify--test-with-actions
;; There could be one or two `changed' events.
(list
- ;; cygwin.
+ ;; SMBSamba. Sometimes, tha last `changed' event is
+ ;; missing, so we add two alternatives.
+ (append
+ '(:random)
+ ;; Just the file monitor.
+ (make-list (* (/ n 2) 5) 'changed)
+ ;; Just the directory monitor. Strange, not all
+ ;; `changed' events do arrive.
+ (make-list (1- (* (/ n 2) 10)) 'changed)
+ (make-list (/ n 2) 'created)
+ (make-list (/ n 2) 'created))
+ (append
+ '(:random)
+ ;; Just the file monitor.
+ (make-list (* (/ n 2) 5) 'changed)
+ ;; Just the directory monitor. This is the alternative
+ ;; with all `changed' events.
+ (make-list (* (/ n 2) 10) 'changed)
+ (make-list (/ n 2) 'created)
+ (make-list (/ n 2) 'created))
+ ;; cygwin.
(append
'(:random)
(make-list (/ n 2) 'changed)
;; directory and the file monitor. The `stopped' event is
;; from the file monitor. It's undecided in which order the
;; directory and the file monitor are triggered.
- (file-notify--test-with-actions '(:random deleted deleted stopped)
+ (file-notify--test-with-actions
+ '((:random deleted deleted stopped)
+ (:random deleted deleted deleted stopped))
(delete-file file-notify--test-tmpfile1))
(should (file-notify-valid-p file-notify--test-desc1))
(should-not (file-notify-valid-p file-notify--test-desc2))
"Check that file notification stop after unmounting the filesystem."
:tags '(:expensive-test)
(skip-unless (file-notify--test-local-enabled))
- ;; This test does not work for w32notify.
- (skip-when (string-equal (file-notify--test-library) "w32notify"))
+ ;; This test does not work for w32notify snd smb-notify.
+ (skip-when (member (file-notify--test-library) '("w32notify" "smb-notify")))
(unwind-protect
(progn
;; the missing directory monitor.
;; * For w32notify, no `deleted' and `stopped' events arrive when a
;; directory is removed.
-;; * For cygwin and w32notify, no `attribute-changed' events arrive.
-;; They send `changed' events instead.
+;; * For cygwin, w32notify, and smb-notify, no `attribute-changed'
+;; events arrive. They send `changed' events instead.
;; * cygwin does not send all expected `changed' and `deleted' events.
;; Probably due to timing issues.