@ -54,10 +54,15 @@ namespace Emby.Server.Implementations.SyncPlay
new Dictionary < Guid , IGroupController > ( ) ;
/// <summary>
/// Lock used for accesing any group .
/// Lock used for accesing the list of groups .
/// </summary>
private readonly object _groupsLock = new object ( ) ;
/// <summary>
/// Lock used for accesing the session-to-group map.
/// </summary>
private readonly object _mapsLock = new object ( ) ;
private bool _disposed = false ;
/// <summary>
@ -97,7 +102,12 @@ namespace Emby.Server.Implementations.SyncPlay
return ;
}
// Locking required to access list of groups.
lock ( _groupsLock )
{
// Locking required as session-to-group map will be edited.
// Locking the group is not required as it is not visible yet.
lock ( _mapsLock )
{
if ( IsSessionInGroup ( session ) )
{
@ -111,6 +121,7 @@ namespace Emby.Server.Implementations.SyncPlay
group . CreateGroup ( session , request , cancellationToken ) ;
}
}
}
/// <inheritdoc />
public void JoinGroup ( SessionInfo session , Guid groupId , JoinGroupRequest request , CancellationToken cancellationToken )
@ -123,6 +134,7 @@ namespace Emby.Server.Implementations.SyncPlay
var user = _userManager . GetUserById ( session . UserId ) ;
// Locking required to access list of groups.
lock ( _groupsLock )
{
_groups . TryGetValue ( groupId , out IGroupController group ) ;
@ -136,6 +148,12 @@ namespace Emby.Server.Implementations.SyncPlay
return ;
}
// Locking required as session-to-group map will be edited.
lock ( _mapsLock )
{
// Group lock required to let other requests end first.
lock ( group )
{
if ( ! group . HasAccessToPlayQueue ( user ) )
{
_logger . LogWarning ( "Session {SessionId} tried to join group {GroupId} but does not have access to some content of the playing queue." , session . Id , group . GroupId . ToString ( ) ) ;
@ -147,7 +165,7 @@ namespace Emby.Server.Implementations.SyncPlay
if ( IsSessionInGroup ( session ) )
{
if ( GetSessionGroup ( session ) . Equals ( groupId ) )
if ( FindJoinedGroupId ( session ) . Equals ( groupId ) )
{
group . SessionRestore ( session , request , cancellationToken ) ;
return ;
@ -160,6 +178,8 @@ namespace Emby.Server.Implementations.SyncPlay
group . SessionJoin ( session , request , cancellationToken ) ;
}
}
}
}
/// <inheritdoc />
public void LeaveGroup ( SessionInfo session , CancellationToken cancellationToken )
@ -170,11 +190,13 @@ namespace Emby.Server.Implementations.SyncPlay
return ;
}
// TODO: determine what happens to users that are in a group and get their permissions revoked .
// Locking required to access list of groups .
lock ( _groupsLock )
{
_sessionToGroupMap . TryGetValue ( session . Id , out var group ) ;
// Locking required as session-to-group map will be edited.
lock ( _mapsLock )
{
var group = FindJoinedGroup ( session ) ;
if ( group = = null )
{
_logger . LogWarning ( "Session {SessionId} does not belong to any group." , session . Id ) ;
@ -184,6 +206,9 @@ namespace Emby.Server.Implementations.SyncPlay
return ;
}
// Group lock required to let other requests end first.
lock ( group )
{
RemoveSessionFromGroup ( session , group ) ;
group . SessionLeave ( session , cancellationToken ) ;
@ -194,6 +219,8 @@ namespace Emby.Server.Implementations.SyncPlay
}
}
}
}
}
/// <inheritdoc />
public List < GroupInfoDto > ListGroups ( SessionInfo session )
@ -205,16 +232,26 @@ namespace Emby.Server.Implementations.SyncPlay
}
var user = _userManager . GetUserById ( session . UserId ) ;
List < GroupInfoDto > list = new List < GroupInfoDto > ( ) ;
// Locking required to access list of groups.
lock ( _groupsLock )
{
return _groups
. Values
. Where ( group = > group . HasAccessToPlayQueue ( user ) )
. Select ( group = > group . GetInfo ( ) )
. ToList ( ) ;
foreach ( var group in _groups . Values )
{
// Locking required as group is not thread-safe.
lock ( group )
{
if ( group . HasAccessToPlayQueue ( user ) )
{
list . Add ( group . GetInfo ( ) ) ;
}
}
}
}
return list ;
}
/// <inheritdoc />
public void HandleRequest ( SessionInfo session , IGroupPlaybackRequest request , CancellationToken cancellationToken )
@ -225,10 +262,7 @@ namespace Emby.Server.Implementations.SyncPlay
return ;
}
lock ( _groupsLock )
{
_sessionToGroupMap . TryGetValue ( session . Id , out var group ) ;
var group = FindJoinedGroup ( session ) ;
if ( group = = null )
{
_logger . LogWarning ( "Session {SessionId} does not belong to any group." , session . Id ) ;
@ -238,6 +272,9 @@ namespace Emby.Server.Implementations.SyncPlay
return ;
}
// Group lock required as GroupController is not thread-safe.
lock ( group )
{
group . HandleRequest ( session , request , cancellationToken ) ;
}
}
@ -260,52 +297,57 @@ namespace Emby.Server.Implementations.SyncPlay
private void OnSessionManagerSessionStarted ( object sender , SessionEventArgs e )
{
var session = e . SessionInfo ;
lock ( _groupsLock )
{
if ( ! IsSessionInGroup ( session ) )
Guid groupId = FindJoinedGroupId ( session ) ;
if ( groupId . Equals ( Guid . Empty ) )
{
return ;
}
var groupId = GetSessionGroup ( session ) ;
var request = new JoinGroupRequest ( groupId ) ;
JoinGroup ( session , groupId , request , CancellationToken . None ) ;
}
}
/// <summary>
/// Checks if a given session has joined a group.
/// </summary>
/// <remarks>
/// Not thread-safe, call only under groups-lock.
/// </remarks>
/// <param name="session">The session.</param>
/// <returns><c>true</c> if the session has joined a group, <c>false</c> otherwise.</returns>
private bool IsSessionInGroup ( SessionInfo session )
{
lock ( _mapsLock )
{
return _sessionToGroupMap . ContainsKey ( session . Id ) ;
}
}
/// <summary>
/// Gets the group joined by the given session, if any.
/// </summary>
/// <remarks>
/// Not thread-safe, call only under groups-lock.
/// </remarks>
/// <param name="session">The session.</param>
/// <returns>The group identifier if the session has joined a group, an empty identifier otherwise.</returns>
private Guid GetSessionGroup ( SessionInfo session )
/// <returns>The group.</returns>
private IGroupController FindJoinedGroup ( SessionInfo session )
{
lock ( _mapsLock )
{
_sessionToGroupMap . TryGetValue ( session . Id , out var group ) ;
return group ? . GroupId ? ? Guid . Empty ;
return group ;
}
}
/// <summary>
/// Gets the group identifier joined by the given session, if any.
/// </summary>
/// <param name="session">The session.</param>
/// <returns>The group identifier if the session has joined a group, an empty identifier otherwise.</returns>
private Guid FindJoinedGroupId ( SessionInfo session )
{
return FindJoinedGroup ( session ) ? . GroupId ? ? Guid . Empty ;
}
/// <summary>
/// Maps a session to a group.
/// </summary>
/// <remarks>
/// Not thread-safe, call only under groups-lock.
/// </remarks>
/// <param name="session">The session.</param>
/// <param name="group">The group.</param>
/// <exception cref="InvalidOperationException">Thrown when the user is in another group already.</exception>
@ -316,6 +358,8 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException ( "Session is null!" ) ;
}
lock ( _mapsLock )
{
if ( IsSessionInGroup ( session ) )
{
throw new InvalidOperationException ( "Session in other group already!" ) ;
@ -323,13 +367,11 @@ namespace Emby.Server.Implementations.SyncPlay
_sessionToGroupMap [ session . Id ] = group ? ? throw new InvalidOperationException ( "Group is null!" ) ;
}
}
/// <summary>
/// Unmaps a session from a group.
/// </summary>
/// <remarks>
/// Not thread-safe, call only under groups-lock.
/// </remarks>
/// <param name="session">The session.</param>
/// <param name="group">The group.</param>
/// <exception cref="InvalidOperationException">Thrown when the user is not found in the specified group.</exception>
@ -345,6 +387,8 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException ( "Group is null!" ) ;
}
lock ( _mapsLock )
{
if ( ! IsSessionInGroup ( session ) )
{
throw new InvalidOperationException ( "Session not in any group!" ) ;
@ -356,6 +400,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException ( "Session was in wrong group!" ) ;
}
}
}
/// <summary>
/// Checks if a given session is allowed to make a given request.