From 4f85726db15941301a9741ee2863a35b2e67d37e Mon Sep 17 00:00:00 2001 From: Jim Borden Date: Thu, 30 Mar 2017 15:11:55 +0900 Subject: [PATCH] Forgotten files from last commit --- .../Assets/C/tests/data/sentences.json | 36 +++ .../Assets/C/tests/data/sentences.json | 36 +++ src/Couchbase.Lite.Tests.UWP/run_tests.bat | 4 + src/Couchbase.Lite/API/IModellable.cs | 25 +++ src/Couchbase.Lite/API/Sync/IReplication.cs | 33 +++ .../API/Sync/ReplicationActivityLevel.cs | 15 ++ .../API/Sync/ReplicationProgress.cs | 19 ++ .../API/Sync/ReplicationStatus.cs | 19 ++ .../Sync/ReplicationStatusChangedEventArgs.cs | 16 ++ .../API/Sync/ReplicationStoppedEventArgs.cs | 16 ++ src/Couchbase.Lite/Sync/Replication.cs | 206 ++++++++++++++++++ src/Couchbase.Lite/Sync/WebSocketTransport.cs | 71 ++++++ src/Couchbase.Lite/Sync/WebSocketWrapper.cs | 128 +++++++++++ 13 files changed, 624 insertions(+) create mode 100644 src/Couchbase.Lite.Tests.Android/Assets/C/tests/data/sentences.json create mode 100644 src/Couchbase.Lite.Tests.UWP/Assets/C/tests/data/sentences.json create mode 100644 src/Couchbase.Lite.Tests.UWP/run_tests.bat create mode 100644 src/Couchbase.Lite/API/IModellable.cs create mode 100644 src/Couchbase.Lite/API/Sync/IReplication.cs create mode 100644 src/Couchbase.Lite/API/Sync/ReplicationActivityLevel.cs create mode 100644 src/Couchbase.Lite/API/Sync/ReplicationProgress.cs create mode 100644 src/Couchbase.Lite/API/Sync/ReplicationStatus.cs create mode 100644 src/Couchbase.Lite/API/Sync/ReplicationStatusChangedEventArgs.cs create mode 100644 src/Couchbase.Lite/API/Sync/ReplicationStoppedEventArgs.cs create mode 100644 src/Couchbase.Lite/Sync/Replication.cs create mode 100644 src/Couchbase.Lite/Sync/WebSocketTransport.cs create mode 100644 src/Couchbase.Lite/Sync/WebSocketWrapper.cs diff --git a/src/Couchbase.Lite.Tests.Android/Assets/C/tests/data/sentences.json b/src/Couchbase.Lite.Tests.Android/Assets/C/tests/data/sentences.json new file mode 100644 index 000000000..32194e6a4 --- /dev/null +++ b/src/Couchbase.Lite.Tests.Android/Assets/C/tests/data/sentences.json @@ -0,0 +1,36 @@ +{"sentence": "It was a dark and stormy night; the rain fell in torrents, except at occasional intervals, when it was checked by a violent gust of wind which swept up the streets (for it is in London that our scene lies), rattling along the house-tops, and fiercely agitating the scanty flame of the lamps that struggled against the darkness."} +{"sentence": "Through one of the obscurest quarters of London, and among haunts little loved by the gentlemen of the police, a man, evidently of the lowest orders, was wending his solitary way."} +{"sentence": "He stopped twice or thrice at different shops and houses of a description correspondent with the appearance of the quartier in which they were situated, and tended inquiry for some article or another which did not seem easily to be met with."} +{"sentence": "All the answers he received were couched in the negative; and as he turned from each door he muttered to himself, in no very elegant phraseology, his disappointment and discontent."} +{"sentence": "At length, at one house, the landlord, a sturdy butcher, after rendering the same reply the inquirer had hitherto received, added, “But if this vill do as vell, Dummie, it is quite at your sarvice!”"} +{"sentence": "Pausing reflectively for a moment, Dummie responded that he thought the thing proffered might do as well; and thrusting it into his ample pocket, he strode away with as rapid a motion as the wind and the rain would allow."} +{"sentence": "He soon came to a nest of low and dingy buildings, at the entrance to which, in half-effaced characters, was written “Thames Court.”"} +{"sentence": "Halting at the most conspicuous of these buildings, an inn or alehouse, through the half-closed windows of which blazed out in ruddy comfort the beams of the hospitable hearth, he knocked hastily at the door."} +{"sentence": "He was admitted by a lady of a certain age, and endowed with a comely rotundity of face and person."} +{"sentence": "“Hast got it, Dummie?” said she, quickly, as she closed the door on the guest."} +{"sentence": "“Noa, noa! not exactly; but I thinks as ‘ow—”"} +{"sentence": "“Pish, you fool!” cried the woman, interrupting him peevishly."} +{"sentence": "“Vy, it is no use desaving me."} +{"sentence": "You knows you has only stepped from my boosing-ken to another, and you has not been arter the book at all."} +{"sentence": "So there’s the poor cretur a raving and a dying, and you—”"} +{"sentence": "“Let I speak!” interrupted Dummie in his turn."} +{"sentence": "“I tells you I vent first to Mother Bussblone’s, who, I knows, chops the whiners morning and evening to the young ladies, and I axes there for a Bible; and she says, says she, ‘I’ as only a “Companion to the Halter,” but you’ll get a Bible, I think, at Master Talkins’, the cobbler as preaches.’"} +{"sentence": "So I goes to Master Talkins, and he says, says he, ‘I ‘as no call for the Bible,—‘cause vy? I ‘as a call vithout; but mayhap you’ll be a getting it at the butcher’s hover the vay,—‘cause vy? The butcher ‘ll be damned!’"} +{"sentence": "So I goes hover the vay, and the butcher says, says he, ‘I ‘as not a Bible, but I ‘as a book of plays bound for all the vorld just like ‘un, and mayhap the poor cretur may n’t see the difference.’"} +{"sentence": "So I takes the plays, Mrs. Margery, and here they be surely!"} +{"sentence": "And how’s poor Judy?”"} +{"sentence": "“Fearsome! she’ll not be over the night, I’m a thinking.”"} +{"sentence": "“Vell, I’ll track up the dancers!”"} +{"sentence": "So saying, Dummie ascended a doorless staircase, across the entrance of which a blanket, stretched angularly from the wall to the chimney, afforded a kind of screen; and presently he stood within a chamber which the dark and painful genius of Crabbe might have delighted to portray."} +{"sentence": "The walls were whitewashed, and at sundry places strange figures and grotesque characters had been traced by some mirthful inmate, in such sable outline as the end of a smoked stick or the edge of a piece of charcoal is wont to produce."} +{"sentence": "The wan and flickering light afforded by a farthing candle gave a sort of grimness and menace to these achievements of pictorial art, especially as they more than once received embellishments from portraits of Satan such as he is accustomed to be drawn."} +{"sentence": "A low fire burned gloomily in the sooty grate, and on the hob hissed “the still small voice” of an iron kettle."} +{"sentence": "On a round deal table were two vials, a cracked cup, a broken spoon of some dull metal, and upon two or three mutilated chairs were scattered various articles of female attire."} +{"sentence": "On another table, placed below a high, narrow, shutterless casement (athwart which, instead of a curtain, a checked apron had been loosely hung, and now waved fitfully to and fro in the gusts of wind that made easy ingress through many a chink and cranny), were a looking-glass, sundry appliances of the toilet, a box of coarse rouge, a few ornaments of more show than value, and a watch, the regular and calm click of which produced that indescribably painful feeling which, we fear, many of our readers who have heard the sound in a sick-chamber can easily recall."} +{"sentence": "A large tester-bed stood opposite to this table, and the looking-glass partially reflected curtains of a faded stripe, and ever and anon (as the position of the sufferer followed the restless emotion of a disordered mind) glimpses of the face of one on whom Death was rapidly hastening."} +{"sentence": "Beside this bed now stood Dummie, a small, thin man dressed in a tattered plush jerkin, from which the rain-drops slowly dripped, and with a thin, yellow, cunning physiognomy grotesquely hideous in feature, but not positively villanous in expression."} +{"sentence": "On the other side of the bed stood a little boy of about three years old, dressed as if belonging to the better classes, although the garb was somewhat tattered and discoloured."} +{"sentence": "The poor child trembled violently, and evidently looked with a feeling of relief on the entrance of Dummie."} +{"sentence": "And now there slowly, and with many a phthisical sigh, heaved towards the foot of the bed the heavy frame of the woman who had accosted Dummie below, and had followed him, haud passibus aequis, to the room of the sufferer; she stood with a bottle of medicine in her hand, shaking its contents up and down, and with a kindly yet timid compassion spread over a countenance crimsoned with habitual libations."} +{"sentence": "This made the scene,—save that on a chair by the bedside lay a profusion of long, glossy, golden ringlets, which had been cut from the head of the sufferer when the fever had begun to mount upwards, but which, with a jealousy that portrayed the darling littleness of a vain heart, she had seized and insisted on retaining near her; and save that, by the fire, perfectly inattentive to the event about to take place within the chamber, and to which we of the biped race attach so awful an importance, lay a large gray cat, curled in a ball, and dozing with half-shut eyes, and ears that now and then denoted, by a gentle inflection, the jar of a louder or nearer sound than usual upon her lethargic senses."} +{"sentence": "The dying woman did not at first attend to the entrance either of Dummie or the female at the foot of the bed, but she turned herself round towards the child, and grasping his arm fiercely, she drew him towards her, and gazed on his terrified features with a look in which exhaustion and an exceeding wanness of complexion were even horribly contrasted by the glare and energy of delirium."} \ No newline at end of file diff --git a/src/Couchbase.Lite.Tests.UWP/Assets/C/tests/data/sentences.json b/src/Couchbase.Lite.Tests.UWP/Assets/C/tests/data/sentences.json new file mode 100644 index 000000000..32194e6a4 --- /dev/null +++ b/src/Couchbase.Lite.Tests.UWP/Assets/C/tests/data/sentences.json @@ -0,0 +1,36 @@ +{"sentence": "It was a dark and stormy night; the rain fell in torrents, except at occasional intervals, when it was checked by a violent gust of wind which swept up the streets (for it is in London that our scene lies), rattling along the house-tops, and fiercely agitating the scanty flame of the lamps that struggled against the darkness."} +{"sentence": "Through one of the obscurest quarters of London, and among haunts little loved by the gentlemen of the police, a man, evidently of the lowest orders, was wending his solitary way."} +{"sentence": "He stopped twice or thrice at different shops and houses of a description correspondent with the appearance of the quartier in which they were situated, and tended inquiry for some article or another which did not seem easily to be met with."} +{"sentence": "All the answers he received were couched in the negative; and as he turned from each door he muttered to himself, in no very elegant phraseology, his disappointment and discontent."} +{"sentence": "At length, at one house, the landlord, a sturdy butcher, after rendering the same reply the inquirer had hitherto received, added, “But if this vill do as vell, Dummie, it is quite at your sarvice!”"} +{"sentence": "Pausing reflectively for a moment, Dummie responded that he thought the thing proffered might do as well; and thrusting it into his ample pocket, he strode away with as rapid a motion as the wind and the rain would allow."} +{"sentence": "He soon came to a nest of low and dingy buildings, at the entrance to which, in half-effaced characters, was written “Thames Court.”"} +{"sentence": "Halting at the most conspicuous of these buildings, an inn or alehouse, through the half-closed windows of which blazed out in ruddy comfort the beams of the hospitable hearth, he knocked hastily at the door."} +{"sentence": "He was admitted by a lady of a certain age, and endowed with a comely rotundity of face and person."} +{"sentence": "“Hast got it, Dummie?” said she, quickly, as she closed the door on the guest."} +{"sentence": "“Noa, noa! not exactly; but I thinks as ‘ow—”"} +{"sentence": "“Pish, you fool!” cried the woman, interrupting him peevishly."} +{"sentence": "“Vy, it is no use desaving me."} +{"sentence": "You knows you has only stepped from my boosing-ken to another, and you has not been arter the book at all."} +{"sentence": "So there’s the poor cretur a raving and a dying, and you—”"} +{"sentence": "“Let I speak!” interrupted Dummie in his turn."} +{"sentence": "“I tells you I vent first to Mother Bussblone’s, who, I knows, chops the whiners morning and evening to the young ladies, and I axes there for a Bible; and she says, says she, ‘I’ as only a “Companion to the Halter,” but you’ll get a Bible, I think, at Master Talkins’, the cobbler as preaches.’"} +{"sentence": "So I goes to Master Talkins, and he says, says he, ‘I ‘as no call for the Bible,—‘cause vy? I ‘as a call vithout; but mayhap you’ll be a getting it at the butcher’s hover the vay,—‘cause vy? The butcher ‘ll be damned!’"} +{"sentence": "So I goes hover the vay, and the butcher says, says he, ‘I ‘as not a Bible, but I ‘as a book of plays bound for all the vorld just like ‘un, and mayhap the poor cretur may n’t see the difference.’"} +{"sentence": "So I takes the plays, Mrs. Margery, and here they be surely!"} +{"sentence": "And how’s poor Judy?”"} +{"sentence": "“Fearsome! she’ll not be over the night, I’m a thinking.”"} +{"sentence": "“Vell, I’ll track up the dancers!”"} +{"sentence": "So saying, Dummie ascended a doorless staircase, across the entrance of which a blanket, stretched angularly from the wall to the chimney, afforded a kind of screen; and presently he stood within a chamber which the dark and painful genius of Crabbe might have delighted to portray."} +{"sentence": "The walls were whitewashed, and at sundry places strange figures and grotesque characters had been traced by some mirthful inmate, in such sable outline as the end of a smoked stick or the edge of a piece of charcoal is wont to produce."} +{"sentence": "The wan and flickering light afforded by a farthing candle gave a sort of grimness and menace to these achievements of pictorial art, especially as they more than once received embellishments from portraits of Satan such as he is accustomed to be drawn."} +{"sentence": "A low fire burned gloomily in the sooty grate, and on the hob hissed “the still small voice” of an iron kettle."} +{"sentence": "On a round deal table were two vials, a cracked cup, a broken spoon of some dull metal, and upon two or three mutilated chairs were scattered various articles of female attire."} +{"sentence": "On another table, placed below a high, narrow, shutterless casement (athwart which, instead of a curtain, a checked apron had been loosely hung, and now waved fitfully to and fro in the gusts of wind that made easy ingress through many a chink and cranny), were a looking-glass, sundry appliances of the toilet, a box of coarse rouge, a few ornaments of more show than value, and a watch, the regular and calm click of which produced that indescribably painful feeling which, we fear, many of our readers who have heard the sound in a sick-chamber can easily recall."} +{"sentence": "A large tester-bed stood opposite to this table, and the looking-glass partially reflected curtains of a faded stripe, and ever and anon (as the position of the sufferer followed the restless emotion of a disordered mind) glimpses of the face of one on whom Death was rapidly hastening."} +{"sentence": "Beside this bed now stood Dummie, a small, thin man dressed in a tattered plush jerkin, from which the rain-drops slowly dripped, and with a thin, yellow, cunning physiognomy grotesquely hideous in feature, but not positively villanous in expression."} +{"sentence": "On the other side of the bed stood a little boy of about three years old, dressed as if belonging to the better classes, although the garb was somewhat tattered and discoloured."} +{"sentence": "The poor child trembled violently, and evidently looked with a feeling of relief on the entrance of Dummie."} +{"sentence": "And now there slowly, and with many a phthisical sigh, heaved towards the foot of the bed the heavy frame of the woman who had accosted Dummie below, and had followed him, haud passibus aequis, to the room of the sufferer; she stood with a bottle of medicine in her hand, shaking its contents up and down, and with a kindly yet timid compassion spread over a countenance crimsoned with habitual libations."} +{"sentence": "This made the scene,—save that on a chair by the bedside lay a profusion of long, glossy, golden ringlets, which had been cut from the head of the sufferer when the fever had begun to mount upwards, but which, with a jealousy that portrayed the darling littleness of a vain heart, she had seized and insisted on retaining near her; and save that, by the fire, perfectly inattentive to the event about to take place within the chamber, and to which we of the biped race attach so awful an importance, lay a large gray cat, curled in a ball, and dozing with half-shut eyes, and ears that now and then denoted, by a gentle inflection, the jar of a louder or nearer sound than usual upon her lethargic senses."} +{"sentence": "The dying woman did not at first attend to the entrance either of Dummie or the female at the foot of the bed, but she turned herself round towards the child, and grasping his arm fiercely, she drew him towards her, and gazed on his terrified features with a look in which exhaustion and an exceeding wanness of complexion were even horribly contrasted by the glare and energy of delirium."} \ No newline at end of file diff --git a/src/Couchbase.Lite.Tests.UWP/run_tests.bat b/src/Couchbase.Lite.Tests.UWP/run_tests.bat new file mode 100644 index 000000000..5494bd409 --- /dev/null +++ b/src/Couchbase.Lite.Tests.UWP/run_tests.bat @@ -0,0 +1,4 @@ +@echo off + +msbuild /p:Configuration=Packaging;Platform=x64 Couchbase.Lite.Tests.UWP.csproj +vstest.console.exe /InIsolation /Platform:x64 AppPackages\Couchbase.Lite.Tests.UWP_1.0.0.0_x64_Packaging_Test\Couchbase.Lite.Tests.UWP_1.0.0.0_x64_Packaging.appx \ No newline at end of file diff --git a/src/Couchbase.Lite/API/IModellable.cs b/src/Couchbase.Lite/API/IModellable.cs new file mode 100644 index 000000000..e9f183199 --- /dev/null +++ b/src/Couchbase.Lite/API/IModellable.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Couchbase.Lite.DB; + +namespace Couchbase.Lite +{ + internal interface IModellable + { + T AsModel() where T : IDocumentModel, new(); + } + + internal static class IDocumentModelExtensions + { + internal static void Save(this IDocumentModel model) + { + var document = model.Document as Document; + if (document == null) { + throw new NotSupportedException("Custom IDocument not supported"); + } + + document.Set(model); + } + } +} diff --git a/src/Couchbase.Lite/API/Sync/IReplication.cs b/src/Couchbase.Lite/API/Sync/IReplication.cs new file mode 100644 index 000000000..14e4797a5 --- /dev/null +++ b/src/Couchbase.Lite/API/Sync/IReplication.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Couchbase.Lite.Sync +{ + internal interface IReplication : IThreadSafe, IDisposable + { + event EventHandler StatusChanged; + + event EventHandler Stopped; + + IDatabase Database { get; } + + Uri RemoteUrl { get; } + + IDatabase OtherDatabase { get; } + + bool Push { get; set; } + + bool Pull { get; set; } + + bool Continuous { get; set; } + + ReplicationStatus Status { get; } + + Exception LastError { get; } + + void Start(); + + void Stop(); + } +} diff --git a/src/Couchbase.Lite/API/Sync/ReplicationActivityLevel.cs b/src/Couchbase.Lite/API/Sync/ReplicationActivityLevel.cs new file mode 100644 index 000000000..cdaca5505 --- /dev/null +++ b/src/Couchbase.Lite/API/Sync/ReplicationActivityLevel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Couchbase.Lite.Sync +{ + internal enum ReplicationActivityLevel + { + Stopped, + Offline, + Connecting, + Idle, + Busy + } +} diff --git a/src/Couchbase.Lite/API/Sync/ReplicationProgress.cs b/src/Couchbase.Lite/API/Sync/ReplicationProgress.cs new file mode 100644 index 000000000..20285f9ce --- /dev/null +++ b/src/Couchbase.Lite/API/Sync/ReplicationProgress.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Couchbase.Lite.Sync +{ + internal struct ReplicationProgress + { + public ulong Completed { get; } + + public ulong Total { get; } + + internal ReplicationProgress(ulong completed, ulong total) + { + Completed = completed; + Total = total; + } + } +} diff --git a/src/Couchbase.Lite/API/Sync/ReplicationStatus.cs b/src/Couchbase.Lite/API/Sync/ReplicationStatus.cs new file mode 100644 index 000000000..084dc702b --- /dev/null +++ b/src/Couchbase.Lite/API/Sync/ReplicationStatus.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Couchbase.Lite.Sync +{ + internal struct ReplicationStatus + { + public ReplicationActivityLevel Activity { get; } + + public ReplicationProgress Progress { get; } + + internal ReplicationStatus(ReplicationActivityLevel activity, ReplicationProgress progress) + { + Activity = activity; + Progress = progress; + } + } +} diff --git a/src/Couchbase.Lite/API/Sync/ReplicationStatusChangedEventArgs.cs b/src/Couchbase.Lite/API/Sync/ReplicationStatusChangedEventArgs.cs new file mode 100644 index 000000000..4673f6b89 --- /dev/null +++ b/src/Couchbase.Lite/API/Sync/ReplicationStatusChangedEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Couchbase.Lite.Sync +{ + internal sealed class ReplicationStatusChangedEventArgs : EventArgs + { + public ReplicationStatus Status { get; } + + internal ReplicationStatusChangedEventArgs(ReplicationStatus status) + { + Status = status; + } + } +} diff --git a/src/Couchbase.Lite/API/Sync/ReplicationStoppedEventArgs.cs b/src/Couchbase.Lite/API/Sync/ReplicationStoppedEventArgs.cs new file mode 100644 index 000000000..26ab0cc29 --- /dev/null +++ b/src/Couchbase.Lite/API/Sync/ReplicationStoppedEventArgs.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Couchbase.Lite.Sync +{ + internal sealed class ReplicationStoppedEventArgs : EventArgs + { + public Exception Error { get; } + + internal ReplicationStoppedEventArgs(Exception error) + { + Error = error; + } + } +} diff --git a/src/Couchbase.Lite/Sync/Replication.cs b/src/Couchbase.Lite/Sync/Replication.cs new file mode 100644 index 000000000..c3f34187f --- /dev/null +++ b/src/Couchbase.Lite/Sync/Replication.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using Couchbase.Lite.DB; +using Couchbase.Lite.Logging; +using Couchbase.Lite.Support; +using LiteCore; +using LiteCore.Interop; +using LiteCore.Util; + +namespace Couchbase.Lite.Sync +{ + internal sealed unsafe class Replication : ThreadSafe, IReplication + { + private const string Tag = nameof(Replication); + private C4Replicator* _repl; + + private static readonly C4ReplicatorMode[] _Modes = { + C4ReplicatorMode.Disabled, C4ReplicatorMode.Disabled, C4ReplicatorMode.OneShot, C4ReplicatorMode.Continuous + }; + + public event EventHandler StatusChanged; + public event EventHandler Stopped; + public IDatabase Database { get; } + public Uri RemoteUrl { get; } + public IDatabase OtherDatabase { get; } + public bool Push { get; set; } + public bool Pull { get; set; } + public bool Continuous { get; set; } + public ReplicationStatus Status { get; private set; } + public Exception LastError { get; private set; } + + static Replication() + { + + } + + public Replication(IDatabase db, Uri remoteUrl, IDatabase otherDb) + { + Database = db; + RemoteUrl = remoteUrl; + OtherDatabase = otherDb; + Push = Pull = true; + } + + ~Replication() + { + Dispose(true); + } + + private static C4ReplicatorMode Mkmode(bool active, bool continuous) + { + return _Modes[2 * Convert.ToInt32(active) + Convert.ToInt32(continuous)]; + } + + private static void StatusChangedCallback(C4ReplicatorStatus status, object context) + { + var repl = context as Replication; + repl?.DoAsync(() => + { + repl.StatusChangedCallback(status); + }); + } + + private void StatusChangedCallback(C4ReplicatorStatus status) + { + SetC4Status(status); + + StatusChanged?.Invoke(this, new ReplicationStatusChangedEventArgs(Status)); + if (status.level == C4ReplicatorActivityLevel.Stopped) { + // Stopped: + Native.c4repl_free(_repl); + _repl = null; + Stopped?.Invoke(this, new ReplicationStoppedEventArgs(LastError)); + (Database as Database)?.ActiveReplications.Remove(this); + } + } + + private void SetC4Status(C4ReplicatorStatus state) + { + Exception error = null; + if (state.error.code > 0) { + error = new LiteCoreException(state.error); + } + + if (LastError != error) { + LastError = error; + } + + //NOTE: ReplicationStatus values need to match C4ReplicatorActivityLevel! + var activity = (ReplicationActivityLevel) state.level; + var progress = new ReplicationProgress(state.progress.completed, state.progress.total); + Status = new ReplicationStatus(activity, progress); + Log.To.Sync.I(Tag, $"{this} is {Status}, progress {state.progress.completed}/{state.progress.total}"); + } + + public override string ToString() + { + var sb = new StringBuilder(3, 3); + if (Pull) { + sb.Append("<"); + } + + if (Continuous) { + sb.Append("*"); + } + + if (Push) { + sb.Append(">"); + } + + var other = RemoteUrl?.AbsoluteUri ?? OtherDatabase.Name; + return $"{GetType().Name}[{sb} {other}]"; + } + + public void Start() + { + AssertSafety(); + if (_repl != null) { + Log.To.Sync.W(Tag, $"{this} has already started"); + return; + } + + if (!Push && !Pull) { + throw new InvalidOperationException("Replication must be either push or pull, or both"); + } + + string pathStr = null; + string dbNameStr = null; + if (RemoteUrl != null) { + pathStr = String.Concat(RemoteUrl.Segments.Take(RemoteUrl.Segments.Length - 1)); + dbNameStr = RemoteUrl.Segments.Last().TrimEnd('/'); + } + + var database = Database as Database; + var otherDatabase = OtherDatabase as Database; + if (database == null) { + throw new NotSupportedException("Custom IDatabase not supported in Replication"); + } + + C4Error err; + using (var scheme = new C4String(RemoteUrl?.Scheme)) + using (var host = new C4String(RemoteUrl?.Host)) + using (var path = new C4String(pathStr)) { + ushort port = 0; + if (RemoteUrl != null) { + port = (ushort)RemoteUrl.Port; + } + + var addr = new C4Address { + scheme = scheme.AsC4Slice(), + hostname = host.AsC4Slice(), + port = port, + path = path.AsC4Slice() + }; + + var callback = new ReplicatorStateChangedCallback(StatusChangedCallback, this); + + var otherDb = otherDatabase == null ? null : otherDatabase.c4db; + _repl = Native.c4repl_new(database.c4db, addr, dbNameStr, otherDb, Mkmode(Push, Continuous), + Mkmode(Pull, Continuous), callback, &err); + } + + C4ReplicatorStatus status; + if (_repl != null) { + status = Native.c4repl_getStatus(_repl); + database.ActiveReplications.Add(this); + } else { + status = new C4ReplicatorStatus { + level = C4ReplicatorActivityLevel.Stopped, + progress = new C4Progress(), + error = err + }; + } + + SetC4Status(status); + + // Post an initial notification: + StatusChangedCallback(status, this); + } + + public void Stop() + { + AssertSafety(); + if (_repl != null) { + Native.c4repl_stop(_repl); + } + } + + private void Dispose(bool finalizing) + { + Native.c4repl_free(_repl); + } + + public void Dispose() + { + DoSync(() => + { + Dispose(false); + }); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Couchbase.Lite/Sync/WebSocketTransport.cs b/src/Couchbase.Lite/Sync/WebSocketTransport.cs new file mode 100644 index 000000000..37f5fccec --- /dev/null +++ b/src/Couchbase.Lite/Sync/WebSocketTransport.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using Couchbase.Lite.Logging; +using LiteCore.Interop; + +namespace Couchbase.Lite.Sync +{ + internal static unsafe class WebSocketTransport + { + private static readonly Dictionary _Sockets = new Dictionary(); + private static int _NextID = 0; + + public static void RegisterWithC4() + { + SocketFactory.RegisterFactory(DoOpen, DoClose, DoWrite, DoCompleteReceive); + } + + private static void DoCompleteReceive(C4Socket* socket, ulong bytecount) + { + var id = (int)socket->nativeHandle; + var socketWrapper = _Sockets[id]; + socketWrapper.CompletedReceive(bytecount); + } + + private static void DoOpen(C4Socket* socket, C4Address* address) + { + var builder = new UriBuilder { + Host = address->hostname.CreateString(), + Scheme = address->scheme.CreateString(), + Port = address->port, + Path = address->path.CreateString() + }; + + Uri uri = null; + try { + uri = builder.Uri; + } catch (Exception) { + Native.c4socket_closed(socket, new C4Error(LiteCoreError.InvalidParameter)); + return; + } + + if (uri == null) { + Native.c4socket_closed(socket, new C4Error(LiteCoreError.InvalidParameter)); + return; + } + + var socketWrapper = new WebSocketWrapper(uri, socket); + var id = Interlocked.Increment(ref _NextID); + socket->nativeHandle = (void*)id; + _Sockets[id] = socketWrapper; + socketWrapper.Start(); + } + + private static void DoClose(C4Socket* socket) + { + var id = (int) socket->nativeHandle; + var socketWrapper = _Sockets[id]; + socketWrapper.CloseSocket(); + } + + private static void DoWrite(C4Socket* socket, byte[] data) + { + var id = (int)socket->nativeHandle; + var socketWrapper = _Sockets[id]; + socketWrapper.Write(data); + } + } +} diff --git a/src/Couchbase.Lite/Sync/WebSocketWrapper.cs b/src/Couchbase.Lite/Sync/WebSocketWrapper.cs new file mode 100644 index 000000000..c0584a61f --- /dev/null +++ b/src/Couchbase.Lite/Sync/WebSocketWrapper.cs @@ -0,0 +1,128 @@ +using System; +using System.Linq; +using System.Net.WebSockets; +using System.Threading; + +using Couchbase.Lite.Support; +using LiteCore.Interop; + +namespace Couchbase.Lite.Sync +{ + internal sealed unsafe class WebSocketWrapper + { + private static readonly TimeSpan ConnectTimeout = TimeSpan.FromSeconds(15); + private static readonly TimeSpan IdleTimeout = TimeSpan.FromSeconds(300); + private const uint MaxReceivedBytesPending = 100 * 1024; + + private readonly C4Socket* _socket; + private readonly Uri _url; + private readonly SerialQueue _queue = new SerialQueue(); + private readonly SerialQueue _c4Queue = new SerialQueue(); + private readonly byte[] _buffer = new byte[MaxReceivedBytesPending]; + private bool _receiving; + private uint _receivedBytesPending; + + public ClientWebSocket WebSocket { get; } = new ClientWebSocket(); + + public WebSocketWrapper(Uri url, C4Socket* socket) + { + _socket = socket; + _url = url; + } + + public void Start() + { + _queue.DispatchAsync(() => + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(ConnectTimeout); + WebSocket.ConnectAsync(_url, cts.Token).ConfigureAwait(false).GetAwaiter().OnCompleted(() => + { + Receive(); + _c4Queue.DispatchAsync(() => + { + Native.c4socket_opened(_socket); + }); + }); + }); + } + + public void CloseSocket() + { + _queue.DispatchAsync(() => + { + WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client requested close", + CancellationToken.None).ConfigureAwait(false).GetAwaiter().OnCompleted(() => + { + _c4Queue.DispatchAsync(() => + { + Native.c4socket_closed(_socket, new C4Error()); + }); + }); + }); + } + + public void Write(byte[] data) + { + _queue.DispatchAsync(() => + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(IdleTimeout); + WebSocket.SendAsync(new ArraySegment(data), WebSocketMessageType.Binary, true, cts.Token) + .ConfigureAwait(false) + .GetAwaiter() + .OnCompleted( + () => + { + _c4Queue.DispatchAsync(() => + { + Native.c4socket_completedWrite(_socket, (ulong) data.Length); + }); + }); + }); + } + + public void CompletedReceive(ulong byteCount) + { + _queue.DispatchAsync(() => + { + _receivedBytesPending -= (uint)byteCount; + Receive(); + }); + } + + private void Receive() + { + if (_receiving) { + return; + } + + _receiving = true; + var cts = new CancellationTokenSource(); + cts.CancelAfter(IdleTimeout); + WebSocket.ReceiveAsync(new ArraySegment(_buffer), cts.Token) + .ContinueWith(t => + { + if (t.IsCanceled) { + return; + } + + if (t.Exception != null) { + // TODO + return; + } + + _receivedBytesPending += (uint)t.Result.Count; + var data = _buffer.Take(t.Result.Count).ToArray(); + _c4Queue.DispatchAsync(() => + { + Native.c4socket_received(_socket, data); + }); + if (!t.Result.EndOfMessage && _receivedBytesPending < MaxReceivedBytesPending) { + Receive(); + } + }, cts.Token); + + } + } +}