From 1f70a76c7e5720dfb39ba4436571b86967a6ac3f Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Tue, 8 Sep 2020 23:57:48 +0100 Subject: [PATCH 1/6] Desktop: Fixes #3729: Fix lock issue when device does not have the right time --- .eslintignore | 2 + .gitignore | 2 + ReactNativeClient/lib/ntpDate.ts | 46 ++++++ .../lib/services/synchronizer/LockHandler.ts | 11 +- ReactNativeClient/lib/vendor/ntp-client.js | 146 ++++++++++++++++++ 5 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 ReactNativeClient/lib/ntpDate.ts create mode 100644 ReactNativeClient/lib/vendor/ntp-client.js diff --git a/.eslintignore b/.eslintignore index 66dc77acd..819cf1f6d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -62,6 +62,7 @@ Modules/TinyMCE/langs/ # AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD CliClient/app/LinkSelector.js +CliClient/build/LinkSelector.js CliClient/tests/synchronizer_LockHandler.js CliClient/tests/synchronizer_MigrationHandler.js ElectronClient/commands/focusElement.js @@ -151,6 +152,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js ReactNativeClient/lib/JoplinServerApi.js +ReactNativeClient/lib/ntpDate.js ReactNativeClient/lib/services/CommandService.js ReactNativeClient/lib/services/keychain/KeychainService.js ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js diff --git a/.gitignore b/.gitignore index c688a2e73..99bfb5e3a 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ Tools/commit_hook.txt # AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD CliClient/app/LinkSelector.js +CliClient/build/LinkSelector.js CliClient/tests/synchronizer_LockHandler.js CliClient/tests/synchronizer_MigrationHandler.js ElectronClient/commands/focusElement.js @@ -142,6 +143,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js ReactNativeClient/lib/JoplinServerApi.js +ReactNativeClient/lib/ntpDate.js ReactNativeClient/lib/services/CommandService.js ReactNativeClient/lib/services/keychain/KeychainService.js ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js diff --git a/ReactNativeClient/lib/ntpDate.ts b/ReactNativeClient/lib/ntpDate.ts new file mode 100644 index 000000000..743a94a87 --- /dev/null +++ b/ReactNativeClient/lib/ntpDate.ts @@ -0,0 +1,46 @@ +const ntpClient = require('lib/vendor/ntp-client'); +const Mutex = require('async-mutex').Mutex; + +let lastSyncTime = 0; +let timeOffset = 0; +const fetchingTimeMutex = new Mutex(); + +const server = { + domain: 'pool.ntp.org', + port: 123, +}; + +async function networkTime():Promise { + return new Promise(function(resolve:Function, reject:Function) { + ntpClient.getNetworkTime(server.domain, server.port, function(error:any, date:Date) { + if (error) { + reject(error); + return; + } + + resolve(date); + }); + }); +} + +function shouldSyncTime() { + return !lastSyncTime || Date.now() - lastSyncTime >= 5 * 1000; +} + +export default async function():Promise { + if (shouldSyncTime()) { + const release = await fetchingTimeMutex.acquire(); + + try { + if (shouldSyncTime()) { + const date = await networkTime(); + lastSyncTime = Date.now(); + timeOffset = date.getTime() - Date.now(); + } + } finally { + release(); + } + } + + return new Date(Date.now() + timeOffset); +} diff --git a/ReactNativeClient/lib/services/synchronizer/LockHandler.ts b/ReactNativeClient/lib/services/synchronizer/LockHandler.ts index 7148f9534..62dddd7ed 100644 --- a/ReactNativeClient/lib/services/synchronizer/LockHandler.ts +++ b/ReactNativeClient/lib/services/synchronizer/LockHandler.ts @@ -1,4 +1,6 @@ import { Dirnames } from './utils/types'; +import ntpDate from 'lib/ntpDate'; + const JoplinError = require('lib/JoplinError'); const { time } = require('lib/time-utils'); const { fileExtension, filename } = require('lib/path-utils.js'); @@ -98,8 +100,8 @@ export default class LockHandler { return output; } - private lockIsActive(lock:Lock):boolean { - return Date.now() - lock.updatedTime < this.lockTtl; + private lockIsActive(lock:Lock, currentDate:Date):boolean { + return currentDate.getTime() - lock.updatedTime < this.lockTtl; } async hasActiveLock(lockType:LockType, clientType:string = null, clientId:string = null) { @@ -112,11 +114,12 @@ export default class LockHandler { // of that type instead. async activeLock(lockType:LockType, clientType:string = null, clientId:string = null) { const locks = await this.locks(lockType); + const currentDate = await ntpDate(); if (lockType === LockType.Exclusive) { const activeLocks = locks .slice() - .filter((lock:Lock) => this.lockIsActive(lock)) + .filter((lock:Lock) => this.lockIsActive(lock, currentDate)) .sort((a:Lock, b:Lock) => { if (a.updatedTime === b.updatedTime) { return a.clientId < b.clientId ? -1 : +1; @@ -134,7 +137,7 @@ export default class LockHandler { for (const lock of locks) { if (clientType && lock.clientType !== clientType) continue; if (clientId && lock.clientId !== clientId) continue; - if (this.lockIsActive(lock)) return lock; + if (this.lockIsActive(lock, currentDate)) return lock; } return null; } diff --git a/ReactNativeClient/lib/vendor/ntp-client.js b/ReactNativeClient/lib/vendor/ntp-client.js new file mode 100644 index 000000000..f921f87b4 --- /dev/null +++ b/ReactNativeClient/lib/vendor/ntp-client.js @@ -0,0 +1,146 @@ +/* + * ntp-client + * https://github.com/moonpyk/node-ntp-client + * + * Copyright (c) 2013 Clément Bourgeois + * Licensed under the MIT license. + */ + +// ---------------------------------------------------------------------------------------- +// 2020-08-09: We vendor the package because although it works +// it has several bugs and is currently unmaintained +// ---------------------------------------------------------------------------------------- + +(function (exports) { + "use strict"; + + var dgram = require('dgram'); + + exports.defaultNtpPort = 123; + exports.defaultNtpServer = "pool.ntp.org"; + + /** + * Amount of acceptable time to await for a response from the remote server. + * Configured default to 10 seconds. + */ + exports.ntpReplyTimeout = 10000; + + /** + * Fetches the current NTP Time from the given server and port. + * @param {string} server IP/Hostname of the remote NTP Server + * @param {number} port Remote NTP Server port number + * @param {function(Object, Date)} callback(err, date) Async callback for + * the result date or eventually error. + */ + exports.getNetworkTime = function (server, port, callback) { + if (callback === null || typeof callback !== "function") { + return; + } + + server = server || exports.defaultNtpServer; + port = port || exports.defaultNtpPort; + + var client = dgram.createSocket("udp4"), + ntpData = new Buffer(48); + + // RFC 2030 -> LI = 0 (no warning, 2 bits), VN = 3 (IPv4 only, 3 bits), Mode = 3 (Client Mode, 3 bits) -> 1 byte + // -> rtol(LI, 6) ^ rotl(VN, 3) ^ rotl(Mode, 0) + // -> = 0x00 ^ 0x18 ^ 0x03 + ntpData[0] = 0x1B; + + for (var i = 1; i < 48; i++) { + ntpData[i] = 0; + } + + // Some errors can happen before/after send() or cause send() to was impossible. + // Some errors will also be given to the send() callback. + // We keep a flag, therefore, to prevent multiple callbacks. + // NOTE : the error callback is not generalised, as the client has to lose the connection also, apparently. + var errorFired = false; + + function closeClient(client) { + try { + client.close(); + } catch (error) { + // Doesn't mater if it could not be closed + } + } + + var timeout = setTimeout(function () { + closeClient(client); + + if (errorFired) { + return; + } + callback(new Error("Timeout waiting for NTP response."), null); + errorFired = true; + }, exports.ntpReplyTimeout); + + client.on('error', function (err) { + clearTimeout(timeout); + + if (errorFired) { + return; + } + + callback(err, null); + errorFired = true; + }); + + client.send(ntpData, 0, ntpData.length, port, server, function (err) { + if (err) { + clearTimeout(timeout); + if (errorFired) { + return; + } + callback(err, null); + errorFired = true; + closeClient(client); + return; + } + + client.once('message', function (msg) { + clearTimeout(timeout); + closeClient(client); + + // Offset to get to the "Transmit Timestamp" field (time at which the reply + // departed the server for the client, in 64-bit timestamp format." + var offsetTransmitTime = 40, + intpart = 0, + fractpart = 0; + + // Get the seconds part + for (var i = 0; i <= 3; i++) { + intpart = 256 * intpart + msg[offsetTransmitTime + i]; + } + + // Get the seconds fraction + for (i = 4; i <= 7; i++) { + fractpart = 256 * fractpart + msg[offsetTransmitTime + i]; + } + + var milliseconds = (intpart * 1000 + (fractpart * 1000) / 0x100000000); + + // **UTC** time + var date = new Date("Jan 01 1900 GMT"); + date.setUTCMilliseconds(date.getUTCMilliseconds() + milliseconds); + + callback(null, date); + }); + }); + }; + + exports.demo = function (argv) { + exports.getNetworkTime( + exports.defaultNtpServer, + exports.defaultNtpPort, + function (err, date) { + if (err) { + console.error(err); + return; + } + + console.log(date); + }); + }; +}(exports)); \ No newline at end of file From f41ba67e15cfba180e1e296a35ab523cbefe7b4c Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Wed, 9 Sep 2020 00:34:27 +0100 Subject: [PATCH 2/6] Improved ntp time --- ReactNativeClient/lib/BaseApplication.js | 3 +++ ReactNativeClient/lib/ntpDate.ts | 11 ++++++++--- ReactNativeClient/lib/vendor/ntp-client.js | 8 +++++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/ReactNativeClient/lib/BaseApplication.js b/ReactNativeClient/lib/BaseApplication.js index abe421749..cc0bb2419 100644 --- a/ReactNativeClient/lib/BaseApplication.js +++ b/ReactNativeClient/lib/BaseApplication.js @@ -44,6 +44,9 @@ const KvStore = require('lib/services/KvStore'); const MigrationService = require('lib/services/MigrationService'); const { toSystemSlashes } = require('lib/path-utils.js'); +const ntpClient = require('lib/vendor/ntp-client'); +ntpClient.dgram = require('dgram'); + class BaseApplication { constructor() { this.logger_ = new Logger(); diff --git a/ReactNativeClient/lib/ntpDate.ts b/ReactNativeClient/lib/ntpDate.ts index 743a94a87..1e2e2fb5e 100644 --- a/ReactNativeClient/lib/ntpDate.ts +++ b/ReactNativeClient/lib/ntpDate.ts @@ -1,7 +1,7 @@ const ntpClient = require('lib/vendor/ntp-client'); const Mutex = require('async-mutex').Mutex; -let lastSyncTime = 0; +let nextSyncTime = 0; let timeOffset = 0; const fetchingTimeMutex = new Mutex(); @@ -24,7 +24,7 @@ async function networkTime():Promise { } function shouldSyncTime() { - return !lastSyncTime || Date.now() - lastSyncTime >= 5 * 1000; + return !nextSyncTime || Date.now() > nextSyncTime; } export default async function():Promise { @@ -34,9 +34,14 @@ export default async function():Promise { try { if (shouldSyncTime()) { const date = await networkTime(); - lastSyncTime = Date.now(); + nextSyncTime = Date.now() + 60 * 1000; timeOffset = date.getTime() - Date.now(); } + } catch (error) { + // Fallback to application time since + // most of the time it's actually correct + nextSyncTime = Date.now() + 20 * 1000; + timeOffset = 0; } finally { release(); } diff --git a/ReactNativeClient/lib/vendor/ntp-client.js b/ReactNativeClient/lib/vendor/ntp-client.js index f921f87b4..2a1660c38 100644 --- a/ReactNativeClient/lib/vendor/ntp-client.js +++ b/ReactNativeClient/lib/vendor/ntp-client.js @@ -14,11 +14,11 @@ (function (exports) { "use strict"; - var dgram = require('dgram'); - exports.defaultNtpPort = 123; exports.defaultNtpServer = "pool.ntp.org"; + exports.dgram = null; + /** * Amount of acceptable time to await for a response from the remote server. * Configured default to 10 seconds. @@ -40,7 +40,9 @@ server = server || exports.defaultNtpServer; port = port || exports.defaultNtpPort; - var client = dgram.createSocket("udp4"), + if (!exports.dgram) throw new Error('dgram package has not been set!!'); + + var client = exports.dgram.createSocket("udp4"), ntpData = new Buffer(48); // RFC 2030 -> LI = 0 (no warning, 2 bits), VN = 3 (IPv4 only, 3 bits), Mode = 3 (Client Mode, 3 bits) -> 1 byte From c9adccad4affe8c7572f9292c7e352b060d3ab32 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Wed, 9 Sep 2020 10:56:17 +0100 Subject: [PATCH 3/6] Get NTP time working on Android --- ReactNativeClient/lib/BaseApplication.js | 1 + ReactNativeClient/lib/ntpDate.ts | 10 +++- ReactNativeClient/lib/vendor/ntp-client.js | 69 ++++++++++++---------- ReactNativeClient/root.js | 4 ++ 4 files changed, 52 insertions(+), 32 deletions(-) diff --git a/ReactNativeClient/lib/BaseApplication.js b/ReactNativeClient/lib/BaseApplication.js index cc0bb2419..de4a9beb5 100644 --- a/ReactNativeClient/lib/BaseApplication.js +++ b/ReactNativeClient/lib/BaseApplication.js @@ -666,6 +666,7 @@ class BaseApplication { reg.dispatch = () => {}; BaseService.logger_ = this.logger_; + require('lib/ntpDate').setLogger(reg.logger()); this.dbLogger_.addTarget('file', { path: `${profileDir}/log-database.txt` }); this.dbLogger_.setLevel(initArgs.logLevel); diff --git a/ReactNativeClient/lib/ntpDate.ts b/ReactNativeClient/lib/ntpDate.ts index 1e2e2fb5e..554c4a613 100644 --- a/ReactNativeClient/lib/ntpDate.ts +++ b/ReactNativeClient/lib/ntpDate.ts @@ -1,8 +1,11 @@ const ntpClient = require('lib/vendor/ntp-client'); +const { Logger } = require('lib/logger.js'); const Mutex = require('async-mutex').Mutex; let nextSyncTime = 0; let timeOffset = 0; +let logger = new Logger(); + const fetchingTimeMutex = new Mutex(); const server = { @@ -27,6 +30,10 @@ function shouldSyncTime() { return !nextSyncTime || Date.now() > nextSyncTime; } +export function setLogger(v:any) { + logger = v; +} + export default async function():Promise { if (shouldSyncTime()) { const release = await fetchingTimeMutex.acquire(); @@ -38,7 +45,8 @@ export default async function():Promise { timeOffset = date.getTime() - Date.now(); } } catch (error) { - // Fallback to application time since + logger.warn('Could not get NTP time - falling back to device time:', error); + // Fallback to device time since // most of the time it's actually correct nextSyncTime = Date.now() + 20 * 1000; timeOffset = 0; diff --git a/ReactNativeClient/lib/vendor/ntp-client.js b/ReactNativeClient/lib/vendor/ntp-client.js index 2a1660c38..235d48d7b 100644 --- a/ReactNativeClient/lib/vendor/ntp-client.js +++ b/ReactNativeClient/lib/vendor/ntp-client.js @@ -11,6 +11,8 @@ // it has several bugs and is currently unmaintained // ---------------------------------------------------------------------------------------- +const Buffer = require('buffer').Buffer; + (function (exports) { "use strict"; @@ -89,47 +91,52 @@ errorFired = true; }); - client.send(ntpData, 0, ntpData.length, port, server, function (err) { - if (err) { - clearTimeout(timeout); - if (errorFired) { + // NOTE: To make it work in React Native (Android), a port need to be bound + // before calling client.send() + + // client.bind(5555, '0.0.0.0', function() { + client.send(ntpData, 0, ntpData.length, port, server, function (err) { + if (err) { + clearTimeout(timeout); + if (errorFired) { + return; + } + callback(err, null); + errorFired = true; + closeClient(client); return; } - callback(err, null); - errorFired = true; - closeClient(client); - return; - } - client.once('message', function (msg) { - clearTimeout(timeout); - closeClient(client); + client.once('message', function (msg) { + clearTimeout(timeout); + closeClient(client); - // Offset to get to the "Transmit Timestamp" field (time at which the reply - // departed the server for the client, in 64-bit timestamp format." - var offsetTransmitTime = 40, - intpart = 0, - fractpart = 0; + // Offset to get to the "Transmit Timestamp" field (time at which the reply + // departed the server for the client, in 64-bit timestamp format." + var offsetTransmitTime = 40, + intpart = 0, + fractpart = 0; - // Get the seconds part - for (var i = 0; i <= 3; i++) { - intpart = 256 * intpart + msg[offsetTransmitTime + i]; - } + // Get the seconds part + for (var i = 0; i <= 3; i++) { + intpart = 256 * intpart + msg[offsetTransmitTime + i]; + } - // Get the seconds fraction - for (i = 4; i <= 7; i++) { - fractpart = 256 * fractpart + msg[offsetTransmitTime + i]; - } + // Get the seconds fraction + for (i = 4; i <= 7; i++) { + fractpart = 256 * fractpart + msg[offsetTransmitTime + i]; + } - var milliseconds = (intpart * 1000 + (fractpart * 1000) / 0x100000000); + var milliseconds = (intpart * 1000 + (fractpart * 1000) / 0x100000000); - // **UTC** time - var date = new Date("Jan 01 1900 GMT"); - date.setUTCMilliseconds(date.getUTCMilliseconds() + milliseconds); + // **UTC** time + var date = new Date("Jan 01 1900 GMT"); + date.setUTCMilliseconds(date.getUTCMilliseconds() + milliseconds); - callback(null, date); + callback(null, date); + }); }); - }); + // }); }; exports.demo = function (argv) { diff --git a/ReactNativeClient/root.js b/ReactNativeClient/root.js index 3ebccdbfe..298252148 100644 --- a/ReactNativeClient/root.js +++ b/ReactNativeClient/root.js @@ -66,6 +66,9 @@ const WelcomeUtils = require('lib/WelcomeUtils'); const { themeStyle } = require('lib/components/global-style.js'); const { uuid } = require('lib/uuid.js'); +const ntpClient = require('lib/vendor/ntp-client'); +ntpClient.dgram = require('react-native-udp'); + const { loadKeychainServiceAndSettings } = require('lib/services/SettingUtils'); const KeychainServiceDriverMobile = require('lib/services/keychain/KeychainServiceDriver.mobile').default; @@ -401,6 +404,7 @@ async function initialize(dispatch, messageHandler) { reg.setShowErrorMessageBoxHandler((message) => { alert(message); }); BaseService.logger_ = mainLogger; + require('lib/ntpDate').setLogger(reg.logger()); reg.logger().info('===================================='); reg.logger().info(`Starting application ${Setting.value('appId')} (${Setting.value('env')})`); From 582ab4ac13e47d2108a43cd6eaaff9db1fb9d5fb Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Wed, 9 Sep 2020 11:39:13 +0100 Subject: [PATCH 4/6] All: Implemented more reliable way to sync device and server clocks that would work with filesystem sync too --- ReactNativeClient/lib/file-api.js | 42 +++++++++++++++++++ .../lib/services/synchronizer/LockHandler.ts | 3 +- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/ReactNativeClient/lib/file-api.js b/ReactNativeClient/lib/file-api.js index f53e97ff5..52200673d 100644 --- a/ReactNativeClient/lib/file-api.js +++ b/ReactNativeClient/lib/file-api.js @@ -6,6 +6,7 @@ const JoplinError = require('lib/JoplinError'); const ArrayUtils = require('lib/ArrayUtils'); const { time } = require('lib/time-utils.js'); const { sprintf } = require('sprintf-js'); +const Mutex = require('async-mutex').Mutex; function requestCanBeRepeated(error) { const errorCode = typeof error === 'object' && error.code ? error.code : null; @@ -57,6 +58,47 @@ class FileApi { this.tempDirName_ = null; this.driver_.fileApi_ = this; this.requestRepeatCount_ = null; // For testing purpose only - normally this value should come from the driver + this.remoteDateOffset_ = 0; + this.remoteDateNextCheckTime_ = 0; + this.remoteDateMutex_ = new Mutex(); + } + + // Approximates the current time on the sync target. It caches the time offset to + // improve performance. + async remoteDate() { + const shouldSyncTime = () => { + return !this.remoteDateNextCheckTime_ || Date.now() > this.remoteDateNextCheckTime_; + }; + + if (shouldSyncTime()) { + const release = await this.remoteDateMutex_.acquire(); + + try { + // Another call might have refreshed the time while we were waiting for the mutex, + // so check again if we need to refresh. + if (shouldSyncTime()) { + const tempFile = `${this.tempDirName()}/timeCheck${Math.random() * 10000}.txt`; + const startTime = Date.now(); + await this.put(tempFile, 'timeCheck'); + const stat = await this.stat(tempFile); + const endTime = Date.now(); + const expectedTime = Math.round((endTime + startTime) / 2); + this.remoteDateOffset_ = stat.updated_time.getTime() - expectedTime; + // The sync target clock should rarely change but the device one might, + // so we need to refresh relatively frequently. + this.remoteDateNextCheckTime_ = Date.now() + 10 * 60 * 1000; + this.delete(tempFile); // No need to await for this call + } + } catch (error) { + this.logger().warn('Could not retrieve remote date - defaulting to device date:', error); + this.remoteDateOffset_ = 0; + this.remoteDateNextCheckTime_ = Date.now() + 60 * 1000; + } finally { + release(); + } + } + + return new Date(Date.now() + this.remoteDateOffset_); } // Ideally all requests repeating should be done at the FileApi level to remove duplicate code in the drivers, but diff --git a/ReactNativeClient/lib/services/synchronizer/LockHandler.ts b/ReactNativeClient/lib/services/synchronizer/LockHandler.ts index 62dddd7ed..17e955a56 100644 --- a/ReactNativeClient/lib/services/synchronizer/LockHandler.ts +++ b/ReactNativeClient/lib/services/synchronizer/LockHandler.ts @@ -1,5 +1,4 @@ import { Dirnames } from './utils/types'; -import ntpDate from 'lib/ntpDate'; const JoplinError = require('lib/JoplinError'); const { time } = require('lib/time-utils'); @@ -114,7 +113,7 @@ export default class LockHandler { // of that type instead. async activeLock(lockType:LockType, clientType:string = null, clientId:string = null) { const locks = await this.locks(lockType); - const currentDate = await ntpDate(); + const currentDate = await this.api_.remoteDate(); if (lockType === LockType.Exclusive) { const activeLocks = locks From b24d06028139559cab955f58e71c0829fd240c25 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Wed, 9 Sep 2020 12:25:31 +0100 Subject: [PATCH 5/6] All: Got clock sync to work on mobile --- .../lib/file-api-driver-dropbox.js | 2 +- ReactNativeClient/lib/file-api.js | 34 ++++++++++++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/ReactNativeClient/lib/file-api-driver-dropbox.js b/ReactNativeClient/lib/file-api-driver-dropbox.js index 876208821..f16c2dd33 100644 --- a/ReactNativeClient/lib/file-api-driver-dropbox.js +++ b/ReactNativeClient/lib/file-api-driver-dropbox.js @@ -44,7 +44,7 @@ class FileApiDriverDropbox { metadataToStat_(md, path) { const output = { path: path, - updated_time: md.server_modified ? new Date(md.server_modified) : new Date(), + updated_time: md.server_modified ? (new Date(md.server_modified)).getTime() : Date.now(), isDir: md['.tag'] === 'folder', }; diff --git a/ReactNativeClient/lib/file-api.js b/ReactNativeClient/lib/file-api.js index 52200673d..f188eac60 100644 --- a/ReactNativeClient/lib/file-api.js +++ b/ReactNativeClient/lib/file-api.js @@ -63,6 +63,31 @@ class FileApi { this.remoteDateMutex_ = new Mutex(); } + + async fetchRemoteDateOffset_() { + const tempFile = `${this.tempDirName()}/timeCheck${Math.round(Math.random() * 1000000)}.txt`; + const startTime = Date.now(); + await this.put(tempFile, 'timeCheck'); + + // Normally it should be possible to read the file back immediately but + // just in case, read it in a loop. + const loopStartTime = Date.now(); + let stat = null; + while (Date.now() - loopStartTime < 5000) { + stat = await this.stat(tempFile); + if (stat) break; + await time.msleep(200); + } + + if (!stat) throw new Error('Timed out trying to get sync target clock time'); + + this.delete(tempFile); // No need to await for this call + + const endTime = Date.now(); + const expectedTime = Math.round((endTime + startTime) / 2); + return stat.updated_time - expectedTime; + } + // Approximates the current time on the sync target. It caches the time offset to // improve performance. async remoteDate() { @@ -77,17 +102,10 @@ class FileApi { // Another call might have refreshed the time while we were waiting for the mutex, // so check again if we need to refresh. if (shouldSyncTime()) { - const tempFile = `${this.tempDirName()}/timeCheck${Math.random() * 10000}.txt`; - const startTime = Date.now(); - await this.put(tempFile, 'timeCheck'); - const stat = await this.stat(tempFile); - const endTime = Date.now(); - const expectedTime = Math.round((endTime + startTime) / 2); - this.remoteDateOffset_ = stat.updated_time.getTime() - expectedTime; + this.remoteDateOffset_ = await this.fetchRemoteDateOffset_(); // The sync target clock should rarely change but the device one might, // so we need to refresh relatively frequently. this.remoteDateNextCheckTime_ = Date.now() + 10 * 60 * 1000; - this.delete(tempFile); // No need to await for this call } } catch (error) { this.logger().warn('Could not retrieve remote date - defaulting to device date:', error); From 2aa7eaa1928e348eac913b91f10c1fecf89ba322 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Wed, 9 Sep 2020 12:39:06 +0100 Subject: [PATCH 6/6] Electron release v1.0.245 --- ElectronClient/package-lock.json | 2 +- ElectronClient/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ElectronClient/package-lock.json b/ElectronClient/package-lock.json index 30d8682ec..6b4ef38a5 100644 --- a/ElectronClient/package-lock.json +++ b/ElectronClient/package-lock.json @@ -1,6 +1,6 @@ { "name": "Joplin", - "version": "1.0.242", + "version": "1.0.245", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/ElectronClient/package.json b/ElectronClient/package.json index e9fca27d0..5f2b730b0 100644 --- a/ElectronClient/package.json +++ b/ElectronClient/package.json @@ -1,6 +1,6 @@ { "name": "Joplin", - "version": "1.0.242", + "version": "1.0.245", "description": "Joplin for Desktop", "main": "main.js", "scripts": {