Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*******************************************************************************
* Copyright (c) 2025 Kichwa Coders Canada, Inc.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.swt.tests.junit;

import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.ProcessBuilder.Redirect;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.List;
import java.util.concurrent.TimeUnit;

import clipboard.ClipboardCommands;

public class RemoteClipboard implements ClipboardCommands {
private ClipboardCommands remote;
private Process remoteClipboardProcess;
private Path remoteClipboardTempDir;

public void start() throws Exception {
assertNull(remote, "Create a new instance to restart");
/*
* The below copy using getPath may be redundant (i.e. it may be possible to run
* the class files from where they currently reside in the bin folder or the
* jar), but this method of setting up the class files is very simple and is
* done the same way that other files are extracted for tests.
*
* If the ClipboardTest starts to get more complicated, or other tests want to
* replicate this design element, then refactoring this is an option.
*/
remoteClipboardTempDir = Files.createTempDirectory("swt-test-Clipboard");
List.of( //
"ClipboardTest", //
"ClipboardCommands", //
"ClipboardCommandsImpl", //
"ClipboardTest$LocalHostOnlySocketFactory" //
).forEach((f) -> {
// extract the files and put them in the temp directory
SwtTestUtil.copyFile("/clipboard/" + f + ".class",
remoteClipboardTempDir.resolve("clipboard/" + f + ".class"));
});

String javaHome = System.getProperty("java.home");
String javaExe = javaHome + "/bin/java" + (SwtTestUtil.isWindowsOS ? ".exe" : "");
assertTrue(Files.exists(Path.of(javaExe)));

ProcessBuilder pb = new ProcessBuilder(javaExe, "clipboard.ClipboardTest")
.directory(remoteClipboardTempDir.toFile());
pb.inheritIO();
pb.redirectOutput(Redirect.PIPE);
remoteClipboardProcess = pb.start();

// Read server output to find the port
int port = SwtTestUtil.runOperationInThread(() -> {
BufferedReader reader = new BufferedReader(new InputStreamReader(remoteClipboardProcess.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith(ClipboardCommands.PORT_MESSAGE)) {
String[] parts = line.split(":");
return Integer.parseInt(parts[1].trim());
}
}
throw new RuntimeException("Failed to get port");
});
assertNotEquals(0, port);
try {
Registry reg = LocateRegistry.getRegistry("127.0.0.1", port);
long stopTime = System.currentTimeMillis() + 10000;
do {
try {
remote = (ClipboardCommands) reg.lookup(ClipboardCommands.ID);
break;
} catch (NotBoundException e) {
// try again because the remote app probably hasn't bound yet
}
} while (System.currentTimeMillis() < stopTime);
} catch (RemoteException e) {

Integer exitValue = null;
boolean waitFor = false;
try {
waitFor = remoteClipboardProcess.waitFor(5, TimeUnit.SECONDS);
if (waitFor) {
exitValue = remoteClipboardProcess.exitValue();
}
} catch (InterruptedException e1) {
Thread.interrupted();
}

String message = "Failed to get remote clipboards command, this seems to happen on macOS on I-build tests. Exception: "
+ e.toString() + " waitFor: " + waitFor + " exitValue: " + exitValue;

// Give some diagnostic information to help track down why this fails on build
// machine. We only hard error on Linux, for other platforms we allow test to
// just be skipped until we track down what is causing
// https://github.com/eclipse-platform/eclipse.platform.swt/issues/2553
assumeTrue(SwtTestUtil.isGTK, message);
throw new RuntimeException(message, e);
}
assertNotNull(remote);

// Run a no-op on the Swing event loop so that we know it is idle
// and we can continue startup
remote.waitUntilReady();
remote.setFocus();
remote.waitUntilReady();
}

@Override
public void stop() throws RemoteException {
try {
stopProcess();
} catch (InterruptedException e) {
Thread.interrupted();
} finally {
deleteRemoteTempDir();
}
}

private void stopProcess() throws RemoteException, InterruptedException {
try {
if (remote != null) {
remote.stop();
remote = null;
}
} finally {
if (remoteClipboardProcess != null) {
try {
remoteClipboardProcess.destroy();
assertTrue(remoteClipboardProcess.waitFor(10, TimeUnit.SECONDS));
} finally {
remoteClipboardProcess.destroyForcibly();
assertTrue(remoteClipboardProcess.waitFor(10, TimeUnit.SECONDS));
remoteClipboardProcess = null;
}
}
}
}

private void deleteRemoteTempDir() {
if (remoteClipboardTempDir != null) {
// At this point the process is ideally destroyed - or at least the test will
// report a failure if it isn't. Clean up the extracted files, but don't
// fail test if we fail to delete
try {
Files.walkFileTree(remoteClipboardTempDir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
System.err.println("SWT Warning: Failed to clean up temp directory " + remoteClipboardTempDir
+ " Error:" + e.toString());
e.printStackTrace();
}
}
}

@Override
public void setContents(String string, int clipboardId) throws RemoteException {
remote.setContents(string, clipboardId);
}

@Override
public void setFocus() throws RemoteException {
remote.setFocus();
}

@Override
public String getStringContents(int clipboardId) throws RemoteException {
return remote.getStringContents(clipboardId);
}

@Override
public void waitUntilReady() throws RemoteException {
remote.waitUntilReady();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;

Expand Down Expand Up @@ -203,7 +206,12 @@ public static boolean isBidi() {
public static void openShell(Shell shell) {
if (shell != null && !shell.getVisible()) {
if (isGTK) {
waitEvent(() -> shell.open(), shell, SWT.Paint, 1000);
if (isGTK4()) {
waitAllEvents(() -> shell.open(), shell, Set.of(SWT.Paint, SWT.Activate, SWT.FocusIn), 1000);
} else {
waitEvent(() -> shell.open(), shell, SWT.Paint, 1000);
}
processEvents();
} else {
shell.open();
}
Expand Down Expand Up @@ -485,6 +493,45 @@ public static boolean waitEvent(Runnable trigger, Control control, int swtEvent,
return true;
}

/**
* Wait until specified control receives all the specified event.
*
* @param trigger may be null. Code that is expected to send event.
* Note that if you trigger it outside, then event may
* arrive *before* you call this function, and it will
* fail to receive event.
* @param control control expected to receive the event
* @param swtEvents events, such as SWT.Paint
* @param timeoutMsec how long to wait for event
* @return <code>true</code> if event was received
*/
public static boolean waitAllEvents(Runnable trigger, Control control, Set<Integer> swtEvents, int timeoutMsec) {
Map<Integer, Listener> eventsLeftToReceive = new HashMap<>();
for (Integer swtEvent : swtEvents) {
Listener listener = event -> {
control.removeListener(swtEvent, eventsLeftToReceive.get(swtEvent));
eventsLeftToReceive.remove(swtEvent);
};
eventsLeftToReceive.put(swtEvent, listener);
control.addListener(swtEvent, listener);
}
try {
if (trigger != null)
trigger.run();

long start = System.currentTimeMillis();
while (!eventsLeftToReceive.isEmpty()) {
if (System.currentTimeMillis() - start > timeoutMsec)
return false;
processEvents();
}
} finally {
eventsLeftToReceive.forEach((swtEvent, listener) -> control.removeListener(swtEvent, listener));
}

return true;
}

/**
* Wait until specified Shell becomes active, or internal timeout elapses.
*
Expand Down Expand Up @@ -642,4 +689,57 @@ public static Path copyFile(String sourceFilename, Path destinationPath) {
return destinationPath;
}

@FunctionalInterface
public interface ExceptionalSupplier<T> {
T get() throws Exception;
}

/**
* When running some operations, such as requesting remote process read the
* clipboard, we need to have the event queue processing otherwise the remote
* won't be able to read our clipboard contribution.
*
* This method starts the supplier in a new thread and runs the event loop until
* the thread completes, or until a timeout is reached.
*/
static <T> T runOperationInThread(ExceptionalSupplier<T> supplier) throws RuntimeException {
return runOperationInThread(10000, supplier);
}

/**
* When running some operations, such as requesting remote process read the
* clipboard, we need to have the event queue processing otherwise the remote
* won't be able to read our clipboard contribution.
*
* This method starts the supplier in a new thread and runs the event loop until
* the thread completes, or until a timeout is reached.
*/
static <T> T runOperationInThread(int timeoutMs, ExceptionalSupplier<T> supplier) throws RuntimeException {
Object[] supplierValue = new Object[1];
Exception[] supplierException = new Exception[1];
Runnable task = () -> {
try {
supplierValue[0] = supplier.get();
} catch (Exception e) {
supplierValue[0] = null;
supplierException[0] = e;
}
};
Thread thread = new Thread(task, SwtTestUtil.class.getName() + ".runOperationInThread");
thread.setDaemon(true);
thread.start();
BooleanSupplier done = () -> !thread.isAlive();
try {
processEvents(timeoutMs, done);
} catch (InterruptedException e) {
throw new RuntimeException("Failed while running thread", e);
}
assertTrue(done.getAsBoolean());
if (supplierException[0] != null) {
throw new RuntimeException("Failed while running thread", supplierException[0]);
}
@SuppressWarnings("unchecked")
T result = (T) supplierValue[0];
return result;
}
}
Loading
Loading