"use strict" const { time } = require('lib/time-utils.js'); const { Logger } = require('lib/logger.js'); const { Resource } = require('lib/models/resource.js'); const { dirname } = require('lib/path-utils.js'); const { FsDriverNode } = require('./fs-driver-node.js'); const lodash = require('lodash'); const exec = require('child_process').exec const fs = require('fs-extra'); const baseDir = dirname(__dirname) + '/tests/fuzzing'; const syncDir = baseDir + '/sync'; const joplinAppPath = __dirname + '/main.js'; let syncDurations = []; const fsDriver = new FsDriverNode(); Logger.fsDriver_ = fsDriver; Resource.fsDriver_ = fsDriver; const logger = new Logger(); logger.addTarget('console'); logger.setLevel(Logger.LEVEL_DEBUG); process.on('unhandledRejection', (reason, p) => { console.error('Unhandled promise rejection', p, 'reason:', reason); }); function createClient(id) { return { 'id': id, 'profileDir': baseDir + '/client' + id, }; } async function createClients() { let output = []; let promises = []; for (let clientId = 0; clientId < 2; clientId++) { let client = createClient(clientId); promises.push(fs.remove(client.profileDir)); promises.push(execCommand(client, 'config sync.target 2').then(() => { return execCommand(client, 'config sync.2.path ' + syncDir); })); output.push(client); } await Promise.all(promises); return output; } function randomElement(array) { if (!array.length) return null; return array[Math.floor(Math.random() * array.length)]; } function randomWord() { const words = ['belief','scandalous','flawless','wrestle','sort','moldy','carve','incompetent','cruel','awful','fang','holistic','makeshift','synonymous','questionable','soft','drop','boot','whimsical','stir','idea','adhesive','present','hilarious','unusual','divergent','probable','depend','suck','belong','advise','straight','encouraging','wing','clam','serve','fill','nostalgic','dysfunctional','aggressive','floor','baby','grease','sisters','print','switch','control','victorious','cracker','dream','wistful','adaptable','reminiscent','inquisitive','pushy','unaccountable','receive','guttural','two','protect','skin','unbiased','plastic','loutish','zip','used','divide','communicate','dear','muddled','dinosaurs','grip','trees','well-off','calendar','chickens','irate','deranged','trip','stream','white','poison','attack','obtain','theory','laborer','omniscient','brake','maniacal','curvy','smoke','babies','punch','hammer','toothbrush','same','crown','jagged','peep','difficult','reject','merciful','useless','doctor','mix','wicked','plant','quickest','roll','suffer','curly','brother','frighten','cold','tremendous','move','knot','lame','imaginary','capricious','raspy','aunt','loving','wink','wooden','hop','free','drab','fire','instrument','border','frame','silent','glue','decorate','distance','powerful','pig','admit','fix','pour','flesh','profuse','skinny','learn','filthy','dress','bloody','produce','innocent','meaty','pray','slimy','sun','kindhearted','dime','exclusive','boast','neat','ruthless','recess','grieving','daily','hateful','ignorant','fence','spring','slim','education','overflow','plastic','gaping','chew','detect','right','lunch','gainful','argue','cloistered','horses','orange','shame','bitter','able','sail','magical','exist','force','wheel','best','suit','spurious','partner','request','dog','gusty','money','gaze','lonely','company','pale','tempt','rat','flame','wobble','superficial','stop','protective','stare','tongue','heal','railway','idiotic','roll','puffy','turn','meeting','new','frightening','sophisticated','poke','elderly','room','stimulating','increase','moor','secret','lean','occur','country','damp','evanescent','alluring','oafish','join','thundering','cars','awesome','advice','unruly','ray','wind','anxious','fly','hammer','adventurous','shop','cook','trucks','nonchalant','addition','base','abashed','excuse','giants','dramatic','piquant','coach','possess','poor','finger','wide-eyed','aquatic','welcome','instruct','expert','evasive','hug','cute','return','mice','damage','turkey','quiet','bewildered','tidy','pointless','outrageous','medical','foolish','curve','grandiose','gullible','hapless','gleaming','third','grin','pipe','egg','act','physical','eager','side','milk','tearful','fertile','average','glamorous','strange','yak','terrific','thin','near','snails','flowery','authority','fish','curious','perpetual','healthy','health','match','fade','chemical','economic','drawer','avoid','lying','minister','lick','powder','decay','desire','furry','faint','beam','sordid','fax','tail','bawdy','cherry','letter','clover','ladybug','teeth','behavior','black','amazing','pink','waste','island','forgetful','needless','lock','waves','boundary','receipt','handy','religion','hypnotic','aftermath','explain','sense','mundane','rambunctious','second','preserve','alarm','dusty','event','blow','weigh','value','glorious','jail','sigh','cemetery','serious','yummy','cattle','understood','limit','alert','fear','lucky','tested','surround','dolls','pleasant','disillusioned','discover','tray','night','seemly','liquid','worry','pen','bent','gruesome','war','teeny-tiny','common','judge','symptomatic','bed','trot','unequaled','flowers','friends','damaged','peel','skip','show','twist','worthless','brush','look','behave','imperfect','week','petite','direction','soda','lively','coal','coil','release','berserk','books','impossible','replace','cough','chunky','torpid','discreet','material','bomb','soothe','crack','hope','license','frightened','breathe','maddening','calculator','committee','paltry','green','subsequent','arrest','gigantic','tasty','metal','willing','man','stem','nonstop','route','impulse','government','comfortable','include','literate','multiply','test','vast','exercise','addicted','agreeable','lace','toes','young','water','end','wash','glossy','round','staking','sink','open','spot','trip','fierce','robust','pastoral','drown','dress','machine','calculating','holiday','crabby','disgusting','plan','sleet','sleepy','typical','borrow','possible','curtain','airplane','industry','nut','rough','wacky','rock','enormous','uninterested','sugar','rake','consist','wrist','basket','chop','wet','street','known','settle','bless','cluttered','wild','expand','angle','snake','yawn','hate','flood','rabid','spiteful','anger','market','bizarre','force','majestic','scissors','beg','rifle','foregoing','cactus','funny','eggnog','wish','high-pitched','drop','camp','scarf','car','groan','wonderful','wealthy','cup','lock','available','previous','jam','political','vacation','three','desk','fry','aspiring','productive','clear','bored','flashy','plug','precede','abhorrent','muddle','flimsy','paste','need','reward','frail','obnoxious','creature','whip','unbecoming','lake','unused','chin','tour','zephyr','experience','building','scrub','correct','hover','panicky','scorch','diligent','hulking','ubiquitous','tedious','aberrant','file','accidental','mist','blue-eyed','trite','nondescript','cows','wait','test','snotty','amuck','jump','lackadaisical','grey','tawdry','strong','land','kind','star','ludicrous','stupid','telling','use','bruise','whirl','cream','harsh','aboriginal','substantial','brawny','tease','pollution','weather','degree','dry','film','obey','closed','dependent','want','undesirable','stamp','relax','foot','obscene','successful','wriggle','drain','greasy','escape','cross','odd','boring','absorbed','houses','suppose','suit','moon','ceaseless','explode','clap','pop','courageous','miss','notebook','delirious','form','pretty','sock','grotesque','noxious','record','stop','saw','thing','dislike','cloth','six','jar','unnatural','spiffy','itchy','secretary','move','certain','unkempt','sassy','queue','shrug','crow','heavenly','desert','screw','vessel','mug','encourage','icy','enthusiastic','throat','whistle','ignore','miniature','squeak','scarecrow','fluttering','hang','icicle','lie','juicy','empty','baseball','various','promise','abortive','descriptive','high','spy','faded','talk','air','messup','decorous','sneaky','mark','sack','ultra','chivalrous','lethal','expect','disgusted','reaction','fireman','private','ritzy','manage','actor','rely','uppity','thread','bat','space','underwear','blood','nine','maid','shelf','hanging','shop','prick','wound','sloppy','offer','increase','clear','slap','rude','poised','wretched','cause','quince','tame','remarkable','abject','sail','guide','subdued','spiky','debonair','chicken','tired','hum','land','scared','splendid','guess','cast','rub','magnificent','ants','overwrought','interfere','gorgeous','office','trade','sniff','melted','bore','point','pet','purple','brake','flavor','toe','prickly','zinc','homely','modern','kindly','whisper','bare','annoyed','glass','noisy','null','thoughtless','skirt','dock','rings','mind','neck','macho','wave','history','play','road','profit','word','opposite','dreary','governor','horse','trust','elbow','kiss','crayon','stitch','excited','needy','arrange','easy','alcoholic','safe','lumpy','monkey','smile','capable','untidy','extra-small','memory','selective','reproduce','old-fashioned','overrated','texture','knit','downtown','risk','pot','sofa','righteous','wren','pull','carry','aboard','listen','classy','thank','shocking','condition','root','attempt','swim','frog','hurt','army','title','handsomely','town','guiltless','thaw','spell','selfish','disturbed','tramp','girls','utopian','noiseless','trail','bashful','business','rhetorical','snail','sign','plausible','left','design','tall','violent','wasteful','beautiful','breezy','tap','murder','talented','needle','creator','imagine','flippant','dead','bone','coherent','relation','aromatic','mountainous','face','ask','picture','pedal','colour','obese','group','top','bubble','pinch','optimal','school','bathe','flagrant','check','deliver','pass','tan','crate','hose','debt','faulty','longing','hollow','invincible','afford','lovely','ticket','changeable','subtract','fumbling','responsible','confused','woman','touch','watch','zesty','library','jail','wrap','terrify','brick','popcorn','cooperative','peck','pocket','property','buzz','tiresome','digestion','exciting','nation','juvenile','shade','copper','wanting','deer','waste','man','join','spotty','amused','mountain','waggish','bushes','tense','river','heartbreaking','help','mine','narrow','smash','scrawny','tame','rain','playground','airport','astonishing','level','befitting','animal','heat','painful','cellar','ski','sedate','knowing','vigorous','change','eight','ship','work','strip','robin','tank','challenge','vacuous','representative','regret','tightfisted','erratic','club','imported','therapeutic','rainstorm','luxuriant','relieved','day','system','apologise','male','prepare','malicious','naive','whistle','curl','hobbies','trousers','stereotyped','dad','endurable','grass','hot','bomb','morning','guide','keen','plot','accept','disastrous','macabre','year','spicy','absorbing','sticks','efficient','drain','warm','rice','utter','fact','marked','ratty','chalk','towering','treat','nest','annoy','jealous','stamp','effect','cautious','jelly','feigned','gabby','corn','volleyball','pan','psychedelic','fairies','silent','zonked','bump','trouble','mass','queen','things','bury','sister','quiet','colossal','puncture','four','attend','love','wiry','vegetable','destruction','note','pies','resolute','load','fancy','tacky','periodic','abandoned','vivacious','blush','wrathful','miscreant','call','striped','wiggly','supreme','hand','impolite','rule','deserted','concern','cover','harbor','waiting','soggy','psychotic','ancient','sponge','domineering','elegant','impartial','unlock','abrasive','count','flight','neighborly','roof','bulb','auspicious','automatic','magic','sign','amusing','orange','branch','sulky','attack','fetch','number','jellyfish','start','alike','touch','sour','wary','minor','punish','connect','protest','pie','kaput','doubtful','friendly','simplistic','smart','vanish','applaud','jumbled','ready','yell','support','squash','raise','parallel','super','jazzy','crush','apathetic','water','food','thrill','permit','heady','last','mine','signal','smoke','preach','x-ray','name','birth','minute','steel','bedroom','female','acrid','riddle','attractive','earth','crack','muscle','alive','guarded','sweater','donkey','doubt','lettuce','magenta','live','farm','glib','bow','fascinated','friend','practise','remember','bleach','hungry','voiceless','pin','sparkling','report','arm','sad','shaggy','parcel','wail','flash','territory','functional','wise','screeching','appliance','future','appear','team','rabbit','porter','paint','flat','amusement','ocean','head','geese','wash','embarrassed','tub','boundless','freezing','mushy','surprise','temporary','marble','recondite','telephone','zipper','pine','reign','pump','tangy','reply','toys','purpose','songs','form','delicious','wood','horn','nutty','fruit','lumber','potato','cheat','cloudy','visit','reduce','destroy','deafening','full','warlike','mitten','cover','earthy','seashore','yarn','tenuous','pause','boil','dogs','tough','knife','shy','naughty','existence','fire','eminent','remove','juice','sleep','voyage','balance','unsightly','plough','ill-fated','pumped','motionless','allow','trade','warm','toad','wave','wall','pigs','circle','rejoice','ear','drink','found','taboo','object','old','temper','plant','public','picayune','blot','delight','carpenter','dispensable','tire','cow','furniture','rightful','mute','gentle','gifted','ragged','stiff','retire','compare','sable','hole','judicious','chilly','sparkle','futuristic','love','bubble','travel','name','numberless','succeed','acoustic','lowly','society','injure','agree','reason','party','wool','careful','hook','bell','ball','attach','scream','development','happy','appreciate','disagree','request','march','rampant','scrape','sack','hair','measure','owe','grubby','vein','boy','punishment','smoggy','wry','immense','shoe','pack','brash','cave','sincere','adorable','fantastic','attraction','racial','jittery','defiant','honey','paper','weight','bee','blind','birthday','toothsome','trick','guard','fog','handle','dirty','salt','rinse','nippy','observe','suggestion','weak','instinctive','frequent','detail','verse','quirky','scattered','toy','aware','distribution','repulsive','draconian','bucket','harm','radiate','bang','shrill','living','rhythm','obsequious','drum','inject','skate','beds','smash','order','stitch','ground','nosy','kick','dusty','home','rot','frame','jam','sky','soap','rescue','energetic','grape','massive','deeply','dazzling','park','pull','number','abundant','barbarous','drag','ajar','close','moan','haircut','shade','married','cats','thirsty','dirt','vagabond','fearful','squealing','squalid','zebra','murky','sheet','fat','follow','bikes','unpack','materialistic','surprise','arch','selection','acoustics','helpless','thoughtful','cry','quarrelsome','arrogant','illegal','sudden','elite','tomatoes','spoil','flower','shivering','front','caption','volcano','ugliest','ambitious','pickle','interrupt','nervous','approve','messy','dust','oceanic','brass','tremble','fine','nerve','lunchroom','hard','engine','erect','flower','cynical','irritating','tight','cobweb','gray','invention','snatch','account','sharp','spooky','squeamish','eatable','share','need','moaning','suspect','rush','rural','false','float','bite','careless','sidewalk','cowardly','stroke','educated','ugly','type','wandering','bolt','mint','fit','large','extra-large','defeated','kitty','tacit','abiding','grandfather','trains','lamp','habitual','fast','offbeat','accurate','many','fortunate','lyrical','charge','illustrious','transport','wakeful','cable','ordinary','string','question','train','fancy','kick','enchanting','jobless','ahead','comparison','loose','dance','add','wonder','stale','earn','reflective','bright','true','statuesque','amount','matter','repair','care','ruin','terrible','elastic','spiders','craven','lamentable','decision','swing','connection','gaudy','knowledge','cheap','lazy','step','dinner','rod','agreement','comb','mean','past','knotty','busy','quicksand','match','early','long','onerous','ambiguous','worried','spade','happen','crook','dapper','grate','announce','plate','haunt','friction','actually','chance','example','rapid','zealous','necessary','ink','mere','shock','huge','jaded','spill','store','fuzzy','table','bottle','halting','spark','end','remain','transport','seat','leg','long-term','clip','grumpy','shake','walk','try','action','soup','short','hurry','square','belligerent','thankful','beginner','small','bumpy','silly','badge','marvelous','wealth','open','unequal','scatter','pest','fool','step','groovy','childlike','door','bouncy','believe','incredible','box','unhealthy','swanky','abrupt','depressed','flaky','famous','detailed','regret','envious','natural','apparel','spare','mark','ten','power','glistening','arrive','animated','slip','heap','shaky','unfasten','contain','inexpensive','introduce','shallow','rule','gather','pump','humorous','acceptable','womanly','giddy','silk','yoke','straw','invite','one','red','growth','unadvised','measly','flap','puzzled','regular','painstaking','little','plain','tumble','rest','fabulous','melt','label','truculent','internal','passenger','zippy','bright','earsplitting','tooth','veil','grip','square','stuff','gate','level','stone','observation','time','workable','bird','realise','spotted','coast','quiver','rebel','entertain','rotten','loss','collect','meal','satisfy','military','bake','cagey','redundant','treatment','knock','blink','scale','board','fair','nebulous','sip','homeless','place','complain','joke','bat','winter','choke','frantic','chubby','highfalutin','troubled','whole','rose','delightful','loaf','afraid','sturdy','class','cultured','yielding','broken','kittens','absurd','discovery','next','disarm','dangerous','lively','reflect','chief','teeny','pencil','ban','grade','size','dashing','thought','breath','empty','hellish','shock','sea','weary','payment','limping','premium','grateful','somber','tax','coach','vulgar','stretch','tow','branch','insurance','yam','stormy','wish','snow','cute','milky','battle','far','roasted','slip','adamant','awake','employ','tangible','tickle','jog','hysterical','meddle','parsimonious','judge','educate','respect','sound','oven','gratis','station','train','purring','steady','carriage','humdrum','voracious','jolly','brainy','proud','elfin','cure','motion','record','quizzical','pail','bike','faithful','approval','vague','fall','store','normal','rock','bear','bounce','giant','satisfying','crooked','lopsided','vest','separate','sneeze','teaching','general','meat','festive','historical','line','north','tip','son','damaging','nimble','broad','list','confuse','first','deserve','steep','last','rich','oval','thick','glow','great','clammy','cheer','untidy','scientific','noise','stomach','undress','big','bite-sized','enter','cake','aloof','abnormal','month','grab','well-groomed','silver','art','berry','giraffe','complete','billowy','thumb','squeal','crib','discussion','even','stretch','mellow','angry','grouchy','absent','snow','middle','stingy','mourn','deep','honorable','nice','verdant','reach','lavish','sin','interest','whine','tug','vengeful','rhyme','stay','upset','hesitant','tent','wire','gold','momentous','yellow','cap','delicate','youthful','twig','burly','devilish','chess','wide','misty','useful','memorise','madly','plants','spectacular','accessible','collar','truck','harmony','uncovered','beef','low','channel','abusive','analyse','observant','snobbish','duck','excellent','intend','wreck','testy','care','shoes','charming','demonic','can','wipe','acidic','watch','decisive','brave','greet','imminent','influence','oranges','seal','eggs','knowledgeable','ashamed','shiny','inconclusive','remind','house','solid','quixotic','describe','support']; return randomElement(words); } function execCommand(client, command, options = {}) { let exePath = 'node ' + joplinAppPath; let cmd = exePath + ' --update-geolocation-disabled --env dev --log-level debug --profile ' + client.profileDir + ' ' + command; logger.info(client.id + ': ' + command); if (options.killAfter) { logger.info('Kill after: ' + options.killAfter); } return new Promise((resolve, reject) => { let childProcess = exec(cmd, (error, stdout, stderr) => { if (error) { if (error.signal == 'SIGTERM') { resolve('Process was killed'); } else { logger.error(stderr); reject(error); } } else { resolve(stdout.trim()); } }); if (options.killAfter) { setTimeout(() => { logger.info('Sending kill signal...'); childProcess.kill(); }, options.killAfter); } }); } async function clientItems(client) { let itemsJson = await execCommand(client, 'dump'); try { return JSON.parse(itemsJson); } catch (error) { throw new Error('Cannot parse JSON: ' + itemsJson); } } function randomTag(items) { let tags = []; for (let i = 0; i < items.length; i++) { if (items[i].type_ != 5) continue; tags.push(items[i]); } return randomElement(tags); } function randomNote(items) { let notes = []; for (let i = 0; i < items.length; i++) { if (items[i].type_ != 1) continue; notes.push(items[i]); } return randomElement(notes); } async function execRandomCommand(client) { let possibleCommands = [ ['mkbook {word}', 40], // CREATE FOLDER ['mknote {word}', 70], // CREATE NOTE [async () => { // DELETE RANDOM ITEM let items = await clientItems(client); let item = randomElement(items); if (!item) return; if (item.type_ == 1) { return execCommand(client, 'rm -f ' + item.id); } else if (item.type_ == 2) { return execCommand(client, 'rm -r -f ' + item.id); } else if (item.type_ == 5) { // tag } else { throw new Error('Unknown type: ' + item.type_); } }, 30], [async () => { // SYNC let avgSyncDuration = averageSyncDuration(); let options = {}; if (!isNaN(avgSyncDuration)) { if (Math.random() >= 0.5) { options.killAfter = avgSyncDuration * Math.random(); } } return execCommand(client, 'sync --random-failures', options); }, 30], [async () => { // UPDATE RANDOM ITEM let items = await clientItems(client); let item = randomNote(items); if (!item) return; return execCommand(client, 'set ' + item.id + ' title "' + randomWord() + '"'); }, 50], [async () => { // ADD TAG let items = await clientItems(client); let note = randomNote(items); if (!note) return; let tag = randomTag(items); let tagTitle = !tag || Math.random() >= 0.9 ? 'tag-' + randomWord() : tag.title; return execCommand(client, 'tag add ' + tagTitle + ' ' + note.id); }, 50], ]; let cmd = null; while (true) { cmd = randomElement(possibleCommands); let r = 1 + Math.floor(Math.random() * 100); if (r <= cmd[1]) break; } cmd = cmd[0]; if (typeof cmd === 'function') { return cmd(); } else { cmd = cmd.replace('{word}', randomWord()); return execCommand(client, cmd); } } function averageSyncDuration() { return lodash.mean(syncDurations); } function randomNextCheckTime() { let output = time.unixMs() + 1000 + Math.random() * 1000 * 120; logger.info('Next sync check: ' + time.unixMsToIso(output) + ' (' + (Math.round((output - time.unixMs()) / 1000)) + ' sec.)'); return output; } function findItem(items, itemId) { for (let i = 0; i < items.length; i++) { if (items[i].id == itemId) return items[i]; } return null; } function compareItems(item1, item2) { let output = []; for (let n in item1) { if (!item1.hasOwnProperty(n)) continue; let p1 = item1[n]; let p2 = item2[n]; if (n == 'notes_') { p1.sort(); p2.sort(); if (JSON.stringify(p1) !== JSON.stringify(p2)) { output.push(n); } } else { if (p1 !== p2) output.push(n); } } return output; } function findMissingItems_(items1, items2) { let output = []; for (let i = 0; i < items1.length; i++) { let item1 = items1[i]; let found = false; for (let j = 0; j < items2.length; j++) { let item2 = items2[j]; if (item1.id == item2.id) { found = true; break; } } if (!found) { output.push(item1); } } return output; } function findMissingItems(items1, items2) { return [ findMissingItems_(items1, items2), findMissingItems_(items2, items1), ]; } async function compareClientItems(clientItems) { let itemCounts = []; for (let i = 0; i < clientItems.length; i++) { let items = clientItems[i]; itemCounts.push(items.length); } logger.info('Item count: ' + itemCounts.join(', ')); let missingItems = findMissingItems(clientItems[0], clientItems[1]); if (missingItems[0].length || missingItems[1].length) { logger.error('Items are different'); logger.error(missingItems); process.exit(1); } let differences = []; let items = clientItems[0]; for (let i = 0; i < items.length; i++) { let item1 = items[i]; for (let clientId = 1; clientId < clientItems.length; clientId++) { let item2 = findItem(clientItems[clientId], item1.id); if (!item2) { logger.error('Item not found on client ' + clientId + ':'); logger.error(item1); process.exit(1); } let diff = compareItems(item1, item2); if (diff.length) { differences.push({ item1: JSON.stringify(item1), item2: JSON.stringify(item2), }); } } } if (differences.length) { logger.error('Found differences between items:'); logger.error(differences); process.exit(1); } } async function main(argv) { await fs.remove(syncDir); let clients = await createClients(); let activeCommandCounts = []; let clientId = 0; for (let i = 0; i < clients.length; i++) { clients[i].activeCommandCount = 0; } function handleCommand(clientId) { if (clients[clientId].activeCommandCount >= 1) return; clients[clientId].activeCommandCount++; execRandomCommand(clients[clientId]).catch((error) => { logger.info('Client ' + clientId + ':'); logger.error(error); }).then((r) => { if (r) { logger.info('Client ' + clientId + ":\n" + r.trim()) } clients[clientId].activeCommandCount--; }); } let nextSyncCheckTime = randomNextCheckTime(); let state = 'commands'; setInterval(async () => { if (state == 'waitForSyncCheck') return; if (state == 'syncCheck') { state = 'waitForSyncCheck'; let clientItems = []; // Up to 3 sync operations must be performed by each clients in order for them // to be perfectly in sync - in order for each items to send their changes // and get those from the other clients, and to also get changes that are // made as a result of a sync operation (eg. renaming a folder that conflicts // with another one). for (let loopCount = 0; loopCount < 3; loopCount++) { for (let i = 0; i < clients.length; i++) { let beforeTime = time.unixMs(); await execCommand(clients[i], 'sync'); syncDurations.push(time.unixMs() - beforeTime); if (syncDurations.length > 20) syncDurations.splice(0, 1); if (loopCount === 2) { let dump = await execCommand(clients[i], 'dump'); clientItems[i] = JSON.parse(dump); } } } await compareClientItems(clientItems); nextSyncCheckTime = randomNextCheckTime(); state = 'commands'; return; } if (state == 'waitForClients') { for (let i = 0; i < clients.length; i++) { if (clients[i].activeCommandCount > 0) return; } state = 'syncCheck'; return; } if (state == 'commands') { if (nextSyncCheckTime <= time.unixMs()) { state = 'waitForClients'; return; } handleCommand(clientId); clientId++; if (clientId >= clients.length) clientId = 0; } }, 100); } main(process.argv).catch((error) => { logger.error(error); });