Bureaucracy
* Fixed signature of Receiver methods * Implemented create/destroy mechanism and bootstrap request in BureauDirector * Added BureauContext class to expose bureau information on top of usual presents stuff * Added BureauContext implementation in BureauClient * Added code to kick off the director when the client logs on * Added credentials for a bureau logon, which is just a token passed to command line launcher * Added concept of bureau types to distinguish between executable and command line arguments; includes interface BureauRegistry.Launcher * Implemented backend for the registry git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@5027 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
@@ -0,0 +1,58 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
//
|
||||||
|
// Narya library - tools for developing networked games
|
||||||
|
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
|
||||||
|
// http://www.threerings.net/code/narya/
|
||||||
|
//
|
||||||
|
// This library is free software; you can redistribute it and/or modify it
|
||||||
|
// under the terms of the GNU Lesser General Public License as published
|
||||||
|
// by the Free Software Foundation; either version 2.1 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This library is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
// Lesser General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public
|
||||||
|
// License along with this library; if not, write to the Free Software
|
||||||
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
|
package com.threerings.bureau;
|
||||||
|
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains a reference to the log object used by the Bureau services.
|
||||||
|
*/
|
||||||
|
public class Log
|
||||||
|
{
|
||||||
|
/** We dispatch our log messages through this logger. */
|
||||||
|
public static Logger log = Logger.getLogger("com.threerings.narya.bureau");
|
||||||
|
|
||||||
|
/** Convenience function. */
|
||||||
|
public static void debug (String message)
|
||||||
|
{
|
||||||
|
log.fine(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience function. */
|
||||||
|
public static void info (String message)
|
||||||
|
{
|
||||||
|
log.info(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience function. */
|
||||||
|
public static void warning (String message)
|
||||||
|
{
|
||||||
|
log.warning(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience function. */
|
||||||
|
public static void logStackTrace (Throwable t)
|
||||||
|
{
|
||||||
|
log.log(Level.WARNING, t.getMessage(), t);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,9 +21,15 @@
|
|||||||
|
|
||||||
package com.threerings.bureau.client;
|
package com.threerings.bureau.client;
|
||||||
|
|
||||||
|
import com.threerings.bureau.data.AgentObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an agent running within a bureau client.
|
* Represents an agent running within a bureau client.
|
||||||
*/
|
*/
|
||||||
public class Agent
|
public class Agent
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* The shared agent object.
|
||||||
|
*/
|
||||||
|
public AgentObject agentObject;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,33 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
//
|
||||||
|
// Narya library - tools for developing networked games
|
||||||
|
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
|
||||||
|
// http://www.threerings.net/code/narya/
|
||||||
|
//
|
||||||
|
// This library is free software; you can redistribute it and/or modify it
|
||||||
|
// under the terms of the GNU Lesser General Public License as published
|
||||||
|
// by the Free Software Foundation; either version 2.1 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This library is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
// Lesser General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public
|
||||||
|
// License along with this library; if not, write to the Free Software
|
||||||
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
package com.threerings.bureau.client;
|
package com.threerings.bureau.client;
|
||||||
|
|
||||||
import com.threerings.presents.client.Client;
|
import com.threerings.presents.client.Client;
|
||||||
import com.threerings.presents.net.Credentials;
|
import com.threerings.presents.net.Credentials;
|
||||||
import com.samskivert.util.RunQueue;
|
import com.samskivert.util.RunQueue;
|
||||||
|
import com.threerings.bureau.data.BureauCredentials;
|
||||||
|
import com.threerings.bureau.util.BureauContext;
|
||||||
|
import com.threerings.presents.dobj.DObjectManager;
|
||||||
|
import com.samskivert.util.Config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a client embedded in a bureau.
|
* Represents a client embedded in a bureau.
|
||||||
@@ -14,8 +39,43 @@ public class BureauClient extends Client
|
|||||||
* @param creds the credentials supplied during connection
|
* @param creds the credentials supplied during connection
|
||||||
* @param runQueue the place to post tasks required by clients
|
* @param runQueue the place to post tasks required by clients
|
||||||
*/
|
*/
|
||||||
public BureauClient (Credentials creds, RunQueue runQueue)
|
public BureauClient (String token, String bureauId, RunQueue runQueue)
|
||||||
{
|
{
|
||||||
super(creds, runQueue);
|
super(null, runQueue);
|
||||||
|
|
||||||
|
_bureauId = bureauId;
|
||||||
|
|
||||||
|
BureauCredentials creds = new BureauCredentials();
|
||||||
|
creds.sessionToken = token;
|
||||||
|
_creds = creds;
|
||||||
|
|
||||||
|
_ctx = createContext();
|
||||||
|
_director = new BureauDirector(_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected BureauContext createContext ()
|
||||||
|
{
|
||||||
|
return new BureauContext () {
|
||||||
|
public BureauDirector getBureauDirector () {
|
||||||
|
return _director;
|
||||||
|
}
|
||||||
|
public DObjectManager getDObjectManager () {
|
||||||
|
return _omgr;
|
||||||
|
}
|
||||||
|
public Client getClient () {
|
||||||
|
return BureauClient.this;
|
||||||
|
}
|
||||||
|
public Config getConfig () {
|
||||||
|
return _config;
|
||||||
|
}
|
||||||
|
public String getBureauId () {
|
||||||
|
return _bureauId;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected BureauContext _ctx;
|
||||||
|
protected String _bureauId;
|
||||||
|
protected BureauDirector _director;
|
||||||
|
protected Config _config = new Config("bureau");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ package com.threerings.bureau.client;
|
|||||||
|
|
||||||
import com.threerings.bureau.client.BureauReceiver;
|
import com.threerings.bureau.client.BureauReceiver;
|
||||||
import com.threerings.presents.client.InvocationDecoder;
|
import com.threerings.presents.client.InvocationDecoder;
|
||||||
import com.threerings.presents.data.ClientObject;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatches calls to a {@link BureauReceiver} instance.
|
* Dispatches calls to a {@link BureauReceiver} instance.
|
||||||
@@ -62,13 +61,13 @@ public class BureauDecoder extends InvocationDecoder
|
|||||||
switch (methodId) {
|
switch (methodId) {
|
||||||
case CREATE_AGENT:
|
case CREATE_AGENT:
|
||||||
((BureauReceiver)receiver).createAgent(
|
((BureauReceiver)receiver).createAgent(
|
||||||
(ClientObject)args[0], ((Integer)args[1]).intValue()
|
((Integer)args[0]).intValue()
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case DESTROY_AGENT:
|
case DESTROY_AGENT:
|
||||||
((BureauReceiver)receiver).destroyAgent(
|
((BureauReceiver)receiver).destroyAgent(
|
||||||
(ClientObject)args[0], ((Integer)args[1]).intValue()
|
((Integer)args[0]).intValue()
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -2,32 +2,127 @@ package com.threerings.bureau.client;
|
|||||||
|
|
||||||
import com.threerings.presents.client.BasicDirector;
|
import com.threerings.presents.client.BasicDirector;
|
||||||
import com.threerings.presents.data.ClientObject;
|
import com.threerings.presents.data.ClientObject;
|
||||||
import com.threerings.presents.util.PresentsContext;
|
import com.threerings.bureau.data.BureauCodes;
|
||||||
|
import com.samskivert.util.HashIntMap;
|
||||||
|
import com.threerings.bureau.data.AgentObject;
|
||||||
|
import com.threerings.bureau.Log;
|
||||||
|
import com.threerings.bureau.util.BureauContext;
|
||||||
|
import com.threerings.presents.client.Client;
|
||||||
|
import com.threerings.presents.dobj.Subscriber;
|
||||||
|
import com.threerings.presents.dobj.ObjectAccessException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows the server to create and destroy agents on a client.
|
* Allows the server to create and destroy agents on a client.
|
||||||
* @see BureauRegistry
|
* @see BureauRegistry
|
||||||
*/
|
*/
|
||||||
public class BureauDirector extends BasicDirector
|
public class BureauDirector extends BasicDirector
|
||||||
implements BureauReceiver
|
implements BureauReceiver, Subscriber<AgentObject>
|
||||||
{
|
{
|
||||||
protected BureauDirector (PresentsContext ctx)
|
public BureauDirector (BureauContext ctx)
|
||||||
{
|
{
|
||||||
super(ctx);
|
super(ctx);
|
||||||
|
_ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// from BureauReceiver
|
||||||
|
public synchronized void createAgent (int agentId)
|
||||||
|
{
|
||||||
|
_ctx.getDObjectManager().subscribeToObject(agentId, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// from BureauReceiver
|
||||||
|
public synchronized void destroyAgent (int agentId)
|
||||||
|
{
|
||||||
|
Agent agent = null;
|
||||||
|
try {
|
||||||
|
agent = _agents.remove(agentId);
|
||||||
|
// TODO: stop the agent somehow
|
||||||
|
}
|
||||||
|
catch (Throwable t) {
|
||||||
|
Log.warning("Could not create agent [id=" + agentId + "]");
|
||||||
|
Log.logStackTrace(t);
|
||||||
|
// TODO: failure notification?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ctx.getDObjectManager().unsubscribeFromObject(agentId, this);
|
||||||
|
doStopAgent(agent);
|
||||||
|
_bureauService.agentDestroyed(_ctx.getClient(), agentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// from Subscriber
|
||||||
|
public synchronized void objectAvailable (AgentObject agentObject)
|
||||||
|
{
|
||||||
|
int oid = agentObject.getOid();
|
||||||
|
try {
|
||||||
|
Agent agent = new Agent();
|
||||||
|
agent.agentObject = agentObject;
|
||||||
|
doStartAgent(agent);
|
||||||
|
_agents.put(oid, agent);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
Log.warning("Could not create agent [obj=" + agentObject + "]");
|
||||||
|
Log.logStackTrace(e);
|
||||||
|
// TODO: failure notification?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: post to runqueue?
|
||||||
|
_bureauService.agentCreated(_ctx.getClient(), oid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// from Subscriber
|
||||||
|
public synchronized void requestFailed (int oid, ObjectAccessException cause)
|
||||||
|
{
|
||||||
|
Log.warning("Could not subscribe to agent [oid=" + oid + "]");
|
||||||
|
Log.logStackTrace(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // from BasicDirector
|
||||||
|
public void clientDidLogon (Client client)
|
||||||
|
{
|
||||||
|
super.clientDidLogon(client);
|
||||||
|
_bureauService.bureauInitialized(_ctx.getClient(), _ctx.getBureauId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // from BasicDirector
|
||||||
|
protected void registerServices (Client client)
|
||||||
|
{
|
||||||
|
super.registerServices(client);
|
||||||
|
|
||||||
|
// Require the bureau services
|
||||||
|
client.addServiceGroup(BureauCodes.BUREAU_GROUP);
|
||||||
|
|
||||||
// Set up our decoder so we can receive method calls
|
// Set up our decoder so we can receive method calls
|
||||||
// from the server
|
// from the server
|
||||||
_ctx.getClient().getInvocationDirector().
|
client.getInvocationDirector().
|
||||||
registerReceiver(new BureauDecoder(this));
|
registerReceiver(new BureauDecoder(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
// from BureauReceiver
|
@Override // from BasicDirector
|
||||||
public void createAgent (ClientObject client, int agentId)
|
protected void fetchServices (Client client)
|
||||||
|
{
|
||||||
|
super.fetchServices(client);
|
||||||
|
|
||||||
|
_bureauService = client.getService(BureauService.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the agent object is ready and it is time to run his code.
|
||||||
|
*/
|
||||||
|
protected void doStartAgent (Agent agent)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
// from BureauReceiver
|
/**
|
||||||
public void destroyAgent (ClientObject client, int agentId)
|
* Called when the agent object is being destroyed and the client code should stop
|
||||||
|
* processing.
|
||||||
|
*/
|
||||||
|
protected void doStopAgent (Agent agent)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected BureauContext _ctx;
|
||||||
|
protected BureauService _bureauService;
|
||||||
|
protected HashIntMap<Agent> _agents = new HashIntMap<Agent>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public interface BureauReceiver extends InvocationReceiver
|
|||||||
* @param client the client receiving the request
|
* @param client the client receiving the request
|
||||||
* @param agentId the id of the <code>AgentObject</code> that needs an <code>Agent</code>
|
* @param agentId the id of the <code>AgentObject</code> that needs an <code>Agent</code>
|
||||||
*/
|
*/
|
||||||
void createAgent (ClientObject client, int agentId);
|
void createAgent (int agentId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroys a previously created agent. Implementors should destroy the agent that was created
|
* Destroys a previously created agent. Implementors should destroy the agent that was created
|
||||||
@@ -46,5 +46,5 @@ public interface BureauReceiver extends InvocationReceiver
|
|||||||
* @param agentId the id of the <code>AgentObject</code> whose <code>Agent</code>
|
* @param agentId the id of the <code>AgentObject</code> whose <code>Agent</code>
|
||||||
* should be destroyed
|
* should be destroyed
|
||||||
*/
|
*/
|
||||||
void destroyAgent (ClientObject client, int agentId);
|
void destroyAgent (int agentId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,16 +29,25 @@ public class AgentObject extends DObject
|
|||||||
/** The field name of the <code>bureauId</code> field. */
|
/** The field name of the <code>bureauId</code> field. */
|
||||||
public static final String BUREAU_ID = "bureauId";
|
public static final String BUREAU_ID = "bureauId";
|
||||||
|
|
||||||
|
/** The field name of the <code>bureauType</code> field. */
|
||||||
|
public static final String BUREAU_TYPE = "bureauType";
|
||||||
|
|
||||||
/** The field name of the <code>agentCode</code> field. */
|
/** The field name of the <code>agentCode</code> field. */
|
||||||
public static final String AGENT_CODE = "agentCode";
|
public static final String AGENT_CODE = "agentCode";
|
||||||
|
|
||||||
/** The field name of the <code>startConfirmed</code> field. */
|
|
||||||
public static final String START_CONFIRMED = "startConfirmed";
|
|
||||||
// AUTO-GENERATED: FIELDS END
|
// AUTO-GENERATED: FIELDS END
|
||||||
|
|
||||||
|
/** The id of the bureau the agent is running in. This is normally a unique id corresponding
|
||||||
|
* to the game or item that requires some server-side processing. */
|
||||||
public String bureauId;
|
public String bureauId;
|
||||||
|
|
||||||
|
/** The type of bureau that the agent is running in. This is normally derived from the kind
|
||||||
|
* of media that the game or item has specified for its code and determines the method of
|
||||||
|
* launching the bureau when the first agent is requested. */
|
||||||
|
public String bureauType;
|
||||||
|
|
||||||
|
/** The location of the code for the agent. This could be a URL to an action script file or
|
||||||
|
* some other description that the bureau can use to load and execute the agent's code. */
|
||||||
public String agentCode;
|
public String agentCode;
|
||||||
public boolean startConfirmed;
|
|
||||||
|
|
||||||
// AUTO-GENERATED: METHODS START
|
// AUTO-GENERATED: METHODS START
|
||||||
/**
|
/**
|
||||||
@@ -57,6 +66,22 @@ public class AgentObject extends DObject
|
|||||||
this.bureauId = value;
|
this.bureauId = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests that the <code>bureauType</code> field be set to the
|
||||||
|
* specified value. The local value will be updated immediately and an
|
||||||
|
* event will be propagated through the system to notify all listeners
|
||||||
|
* that the attribute did change. Proxied copies of this object (on
|
||||||
|
* clients) will apply the value change when they received the
|
||||||
|
* attribute changed notification.
|
||||||
|
*/
|
||||||
|
public void setBureauType (String value)
|
||||||
|
{
|
||||||
|
String ovalue = this.bureauType;
|
||||||
|
requestAttributeChange(
|
||||||
|
BUREAU_TYPE, value, ovalue);
|
||||||
|
this.bureauType = value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests that the <code>agentCode</code> field be set to the
|
* Requests that the <code>agentCode</code> field be set to the
|
||||||
* specified value. The local value will be updated immediately and an
|
* specified value. The local value will be updated immediately and an
|
||||||
@@ -72,21 +97,5 @@ public class AgentObject extends DObject
|
|||||||
AGENT_CODE, value, ovalue);
|
AGENT_CODE, value, ovalue);
|
||||||
this.agentCode = value;
|
this.agentCode = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests that the <code>startConfirmed</code> field be set to the
|
|
||||||
* specified value. The local value will be updated immediately and an
|
|
||||||
* event will be propagated through the system to notify all listeners
|
|
||||||
* that the attribute did change. Proxied copies of this object (on
|
|
||||||
* clients) will apply the value change when they received the
|
|
||||||
* attribute changed notification.
|
|
||||||
*/
|
|
||||||
public void setStartConfirmed (boolean value)
|
|
||||||
{
|
|
||||||
boolean ovalue = this.startConfirmed;
|
|
||||||
requestAttributeChange(
|
|
||||||
START_CONFIRMED, Boolean.valueOf(value), Boolean.valueOf(ovalue));
|
|
||||||
this.startConfirmed = value;
|
|
||||||
}
|
|
||||||
// AUTO-GENERATED: METHODS END
|
// AUTO-GENERATED: METHODS END
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
//
|
||||||
|
// Narya library - tools for developing networked games
|
||||||
|
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
|
||||||
|
// http://www.threerings.net/code/narya/
|
||||||
|
//
|
||||||
|
// This library is free software; you can redistribute it and/or modify it
|
||||||
|
// under the terms of the GNU Lesser General Public License as published
|
||||||
|
// by the Free Software Foundation; either version 2.1 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This library is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
// Lesser General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public
|
||||||
|
// License along with this library; if not, write to the Free Software
|
||||||
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
|
package com.threerings.bureau.data;
|
||||||
|
|
||||||
|
import com.threerings.presents.net.Credentials;
|
||||||
|
import com.threerings.util.Name;
|
||||||
|
|
||||||
|
public class BureauCredentials extends Credentials
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The token to pass to the server when logging in. This is usually just passed to the bureau
|
||||||
|
* on the command line to guard against outside connections being established. */
|
||||||
|
public String sessionToken;
|
||||||
|
|
||||||
|
public BureauCredentials ()
|
||||||
|
{
|
||||||
|
super(new Name("$$$BUREAU$$$"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,89 +23,433 @@ package com.threerings.bureau.server;
|
|||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
import com.threerings.bureau.data.AgentObject;
|
import com.threerings.bureau.data.AgentObject;
|
||||||
import com.threerings.bureau.data.BureauCodes;
|
import com.threerings.bureau.data.BureauCodes;
|
||||||
import com.threerings.presents.data.ClientObject;
|
import com.threerings.presents.data.ClientObject;
|
||||||
import com.threerings.presents.dobj.RootDObjectManager;
|
import com.threerings.presents.dobj.RootDObjectManager;
|
||||||
import com.threerings.presents.server.InvocationManager;
|
import com.threerings.presents.server.InvocationManager;
|
||||||
|
import static com.samskivert.util.StringUtil.safeToString;
|
||||||
|
|
||||||
|
import com.threerings.bureau.Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstracts the launching and termination of external processes (bureaus) that host instances of
|
* Abstracts the launching and termination of external processes (bureaus) that host instances of
|
||||||
* server-side code (agents).
|
* server-side code (agents).
|
||||||
* TODO: this class needs lots of fleshing out and as such is not 100% documented
|
|
||||||
*/
|
*/
|
||||||
public class BureauRegistry
|
public class BureauRegistry
|
||||||
implements BureauProvider
|
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The binary executable for the external vm being used.
|
* Defines a way to launch a bureau. Launchers are set by the server on startup and are
|
||||||
* TODO: what args are needed?
|
* invoked whenever an agent is requested whose bureauType matches the registered type.
|
||||||
* TODO: how to format the args?
|
* @see registerBureau
|
||||||
* TODO: where does the binary path really come from - a bureau specification?
|
|
||||||
*/
|
*/
|
||||||
public String vmBinary = "java %{jar} %{host}:%{port} %{agentclassname}";
|
public static interface Launcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Launches a new bureau using the given server connect-back url and other information.
|
||||||
|
* Called by the registry when it decides a new bureau is needed.
|
||||||
|
* @param serverUrl the url the bureau should use to connect back to the server
|
||||||
|
* @param bureauId the id of the bureau being launched
|
||||||
|
* @param token the token string to use for the credentials when logging in
|
||||||
|
*/
|
||||||
|
Process launch (String serverUrl, String bureauId, String token);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new registry, prepared to provide bureau services.
|
* Creates a new registry, prepared to provide bureau services.
|
||||||
*/
|
*/
|
||||||
public BureauRegistry (InvocationManager invmgr, RootDObjectManager omgr)
|
public BureauRegistry (
|
||||||
|
String serverUrl,
|
||||||
|
InvocationManager invmgr,
|
||||||
|
RootDObjectManager omgr)
|
||||||
{
|
{
|
||||||
|
_serverUrl = serverUrl;
|
||||||
_invmgr = invmgr;
|
_invmgr = invmgr;
|
||||||
_omgr = omgr;
|
_omgr = omgr;
|
||||||
|
|
||||||
invmgr.registerDispatcher(
|
BureauProvider provider = new BureauProvider () {
|
||||||
new BureauDispatcher(this),
|
public void bureauInitialized (ClientObject client, String bureauId) {
|
||||||
|
BureauRegistry.this.bureauInitialized(client, bureauId);
|
||||||
|
}
|
||||||
|
public void agentCreated (ClientObject client, int agentId) {
|
||||||
|
BureauRegistry.this.agentCreated(client, agentId);
|
||||||
|
}
|
||||||
|
public void agentDestroyed (ClientObject client, int agentId) {
|
||||||
|
BureauRegistry.this.agentDestroyed(client, agentId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_invmgr.registerDispatcher(
|
||||||
|
new BureauDispatcher(provider),
|
||||||
BureauCodes.BUREAU_GROUP);
|
BureauCodes.BUREAU_GROUP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a launcher of a given type. When an agent is started and no bureaus are currently
|
||||||
|
* running, the <code>bureauType</code> is used to determine the <code>Launcher</code> to call.
|
||||||
|
* @param bureauType the type of bureau that will be launched
|
||||||
|
* @param launcher the launcher to be used when the <code>bureauType</code> is requested
|
||||||
|
*/
|
||||||
|
public void setLauncher (String bureauType, Launcher launcher)
|
||||||
|
{
|
||||||
|
if (_launchers.get(bureauType) != null) {
|
||||||
|
Log.warning("Launcher for type already exists [type=" +
|
||||||
|
bureauType + "]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_launchers.put(bureauType, launcher);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a new agent using the data in the given object, creating a new bureau if necessary.
|
* Starts a new agent using the data in the given object, creating a new bureau if necessary.
|
||||||
*/
|
*/
|
||||||
public void startAgent (AgentObject agent)
|
public synchronized void startAgent (AgentObject agent)
|
||||||
{
|
{
|
||||||
// TODO: create new bureau if needed
|
Bureau bureau = _bureaus.get(agent.bureauId);
|
||||||
// TODO: otherwise call BureauSender.createAgent and pend
|
if (bureau != null && bureau.ready()) {
|
||||||
|
|
||||||
|
Log.info("Bureau ready, sending createAgent " +
|
||||||
|
safeToString(agent));
|
||||||
|
|
||||||
|
BureauSender.createAgent(bureau.clientObj, agent.getOid());
|
||||||
|
// !TODO: is this the right place to register the object?
|
||||||
|
_omgr.registerObject(agent);
|
||||||
|
bureau.agentStates.put(agent, Bureau.STARTED);
|
||||||
|
|
||||||
|
bureau.summarize();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bureau == null) {
|
||||||
|
|
||||||
|
Launcher launcher = _launchers.get(agent.bureauType);
|
||||||
|
if (launcher == null) {
|
||||||
|
Log.warning("Launcher not found for agent's " +
|
||||||
|
"bureau type " + safeToString(agent));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.info("Creating new bureau " +
|
||||||
|
safeToString(agent) + " " +
|
||||||
|
safeToString(launcher));
|
||||||
|
|
||||||
|
bureau = new Bureau();
|
||||||
|
|
||||||
|
try {
|
||||||
|
bureau.process = launcher.launch(_serverUrl, agent.bureauId, "");
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
Log.warning("Could not launch process for bureau " +
|
||||||
|
safeToString(agent));
|
||||||
|
Log.logStackTrace(e);
|
||||||
|
|
||||||
|
// !TODO: how to hook into caller and keep the other clients from hanging
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_bureaus.put(agent.bureauId, bureau);
|
||||||
|
}
|
||||||
|
|
||||||
|
_omgr.registerObject(agent);
|
||||||
|
bureau.agentStates.put(agent, Bureau.PENDING);
|
||||||
|
|
||||||
|
bureau.summarize();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destorys a previously started agent using the data in the given object.
|
* Destroys a previously started agent using the data in the given object.
|
||||||
*/
|
*/
|
||||||
public void destroyAgent (AgentObject agent)
|
public synchronized void destroyAgent (AgentObject agent)
|
||||||
{
|
{
|
||||||
// TODO: call BureauSender.destroyAgent and pend
|
FoundAgent found = resolve(null, agent.getOid(), "destroyAgent");
|
||||||
|
|
||||||
|
if (found == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.warning("Destroying agent " + safeToString(agent));
|
||||||
|
|
||||||
|
// transition the agent to a new state and perform the effect of the transition
|
||||||
|
switch (found.state) {
|
||||||
|
|
||||||
|
case Bureau.PENDING:
|
||||||
|
found.bureau.agentStates.remove(found.agent);
|
||||||
|
// !TODO: is the the right place to destroy it?
|
||||||
|
_omgr.destroyObject(found.agent.getOid());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Bureau.STARTED:
|
||||||
|
found.bureau.agentStates.put(found.agent, Bureau.STILL_BORN);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Bureau.RUNNING:
|
||||||
|
BureauSender.destroyAgent(found.bureau.clientObj, agent.getOid());
|
||||||
|
found.bureau.agentStates.put(found.agent, Bureau.DESTROYED);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Bureau.DESTROYED:
|
||||||
|
case Bureau.STILL_BORN:
|
||||||
|
Log.warning("Acknowledging a request to destory an agent, but agent " +
|
||||||
|
"is in state " + found.state + ", ignoring request " +
|
||||||
|
safeToString(found.agent));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
found.bureau.summarize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for when the bureau client acknowledges starting up. Starts all pending agents and
|
||||||
|
* causes subsequent agent start requests to be sent directly to the bureau.
|
||||||
|
*/
|
||||||
|
protected synchronized void bureauInitialized (ClientObject client, String bureauId)
|
||||||
|
{
|
||||||
|
Bureau bureau = _bureaus.get(bureauId);
|
||||||
|
if (bureau == null) {
|
||||||
|
Log.warning("Acknowledging initialization of non-existent bureau " +
|
||||||
|
safeToString(bureauId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bureau.clientObj = client;
|
||||||
|
|
||||||
|
Log.info("Bureau created " + safeToString(bureau) +
|
||||||
|
", launching pending agents");
|
||||||
|
|
||||||
|
// find all pending agents
|
||||||
|
Set<AgentObject> pending = new HashSet<AgentObject>();
|
||||||
|
|
||||||
|
for (Map.Entry<AgentObject, Integer> entry :
|
||||||
|
bureau.agentStates.entrySet()) {
|
||||||
|
|
||||||
|
if (entry.getValue() == Bureau.PENDING) {
|
||||||
|
pending.add(entry.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create them
|
||||||
|
for (AgentObject agent : pending) {
|
||||||
|
Log.info("Creating agent " + safeToString(agent));
|
||||||
|
BureauSender.createAgent(bureau.clientObj, agent.getOid());
|
||||||
|
bureau.agentStates.put(agent, Bureau.STARTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
bureau.summarize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for when the bureau client acknowledges the creation of an agent.
|
||||||
|
*/
|
||||||
|
protected synchronized void agentCreated (ClientObject client, int agentId)
|
||||||
|
{
|
||||||
|
FoundAgent found = resolve(client, agentId, "agentCreated");
|
||||||
|
if (found == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (found.state) {
|
||||||
|
case Bureau.STARTED:
|
||||||
|
found.bureau.agentStates.put(found.agent, Bureau.RUNNING);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Bureau.STILL_BORN:
|
||||||
|
BureauSender.destroyAgent(found.bureau.clientObj, agentId);
|
||||||
|
found.bureau.agentStates.put(found.agent, Bureau.DESTROYED);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Bureau.PENDING:
|
||||||
|
case Bureau.RUNNING:
|
||||||
|
case Bureau.DESTROYED:
|
||||||
|
Log.warning("Received acknowledgement of the creation of an " +
|
||||||
|
"agent in state " + found.state + ", ignoring request " +
|
||||||
|
safeToString(found.agent));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
found.bureau.summarize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for when the bureau client acknowledges the destruction of an agent.
|
||||||
|
*/
|
||||||
|
protected synchronized void agentDestroyed (ClientObject client, int agentId)
|
||||||
|
{
|
||||||
|
FoundAgent found = resolve(client, agentId, "agentDestroyed");
|
||||||
|
if (found == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (found.state) {
|
||||||
|
case Bureau.DESTROYED:
|
||||||
|
found.bureau.agentStates.remove(found.agent);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Bureau.PENDING:
|
||||||
|
case Bureau.STARTED:
|
||||||
|
case Bureau.RUNNING:
|
||||||
|
case Bureau.STILL_BORN:
|
||||||
|
Log.warning("Acknowledging agent destruction, but state is " +
|
||||||
|
found.state + ", ignoring request " +
|
||||||
|
safeToString(found.agent));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
found.bureau.summarize();
|
||||||
|
|
||||||
// TODO: schedule a shutdown event for the bureau if this is the last agent
|
// TODO: schedule a shutdown event for the bureau if this is the last agent
|
||||||
}
|
}
|
||||||
|
|
||||||
// from BureauProvider
|
/**
|
||||||
public void bureauInitialized (ClientObject client, String bureauId)
|
* Does lots of null checks and lookups and resolves the given information into FoundAgent.
|
||||||
|
*/
|
||||||
|
protected FoundAgent resolve (ClientObject client, int agentId, String resolver)
|
||||||
{
|
{
|
||||||
// TODO: create pending agents
|
com.threerings.presents.dobj.DObject dobj = _omgr.getObject(agentId);
|
||||||
// TODO: synchronization
|
if (dobj == null) {
|
||||||
|
Log.warning("Non-existent agent in " + resolver +
|
||||||
|
" [agentId=" + agentId + "]");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(dobj instanceof AgentObject)) {
|
||||||
|
Log.warning("Object not an agent in " + resolver +
|
||||||
|
" " + safeToString(dobj));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentObject agent = (AgentObject)dobj;
|
||||||
|
Bureau bureau = _bureaus.get(agent.bureauId);
|
||||||
|
if (bureau == null) {
|
||||||
|
Log.warning("Bureau not found for agent in " + resolver +
|
||||||
|
" " + safeToString(agent));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bureau.agentStates.containsKey(agent)) {
|
||||||
|
Log.warning("Bureau does not have agent in " + resolver +
|
||||||
|
" " + safeToString(agent));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bureau.clientObj == null) {
|
||||||
|
Log.warning("Bureau not yet connected in " + resolver +
|
||||||
|
" " + safeToString(agent));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client != null && bureau.clientObj != client) {
|
||||||
|
Log.warning("Masquerading request in " + resolver +
|
||||||
|
" " + safeToString(agent) +
|
||||||
|
" " + safeToString(bureau.clientObj) +
|
||||||
|
" " + safeToString(client));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FoundAgent(bureau, agent, bureau.agentStates.get(agent));
|
||||||
}
|
}
|
||||||
|
|
||||||
// from BureauProvider
|
// Models the results of searching for an agent
|
||||||
public void agentCreated (ClientObject client, int agentId)
|
protected static class FoundAgent
|
||||||
{
|
{
|
||||||
// TODO: remove from pending
|
FoundAgent (
|
||||||
|
Bureau bureau,
|
||||||
|
AgentObject agent,
|
||||||
|
int state)
|
||||||
|
{
|
||||||
|
this.bureau = bureau;
|
||||||
|
this.agent = agent;
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bureau containing the agent
|
||||||
|
Bureau bureau;
|
||||||
|
|
||||||
|
// The object
|
||||||
|
AgentObject agent;
|
||||||
|
|
||||||
|
// The state of the agent
|
||||||
|
int state;
|
||||||
}
|
}
|
||||||
|
|
||||||
// from BureauProvider
|
// Models a bureau, including the process handle, all running agents and their states
|
||||||
public void agentDestroyed (ClientObject caller, int arg1)
|
|
||||||
{
|
|
||||||
// TODO: remove from pending destroyed
|
|
||||||
}
|
|
||||||
|
|
||||||
// first stab at the data structures for holding bureaus and agents
|
|
||||||
protected static class Bureau
|
protected static class Bureau
|
||||||
{
|
{
|
||||||
|
// Agent states {
|
||||||
|
|
||||||
|
// Not yet stated, waiting for bureau to ack
|
||||||
|
static final int PENDING = 0;
|
||||||
|
|
||||||
|
// Bureau acked, agent told to start
|
||||||
|
static final int STARTED = 1;
|
||||||
|
|
||||||
|
// Agent ack'ed, now live and hosting, ready to tell other clients
|
||||||
|
static final int RUNNING = 2;
|
||||||
|
|
||||||
|
// Agent destruction requested, waiting for acknowledge (after which the agent is removed
|
||||||
|
// from the Bureau, so has no state)
|
||||||
|
static final int DESTROYED = 3;
|
||||||
|
|
||||||
|
// Edge case: destroy request prior to RUNNING
|
||||||
|
static final int STILL_BORN = 4;
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Should be non-null once the bureau is kicked off
|
||||||
Process process;
|
Process process;
|
||||||
|
|
||||||
|
// The bureau's key in the map of bureaus. All requests for this bureau
|
||||||
|
// with this id should be associated with one instance
|
||||||
|
String bureauId;
|
||||||
|
|
||||||
|
// The client object of the bureau that has opened a dobj connection to
|
||||||
|
// the registry
|
||||||
ClientObject clientObj;
|
ClientObject clientObj;
|
||||||
Set<AgentObject> agents;
|
|
||||||
|
// The states of the various agents allocated to this bureau
|
||||||
|
Map<AgentObject, Integer> agentStates =
|
||||||
|
new HashMap<AgentObject, Integer>();
|
||||||
|
|
||||||
|
boolean ready ()
|
||||||
|
{
|
||||||
|
return clientObj != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int countInState (int state)
|
||||||
|
{
|
||||||
|
Integer state1 = state;
|
||||||
|
if (!agentStates.containsValue(state1)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
for (Map.Entry<AgentObject, Integer> me : agentStates.entrySet()) {
|
||||||
|
if (me.getValue() == state1) {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void summarize ()
|
||||||
|
{
|
||||||
|
Log.info("Bureau " + bureauId + " [" +
|
||||||
|
countInState(PENDING) + " pending, " +
|
||||||
|
countInState(STARTED) + " started, " +
|
||||||
|
countInState(RUNNING) + " running, " +
|
||||||
|
countInState(DESTROYED) + " destroyed, " +
|
||||||
|
countInState(STILL_BORN) + " still born]");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected String _serverUrl;
|
||||||
protected InvocationManager _invmgr;
|
protected InvocationManager _invmgr;
|
||||||
protected RootDObjectManager _omgr;
|
protected RootDObjectManager _omgr;
|
||||||
protected Map<String, Bureau> _bureaus;
|
protected Map<String, Launcher> _launchers = new HashMap<String, Launcher>();
|
||||||
protected Map<String, Set<AgentObject>> _pending;
|
protected Map<String, Bureau> _bureaus = new HashMap<String, Bureau>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ public class BureauSender extends InvocationSender
|
|||||||
* BureauReceiver#createAgent} on a client.
|
* BureauReceiver#createAgent} on a client.
|
||||||
*/
|
*/
|
||||||
public static void createAgent (
|
public static void createAgent (
|
||||||
ClientObject target, ClientObject arg1, int arg2)
|
ClientObject target, int arg1)
|
||||||
{
|
{
|
||||||
sendNotification(
|
sendNotification(
|
||||||
target, BureauDecoder.RECEIVER_CODE, BureauDecoder.CREATE_AGENT,
|
target, BureauDecoder.RECEIVER_CODE, BureauDecoder.CREATE_AGENT,
|
||||||
new Object[] { arg1, Integer.valueOf(arg2) });
|
new Object[] { Integer.valueOf(arg1) });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,11 +49,11 @@ public class BureauSender extends InvocationSender
|
|||||||
* BureauReceiver#destroyAgent} on a client.
|
* BureauReceiver#destroyAgent} on a client.
|
||||||
*/
|
*/
|
||||||
public static void destroyAgent (
|
public static void destroyAgent (
|
||||||
ClientObject target, ClientObject arg1, int arg2)
|
ClientObject target, int arg1)
|
||||||
{
|
{
|
||||||
sendNotification(
|
sendNotification(
|
||||||
target, BureauDecoder.RECEIVER_CODE, BureauDecoder.DESTROY_AGENT,
|
target, BureauDecoder.RECEIVER_CODE, BureauDecoder.DESTROY_AGENT,
|
||||||
new Object[] { arg1, Integer.valueOf(arg2) });
|
new Object[] { Integer.valueOf(arg1) });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
//
|
||||||
|
// Narya library - tools for developing networked games
|
||||||
|
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
|
||||||
|
// http://www.threerings.net/code/narya/
|
||||||
|
//
|
||||||
|
// This library is free software; you can redistribute it and/or modify it
|
||||||
|
// under the terms of the GNU Lesser General Public License as published
|
||||||
|
// by the Free Software Foundation; either version 2.1 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This library is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
// Lesser General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Lesser General Public
|
||||||
|
// License along with this library; if not, write to the Free Software
|
||||||
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
|
package com.threerings.bureau.util;
|
||||||
|
|
||||||
|
import com.threerings.presents.util.PresentsContext;
|
||||||
|
import com.threerings.bureau.client.BureauDirector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the objects held on a bureau client. This includes usual set of objects found on a
|
||||||
|
* standard presents client.
|
||||||
|
*/
|
||||||
|
public interface BureauContext extends PresentsContext
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Access the director object.
|
||||||
|
*/
|
||||||
|
BureauDirector getBureauDirector ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access the bureau id.
|
||||||
|
*/
|
||||||
|
String getBureauId ();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user