From be4011cda1b7fe2c7bbb738964d5b1334b15a2f3 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Fri, 25 Dec 2015 17:07:25 -0800 Subject: [PATCH 01/12] Replace old ActiveLock system with a slightly better one CyTube has been crashing recently due to things attempting to release the reference after the channel was already closed (apparently the uncaughtException handler isn't called for this?). This newer implementation keeps track of what is ref'ing and unref'ing it, so it can log an error if it detects a discrepancy. Also changed the server to not delete the refCounter field from the channel when it's unloaded, so that should reduce the number of errors stemming from it being null/undefined. --- src/channel/channel.js | 129 ++++++++++++++++++++-------------- src/channel/kickban.js | 24 ++++--- src/channel/library.js | 11 +-- src/channel/mediarefresher.js | 29 ++++---- src/channel/playlist.js | 104 ++++++--------------------- src/server.js | 4 +- 6 files changed, 138 insertions(+), 163 deletions(-) diff --git a/src/channel/channel.js b/src/channel/channel.js index 6c90c77a..b348ceab 100644 --- a/src/channel/channel.js +++ b/src/channel/channel.js @@ -12,39 +12,65 @@ import * as ChannelStore from '../channel-storage/channelstore'; import { ChannelStateSizeError } from '../errors'; import Promise from 'bluebird'; -/** - * Previously, async channel functions were riddled with race conditions due to - * an event causing the channel to be unloaded while a pending callback still - * needed to reference it. - * - * This solution should be better than constantly checking whether the channel - * has been unloaded in nested callbacks. The channel won't be unloaded until - * nothing needs it anymore. Conceptually similar to a reference count. - */ -function ActiveLock(channel) { - this.channel = channel; - this.count = 0; -} +class ReferenceCounter { + constructor(channel) { + this.channel = channel; + this.channelName = channel.name; + this.refCount = 0; + this.references = {}; + } -ActiveLock.prototype = { - lock: function () { - this.count++; - }, + ref(caller) { + if (caller) { + if (this.references.hasOwnProperty(caller)) { + this.references[caller]++; + } else { + this.references[caller] = 1; + } + } - release: function () { - this.count--; - if (this.count === 0) { - /* sanity check */ - if (this.channel.users.length > 0) { - Logger.errlog.log("Warning: ActiveLock count=0 but users.length > 0 (" + - "channel: " + this.channel.name + ")"); - this.count = this.channel.users.length; + this.refCount++; + } + + unref(caller) { + if (caller) { + if (this.references.hasOwnProperty(caller)) { + this.references[caller]--; + if (this.references[caller] === 0) { + delete this.references[caller]; + } + } else { + Logger.errlog.log("ReferenceCounter::unref() called by caller [" + + caller + "] but this caller had no active references! " + + `(channel: ${this.channelName})`); + } + } + + this.refCount--; + this.checkRefCount(); + } + + checkRefCount() { + if (this.refCount === 0) { + if (Object.keys(this.references).length > 0) { + Logger.errlog.log("ReferenceCounter::refCount reached 0 but still had " + + "active references: " + + JSON.stringify(Object.keys(this.references)) + + ` (channel: ${this.channelName})`); + for (var caller in this.references) { + this.refCount += this.references[caller]; + } + } else if (this.channel.users.length > 0) { + Logger.errlog.log("ReferenceCounter::refCount reached 0 but still had " + + this.channel.users.length + " active users" + + ` (channel: ${this.channelName})`); + this.refCount = this.channel.users.length; } else { this.channel.emit("empty"); } } } -}; +} function Channel(name) { MakeEmitter(this); @@ -54,7 +80,7 @@ function Channel(name) { this.logger = new Logger.Logger(path.join(__dirname, "..", "..", "chanlogs", this.uniqueName + ".log")); this.users = []; - this.activeLock = new ActiveLock(this); + this.refCounter = new ReferenceCounter(this); this.flags = 0; var self = this; db.channels.load(this, function (err) { @@ -238,15 +264,16 @@ Channel.prototype.saveState = function () { }; Channel.prototype.checkModules = function (fn, args, cb) { - var self = this; + const self = this; + const refCaller = `Channel::checkModules/${fn}`; this.waitFlag(Flags.C_READY, function () { - self.activeLock.lock(); + self.refCounter.ref(refCaller); var keys = Object.keys(self.modules); var next = function (err, result) { if (result !== ChannelModule.PASSTHROUGH) { /* Either an error occured, or the module denied the user access */ cb(err, result); - self.activeLock.release(); + self.refCounter.unref(refCaller); return; } @@ -254,7 +281,7 @@ Channel.prototype.checkModules = function (fn, args, cb) { if (m === undefined) { /* No more modules to check */ cb(null, ChannelModule.PASSTHROUGH); - self.activeLock.release(); + self.refCounter.unref(refCaller); return; } @@ -278,13 +305,13 @@ Channel.prototype.notifyModules = function (fn, args) { }; Channel.prototype.joinUser = function (user, data) { - var self = this; + const self = this; - self.activeLock.lock(); + self.refCounter.ref("Channel::user"); self.waitFlag(Flags.C_READY, function () { /* User closed the connection before the channel finished loading */ if (user.socket.disconnected) { - self.activeLock.release(); + self.refCounter.unref("Channel::user"); return; } @@ -293,7 +320,7 @@ Channel.prototype.joinUser = function (user, data) { if (err) { Logger.errlog.log("user.refreshAccount failed at Channel.joinUser"); Logger.errlog.log(err.stack); - self.activeLock.release(); + self.refCounter.unref("Channel::user"); return; } @@ -304,8 +331,10 @@ Channel.prototype.joinUser = function (user, data) { } function afterAccount() { - if (self.dead || user.socket.disconnected) { - if (self.activeLock) self.activeLock.release(); + if (user.socket.disconnected) { + self.refCounter.unref("Channel::user"); + return; + } else if (self.dead) { return; } @@ -318,9 +347,7 @@ Channel.prototype.joinUser = function (user, data) { } else { user.account.channelRank = 0; user.account.effectiveRank = user.account.globalRank; - if (self.activeLock) { - self.activeLock.release(); - } + self.refCounter.unref("Channel::user"); } }); } @@ -408,7 +435,7 @@ Channel.prototype.partUser = function (user) { }); this.sendUsercount(this.users); - this.activeLock.release(); + this.refCounter.unref("Channel::user"); user.die(); }; @@ -555,20 +582,20 @@ Channel.prototype.sendUserJoin = function (users, user) { }; Channel.prototype.readLog = function (cb) { - var maxLen = 102400; - var file = this.logger.filename; - this.activeLock.lock(); - var self = this; + const maxLen = 102400; + const file = this.logger.filename; + this.refCounter.ref("Channel::readLog"); + const self = this; fs.stat(file, function (err, data) { if (err) { - self.activeLock.release(); + self.refCounter.unref("readLog"); return cb(err, null); } - var start = Math.max(data.size - maxLen, 0); - var end = data.size - 1; + const start = Math.max(data.size - maxLen, 0); + const end = data.size - 1; - var read = fs.createReadStream(file, { + const read = fs.createReadStream(file, { start: start, end: end }); @@ -579,7 +606,7 @@ Channel.prototype.readLog = function (cb) { }); read.on("end", function () { cb(null, buffer); - self.activeLock.release(); + self.refCounter.unref("Channel::readLog"); }); }); }; @@ -648,7 +675,7 @@ Channel.prototype.packInfo = function (isAdmin) { } if (isAdmin) { - data.activeLockCount = this.activeLock.count; + data.activeLockCount = this.refCounter.refCount; } var self = this; diff --git a/src/channel/kickban.js b/src/channel/kickban.js index c9f306aa..0d11e0c9 100644 --- a/src/channel/kickban.js +++ b/src/channel/kickban.js @@ -73,9 +73,10 @@ KickBanModule.prototype.onUserPostJoin = function (user) { return; } - var chan = this.channel; + const chan = this.channel; + const refCaller = "KickBanModule::onUserPostJoin"; user.waitFlag(Flags.U_LOGGED_IN, function () { - chan.activeLock.lock(); + chan.refCounter.ref(refCaller); db.channels.isNameBanned(chan.name, user.getName(), function (err, banned) { if (!err && banned) { user.kick("You are banned from this channel."); @@ -84,7 +85,7 @@ KickBanModule.prototype.onUserPostJoin = function (user) { "name is banned)"); } } - chan.activeLock.release(); + chan.refCounter.unref(refCaller); }); }); @@ -222,10 +223,10 @@ KickBanModule.prototype.handleCmdBan = function (user, msg, meta) { var name = args.shift().toLowerCase(); var reason = args.join(" "); - var chan = this.channel; - chan.activeLock.lock(); + const chan = this.channel; + chan.refCounter.ref("KickBanModule::handleCmdBan"); this.banName(user, name, reason, function (err) { - chan.activeLock.release(); + chan.refCounter.unref("KickBanModule::handleCmdBan"); }); }; @@ -249,10 +250,10 @@ KickBanModule.prototype.handleCmdIPBan = function (user, msg, meta) { } var reason = args.join(" "); - var chan = this.channel; - chan.activeLock.lock(); + const chan = this.channel; + chan.refCounter.ref("KickBanModule::handleCmdIPBan"); this.banAll(user, name, range, reason, function (err) { - chan.activeLock.release(); + chan.refCounter.unref("KickBanModule::handleCmdIPBan"); }); }; @@ -416,9 +417,10 @@ KickBanModule.prototype.handleUnban = function (user, data) { } var self = this; - this.channel.activeLock.lock(); + this.channel.refCounter.ref("KickBanModule::handleUnban"); db.channels.unbanId(this.channel.name, data.id, function (err) { if (err) { + self.channel.refCounter.unref("KickBanModule::handleUnban"); return user.socket.emit("errorMsg", { msg: err }); @@ -431,7 +433,7 @@ KickBanModule.prototype.handleUnban = function (user, data) { self.channel.modules.chat.sendModMessage(user.getName() + " unbanned " + data.name, banperm); } - self.channel.activeLock.release(); + self.channel.refCounter.unref("KickBanModule::handleUnban"); }); }; diff --git a/src/channel/library.js b/src/channel/library.js index 2d8eeb78..afd6c0ca 100644 --- a/src/channel/library.js +++ b/src/channel/library.js @@ -51,16 +51,19 @@ LibraryModule.prototype.handleUncache = function (user, data) { return; } - var chan = this.channel; - chan.activeLock.lock(); + const chan = this.channel; + chan.refCounter.ref("LibraryModule::handleUncache"); db.channels.deleteFromLibrary(chan.name, data.id, function (err, res) { - if (chan.dead || err) { + if (chan.dead) { + return; + } else if (err) { + chan.refCounter.unref("LibraryModule::handleUncache"); return; } chan.logger.log("[library] " + user.getName() + " deleted " + data.id + "from the library"); - chan.activeLock.release(); + chan.refCounter.unref("LibraryModule::handleUncache"); }); }; diff --git a/src/channel/mediarefresher.js b/src/channel/mediarefresher.js index 228a0b7e..a08b39b1 100644 --- a/src/channel/mediarefresher.js +++ b/src/channel/mediarefresher.js @@ -63,8 +63,8 @@ MediaRefresherModule.prototype.initVimeo = function (data, cb) { return; } - var self = this; - self.channel.activeLock.lock(); + const self = this; + self.channel.refCounter.ref("MediaRefresherModule::initVimeo"); Vimeo.extract(data.id).then(function (direct) { if (self.dead || self.channel.dead) return; @@ -74,12 +74,13 @@ MediaRefresherModule.prototype.initVimeo = function (data, cb) { self.channel.logger.log("[mediarefresher] Refreshed vimeo video with ID " + data.id); } - self.channel.activeLock.release(); if (cb) cb(); }).catch(function (err) { Logger.errlog.log("Unexpected vimeo::extract() fail: " + err.stack); if (cb) cb(); + }).finally(() => { + self.channel.refCounter.unref("MediaRefresherModule::initVimeo"); }); }; @@ -90,7 +91,7 @@ MediaRefresherModule.prototype.refreshGoogleDocs = function (media, cb) { return; } - self.channel.activeLock.lock(); + self.channel.refCounter.ref("MediaRefresherModule::refreshGoogleDocs"); InfoGetter.getMedia(media.id, "gd", function (err, data) { if (self.dead || self.channel.dead) { return; @@ -108,7 +109,7 @@ MediaRefresherModule.prototype.refreshGoogleDocs = function (media, cb) { self.channel.logger.log("[mediarefresher] Google Docs refresh failed " + "(likely redirect to login page-- make sure it is shared " + "correctly)"); - self.channel.activeLock.release(); + self.channel.refCounter.unref("MediaRefresherModule::refreshGoogleDocs"); if (cb) cb(); return; case "Access Denied": @@ -119,7 +120,7 @@ MediaRefresherModule.prototype.refreshGoogleDocs = function (media, cb) { case "Google Drive videos must be shared publicly": self.channel.logger.log("[mediarefresher] Google Docs refresh failed: " + err); - self.channel.activeLock.release(); + self.channel.refCounter.unref("MediaRefresherModule::refreshGoogleDocs"); if (cb) cb(); return; default: @@ -128,14 +129,14 @@ MediaRefresherModule.prototype.refreshGoogleDocs = function (media, cb) { err); Logger.errlog.log("Google Docs refresh failed for ID " + media.id + ": " + err); - self.channel.activeLock.release(); + self.channel.refCounter.unref("MediaRefresherModule::refreshGoogleDocs"); if (cb) cb(); return; } } if (media !== self._media) { - self.channel.activeLock.release(); + self.channel.refCounter.unref("MediaRefresherModule::refreshGoogleDocs"); if (cb) cb(); return; } @@ -143,7 +144,7 @@ MediaRefresherModule.prototype.refreshGoogleDocs = function (media, cb) { self.channel.logger.log("[mediarefresher] Refreshed Google Docs video with ID " + media.id); media.meta = data.meta; - self.channel.activeLock.release(); + self.channel.refCounter.unref("MediaRefresherModule::refreshGoogleDocs"); if (cb) cb(); }); }; @@ -155,7 +156,7 @@ MediaRefresherModule.prototype.initGooglePlus = function (media, cb) { return; } - self.channel.activeLock.lock(); + self.channel.refCounter.ref("MediaRefresherModule::initGooglePlus"); InfoGetter.getMedia(media.id, "gp", function (err, data) { if (self.dead || self.channel.dead) { return; @@ -177,7 +178,7 @@ MediaRefresherModule.prototype.initGooglePlus = function (media, cb) { "and is shared publicly"): self.channel.logger.log("[mediarefresher] Google+ refresh failed: " + err); - self.channel.activeLock.release(); + self.channel.refCounter.unref("MediaRefresherModule::initGooglePlus"); if (cb) cb(); return; default: @@ -186,14 +187,14 @@ MediaRefresherModule.prototype.initGooglePlus = function (media, cb) { err); Logger.errlog.log("Google+ refresh failed for ID " + media.id + ": " + err); - self.channel.activeLock.release(); + self.channel.refCounter.unref("MediaRefresherModule::initGooglePlus"); if (cb) cb(); return; } } if (media !== self._media) { - self.channel.activeLock.release(); + self.channel.refCounter.unref("MediaRefresherModule::initGooglePlus"); if (cb) cb(); return; } @@ -201,7 +202,7 @@ MediaRefresherModule.prototype.initGooglePlus = function (media, cb) { self.channel.logger.log("[mediarefresher] Refreshed Google+ video with ID " + media.id); media.meta = data.meta; - self.channel.activeLock.release(); + self.channel.refCounter.unref("MediaRefresherModule::initGooglePlus"); if (cb) cb(); }); }; diff --git a/src/channel/playlist.js b/src/channel/playlist.js index 295c6a56..0beb9777 100644 --- a/src/channel/playlist.js +++ b/src/channel/playlist.js @@ -447,15 +447,15 @@ PlaylistModule.prototype.queueStandard = function (user, data) { }); }; - var self = this; - this.channel.activeLock.lock(); + const self = this; + this.channel.refCounter.ref("PlaylistModule::queueStandard"); this.semaphore.queue(function (lock) { var lib = self.channel.modules.library; if (lib && self.channel.is(Flags.C_REGISTERED) && !util.isLive(data.type)) { lib.getItem(data.id, function (err, item) { if (err && err !== "Item not in library") { error(err+""); - self.channel.activeLock.release(); + self.channel.refCounter.unref("PlaylistModule::queueStandard"); return lock.release(); } @@ -464,7 +464,7 @@ PlaylistModule.prototype.queueStandard = function (user, data) { data.shouldAddToLibrary = false; self._addItem(item, data, user, function () { lock.release(); - self.channel.activeLock.release(); + self.channel.refCounter.unref("PlaylistModule::queueStandard"); }); } else { handleLookup(); @@ -479,25 +479,13 @@ PlaylistModule.prototype.queueStandard = function (user, data) { InfoGetter.getMedia(data.id, data.type, function (err, media) { if (err) { error(XSS.sanitizeText(String(err))); - if (self.channel && self.channel.activeLock) { - self.channel.activeLock.release(); - } else { - Logger.errlog.log("Attempted release of channel lock after " + - "channel was already unloaded in queueStandard: " + - channelName + " " + data.type + ":" + data.id); - } + self.channel.refCounter.unref("PlaylistModule::queueStandard"); return lock.release(); } self._addItem(media, data, user, function () { lock.release(); - if (self.channel && self.channel.activeLock) { - self.channel.activeLock.release(); - } else { - Logger.errlog.log("Attempted release of channel lock after " + - "channel was already unloaded in queueStandard: " + - channelName + " " + data.type + ":" + data.id); - } + self.channel.refCounter.unref("PlaylistModule::queueStandard"); }); }); } @@ -536,12 +524,12 @@ PlaylistModule.prototype.queueYouTubePlaylist = function (user, data) { } } - self.channel.activeLock.lock(); + self.channel.refCounter.ref("PlaylistModule::queueYouTubePlaylist"); vids.forEach(function (media) { data.link = util.formatLink(media.id, media.type); self._addItem(media, data, user); }); - self.channel.activeLock.release(); + self.channel.refCounter.unref("PlaylistModule::queueYouTubePlaylist"); lock.release(); }); @@ -560,7 +548,7 @@ PlaylistModule.prototype.handleDelete = function (user, data) { } var plitem = this.items.find(data); - self.channel.activeLock.lock(); + self.channel.refCounter.ref("PlaylistModule::handleDelete"); this.semaphore.queue(function (lock) { if (self._delete(data)) { self.channel.logger.log("[playlist] " + user.getName() + " deleted " + @@ -568,7 +556,7 @@ PlaylistModule.prototype.handleDelete = function (user, data) { } lock.release(); - self.channel.activeLock.release(); + self.channel.refCounter.unref("PlaylistModule::handleDelete"); }); }; @@ -602,27 +590,27 @@ PlaylistModule.prototype.handleMoveMedia = function (user, data) { return; } - var self = this; - self.channel.activeLock.lock(); + const self = this; + self.channel.refCounter.ref("PlaylistModule::handleMoveMedia"); self.semaphore.queue(function (lock) { if (!self.items.remove(data.from)) { - self.channel.activeLock.release(); + self.channel.refCounter.unref("PlaylistModule::handleMoveMedia"); return lock.release(); } if (data.after === "prepend") { if (!self.items.prepend(from)) { - self.channel.activeLock.release(); + self.channel.refCounter.unref("PlaylistModule::handleMoveMedia"); return lock.release(); } } else if (data.after === "append") { if (!self.items.append(from)) { - self.channel.activeLock.release(); + self.channel.refCounter.unref("PlaylistModule::handleMoveMedia"); return lock.release(); } } else { if (!self.items.insertAfter(from, data.after)) { - self.channel.activeLock.release(); + self.channel.refCounter.unref("PlaylistModule::handleMoveMedia"); return lock.release(); } } @@ -633,7 +621,7 @@ PlaylistModule.prototype.handleMoveMedia = function (user, data) { from.media.title + (after ? " after " + after.media.title : "")); lock.release(); - self.channel.activeLock.release(); + self.channel.refCounter.unref("PlaylistModule::handleMoveMedia"); }); }; @@ -1128,55 +1116,6 @@ PlaylistModule.prototype._leadLoop = function() { } }; -PlaylistModule.prototype.refreshGoogleDocs = function (cb) { - var self = this; - - if (self.dead || !self.channel || self.channel.dead) { - return; - } - - var abort = function () { - if (self.current) { - self.current.media.meta.object = self.current.media.meta.object || null; - self.current.media.meta.failed = true; - } - if (cb) { - cb(); - } - }; - - if (!this.current || this.current.media.type !== "gd") { - return abort(); - } - - self.channel.activeLock.lock(); - InfoGetter.getMedia(this.current.media.id, "gd", function (err, media) { - if (err) { - Logger.errlog.log("Google Docs autorefresh failed: " + err); - Logger.errlog.log("ID was: " + self.current.media.id); - if (self.current) { - self.current.media.meta.object = self.current.media.meta.object || null; - self.current.media.meta.failed = true; - } - if (cb) { - cb(); - } - self.channel.activeLock.release(); - } else { - if (!self.current || self.current.media.type !== "gd") { - self.channel.activeLock.release(); - return abort(); - } - - self.current.media.meta = media.meta; - self.current.media.meta.expiration = Date.now() + 3600000; - self.channel.logger.log("[playlist] Auto-refreshed Google Doc video"); - cb && cb(); - self.channel.activeLock.release(); - } - }); -}; - PlaylistModule.prototype._playNext = function () { if (!this.current) { return; @@ -1335,10 +1274,11 @@ PlaylistModule.prototype.handleQueuePlaylist = function (user, data) { pos: data.pos }; - var self = this; - self.channel.activeLock.lock(); + const self = this; + self.channel.refCounter.ref("PlaylistModule::handleQueuePlaylist"); db.getUserPlaylist(user.getName(), data.name, function (err, pl) { if (err) { + self.channel.refCounter.unref("PlaylistModule::handleQueuePlaylist"); return user.socket.emit("errorMsg", { msg: "Playlist load failed: " + err }); @@ -1369,7 +1309,6 @@ PlaylistModule.prototype.handleQueuePlaylist = function (user, data) { var m = new Media(item.id, item.title, item.seconds, item.type, item.meta); self._addItem(m, qdata, user); }); - self.channel.activeLock.release(); } catch (e) { Logger.errlog.log("Loading user playlist failed!"); Logger.errlog.log("PL: " + user.getName() + "-" + data.name); @@ -1378,7 +1317,8 @@ PlaylistModule.prototype.handleQueuePlaylist = function (user, data) { msg: "Internal error occurred when loading playlist.", link: null }); - self.channel.activeLock.release(); + } finally { + self.channel.refCounter.unref("PlaylistModule::handleQueuePlaylist"); } }); }; diff --git a/src/server.js b/src/server.js index fd79c9f1..290e9b29 100644 --- a/src/server.js +++ b/src/server.js @@ -208,7 +208,9 @@ Server.prototype.unloadChannel = function (chan) { // Empty all outward references from the channel var keys = Object.keys(chan); for (var i in keys) { - delete chan[keys[i]]; + if (keys[i] !== "refCounter") { + delete chan[keys[i]]; + } } chan.dead = true; }; From 865a7453d9e742babffbe4013279ab3b19241b24 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 3 Jan 2016 22:53:29 -0800 Subject: [PATCH 02/12] Undo HD layout before applying synchtube, fluid (#549) --- www/js/util.js | 73 +++++++++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/www/js/util.js b/www/js/util.js index a2c3ff43..942add40 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -1575,6 +1575,39 @@ function pingMessage(isHighlight) { /* layouts */ +function undoHDLayout() { + $("body").removeClass("hd"); + $("#drinkbar").detach().removeClass().addClass("col-lg-12 col-md-12") + .appendTo("#drinkbarwrap"); + $("#chatwrap").detach().removeClass().addClass("col-lg-5 col-md-5") + .appendTo("#main"); + $("#videowrap").detach().removeClass().addClass("col-lg-7 col-md-7") + .appendTo("#main"); + + $("#leftcontrols").detach().removeClass().addClass("col-lg-5 col-md-5") + .prependTo("#controlsrow"); + + $("#plcontrol").detach().appendTo("#rightcontrols"); + $("#videocontrols").detach().appendTo("#rightcontrols"); + + $("#playlistrow").prepend('
'); + $("#leftpane").append('
'); + + $("#pollwrap").detach().removeClass().addClass("col-lg-12 col-md-12") + .appendTo("#leftpane-inner"); + $("#playlistmanagerwrap").detach().removeClass().addClass("col-lg-12 col-md-12") + .css("margin-top", "10px") + .appendTo("#leftpane-inner"); + + $("#rightpane").detach().removeClass().addClass("col-lg-7 col-md-7") + .appendTo("#playlistrow"); + + $("nav").addClass("navbar-fixed-top"); + $("#mainpage").css("padding-top", "60px"); + $("#queue").css("max-height", "500px"); + $("#messagebuffer, #userlist").css("max-height", ""); +} + function compactLayout() { /* Undo synchtube layout */ if ($("body").hasClass("synchtube")) { @@ -1597,36 +1630,7 @@ function compactLayout() { /* Undo HD layout */ if ($("body").hasClass("hd")) { - $("body").removeClass("hd"); - $("#drinkbar").detach().removeClass().addClass("col-lg-12 col-md-12") - .appendTo("#drinkbarwrap"); - $("#chatwrap").detach().removeClass().addClass("col-lg-5 col-md-5") - .appendTo("#main"); - $("#videowrap").detach().removeClass().addClass("col-lg-7 col-md-7") - .appendTo("#main"); - - $("#leftcontrols").detach().removeClass().addClass("col-lg-5 col-md-5") - .prependTo("#controlsrow"); - - $("#plcontrol").detach().appendTo("#rightcontrols"); - $("#videocontrols").detach().appendTo("#rightcontrols"); - - $("#playlistrow").prepend('
'); - $("#leftpane").append('
'); - - $("#pollwrap").detach().removeClass().addClass("col-lg-12 col-md-12") - .appendTo("#leftpane-inner"); - $("#playlistmanagerwrap").detach().removeClass().addClass("col-lg-12 col-md-12") - .css("margin-top", "10px") - .appendTo("#leftpane-inner"); - - $("#rightpane").detach().removeClass().addClass("col-lg-7 col-md-7") - .appendTo("#playlistrow"); - - $("nav").addClass("navbar-fixed-top"); - $("#mainpage").css("padding-top", "60px"); - $("#queue").css("max-height", "500px"); - $("#messagebuffer, #userlist").css("max-height", ""); + undoHDLayout(); } $("body").addClass("compact"); @@ -1634,6 +1638,9 @@ function compactLayout() { } function fluidLayout() { + if ($("body").hasClass("hd")) { + undoHDLayout(); + } $(".container").removeClass("container").addClass("container-fluid"); $("footer .container-fluid").removeClass("container-fluid").addClass("container"); $("body").addClass("fluid"); @@ -1641,6 +1648,9 @@ function fluidLayout() { } function synchtubeLayout() { + if ($("body").hasClass("hd")) { + undoHDLayout(); + } if($("#userlisttoggle").hasClass("glyphicon-chevron-right")){ $("#userlisttoggle").removeClass("glyphicon-chevron-right").addClass("glyphicon-chevron-left") } @@ -1652,6 +1662,9 @@ function synchtubeLayout() { $("body").addClass("synchtube"); } +/* + * "HD" is kind of a misnomer. Should be renamed at some point. + */ function hdLayout() { var videowrap = $("#videowrap"), chatwrap = $("#chatwrap"), From 1ac69709eef05d8604b1d3703ba28b870ec2e8d2 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Mon, 4 Jan 2016 20:35:02 -0800 Subject: [PATCH 03/12] Minor fix to refcounter logic --- src/channel/channel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/channel/channel.js b/src/channel/channel.js index b348ceab..06342694 100644 --- a/src/channel/channel.js +++ b/src/channel/channel.js @@ -588,7 +588,7 @@ Channel.prototype.readLog = function (cb) { const self = this; fs.stat(file, function (err, data) { if (err) { - self.refCounter.unref("readLog"); + self.refCounter.unref("Channel::readLog"); return cb(err, null); } From eeaffe1f617add92580c6b0240074887a04b8a3a Mon Sep 17 00:00:00 2001 From: calzoneman Date: Wed, 6 Jan 2016 21:42:48 -0800 Subject: [PATCH 04/12] Update socket.io to version 1.4.0 --- NEWS.md | 10 ++++++++++ config.template.yaml | 5 +++++ package.json | 4 ++-- src/config.js | 3 ++- src/counters.js | 11 ++++++++++- src/database.js | 2 +- src/io/ioserver.js | 7 +++++-- 7 files changed, 35 insertions(+), 7 deletions(-) diff --git a/NEWS.md b/NEWS.md index 4ad698a4..aec341ba 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,13 @@ +2016-01-06 +========== + +This release updates socket.io to version 1.4.0. The updates to socket.io +include a few security-related fixes, so please be sure to run `npm install` +to ensure the updated version is installed before restarting your CyTube server. + + * https://nodesecurity.io/advisories/67 + * https://github.com/socketio/engine.io/commit/391ce0dc8b88a6609d88db83ea064040a05ab803 + 2015-10-25 ========== diff --git a/config.template.yaml b/config.template.yaml index f50e2a20..5930aec4 100644 --- a/config.template.yaml +++ b/config.template.yaml @@ -105,6 +105,11 @@ io: default-port: 1337 # limit the number of concurrent socket connections per IP address ip-connection-limit: 10 + # Whether or not to use zlib to compress each socket message (this option is + # passed through to socket.io/engine.io). + # Note that while this may save a little bandwidth, it also consumes a lot + # more CPU and will bottleneck pretty quickly under heavy load. + per-message-deflate: false # Mailer details (used for sending password reset links) # see https://github.com/andris9/Nodemailer diff --git a/package.json b/package.json index 27969620..a27b0ab8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Calvin Montgomery", "name": "CyTube", "description": "Online media synchronizer and chat", - "version": "3.12.1", + "version": "3.13.0", "repository": { "url": "http://github.com/calzoneman/sync" }, @@ -33,7 +33,7 @@ "q": "^1.4.1", "sanitize-html": "git://github.com/calzoneman/sanitize-html", "serve-static": "^1.10.0", - "socket.io": "^1.3.7", + "socket.io": "^1.4.0", "source-map-support": "^0.3.2", "status-message-polyfill": "calzoneman/status-message-polyfill", "yamljs": "^0.1.6" diff --git a/src/config.js b/src/config.js index 5fa5a93b..e437efb8 100644 --- a/src/config.js +++ b/src/config.js @@ -50,7 +50,8 @@ var defaults = { io: { domain: "http://localhost", "default-port": 1337, - "ip-connection-limit": 10 + "ip-connection-limit": 10, + "per-message-deflate": false }, mail: { enabled: false, diff --git a/src/counters.js b/src/counters.js index 21f679a4..7945fe8b 100644 --- a/src/counters.js +++ b/src/counters.js @@ -25,11 +25,20 @@ Socket.prototype.packet = function () { exports.add('socket.io:packet'); }; +function getConnectedSockets() { + var sockets = io.instance.sockets.sockets; + if (typeof sockets.length === 'number') { + return sockets.length; + } else { + return Object.keys(sockets).length; + } +} + setInterval(function () { try { counters['memory:rss'] = process.memoryUsage().rss / 1048576; counters['load:1min'] = os.loadavg()[0]; - counters['socket.io:count'] = io.instance.sockets.sockets.length; + counters['socket.io:count'] = getConnectedSockets(); counterLog.log(JSON.stringify(counters)); } catch (e) { Logger.errlog.log(e.stack); diff --git a/src/database.js b/src/database.js index c26883ee..cfdf14e0 100644 --- a/src/database.js +++ b/src/database.js @@ -583,7 +583,7 @@ module.exports.loadAnnouncement = function () { var sv = Server.getServer(); sv.announcement = announcement; for (var id in sv.ioServers) { - sv.ioServers[id].sockets.emit("announcement", announcement); + sv.ioServers[id].emit("announcement", announcement); } }); }; diff --git a/src/io/ioserver.js b/src/io/ioserver.js index 0f625783..d2e95a87 100644 --- a/src/io/ioserver.js +++ b/src/io/ioserver.js @@ -242,6 +242,9 @@ function handleConnection(sock) { module.exports = { init: function (srv, webConfig) { var bound = {}; + const ioOptions = { + perMessageDeflate: Config.get("io.per-message-deflate") + }; var io = sio.instance = sio(); io.use(handleAuth); @@ -259,7 +262,7 @@ module.exports = { } if (id in srv.servers) { - io.attach(srv.servers[id]); + io.attach(srv.servers[id], ioOptions); } else { var server = require("http").createServer().listen(bind.port, bind.ip); server.on("clientError", function (err, socket) { @@ -268,7 +271,7 @@ module.exports = { } catch (e) { } }); - io.attach(server); + io.attach(server, ioOptions); } bound[id] = null; From f46891b6ed15cd5db1c8fef1c471bbe4eb66b5ac Mon Sep 17 00:00:00 2001 From: calzoneman Date: Thu, 7 Jan 2016 17:38:05 -0800 Subject: [PATCH 05/12] Defer to mediaquery for anonymous vimeo lookup --- package.json | 2 +- src/get-info.js | 50 ++++++------------------------------------------- 2 files changed, 7 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index a27b0ab8..2ab706c2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Calvin Montgomery", "name": "CyTube", "description": "Online media synchronizer and chat", - "version": "3.13.0", + "version": "3.13.1", "repository": { "url": "http://github.com/calzoneman/sync" }, diff --git a/src/get-info.js b/src/get-info.js index 88955243..9e23f8b3 100644 --- a/src/get-info.js +++ b/src/get-info.js @@ -9,6 +9,7 @@ var Config = require("./config"); var ffmpeg = require("./ffmpeg"); var mediaquery = require("cytube-mediaquery"); var YouTube = require("cytube-mediaquery/lib/provider/youtube"); +var Vimeo = require("cytube-mediaquery/lib/provider/vimeo"); /* * Preference map of quality => youtube formats. @@ -158,50 +159,11 @@ var Getters = { return Getters.vi_oauth(id, callback); } - var options = { - host: "vimeo.com", - port: 443, - path: "/api/v2/video/" + id + ".json", - method: "GET", - dataType: "jsonp", - timeout: 1000 - }; - - urlRetrieve(https, options, function (status, data) { - switch (status) { - case 200: - break; /* Request is OK, skip to handling data */ - case 400: - return callback("Invalid request", null); - case 403: - return callback("Private video", null); - case 404: - return callback("Video not found", null); - case 500: - case 503: - return callback("Service unavailable", null); - default: - return callback("HTTP " + status, null); - } - - try { - data = JSON.parse(data); - data = data[0]; - var seconds = data.duration; - var title = data.title; - var media = new Media(id, title, seconds, "vi"); - callback(false, media); - } catch(e) { - var err = e; - /** - * This should no longer be necessary as the outer handler - * checks for HTTP 404 - */ - if (buffer.match(/not found/)) - err = "Video not found"; - - callback(err, null); - } + Vimeo.lookup(id).then(video => { + video = new Media(video.id, video.title, video.duration, "vi"); + callback(null, video); + }).catch(error => { + callback(error.message); }); }, From be0759069ee56a0249725be57538badb78231084 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Thu, 7 Jan 2016 22:15:21 -0800 Subject: [PATCH 06/12] package: bump cytubefilters --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2ab706c2..2de76727 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Calvin Montgomery", "name": "CyTube", "description": "Online media synchronizer and chat", - "version": "3.13.1", + "version": "3.14.0", "repository": { "url": "http://github.com/calzoneman/sync" }, @@ -19,7 +19,7 @@ "create-error": "^0.3.1", "csrf": "^3.0.0", "cytube-mediaquery": "git://github.com/CyTube/mediaquery", - "cytubefilters": "git://github.com/calzoneman/cytubefilters#095b7956", + "cytubefilters": "git://github.com/calzoneman/cytubefilters#f81ee514", "express": "^4.13.3", "express-minify": "^0.1.6", "graceful-fs": "^4.1.2", From d7da01a7d04e49c93c14a518820bdedec5a32888 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Fri, 8 Jan 2016 00:08:08 -0800 Subject: [PATCH 07/12] package: bump cytubefilters --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2de76727..199608fb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Calvin Montgomery", "name": "CyTube", "description": "Online media synchronizer and chat", - "version": "3.14.0", + "version": "3.14.1", "repository": { "url": "http://github.com/calzoneman/sync" }, @@ -19,7 +19,7 @@ "create-error": "^0.3.1", "csrf": "^3.0.0", "cytube-mediaquery": "git://github.com/CyTube/mediaquery", - "cytubefilters": "git://github.com/calzoneman/cytubefilters#f81ee514", + "cytubefilters": "git://github.com/calzoneman/cytubefilters#67c7c69a", "express": "^4.13.3", "express-minify": "^0.1.6", "graceful-fs": "^4.1.2", From eba787942cfdfd94c51b002da0b2cbbc1ca08001 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sat, 9 Jan 2016 11:59:23 -0800 Subject: [PATCH 08/12] package: bump source-map-support --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 199608fb..437bd331 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Calvin Montgomery", "name": "CyTube", "description": "Online media synchronizer and chat", - "version": "3.14.1", + "version": "3.14.2", "repository": { "url": "http://github.com/calzoneman/sync" }, @@ -34,7 +34,7 @@ "sanitize-html": "git://github.com/calzoneman/sanitize-html", "serve-static": "^1.10.0", "socket.io": "^1.4.0", - "source-map-support": "^0.3.2", + "source-map-support": "^0.4.0", "status-message-polyfill": "calzoneman/status-message-polyfill", "yamljs": "^0.1.6" }, From ba54848db57662102da3cab9190f7b2c96003dea Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sat, 30 Jan 2016 19:42:55 -0800 Subject: [PATCH 09/12] mediarefresher: fix memory leak from dangling timers --- src/channel/mediarefresher.js | 15 ++++++++++++++- src/server.js | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/channel/mediarefresher.js b/src/channel/mediarefresher.js index a08b39b1..411d0512 100644 --- a/src/channel/mediarefresher.js +++ b/src/channel/mediarefresher.js @@ -44,6 +44,15 @@ MediaRefresherModule.prototype.onPreMediaChange = function (data, cb) { } }; +MediaRefresherModule.prototype.unload = function () { + try { + clearInterval(this._interval); + this._interval = null; + } catch (error) { + Logger.errlog.log(error.stack); + } +}; + MediaRefresherModule.prototype.initGoogleDocs = function (data, cb) { var self = this; self.refreshGoogleDocs(data, cb); @@ -66,8 +75,10 @@ MediaRefresherModule.prototype.initVimeo = function (data, cb) { const self = this; self.channel.refCounter.ref("MediaRefresherModule::initVimeo"); Vimeo.extract(data.id).then(function (direct) { - if (self.dead || self.channel.dead) + if (self.dead || self.channel.dead) { + self.unload(); return; + } if (self._media === data) { data.meta.direct = direct; @@ -88,6 +99,7 @@ MediaRefresherModule.prototype.refreshGoogleDocs = function (media, cb) { var self = this; if (self.dead || self.channel.dead) { + self.unload(); return; } @@ -153,6 +165,7 @@ MediaRefresherModule.prototype.initGooglePlus = function (media, cb) { var self = this; if (self.dead || self.channel.dead) { + self.unload(); return; } diff --git a/src/server.js b/src/server.js index 290e9b29..795efece 100644 --- a/src/server.js +++ b/src/server.js @@ -195,6 +195,24 @@ Server.prototype.unloadChannel = function (chan) { chan.notifyModules("unload", []); Object.keys(chan.modules).forEach(function (k) { chan.modules[k].dead = true; + /* + * Automatically clean up any timeouts/intervals assigned + * to properties of channel modules. Prevents a memory leak + * in case of forgetting to clear the timer on the "unload" + * module event. + */ + Object.keys(chan.modules[k]).forEach(function (prop) { + if (chan.modules[k][prop] && chan.modules[k][prop]._onTimeout) { + Logger.errlog.log("Warning: detected non-null timer when unloading " + + "module " + k + ": " + prop); + try { + clearTimeout(chan.modules[k][prop]); + clearInterval(chan.modules[k][prop]); + } catch (error) { + Logger.errlog.log(error.stack); + } + } + }); }); for (var i = 0; i < this.channels.length; i++) { From 65d4ea94965f15be8ee78904cf036111010800cc Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sun, 31 Jan 2016 11:17:19 -0800 Subject: [PATCH 10/12] Fix #555 --- templates/nav.jade | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/nav.jade b/templates/nav.jade index 3645e99d..932f0c02 100644 --- a/templates/nav.jade +++ b/templates/nav.jade @@ -27,7 +27,7 @@ mixin navdefaultlinks(page) b.caret ul.dropdown-menu if loggedIn - li: a(href="/logout?dest=#{encodeURIComponent(baseUrl + page)}&_csrf=#{csrfToken}") Logout + li: a(href="javascript:$('#logoutform').submit();") Log out li.divider li: a(href="#{loginDomain}/account/channels") Channels li: a(href="#{loginDomain}/account/profile") Profile @@ -72,5 +72,5 @@ mixin navlogoutform(redirect) input(type="hidden", name="_csrf", value=csrfToken) span#welcome Welcome, #{loginName} span  ·  - input#logout.navbar-link(type="submit", value="Logout") + input#logout.navbar-link(type="submit", value="Log out") From b3c85e8534a0cf72d226febe2c0234c420fec1ef Mon Sep 17 00:00:00 2001 From: calzoneman Date: Sat, 6 Feb 2016 19:40:50 -0800 Subject: [PATCH 11/12] Limit requestPlaylist to once per 60 seconds If clients call it quickly in succession with large playlists, it can cause node to get stuck stringifying socket.io frames and cause an out of memory crash. --- package.json | 2 +- src/channel/playlist.js | 23 ++++++++++++++++++++--- src/user.js | 1 + www/js/ui.js | 27 ++++++++++++++++++++++++--- 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 437bd331..658856d9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Calvin Montgomery", "name": "CyTube", "description": "Online media synchronizer and chat", - "version": "3.14.2", + "version": "3.14.3", "repository": { "url": "http://github.com/calzoneman/sync" }, diff --git a/src/channel/playlist.js b/src/channel/playlist.js index 0beb9777..f4a40bf0 100644 --- a/src/channel/playlist.js +++ b/src/channel/playlist.js @@ -11,6 +11,13 @@ var CustomEmbedFilter = require("../customembed").filter; var XSS = require("../xss"); const MAX_ITEMS = Config.get("playlist.max-items"); +// Limit requestPlaylist to once per 60 seconds +const REQ_PLAYLIST_THROTTLE = { + burst: 1, + sustained: 0, + cooldown: 60 +}; + const TYPE_QUEUE = { id: "string,boolean", @@ -216,9 +223,7 @@ PlaylistModule.prototype.onUserPostJoin = function (user) { user.socket.on("playerReady", function () { self.sendChangeMedia([user]); }); - user.socket.on("requestPlaylist", function () { - self.sendPlaylist([user]); - }); + user.socket.on("requestPlaylist", this.handleRequestPlaylist.bind(this, user)); user.on("login", function () { self.sendPlaylist([user]); }); @@ -1323,4 +1328,16 @@ PlaylistModule.prototype.handleQueuePlaylist = function (user, data) { }); }; +PlaylistModule.prototype.handleRequestPlaylist = function (user) { + if (user.reqPlaylistLimiter.throttle(REQ_PLAYLIST_THROTTLE)) { + user.socket.emit("errorMsg", { + msg: "Get Playlist URLs is limited to 1 usage every 60 seconds. " + + "Please try again later.", + code: "REQ_PLAYLIST_LIMIT_REACHED" + }); + } else { + this.sendPlaylist([user]); + } +}; + module.exports = PlaylistModule; diff --git a/src/user.js b/src/user.js index 0c21b68f..1e6612c0 100644 --- a/src/user.js +++ b/src/user.js @@ -21,6 +21,7 @@ function User(socket) { self.channel = null; self.queueLimiter = util.newRateLimiter(); self.chatLimiter = util.newRateLimiter(); + self.reqPlaylistLimiter = util.newRateLimiter(); self.awaytimer = false; var announcement = Server.getServer().announcement; diff --git a/www/js/ui.js b/www/js/ui.js index 5219aa85..bb80663a 100644 --- a/www/js/ui.js +++ b/www/js/ui.js @@ -490,9 +490,14 @@ $("#voteskip").click(function() { $("#getplaylist").click(function() { var callback = function(data) { hidePlayer(); - socket.listeners("playlist").splice( - socket.listeners("playlist").indexOf(callback) - ); + var idx = socket.listeners("errorMsg").indexOf(errCallback); + if (idx >= 0) { + socket.listeners("errorMsg").splice(idx); + } + idx = socket.listeners("playlist").indexOf(callback); + if (idx >= 0) { + socket.listeners("playlist").splice(idx); + } var list = []; for(var i = 0; i < data.length; i++) { var entry = formatURL(data[i].media); @@ -524,6 +529,22 @@ $("#getplaylist").click(function() { outer.modal(); }; socket.on("playlist", callback); + var errCallback = function(data) { + if (data.code !== "REQ_PLAYLIST_LIMIT_REACHED") { + return; + } + + var idx = socket.listeners("errorMsg").indexOf(errCallback); + if (idx >= 0) { + socket.listeners("errorMsg").splice(idx); + } + + idx = socket.listeners("playlist").indexOf(callback); + if (idx >= 0) { + socket.listeners("playlist").splice(idx); + } + }; + socket.on("errorMsg", errCallback); socket.emit("requestPlaylist"); }); From 2eb17f4c320fd0442668de912553b8e9a3de33d9 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Tue, 9 Feb 2016 19:44:07 -0800 Subject: [PATCH 12/12] Fix MIME mapping for ogg/vorbis -> audio/ogg --- player/raw-file.coffee | 2 +- www/js/player.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/player/raw-file.coffee b/player/raw-file.coffee index 8cf63182..8053cd9c 100644 --- a/player/raw-file.coffee +++ b/player/raw-file.coffee @@ -5,7 +5,7 @@ codecToMimeType = (codec) -> when 'matroska/vp8', 'matroska/vp9' then 'video/webm' when 'ogg/theora' then 'video/ogg' when 'mp3' then 'audio/mp3' - when 'vorbis' then 'audio/vorbis' + when 'vorbis' then 'audio/ogg' else 'video/flv' window.FilePlayer = class FilePlayer extends VideoJSPlayer diff --git a/www/js/player.js b/www/js/player.js index 1cf52a40..3e6764cc 100644 --- a/www/js/player.js +++ b/www/js/player.js @@ -654,7 +654,7 @@ case 'mp3': return 'audio/mp3'; case 'vorbis': - return 'audio/vorbis'; + return 'audio/ogg'; default: return 'video/flv'; }