mirror of
https://github.com/alchemy-fr/Phraseanet.git
synced 2025-10-12 04:23:19 +00:00
Bind task manager backbone app to websocket using autobahn
This commit is contained in:
@@ -52,7 +52,7 @@ module.exports = function(grunt) {
|
|||||||
"autobahnjs": {
|
"autobahnjs": {
|
||||||
"expand": true,
|
"expand": true,
|
||||||
"src": [
|
"src": [
|
||||||
"<%= path.bower %>/autobahnjs/build/autobahn.min.js",
|
"<%= path.bower %>/autobahnjs/build/autobahn.js",
|
||||||
"<%= path.bower %>/autobahnjs/LICENSE"
|
"<%= path.bower %>/autobahnjs/LICENSE"
|
||||||
],
|
],
|
||||||
"dest": "<%= path.asset %>/autobahnjs/",
|
"dest": "<%= path.asset %>/autobahnjs/",
|
||||||
|
@@ -14,6 +14,7 @@ namespace Alchemy\Phrasea\Controller\Admin;
|
|||||||
use Alchemy\Phrasea\Exception\InvalidArgumentException;
|
use Alchemy\Phrasea\Exception\InvalidArgumentException;
|
||||||
use Alchemy\Phrasea\Form\TaskForm;
|
use Alchemy\Phrasea\Form\TaskForm;
|
||||||
use Alchemy\Phrasea\Model\Entities\Task;
|
use Alchemy\Phrasea\Model\Entities\Task;
|
||||||
|
use Alchemy\Phrasea\TaskManager\TaskManagerStatus;
|
||||||
use Silex\Application;
|
use Silex\Application;
|
||||||
use Silex\ControllerProviderInterface;
|
use Silex\ControllerProviderInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -46,6 +47,10 @@ class TaskManager implements ControllerProviderInterface
|
|||||||
->get('/scheduler', 'controller.admin.task:getScheduler')
|
->get('/scheduler', 'controller.admin.task:getScheduler')
|
||||||
->bind('admin_scheduler');
|
->bind('admin_scheduler');
|
||||||
|
|
||||||
|
$controllers
|
||||||
|
->get('/live', 'controller.admin.task:getLiveInformation')
|
||||||
|
->bind('admin_tasks_live_info');
|
||||||
|
|
||||||
$controllers
|
$controllers
|
||||||
->post('/tasks/create', 'controller.admin.task:postCreateTask')
|
->post('/tasks/create', 'controller.admin.task:postCreateTask')
|
||||||
->bind('admin_tasks_task_create');
|
->bind('admin_tasks_task_create');
|
||||||
@@ -124,22 +129,38 @@ class TaskManager implements ControllerProviderInterface
|
|||||||
return $app->redirectPath('admin_tasks_list');
|
return $app->redirectPath('admin_tasks_list');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getLiveInformation(Application $app, Request $request)
|
||||||
|
{
|
||||||
|
if ($request->getRequestFormat() !== "json") {
|
||||||
|
$app->abort(406, 'Only JSON format is accepted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($app['manipulator.task']->getRepository()->findAll() as $task) {
|
||||||
|
$tasks[$task->getId()] = $app['task-manager.live-information']->getTask($task);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $app->json([
|
||||||
|
'manager' => $app['task-manager.live-information']->getManager(),
|
||||||
|
'tasks' => $tasks
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function getScheduler(Application $app, Request $request)
|
public function getScheduler(Application $app, Request $request)
|
||||||
{
|
{
|
||||||
if ($request->getRequestFormat() !== "json") {
|
if ($request->getRequestFormat() !== "json") {
|
||||||
$app->abort(406, 'Only JSON format is accepted.');
|
$app->abort(406, 'Only JSON format is accepted.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$scheduler = array_replace($app['task-manager.live-information']->getManager(), [
|
return $app->json([
|
||||||
'name' => $app->trans('Task Scheduler'),
|
'name' => $app->trans('Task Scheduler'),
|
||||||
|
'configuration' => $app['task-manager.status']->getStatus(),
|
||||||
'urls' => [
|
'urls' => [
|
||||||
'start' => $app->path('admin_tasks_scheduler_start'),
|
'start' => $app->path('admin_tasks_scheduler_start'),
|
||||||
'stop' => $app->path('admin_tasks_scheduler_stop'),
|
'stop' => $app->path('admin_tasks_scheduler_stop'),
|
||||||
'log' => $app->path('admin_tasks_scheduler_log'),
|
'log' => $app->path('admin_tasks_scheduler_log'),
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $app->json($scheduler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTasks(Application $app, Request $request)
|
public function getTasks(Application $app, Request $request)
|
||||||
@@ -147,11 +168,11 @@ class TaskManager implements ControllerProviderInterface
|
|||||||
$tasks = [];
|
$tasks = [];
|
||||||
|
|
||||||
foreach ($app['repo.tasks']->findAll() as $task) {
|
foreach ($app['repo.tasks']->findAll() as $task) {
|
||||||
$tasks[] = array_replace(
|
$tasks[] = [
|
||||||
$app['task-manager.live-information']->getTask($task), [
|
|
||||||
'id' => $task->getId(),
|
'id' => $task->getId(),
|
||||||
'name' => $task->getName()
|
'name' => $task->getName(),
|
||||||
]);
|
'configuration' => $task->getStatus()
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->getRequestFormat() === "json") {
|
if ($request->getRequestFormat() === "json") {
|
||||||
@@ -165,10 +186,11 @@ class TaskManager implements ControllerProviderInterface
|
|||||||
return $app['twig']->render('admin/task-manager/index.html.twig', [
|
return $app['twig']->render('admin/task-manager/index.html.twig', [
|
||||||
'available_jobs' => $app['task-manager.available-jobs'],
|
'available_jobs' => $app['task-manager.available-jobs'],
|
||||||
'tasks' => $tasks,
|
'tasks' => $tasks,
|
||||||
'scheduler' => array_replace(
|
'scheduler' => [
|
||||||
$app['task-manager.live-information']->getManager(), [
|
'id' => '',
|
||||||
'name' => $app->trans('Task Scheduler')
|
'name' => $app->trans('Task Scheduler'),
|
||||||
])
|
'configuration' => $app['task-manager.status']->getStatus(),
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -51,6 +51,7 @@ $groups = [
|
|||||||
, '//assets/blueimp-load-image/load-image.js'
|
, '//assets/blueimp-load-image/load-image.js'
|
||||||
, '//assets/jquery-file-upload/jquery.iframe-transport.js'
|
, '//assets/jquery-file-upload/jquery.iframe-transport.js'
|
||||||
, '//assets/jquery-file-upload/jquery.fileupload.js'
|
, '//assets/jquery-file-upload/jquery.fileupload.js'
|
||||||
|
, '//assets/autobahnjs/autobahn.js'
|
||||||
],
|
],
|
||||||
'report' => [
|
'report' => [
|
||||||
'//assets/jquery.ui/i18n/jquery-ui-i18n.js'
|
'//assets/jquery.ui/i18n/jquery-ui-i18n.js'
|
||||||
|
@@ -1,19 +1,25 @@
|
|||||||
|
<div id="task-manager-app">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>{{ 'Task Scheduler' | trans }}
|
<h1>{{ 'Task Scheduler' | trans }}
|
||||||
<small style="font-size:16px;">
|
<small style="font-size:16px;">
|
||||||
{% set updateTime %}
|
{% set updateTime %}
|
||||||
<span id="pingTime">{{ "now"|date(constant("DateTime::ISO8601")) }}</span>
|
<span id="pingTime" class="ping-view">{{ "now"|date(constant("DateTime::ISO8601")) }}</span>
|
||||||
{% endset %}
|
{% endset %}
|
||||||
{% trans with {'%updateTime%' : updateTime} %}Last update on %updateTime%{% endtrans %}
|
{% trans with {'%updateTime%' : updateTime} %}Last update on %updateTime%{% endtrans %}
|
||||||
</small>
|
</small>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="task-manager-app">
|
|
||||||
<table class="admintable">
|
<table class="admintable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th class="refresh-view" style="width:80px;" data-refresh-url="{{ path('admin_tasks_live_info') }}">
|
||||||
|
<button class="btn btn-refresh">
|
||||||
|
<i class="icon icon-refresh"/>
|
||||||
|
</button>
|
||||||
|
<i id="spinner" style="font-size:16px" class='icon-spinner icon-spin'>
|
||||||
|
</th>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>PID</th>
|
<th>PID</th>
|
||||||
<th>!</th>
|
<th>!</th>
|
||||||
@@ -22,7 +28,7 @@
|
|||||||
<th>{{ "name" | trans | upper }}</th>
|
<th>{{ "name" | trans | upper }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="scheduler-view">
|
<tbody class="scheduler-view">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="menu">
|
<td class="menu">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
@@ -49,14 +55,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>{{ scheduler["process-id"] }}</td>
|
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>{{ scheduler["actual"] }}</td>
|
<td></td>
|
||||||
|
<td></td>
|
||||||
<td>{{ scheduler["configuration"] }}</td>
|
<td>{{ scheduler["configuration"] }}</td>
|
||||||
<td>{{ scheduler["name"] }}</td>
|
<td>{{ scheduler["name"] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
<tbody id="tasks-list-view">
|
<tbody class="tasks-list-view">
|
||||||
{% for task in tasks %}
|
{% for task in tasks %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="menu">
|
<td class="menu">
|
||||||
@@ -93,10 +99,10 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{% if task["id"] != "taskmanager" %}{{ task["id"] }}{% endif %}</td>
|
<td>{{ task["id"] }}</td>
|
||||||
<td>{{ task["process-id"] }}</td>
|
<td></td>
|
||||||
|
<td></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>{{ task["actual"] }}</td>
|
|
||||||
<td>{{ task["configuration"] }}</td>
|
<td>{{ task["configuration"] }}</td>
|
||||||
<td>{{ task["name"] }}</td>
|
<td>{{ task["name"] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -115,6 +121,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#<script src="/assets/autobahnjs/autobahn.js"></script>#}
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
$("form[name='form-create-task'] select").bind("change", function() {
|
$("form[name='form-create-task'] select").bind("change", function() {
|
||||||
@@ -126,4 +134,5 @@
|
|||||||
{# include js templates #}
|
{# include js templates #}
|
||||||
{% include 'admin/task-manager/templates.html.twig' %}
|
{% include 'admin/task-manager/templates.html.twig' %}
|
||||||
|
|
||||||
<script type="text/javascript" src="{{ path('minifier', { 'f' : 'assets/requirejs/require.js,/scripts/apps/admin/tasks-manager/main.js' }) }}"></script>
|
<script type="text/javascript" src="{{ path('minifier', { 'f' : 'assets/requirejs/require.js' }) }}"></script>
|
||||||
|
<script type="text/javascript" src="{{ path('minifier', { 'f' : 'scripts/apps/admin/tasks-manager/main.js' }) }}"></script>
|
||||||
|
@@ -12,58 +12,90 @@ define([
|
|||||||
"underscore",
|
"underscore",
|
||||||
"backbone",
|
"backbone",
|
||||||
"models/scheduler",
|
"models/scheduler",
|
||||||
|
"common/websockets/connection",
|
||||||
"apps/admin/tasks-manager/views/scheduler",
|
"apps/admin/tasks-manager/views/scheduler",
|
||||||
"apps/admin/tasks-manager/views/tasks",
|
"apps/admin/tasks-manager/views/tasks",
|
||||||
"apps/admin/tasks-manager/views/ping",
|
"apps/admin/tasks-manager/views/ping",
|
||||||
|
"apps/admin/tasks-manager/views/refresh",
|
||||||
"apps/admin/tasks-manager/collections/tasks"
|
"apps/admin/tasks-manager/collections/tasks"
|
||||||
], function ($, _, Backbone, Scheduler, SchedulerView, TasksView, PingView, TasksCollection) {
|
], function ($, _, Backbone, Scheduler, WSConnection, SchedulerView, TasksView, PingView, RefreshView, TasksCollection) {
|
||||||
var create = function() {
|
var create = function() {
|
||||||
window.TaskManagerApp = {
|
window.TaskManagerApp = {
|
||||||
$scope: $("#task-manager-app"),
|
$scope: $("#task-manager-app"),
|
||||||
$tasksListView : $("#tasks-list-view", this.$scope),
|
$tasksListView : $(".tasks-list-view", this.$scope),
|
||||||
$schedulerView : $("#scheduler-view", this.$scope),
|
$schedulerView : $(".scheduler-view", this.$scope),
|
||||||
$pingView : $("#pingTime", this.$scope)
|
$pingView : $(".ping-view", this.$scope),
|
||||||
|
$refreshView : $(".refresh-view", this.$scope),
|
||||||
|
eventAggregator: _.extend({}, Backbone.Events),
|
||||||
|
wsuri: "ws://dev.phrasea.net:9090/websockets",
|
||||||
|
wstopic: "http://phraseanet.com/topics/admin/task-manager"
|
||||||
};
|
};
|
||||||
|
|
||||||
TaskManagerApp.tasksCollection = new TasksCollection();
|
TaskManagerApp.tasksCollection = new TasksCollection();
|
||||||
TaskManagerApp.Scheduler = new Scheduler();
|
TaskManagerApp.Scheduler = new Scheduler();
|
||||||
|
TaskManagerApp.pingView = new PingView({el: TaskManagerApp.$pingView});
|
||||||
TaskManagerApp.pingView = new PingView({
|
TaskManagerApp.refreshView = new RefreshView({
|
||||||
el: TaskManagerApp.$pingView
|
el: TaskManagerApp.$refreshView,
|
||||||
|
pingView: TaskManagerApp.pingView,
|
||||||
|
tasksCollection: TaskManagerApp.tasksCollection,
|
||||||
|
scheduler: TaskManagerApp.Scheduler
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var load = function() {
|
var load = function() {
|
||||||
|
TaskManagerApp.refreshView.refreshAction();
|
||||||
// fetch objects
|
// fetch objects
|
||||||
$.when.apply($, [
|
$.when.apply($, [
|
||||||
TaskManagerApp.tasksCollection.fetch(),
|
TaskManagerApp.tasksCollection.fetch(),
|
||||||
TaskManagerApp.Scheduler.fetch()
|
TaskManagerApp.Scheduler.fetch()
|
||||||
]).done(
|
]).done(
|
||||||
function () {
|
function () {
|
||||||
TaskManagerApp.schedulerView = new SchedulerView({
|
// Init & render views
|
||||||
model: TaskManagerApp.Scheduler,
|
TaskManagerApp.schedulerView = new SchedulerView({model: TaskManagerApp.Scheduler, el: TaskManagerApp.$schedulerView});
|
||||||
el: TaskManagerApp.$schedulerView
|
TaskManagerApp.tasksView = new TasksView({collection: TaskManagerApp.tasksCollection, el: TaskManagerApp.$tasksListView});
|
||||||
});
|
|
||||||
TaskManagerApp.tasksView = new TasksView({
|
|
||||||
collection: TaskManagerApp.tasksCollection,
|
|
||||||
el: TaskManagerApp.$tasksListView
|
|
||||||
});
|
|
||||||
|
|
||||||
// render views
|
|
||||||
TaskManagerApp.tasksView.render();
|
TaskManagerApp.tasksView.render();
|
||||||
TaskManagerApp.schedulerView.render();
|
TaskManagerApp.schedulerView.render();
|
||||||
|
|
||||||
|
// Sets connection to the web socket
|
||||||
|
var ws = new WSConnection({url:TaskManagerApp.wsuri, topic: TaskManagerApp.wstopic, eventAggregator: TaskManagerApp.eventAggregator});
|
||||||
|
ws.run();
|
||||||
|
|
||||||
|
// On ticks re-render ping view, update tasks & scheduler model
|
||||||
|
TaskManagerApp.eventAggregator.on("ws:manager-tick", function(response) {
|
||||||
|
var $this = this;
|
||||||
|
$this.pingView.render();
|
||||||
|
$this.Scheduler.set({"actual": "started", "process-id": response.message.manager["process-id"]});
|
||||||
|
_.each(response.message.jobs, function(data, id) {
|
||||||
|
var jobModel = $this.tasksCollection.get(id);
|
||||||
|
if ("undefined" !== jobModel) {
|
||||||
|
jobModel.set({"actual": data["status"], "process-id": data["process-id"]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
var initialize = function () {
|
var initialize = function () {
|
||||||
create();
|
create();
|
||||||
|
var regexp = /task-manager/;
|
||||||
|
$(document).ajaxComplete(function(event, request, settings) {
|
||||||
|
if ("undefined" !== typeof settings && regexp.test(settings.url)) {
|
||||||
|
TaskManagerApp.refreshView.loadState(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).ajaxStart(function(event, request, settings) {
|
||||||
|
if ("undefined" !== typeof settings && regexp.test(settings.url)) {
|
||||||
|
TaskManagerApp.refreshView.loadState(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
load();
|
load();
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
create: create,
|
|
||||||
load: load,
|
|
||||||
initialize: initialize
|
initialize: initialize
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
72
www/scripts/apps/admin/tasks-manager/views/refresh.js
Normal file
72
www/scripts/apps/admin/tasks-manager/views/refresh.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of Phraseanet
|
||||||
|
*
|
||||||
|
* (c) 2005-2014 Alchemy
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
define([
|
||||||
|
"jquery",
|
||||||
|
"underscore",
|
||||||
|
"backbone",
|
||||||
|
""
|
||||||
|
], function ($, _, Backbone) {
|
||||||
|
var RefreshView = Backbone.View.extend({
|
||||||
|
initialize: function(options) {
|
||||||
|
if (!"pingView" in options) {
|
||||||
|
throw "You must set the ping view"
|
||||||
|
}
|
||||||
|
this.pingView = options.pingView;
|
||||||
|
if (!"scheduler" in options) {
|
||||||
|
throw "You must set the scheduler model"
|
||||||
|
}
|
||||||
|
this.scheduler = options.scheduler;
|
||||||
|
if (!"tasksCollection" in options) {
|
||||||
|
throw "You must set the tasks collection model"
|
||||||
|
}
|
||||||
|
this.tasksCollection = options.tasksCollection;
|
||||||
|
|
||||||
|
this.refreshUrl = this.$el.data('refresh-url');
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
"click .btn-refresh": "refreshAction"
|
||||||
|
},
|
||||||
|
refreshAction: function(event) {
|
||||||
|
var $this = this;
|
||||||
|
$.ajax({
|
||||||
|
dataType: "json",
|
||||||
|
url: $this.refreshUrl,
|
||||||
|
data: {},
|
||||||
|
success: function(response) {
|
||||||
|
$this.pingView.render();
|
||||||
|
$this.scheduler.set({
|
||||||
|
"actual": response.manager["actual"],
|
||||||
|
"process-id": response.manager["process-id"],
|
||||||
|
"configuration": response.manager["configuration"]
|
||||||
|
});
|
||||||
|
_.each(response.tasks, function(data, id) {
|
||||||
|
var jobModel = $this.tasksCollection.get(id);
|
||||||
|
if ("undefined" !== jobModel) {
|
||||||
|
jobModel.set({
|
||||||
|
"actual": data["actual"],
|
||||||
|
"process-id": data["process-id"],
|
||||||
|
"configuration": data["configuration"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
loadState: function(state) {
|
||||||
|
if (state) {
|
||||||
|
$("#spinner", this.$el).addClass('icon-spinner icon-spin');
|
||||||
|
} else {
|
||||||
|
$("#spinner", this.$el).removeClass('icon-spinner icon-spin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return RefreshView;
|
||||||
|
});
|
36
www/scripts/common/websockets/connection.js
Normal file
36
www/scripts/common/websockets/connection.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
define([
|
||||||
|
"underscore"
|
||||||
|
], function (_) {
|
||||||
|
return function (options) {
|
||||||
|
if (!"url" in options) {
|
||||||
|
throw "You must set the websocket 'url'"
|
||||||
|
}
|
||||||
|
if (!"topic" in options) {
|
||||||
|
throw "You must set the websocket 'topic'"
|
||||||
|
}
|
||||||
|
if (!"eventAggregator" in options) {
|
||||||
|
throw "You must set an event aggregator"
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventAggregator = options.eventAggregator;
|
||||||
|
|
||||||
|
return {
|
||||||
|
run: function() {
|
||||||
|
// autobahn js is defined as a global object there is no way to load
|
||||||
|
// it as a UMD module
|
||||||
|
ab.connect(options.url, function (session) {
|
||||||
|
eventAggregator.trigger("ws:connect", session);
|
||||||
|
session.subscribe(options.topic, function (topic, msg) {
|
||||||
|
// double encoded string
|
||||||
|
var msg = JSON.parse(JSON.parse(msg));
|
||||||
|
eventAggregator.trigger("ws:"+msg.event, msg, session);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
function (code, reason) {
|
||||||
|
eventAggregator.trigger("ws:session-gone", code,reason);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
Reference in New Issue
Block a user