SEBSERV-182
This commit is contained in:
parent
c217d4d854
commit
c7952b32bc
7 changed files with 101 additions and 50 deletions
|
@ -36,6 +36,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
|
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||||
|
import ch.ethz.seb.sebserver.gui.content.MonitoringRunningExam.ProctoringUpdateErrorHandler;
|
||||||
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
|
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
|
||||||
import ch.ethz.seb.sebserver.gui.service.ResourceService;
|
import ch.ethz.seb.sebserver.gui.service.ResourceService;
|
||||||
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
|
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
|
||||||
|
@ -269,11 +270,14 @@ public class MonitoringClientConnection implements TemplateComposer {
|
||||||
|
|
||||||
final Supplier<EntityTable<ClientNotification>> notificationTableSupplier = _notificationTableSupplier;
|
final Supplier<EntityTable<ClientNotification>> notificationTableSupplier = _notificationTableSupplier;
|
||||||
// server push update
|
// server push update
|
||||||
|
final ProctoringUpdateErrorHandler proctoringUpdateErrorHandler =
|
||||||
|
new ProctoringUpdateErrorHandler(this.pageService, pageContext);
|
||||||
|
|
||||||
this.serverPushService.runServerPush(
|
this.serverPushService.runServerPush(
|
||||||
new ServerPushContext(
|
new ServerPushContext(
|
||||||
content,
|
content,
|
||||||
Utils.truePredicate(),
|
Utils.truePredicate(),
|
||||||
MonitoringRunningExam.createServerPushUpdateErrorHandler(this.pageService, pageContext)),
|
proctoringUpdateErrorHandler),
|
||||||
this.pollInterval,
|
this.pollInterval,
|
||||||
context -> clientConnectionDetails.updateData(),
|
context -> clientConnectionDetails.updateData(),
|
||||||
context -> clientConnectionDetails.updateGUI(notificationTableSupplier, pageContext));
|
context -> clientConnectionDetails.updateGUI(notificationTableSupplier, pageContext));
|
||||||
|
|
|
@ -149,13 +149,22 @@ public class MonitoringRunningExam implements TemplateComposer {
|
||||||
restService.getBuilder(GetClientConnectionDataList.class)
|
restService.getBuilder(GetClientConnectionDataList.class)
|
||||||
.withURIVariable(API.PARAM_PARENT_MODEL_ID, exam.getModelId());
|
.withURIVariable(API.PARAM_PARENT_MODEL_ID, exam.getModelId());
|
||||||
|
|
||||||
|
final ProctoringUpdateErrorHandler proctoringUpdateErrorHandler =
|
||||||
|
new ProctoringUpdateErrorHandler(this.pageService, pageContext);
|
||||||
|
|
||||||
|
final ServerPushContext pushContext = new ServerPushContext(
|
||||||
|
content,
|
||||||
|
Utils.truePredicate(),
|
||||||
|
proctoringUpdateErrorHandler);
|
||||||
|
|
||||||
final ClientConnectionTable clientTable = new ClientConnectionTable(
|
final ClientConnectionTable clientTable = new ClientConnectionTable(
|
||||||
this.pageService,
|
this.pageService,
|
||||||
tablePane,
|
tablePane,
|
||||||
this.asyncRunner,
|
this.asyncRunner,
|
||||||
exam,
|
exam,
|
||||||
indicators,
|
indicators,
|
||||||
restCall);
|
restCall,
|
||||||
|
pushContext);
|
||||||
|
|
||||||
clientTable
|
clientTable
|
||||||
.withDefaultAction(
|
.withDefaultAction(
|
||||||
|
@ -172,10 +181,7 @@ public class MonitoringRunningExam implements TemplateComposer {
|
||||||
ActionDefinition.MONITOR_EXAM_NEW_PROCTOR_ROOM));
|
ActionDefinition.MONITOR_EXAM_NEW_PROCTOR_ROOM));
|
||||||
|
|
||||||
this.serverPushService.runServerPush(
|
this.serverPushService.runServerPush(
|
||||||
new ServerPushContext(
|
pushContext,
|
||||||
content,
|
|
||||||
context -> clientTable.getUpdateErrors() < 5,
|
|
||||||
createServerPushUpdateErrorHandler(this.pageService, pageContext)),
|
|
||||||
this.pollInterval,
|
this.pollInterval,
|
||||||
context -> clientTable.updateValues(),
|
context -> clientTable.updateValues(),
|
||||||
updateTableGUI(clientTable));
|
updateTableGUI(clientTable));
|
||||||
|
@ -281,18 +287,26 @@ public class MonitoringRunningExam implements TemplateComposer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final ProctoringUpdateErrorHandler proctoringUpdateErrorHandler =
|
||||||
|
new ProctoringUpdateErrorHandler(this.pageService, pageContext);
|
||||||
|
|
||||||
|
final ServerPushContext pushContext = new ServerPushContext(
|
||||||
|
parent,
|
||||||
|
Utils.truePredicate(),
|
||||||
|
proctoringUpdateErrorHandler);
|
||||||
|
|
||||||
this.monitoringProctoringService.initCollectingRoomActions(
|
this.monitoringProctoringService.initCollectingRoomActions(
|
||||||
|
pushContext,
|
||||||
pageContext,
|
pageContext,
|
||||||
actionBuilder,
|
actionBuilder,
|
||||||
proctoringSettings,
|
proctoringSettings,
|
||||||
proctoringGUIService);
|
proctoringGUIService);
|
||||||
|
|
||||||
this.serverPushService.runServerPush(
|
this.serverPushService.runServerPush(
|
||||||
new ServerPushContext(
|
pushContext,
|
||||||
parent,
|
|
||||||
Utils.truePredicate(),
|
|
||||||
createServerPushUpdateErrorHandler(this.pageService, pageContext)),
|
|
||||||
this.proctoringRoomUpdateInterval,
|
this.proctoringRoomUpdateInterval,
|
||||||
context -> this.monitoringProctoringService.updateCollectingRoomActions(
|
context -> this.monitoringProctoringService.updateCollectingRoomActions(
|
||||||
|
context,
|
||||||
pageContext,
|
pageContext,
|
||||||
actionBuilder,
|
actionBuilder,
|
||||||
proctoringSettings,
|
proctoringSettings,
|
||||||
|
@ -480,30 +494,53 @@ public class MonitoringRunningExam implements TemplateComposer {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static final Function<Exception, Boolean> createServerPushUpdateErrorHandler(
|
static final class ProctoringUpdateErrorHandler implements Function<Exception, Boolean> {
|
||||||
final PageService pageService,
|
|
||||||
final PageContext pageContext) {
|
|
||||||
|
|
||||||
return error -> {
|
private final PageService pageService;
|
||||||
log.error("Fialed to update server push: {}", error.getMessage());
|
private final PageContext pageContext;
|
||||||
|
|
||||||
|
private int errors = 0;
|
||||||
|
|
||||||
|
public ProctoringUpdateErrorHandler(
|
||||||
|
final PageService pageService,
|
||||||
|
final PageContext pageContext) {
|
||||||
|
|
||||||
|
this.pageService = pageService;
|
||||||
|
this.pageContext = pageContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkUserSession() {
|
||||||
try {
|
try {
|
||||||
pageService.getCurrentUser().get();
|
this.pageService.getCurrentUser().get();
|
||||||
|
return true;
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.error("Failed to verify current user after server push error: {}", e.getMessage());
|
try {
|
||||||
log.info("Force logout and session cleanup...");
|
this.pageContext.forwardToLoginPage();
|
||||||
pageContext.forwardToLoginPage();
|
final MessageBox logoutSuccess = new Message(
|
||||||
final MessageBox logoutSuccess = new Message(
|
this.pageContext.getShell(),
|
||||||
pageContext.getShell(),
|
this.pageService.getI18nSupport().getText("sebserver.logout"),
|
||||||
pageService.getI18nSupport().getText("sebserver.logout"),
|
Utils.formatLineBreaks(
|
||||||
Utils.formatLineBreaks(
|
this.pageService.getI18nSupport()
|
||||||
pageService.getI18nSupport().getText("sebserver.logout.invalid-session.message")),
|
.getText("sebserver.logout.invalid-session.message")),
|
||||||
SWT.ICON_INFORMATION,
|
SWT.ICON_INFORMATION,
|
||||||
pageService.getI18nSupport());
|
this.pageService.getI18nSupport());
|
||||||
logoutSuccess.open(null);
|
logoutSuccess.open(null);
|
||||||
|
} catch (final Exception ee) {
|
||||||
|
log.warn("Unable to auto-logout: ", ee.getMessage());
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
}
|
||||||
};
|
|
||||||
|
@Override
|
||||||
|
public Boolean apply(final Exception error) {
|
||||||
|
this.errors++;
|
||||||
|
log.error("Failed to update server push: {}", error.getMessage());
|
||||||
|
if (this.errors > 5) {
|
||||||
|
checkUserSession();
|
||||||
|
}
|
||||||
|
return this.errors > 5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ public final class ServerPushContext {
|
||||||
|
|
||||||
public final Composite anchor;
|
public final Composite anchor;
|
||||||
public final Predicate<ServerPushContext> runAgain;
|
public final Predicate<ServerPushContext> runAgain;
|
||||||
public final Function<Exception, Boolean> errorHandler;
|
final Function<Exception, Boolean> errorHandler;
|
||||||
boolean internalStop = false;
|
boolean internalStop = false;
|
||||||
|
|
||||||
public ServerPushContext(
|
public ServerPushContext(
|
||||||
|
@ -36,6 +36,12 @@ public final class ServerPushContext {
|
||||||
this.runAgain = runAgain;
|
this.runAgain = runAgain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void reportError(final Exception error) {
|
||||||
|
if (this.errorHandler != null) {
|
||||||
|
this.internalStop = this.errorHandler.apply(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean runAgain() {
|
public boolean runAgain() {
|
||||||
return !this.internalStop && this.runAgain.test(this);
|
return !this.internalStop && this.runAgain.test(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@ import ch.ethz.seb.sebserver.gui.service.ResourceService;
|
||||||
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
|
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
|
||||||
import ch.ethz.seb.sebserver.gui.service.page.PageService;
|
import ch.ethz.seb.sebserver.gui.service.page.PageService;
|
||||||
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
|
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
|
||||||
|
import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.DisposedOAuth2RestTemplateException;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.DisposedOAuth2RestTemplateException;
|
||||||
import ch.ethz.seb.sebserver.gui.service.session.IndicatorData.ThresholdColor;
|
import ch.ethz.seb.sebserver.gui.service.session.IndicatorData.ThresholdColor;
|
||||||
|
@ -95,6 +96,8 @@ public final class ClientConnectionTable {
|
||||||
private final AsyncRunner asyncRunner;
|
private final AsyncRunner asyncRunner;
|
||||||
private final Exam exam;
|
private final Exam exam;
|
||||||
private final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCallBuilder;
|
private final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCallBuilder;
|
||||||
|
private final ServerPushContext pushConext;
|
||||||
|
|
||||||
private final Map<Long, IndicatorData> indicatorMapping;
|
private final Map<Long, IndicatorData> indicatorMapping;
|
||||||
private final Table table;
|
private final Table table;
|
||||||
private final ColorData colorData;
|
private final ColorData colorData;
|
||||||
|
@ -116,7 +119,7 @@ public final class ClientConnectionTable {
|
||||||
private boolean forceUpdateAll = false;
|
private boolean forceUpdateAll = false;
|
||||||
private boolean updateInProgress = false;
|
private boolean updateInProgress = false;
|
||||||
|
|
||||||
private int updateErrors = 0;
|
//private int updateErrors = 0;
|
||||||
|
|
||||||
public ClientConnectionTable(
|
public ClientConnectionTable(
|
||||||
final PageService pageService,
|
final PageService pageService,
|
||||||
|
@ -124,12 +127,14 @@ public final class ClientConnectionTable {
|
||||||
final AsyncRunner asyncRunner,
|
final AsyncRunner asyncRunner,
|
||||||
final Exam exam,
|
final Exam exam,
|
||||||
final Collection<Indicator> indicators,
|
final Collection<Indicator> indicators,
|
||||||
final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCallBuilder) {
|
final RestCall<Collection<ClientConnectionData>>.RestCallBuilder restCallBuilder,
|
||||||
|
final ServerPushContext pushConext) {
|
||||||
|
|
||||||
this.pageService = pageService;
|
this.pageService = pageService;
|
||||||
this.asyncRunner = asyncRunner;
|
this.asyncRunner = asyncRunner;
|
||||||
this.exam = exam;
|
this.exam = exam;
|
||||||
this.restCallBuilder = restCallBuilder;
|
this.restCallBuilder = restCallBuilder;
|
||||||
|
this.pushConext = pushConext;
|
||||||
|
|
||||||
final WidgetFactory widgetFactory = pageService.getWidgetFactory();
|
final WidgetFactory widgetFactory = pageService.getWidgetFactory();
|
||||||
final ResourceService resourceService = pageService.getResourceService();
|
final ResourceService resourceService = pageService.getResourceService();
|
||||||
|
@ -190,9 +195,9 @@ public final class ClientConnectionTable {
|
||||||
this.table.layout();
|
this.table.layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getUpdateErrors() {
|
// public int getUpdateErrors() {
|
||||||
return this.updateErrors;
|
// return this.updateErrors;
|
||||||
}
|
// }
|
||||||
|
|
||||||
public WidgetFactory getWidgetFactory() {
|
public WidgetFactory getWidgetFactory() {
|
||||||
return this.pageService.getWidgetFactory();
|
return this.pageService.getWidgetFactory();
|
||||||
|
@ -322,14 +327,17 @@ public final class ClientConnectionTable {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateInProgress = true;
|
this.updateInProgress = true;
|
||||||
this.asyncRunner.runAsync(this::updateValuesAsync);
|
final boolean needsSync = this.tableMapping != null &&
|
||||||
|
this.table != null &&
|
||||||
|
this.tableMapping.size() != this.table.getItemCount();
|
||||||
|
this.asyncRunner.runAsync(() -> updateValuesAsync(needsSync));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateValuesAsync() {
|
private void updateValuesAsync(final boolean needsSync) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (this.statusFilterChanged || this.forceUpdateAll) {
|
if (this.statusFilterChanged || this.forceUpdateAll || needsSync) {
|
||||||
this.toDelete.clear();
|
this.toDelete.clear();
|
||||||
this.toDelete.addAll(this.tableMapping.keySet());
|
this.toDelete.addAll(this.tableMapping.keySet());
|
||||||
}
|
}
|
||||||
|
@ -337,8 +345,8 @@ public final class ClientConnectionTable {
|
||||||
.withHeader(API.EXAM_MONITORING_STATE_FILTER, this.statusFilterParam)
|
.withHeader(API.EXAM_MONITORING_STATE_FILTER, this.statusFilterParam)
|
||||||
.call()
|
.call()
|
||||||
.get(error -> {
|
.get(error -> {
|
||||||
log.error("Unexpected error while trying to get client connection table data: ", error);
|
|
||||||
recoverFromDisposedRestTemplate(error);
|
recoverFromDisposedRestTemplate(error);
|
||||||
|
this.pushConext.reportError(error);
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
})
|
})
|
||||||
.forEach(data -> {
|
.forEach(data -> {
|
||||||
|
@ -367,11 +375,9 @@ public final class ClientConnectionTable {
|
||||||
|
|
||||||
this.forceUpdateAll = false;
|
this.forceUpdateAll = false;
|
||||||
this.updateInProgress = false;
|
this.updateInProgress = false;
|
||||||
this.updateErrors = 0;
|
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.error("Unexpected error while updating client connection table: ", e);
|
this.pushConext.reportError(e);
|
||||||
this.updateErrors++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService;
|
||||||
import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder;
|
import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder;
|
||||||
import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent;
|
import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent;
|
||||||
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
|
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
|
||||||
|
import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection;
|
||||||
|
@ -152,6 +153,7 @@ public class MonitoringProctoringService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initCollectingRoomActions(
|
public void initCollectingRoomActions(
|
||||||
|
final ServerPushContext pushContext,
|
||||||
final PageContext pageContext,
|
final PageContext pageContext,
|
||||||
final PageActionBuilder actionBuilder,
|
final PageActionBuilder actionBuilder,
|
||||||
final ProctoringServiceSettings proctoringSettings,
|
final ProctoringServiceSettings proctoringSettings,
|
||||||
|
@ -159,6 +161,7 @@ public class MonitoringProctoringService {
|
||||||
|
|
||||||
proctoringGUIService.clearCollectingRoomActionState();
|
proctoringGUIService.clearCollectingRoomActionState();
|
||||||
updateCollectingRoomActions(
|
updateCollectingRoomActions(
|
||||||
|
pushContext,
|
||||||
pageContext,
|
pageContext,
|
||||||
actionBuilder,
|
actionBuilder,
|
||||||
proctoringSettings,
|
proctoringSettings,
|
||||||
|
@ -166,6 +169,7 @@ public class MonitoringProctoringService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateCollectingRoomActions(
|
public void updateCollectingRoomActions(
|
||||||
|
final ServerPushContext pushContext,
|
||||||
final PageContext pageContext,
|
final PageContext pageContext,
|
||||||
final PageActionBuilder actionBuilder,
|
final PageActionBuilder actionBuilder,
|
||||||
final ProctoringServiceSettings proctoringSettings,
|
final ProctoringServiceSettings proctoringSettings,
|
||||||
|
@ -179,7 +183,7 @@ public class MonitoringProctoringService {
|
||||||
.getBuilder(GetCollectingRooms.class)
|
.getBuilder(GetCollectingRooms.class)
|
||||||
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
|
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
|
||||||
.call()
|
.call()
|
||||||
.onError(error -> log.error("Failed to update proctoring rooms on GUI {}", error.getMessage()))
|
.onError(error -> pushContext.reportError(error))
|
||||||
.getOr(Collections.emptyList())
|
.getOr(Collections.emptyList())
|
||||||
.stream()
|
.stream()
|
||||||
.forEach(room -> {
|
.forEach(room -> {
|
||||||
|
|
|
@ -99,9 +99,6 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
window.addEventListener('unload', () => {
|
window.addEventListener('unload', () => {
|
||||||
ZoomMtg.muteAll({
|
|
||||||
muteAll: true
|
|
||||||
});
|
|
||||||
ZoomMtg.endMeeting({});
|
ZoomMtg.endMeeting({});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -21,7 +21,7 @@ import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService
|
||||||
public class ZoomWindowScriptResolverTest {
|
public class ZoomWindowScriptResolverTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testJitsiWindowScriptResolver() {
|
public void testZoomWindowScriptResolver() {
|
||||||
final DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader();
|
final DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader();
|
||||||
final Resource resource = defaultResourceLoader.getResource(ZoomWindowScriptResolver.RES_PATH);
|
final Resource resource = defaultResourceLoader.getResource(ZoomWindowScriptResolver.RES_PATH);
|
||||||
final ZoomWindowScriptResolver zoomWindowScriptResolver = new ZoomWindowScriptResolver(resource);
|
final ZoomWindowScriptResolver zoomWindowScriptResolver = new ZoomWindowScriptResolver(resource);
|
||||||
|
@ -166,9 +166,6 @@ public class ZoomWindowScriptResolverTest {
|
||||||
+ " })\r\n"
|
+ " })\r\n"
|
||||||
+ " \r\n"
|
+ " \r\n"
|
||||||
+ " window.addEventListener('unload', () => {\r\n"
|
+ " window.addEventListener('unload', () => {\r\n"
|
||||||
+ " ZoomMtg.muteAll({\r\n"
|
|
||||||
+ " muteAll: true\r\n"
|
|
||||||
+ " });\r\n"
|
|
||||||
+ " ZoomMtg.endMeeting({});\r\n"
|
+ " ZoomMtg.endMeeting({});\r\n"
|
||||||
+ " });\r\n"
|
+ " });\r\n"
|
||||||
+ " </script>\r\n"
|
+ " </script>\r\n"
|
||||||
|
|
Loading…
Reference in a new issue