mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-16 14:23:28 +07:00
built-in ws: reload only associated page (WEB-49146)
GitOrigin-RevId: 73f2de71c4bc05f12130af555ec86bc524903553
This commit is contained in:
committed by
intellij-monorepo-bot
parent
3fbeaa8ae3
commit
4baf87789c
@@ -2,8 +2,8 @@
|
|||||||
package org.jetbrains.builtInWebServer.liveReload;
|
package org.jetbrains.builtInWebServer.liveReload;
|
||||||
|
|
||||||
import com.intellij.openapi.vfs.AsyncFileListener;
|
import com.intellij.openapi.vfs.AsyncFileListener;
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
|
||||||
import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
|
import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
|
||||||
|
import com.intellij.util.containers.ContainerUtil;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
@@ -11,26 +11,9 @@ import java.util.List;
|
|||||||
|
|
||||||
class WebServerFileContentListener implements AsyncFileListener {
|
class WebServerFileContentListener implements AsyncFileListener {
|
||||||
|
|
||||||
private static final ChangeApplier RELOAD_ALL = new ChangeApplier() {
|
|
||||||
@Override
|
|
||||||
public void afterVfsChange() {
|
|
||||||
WebServerPageConnectionService.getInstance().reloadAll();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public ChangeApplier prepareChange(@NotNull List<? extends @NotNull VFileEvent> events) {
|
public ChangeApplier prepareChange(@NotNull List<? extends @NotNull VFileEvent> events) {
|
||||||
boolean hasRelatedFileChanged = false;
|
return WebServerPageConnectionService.getInstance().reloadRelatedClients(ContainerUtil.map(events, VFileEvent::getFile));
|
||||||
for (VFileEvent event : events) {
|
|
||||||
VirtualFile file = event.getFile();
|
|
||||||
if (file != null && WebServerPageConnectionService.getInstance().isFileRequested(file)) {
|
|
||||||
hasRelatedFileChanged = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hasRelatedFileChanged) return null;
|
|
||||||
|
|
||||||
return RELOAD_ALL;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||||
package org.jetbrains.builtInWebServer.liveReload;
|
package org.jetbrains.builtInWebServer.liveReload;
|
||||||
|
|
||||||
|
import com.google.common.net.HttpHeaders;
|
||||||
import com.intellij.openapi.Disposable;
|
import com.intellij.openapi.Disposable;
|
||||||
import com.intellij.openapi.application.ApplicationManager;
|
import com.intellij.openapi.application.ApplicationManager;
|
||||||
import com.intellij.openapi.components.Service;
|
import com.intellij.openapi.components.Service;
|
||||||
import com.intellij.openapi.util.Disposer;
|
import com.intellij.openapi.util.Disposer;
|
||||||
|
import com.intellij.openapi.util.text.StringUtil;
|
||||||
|
import com.intellij.openapi.vfs.AsyncFileListener;
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
import com.intellij.openapi.vfs.VirtualFile;
|
||||||
import com.intellij.openapi.vfs.VirtualFileManager;
|
import com.intellij.openapi.vfs.VirtualFileManager;
|
||||||
import com.intellij.util.io.NettyKt;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import io.netty.handler.codec.http.FullHttpRequest;
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
@@ -15,36 +17,70 @@ import io.netty.handler.codec.http.QueryStringDecoder;
|
|||||||
import io.netty.util.CharsetUtil;
|
import io.netty.util.CharsetUtil;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.jetbrains.builtInWebServer.BuiltInServerOptions;
|
|
||||||
import org.jetbrains.io.jsonRpc.Client;
|
import org.jetbrains.io.jsonRpc.Client;
|
||||||
import org.jetbrains.io.jsonRpc.ClientManager;
|
import org.jetbrains.io.jsonRpc.ClientManager;
|
||||||
import org.jetbrains.io.jsonRpc.JsonRpcServer;
|
import org.jetbrains.io.jsonRpc.JsonRpcServer;
|
||||||
import org.jetbrains.io.jsonRpc.MessageServer;
|
import org.jetbrains.io.jsonRpc.MessageServer;
|
||||||
|
import org.jetbrains.io.webSocket.WebSocketClient;
|
||||||
import org.jetbrains.io.webSocket.WebSocketHandshakeHandler;
|
import org.jetbrains.io.webSocket.WebSocketHandshakeHandler;
|
||||||
|
|
||||||
import java.util.List;
|
import java.net.URI;
|
||||||
import java.util.Map;
|
import java.util.*;
|
||||||
import java.util.Objects;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides support for automatic reloading of pages opened on built-in web server on related files modification.
|
||||||
|
*
|
||||||
|
* Implementation:
|
||||||
|
*
|
||||||
|
* <-- html page with {@link #RELOAD_URL_PARAM} is requested
|
||||||
|
* --> response with modified html which opens WebSocket connection listening for reload message
|
||||||
|
*
|
||||||
|
* <-- script or other resource of html is requested
|
||||||
|
* start listening for related file changes
|
||||||
|
*
|
||||||
|
* file is changed
|
||||||
|
* --> reload associated pages by sending WebSocket message
|
||||||
|
*/
|
||||||
@Service(Service.Level.APP)
|
@Service(Service.Level.APP)
|
||||||
public final class WebServerPageConnectionService {
|
public final class WebServerPageConnectionService {
|
||||||
|
|
||||||
public static final String RELOAD_URL_PARAM = "_ij_reload";
|
public static final String RELOAD_URL_PARAM = "_ij_reload";
|
||||||
private static final String RELOAD_WS_REQUEST = "reload";
|
private static final String RELOAD_WS_REQUEST = "reload";
|
||||||
private static final String RELOAD_WS_URL_PREFIX = "jb-server-page";
|
private static final String RELOAD_WS_URL_PREFIX = "jb-server-page";
|
||||||
|
private static final String RELOAD_CLIENT_ID_URL_PARAMETER = "reloadServiceClientId";
|
||||||
|
|
||||||
private final @NotNull ByteBuf RELOAD_PAGE_MESSAGE = Unpooled.copiedBuffer(RELOAD_WS_REQUEST, CharsetUtil.US_ASCII).asReadOnly();
|
private final @NotNull ByteBuf RELOAD_PAGE_MESSAGE = Unpooled.copiedBuffer(RELOAD_WS_REQUEST, CharsetUtil.US_ASCII).asReadOnly();
|
||||||
|
|
||||||
private @Nullable ClientManager myServer;
|
private @Nullable ClientManager myServer;
|
||||||
private @Nullable JsonRpcServer myRpcServer;
|
private @Nullable JsonRpcServer myRpcServer;
|
||||||
|
|
||||||
private final @NotNull AtomicInteger ourListenersCount = new AtomicInteger(0);
|
private final @NotNull AtomicInteger myClientsCount = new AtomicInteger(0);
|
||||||
private volatile @Nullable Disposable ourListenerDisposable;
|
private volatile @Nullable Disposable myListenerDisposable;
|
||||||
private final @NotNull Set<VirtualFile> myRequestedFiles = ConcurrentHashMap.newKeySet();
|
private final @NotNull AtomicInteger myTotalClientsCount = new AtomicInteger(0);
|
||||||
|
private final @NotNull Set<VirtualFile> myRequestedFilesWithoutReferrer = ConcurrentHashMap.newKeySet();
|
||||||
|
private final @NotNull Map<String, RequestedPage> myRequestedPages = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final @NotNull AsyncFileListener.ChangeApplier RELOAD_ALL = new AsyncFileListener.ChangeApplier() {
|
||||||
|
@Override
|
||||||
|
public void afterVfsChange() {
|
||||||
|
myRequestedFilesWithoutReferrer.clear();
|
||||||
|
Iterator<Map.Entry<String, RequestedPage>> iterator = myRequestedPages.entrySet().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Map.Entry<String, RequestedPage> next = iterator.next();
|
||||||
|
next.getValue().myClient.cancel(false);
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientManager server = myServer;
|
||||||
|
if (server != null) {
|
||||||
|
server.send(-1, RELOAD_PAGE_MESSAGE.retainedDuplicate(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public static WebServerPageConnectionService getInstance() {
|
public static WebServerPageConnectionService getInstance() {
|
||||||
return ApplicationManager.getApplication().getService(WebServerPageConnectionService.class);
|
return ApplicationManager.getApplication().getService(WebServerPageConnectionService.class);
|
||||||
@@ -59,19 +95,35 @@ public final class WebServerPageConnectionService {
|
|||||||
isReloadRequest = decoder.parameters().containsKey(RELOAD_URL_PARAM);
|
isReloadRequest = decoder.parameters().containsKey(RELOAD_URL_PARAM);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isReloadRequest || !myRequestedFiles.isEmpty()) {
|
if (!isReloadRequest && myRequestedPages.isEmpty()) return null;
|
||||||
VirtualFile file = fileSupplier.get();
|
|
||||||
if (file != null) {
|
VirtualFile file = fileSupplier.get();
|
||||||
myRequestedFiles.add(file);
|
if (!isReloadRequest && file != null) {
|
||||||
|
String referer = request.headers().get(HttpHeaders.REFERER);
|
||||||
|
RequestedPage requestedPage = null;
|
||||||
|
try {
|
||||||
|
URI refererUri = URI.create(referer);
|
||||||
|
String refererWithoutHost = refererUri.getPath() + "?" + refererUri.getQuery();
|
||||||
|
requestedPage = myRequestedPages.get(refererWithoutHost);
|
||||||
|
}
|
||||||
|
catch (Throwable ignore) {}
|
||||||
|
if (requestedPage != null) {
|
||||||
|
requestedPage.myFiles.add(file);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
myRequestedFilesWithoutReferrer.add(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isReloadRequest) return null;
|
if (!isReloadRequest) return null;
|
||||||
|
|
||||||
String host = NettyKt.getHost(request);
|
int clientId = myTotalClientsCount.incrementAndGet();
|
||||||
if (host == null) host = "localhost:" + BuiltInServerOptions.getInstance().getEffectiveBuiltInServerPort();
|
myRequestedPages.put(uri, new RequestedPage(clientId, file));
|
||||||
|
|
||||||
return new StringBuilder()
|
return new StringBuilder()
|
||||||
.append("\n<script>\n")
|
.append("\n<script>\n")
|
||||||
.append("new WebSocket('ws://").append(host).append("/").append(RELOAD_WS_URL_PREFIX).append("').onmessage = function (msg) {\n")
|
.append("new WebSocket('ws://' + window.location.host + '/").append(RELOAD_WS_URL_PREFIX)
|
||||||
|
.append("?").append(RELOAD_CLIENT_ID_URL_PARAMETER).append("=").append(clientId)
|
||||||
|
.append("').onmessage = function (msg) {\n")
|
||||||
.append(" if (msg.data === '").append(RELOAD_WS_REQUEST).append("') {\n")
|
.append(" if (msg.data === '").append(RELOAD_WS_REQUEST).append("') {\n")
|
||||||
.append(" window.location.reload();\n")
|
.append(" window.location.reload();\n")
|
||||||
.append(" }\n")
|
.append(" }\n")
|
||||||
@@ -79,30 +131,64 @@ public final class WebServerPageConnectionService {
|
|||||||
.append("</script>");
|
.append("</script>");
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isFileRequested(@NotNull VirtualFile file) {
|
public @Nullable AsyncFileListener.ChangeApplier reloadRelatedClients(@NotNull List<VirtualFile> modifiedFiles) {
|
||||||
return myRequestedFiles.contains(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void reloadAll() {
|
|
||||||
ClientManager server = myServer;
|
ClientManager server = myServer;
|
||||||
if (server != null) {
|
if (server == null) return null;
|
||||||
server.send(-1, RELOAD_PAGE_MESSAGE.retainedDuplicate(), null);
|
|
||||||
|
Set<RequestedPage> affectedPages = new HashSet<>();
|
||||||
|
for (VirtualFile modifiedFile : modifiedFiles) {
|
||||||
|
if (myRequestedFilesWithoutReferrer.contains(modifiedFile)) {
|
||||||
|
return RELOAD_ALL;
|
||||||
|
}
|
||||||
|
for (RequestedPage requestedPage : myRequestedPages.values()) {
|
||||||
|
if (requestedPage.myFiles.contains(modifiedFile)) {
|
||||||
|
affectedPages.add(requestedPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new AsyncFileListener.ChangeApplier() {
|
||||||
|
@Override
|
||||||
|
public void afterVfsChange() {
|
||||||
|
for (RequestedPage affectedPage : affectedPages) {
|
||||||
|
affectedPage.myClient.thenAccept(client -> {
|
||||||
|
client.send(RELOAD_PAGE_MESSAGE.retainedDuplicate());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clientConnected() {
|
private void clientConnected(@NotNull WebSocketClient client, int clientId) {
|
||||||
if (ourListenersCount.incrementAndGet() == 1) {
|
if (myClientsCount.incrementAndGet() == 1) {
|
||||||
Disposable disposable =
|
Disposable disposable =
|
||||||
Disposer.newDisposable(ApplicationManager.getApplication(), WebServerFileContentListener.class.getSimpleName());
|
Disposer.newDisposable(ApplicationManager.getApplication(), WebServerFileContentListener.class.getSimpleName());
|
||||||
VirtualFileManager.getInstance().addAsyncFileListener(new WebServerFileContentListener(), disposable);
|
VirtualFileManager.getInstance().addAsyncFileListener(new WebServerFileContentListener(), disposable);
|
||||||
ourListenerDisposable = disposable;
|
myListenerDisposable = disposable;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (RequestedPage requestedPage : myRequestedPages.values()) {
|
||||||
|
if (requestedPage.myClientId == clientId) {
|
||||||
|
requestedPage.myClient.complete(client);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clientDisconnected() {
|
private void clientDisconnected(@NotNull WebSocketClient client) {
|
||||||
if (ourListenersCount.decrementAndGet() == 0) {
|
if (myClientsCount.decrementAndGet() == 0) {
|
||||||
Disposer.dispose(Objects.requireNonNull(ourListenerDisposable));
|
Disposer.dispose(Objects.requireNonNull(myListenerDisposable));
|
||||||
ourListenerDisposable = null;
|
myListenerDisposable = null;
|
||||||
|
}
|
||||||
|
String requestedPageKey = null;
|
||||||
|
for (Map.Entry<String, RequestedPage> requestedPage : myRequestedPages.entrySet()) {
|
||||||
|
if (requestedPage.getValue().myClient.isDone() && requestedPage.getValue().myClient.getNow(null) == client) {
|
||||||
|
requestedPageKey = requestedPage.getKey();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (requestedPageKey != null) {
|
||||||
|
myRequestedPages.remove(requestedPageKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,12 +214,30 @@ public final class WebServerPageConnectionService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void connected(@NotNull Client client, @Nullable Map<String, List<String>> parameters) {
|
public void connected(@NotNull Client client, @Nullable Map<String, List<String>> parameters) {
|
||||||
getInstance().clientConnected();
|
if (parameters == null || !(client instanceof WebSocketClient)) return;
|
||||||
|
List<String> ids = parameters.get(RELOAD_CLIENT_ID_URL_PARAMETER);
|
||||||
|
if (ids.size() != 1) return;
|
||||||
|
int id = StringUtil.parseInt(ids.get(0), -1);
|
||||||
|
if (id == -1) return;
|
||||||
|
getInstance().clientConnected((WebSocketClient)client, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disconnected(@NotNull Client client) {
|
public void disconnected(@NotNull Client client) {
|
||||||
getInstance().clientDisconnected();
|
if (client instanceof WebSocketClient) {
|
||||||
|
getInstance().clientDisconnected((WebSocketClient)client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RequestedPage {
|
||||||
|
private final int myClientId;
|
||||||
|
private final @NotNull Set<VirtualFile> myFiles = ConcurrentHashMap.newKeySet();
|
||||||
|
private final @NotNull CompletableFuture<WebSocketClient> myClient = new CompletableFuture<>();
|
||||||
|
|
||||||
|
private RequestedPage(int clientId, @NotNull VirtualFile requestedPageFile) {
|
||||||
|
myClientId = clientId;
|
||||||
|
myFiles.add(requestedPageFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user