diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 2b9d4d4c..6687bd43 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -18,16 +18,30 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 - - name: Set up JDK 1.8 + - name: Set up JDK 11 uses: actions/setup-java@v1 with: - java-version: 1.11 + java-version: 11 + - name: Cache SonarCloud packages + uses: actions/cache@v1 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle run: ./gradlew build + - name: Jacoco Test Report + run: ./gradlew jacocoTestReport - name: Sonar scan env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_LOGIN: ${{ secrets.SONAR_LOGIN }} - run: ./gradlew sonarqube + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: ./gradlew sonarqube --info diff --git a/README.md b/README.md index 5cd8f0c0..b66d5165 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,11 @@ # x11-client -x11-client enables java and other jvm languages to -talk directly to an x11 server without binding to a -C library. The client is similar to Xlib for C but -uses objects to represent the protocol resulting in -a simplified client. It supports the core protocol -as well as several extensions. The client is -designed with Xlib in mind and follows the same -pattern of queuing one-way requests before sending -them to the server. +x11-client enables java and other jvm languages to talk directly to a x11 +server without binding to a C library. The client is similar to X11lib for C +but uses objects to represent the protocol resulting in a simplified client. It +supports the core protocol as well as several extensions. The client is +similar to X11lib and follows the same pattern of queuing one-way requests +before sending them to the server. ![Java CI with Gradle](https://github.com/moaxcp/x11-client/workflows/Java%20CI%20with%20Gradle/badge.svg?branch=master) [![maven central](https://img.shields.io/maven-central/v/com.github.moaxcp.x11/x11-client)](https://search.maven.org/artifact/com.github.moaxcp.x11/x11-client) @@ -17,8 +14,7 @@ them to the server. # Users -This library can be added to your project using -maven or gradle. +This library can be added to your project using maven or gradle. ## Maven @@ -37,13 +33,15 @@ maven or gradle. implementation 'com.github.moaxcp.x11:x11-client:0.1.0' ``` -The library has one dependency for using unix -sockets. +The library has one dependency for using unix sockets. + +``` +implementation 'com.kohlschutter.junixsocket:junixsocket-core:2.3.2' +``` # Usage -Requests are built by the user and passed to the -client. +Users build requests using the builder pattern. ``` CreateWindow window = CreateWindow.builder() @@ -63,11 +61,9 @@ CreateWindow window = CreateWindow.builder() .build(); ``` -The builder pattern is used for all protocol -objects. Value masks are not exposed through the -builders. Value mask bits are set as needed when -setting values in the builder. An example of this is -shown above where the event masks are enabled. +All protocol objects use the builder pattern. All fields using a value_mask +will automatically set the bit in the value_mask when the builder method is +called. An example of this is shown above when eventMaskEnable is called. ``` ... @@ -75,9 +71,11 @@ shown above where the event masks are enabled. ... ``` -A client is a resource and should be managed in a -try-with-resources block. This will ensure the -socket connection is closed correctly. +This enables the value_mask bit for `eventMask` and turns on the `eventMask` +bits for EXPOSURE and KEY_PRESS. + +A client is a resource and should be managed in a try-with-resources block. The +try-with-resources block will close the socket connection correctly. ``` try(X11Client client = X11Client.connect(new DisplayName(":1"))) { @@ -91,29 +89,24 @@ In this block requests can be sent through the client. client.send(window); ``` -Creating a window is a one-way request. These -requests do not recieve a resonse from the server. -Similar to Xlib, the client queues one-way requests -until a two-way request is sent. This queue can be -inspected and flushed to the server. +Creating a window is a one-way request. These requests do not receive a +response from the server. Similar to X11lib, the client queues one-way requests +until a two-way request is sent. This queue can be inspected and flushed to the +server. ``` client.flush(); ``` -The x11 protocol is represented by classes which -implement certain interfaces. The client can read -and write these objects using the read and write -methods defined in each object’s class. Here is a -diagram of the class hierarchy for the protocol: +This project converts the x11 protocol to classes which implement certain +interfaces. The client can read and write these objects using the read and +write methods defined in each object’s class. Here is a diagram of the class +hierarchy for the protocol: -X11ClientExceptions may be thrown when calling -methods on a client. These exceptions represent -IOExceptions with the socket. X11ErrorExceptions -represent Errors from the X11 Server. Errors can be -handled using the standard try/catch method rather -than using an error handler callback as is done with -Xlib. +X11ClientExceptions may be thrown when calling methods on a client. These +exceptions represent IOExceptions with the socket. X11ErrorExceptions represent +Errors from the X11 Server. Errors can be handled using the standard try/catch +method rather than using an error handler callback as is done with X11lib. ``` try { @@ -123,33 +116,30 @@ try { } ``` -All supported x11 extensions are automatically -loaded by the client on startup. Users can check if -an extension is loaded or activated by calling +All supported x11 extensions are automatically loaded by the client on startup. +Users can check if an extension is loaded or activated by calling ``` client.loadedPlugin("BIG-REQUESTS"); client.activePlugin("BIG-REQUESTS"); ``` -If an extension is supported, then XRequests for -that extension can be sent to the client. +If an extension is supported, then XRequests for that extension can be sent to +the client. ``` client.send(Enable.builder().build()); ``` -Otherwise, sending an unsupported request will -result in an exception. +Otherwise, sending an unsupported request will result in an exception. ``` -could not find plugin for request "%s" +could not find plugin for request ``` # Examples -These examples are conversions of an Xlib example -written in C. +These examples are conversions of a X11lib example written in C. ## Hello World @@ -215,106 +205,112 @@ try(X11Client x11Client = X11Client.connect()) { ## Request Prossesing -OneWayRequests are requests which the client expects -no response from the server. These requests are -queued and only sent when the client is flushed. +OneWayRequests are requests which the client expects no response from the +server. `OneWayRequests are queued and are only sent when the client is flushed. There are 3 ways in which the client will be flushed. -1. Manually by calling the flush() method -2. sending a TwoWayRequest -3. When the event queue is empty and getNextEvent() is called +1. Manually by calling the `flush()` method +2. sending a `TwoWayRequest` +3. When the event queue is empty and `getNextEvent()` is called -TwoWayRequests are requests where the client expects a response from the server. These requests cause the -client to flush the OneWayRequest queue and send the TwoWayRequest. Next the client reads input from the -server and attempts to find the corresponding XReply and return it. The protocol object read from the -server can be a XEvent or XError rather than an XReply. The client needs to handle these before it can -find and return the XReply. -When a XEvent is read it is stored in the event queue. +TwoWayRequests are requests where the client expects a response from the +server. These requests cause the client to flush the `OneWayRequest` queue and +send the `TwoWayRequest`. Next the client reads input from the server and +attempts to find the corresponding `XReply` and return it. The protocol object +read from the server can be a `XEvent` or `XError` rather than an `XReply`. The +client needs to handle these before it can find and return the `XReply`.When a +`XEvent` is read it is stored in the event queue. -When an XError is read it may be for the current request or any of the previous OneWayRequests. Any time -an XError is found an exception will be thrown. +When an `XError` is read it may be for the current request or any of the +previous OneWayRequests. Anytime an `XError` is found an exception will be +thrown. ## Error Handling -NOTE: error handling is not currently implemented. This could be in the form of providing an error -handler when sending requests. +NOTE: error handling per request has not been implemented. This could be in the +form of providing an error handler when sending requests. ## Event Prossesing -Events are sent by the server any time the client registers to listen for them. Events are processed by -calling client.getNextEvent(). Events can be sent by the server at any time, so they are stored in an -event queue when processing TwoWayRequests from the server. +Events can be sent by the server at any time, so they are stored in an event +queue when processing TwoWayRequests. This allows the user to receive events by +calling `client.getNextEvent()`. -The event queue is an internal queue containing events which are deferred while processing a -TwoWayRequest. client.getNextEvent() will empty this queue before reading events from the server. When -the queue is empty getNextEvent() will flush the OneWayRequest queue and attempt to find and return a -XEvent from the server. +The event queue is an internal queue containing events which are deferred while +processing a TwoWayRequest. `client.getNextEvent()` will empty this queue +before reading events from the server. When the queue is empty getNextEvent() +will flush the `OneWayRequest` queue and attempt to find and return a `XEvent` +from the server. -When reading events from the server an error can be read. These errors can only be a result of -OneWayRequests that are sent after the user calls flush. +When reading events from the server an error can be read. These errors can only +be a result of OneWayRequests that are sent after `flush()` is called. These +errors will be thrown as an exception. -Note: Receiving an XReply while processing events should not be expected since sending TwoWayRequests -clears the stream of replies. +Note: Receiving an XReply while processing events should not be expected since +sending TwoWayRequests clears the stream of replies. ## Concurrency -The client is not thread safe and invocations from one thread must be isolated from invocations from -another thread. The connection socket, one-way request queue and event queue are shared mutable data. -All invocations must be synchronized in some way. Protocol objects are immutable and may be shared -without synchronization. +The client is not thread safe and invocations from one thread must be isolated +from invocations of another thread. The connection socket, one-way request +queue, and event queue are shared mutable data. All invocations must be +synchronized in some way. Protocol objects are immutable, which allows them to +be shared without synchronization. # Contributors -I am not an x11 programmer but I find the protocol to be an interesting challenge and learning -experience. The only other x11 client implementation for java that I have found is escher. Escher is very -hand written and has many issues. The goal of this client is to automate the generation of the protocol -and make a clear distinction between the client and any framework that may provide things like resource -management and event dispatch. +I am not an x11 programmer but I find the protocol to be an interesting +challenge and learning experience. The only other x11 client implementation for +java that I have found is escher. Escher is very hand written and has many +issues. The goal of this client is to automate the generation of the protocol +and make a clear distinction between the client and any framework that may +provide things like resource management and event dispatch. -This project uses the xcb xmls to generate protocol classes for the core protocol and extensions. It uses -a custom gradle plugin to generate the classes. Be sure to check the javadoc to view supported protocol -objects. +This project uses the xcb xmls to generate protocol classes for the core +protocol and extensions. It uses a custom gradle plugin to generate the +classes. Be sure to check the javadoc to view supported protocol objects. -All protocol objects support read and write methods regardless of type. This means that these objects -can also be used to build an x11 server and are not tied specifically to the client. +All protocol objects support read and write methods regardless of type. This +means that these objects can also be used to build an x11 server and are not +tied specifically to the client. -Xlib and XCB provides convenient methods rather than directly using the protocol. I have avoided adding -convenience methods to the client but may do so in the future. Methods such as createSimpleWindow can be -added. If you have any suggestions on methods that can be added feel free to submit an issue or PR. +Xlib and XCB provides convenient methods rather than directly using the +protocol. I have avoided adding convenience methods to the client but may do +so in the future. Methods such as createSimpleWindow can be added. If you have +any suggestions on methods that can be added feel free to submit an issue or +PR. -The core protocol and every supported extension implements a plugin which enables the client to figure -out which class to use when reading errors and events from the server. These plugins are generated -durring the build process. Plugins are discovered and loaded using the ServiceLoader pattern. +The core protocol and every supported extension implements a plugin which +enables the client to figure out which class to use when reading errors and +events from the server. These plugins are generated durring the build process. +Plugins are discovered and loaded using the ServiceLoader pattern. -Support is needed for a few things in the protocol before all extensions can be supported. Contributions -are welcome! +Support is needed for a few things in the protocol before all extensions can +be supported. Contributions are welcome! -fd – file descriptors. I believe these should work like an int field. If this is true this should be -easy to implement. +fd – file descriptors. I believe these should work like an int field. If this +is true this should be easy to implement. -sumOf expressions – creates a sum value which is used to determine list sizes. The list is the size of a -sumOf function called on another list. +sumOf expressions – creates a sum value which is used to determine list sizes. +The list is the size of a sumOf function called on another list. -Polymorphism – some objects use a case switch which seems to describe a polymorphic object. There is -usually a type field which describes the type and each switch case provides additional fields for that -type. The generation code needs to support generating multiple objects when it runs into an object with -these switch constructs. Reading and writing will be tricky since the type field can be deep within the -protocol. These switches may also be nested. +Polymorphism – some objects use a case switch which seems to describe a +polymorphic object. There is usually a type field which describes the type and +each switch case provides additional fields for that type. The generation code +needs to support generating multiple objects when it runs into an object with +these switch constructs. Reading and writing will be tricky since the type +field can be deep within the protocol. These switches may also be nested. # Frameworks -There is a need for higher levels of abstraction -such as Window, Pixmap, and GraphicsContext. As well -as managing the creation and destruction of these -objects. There is also a need for dispatching events -in a consistent way. These abstractions will be -needed for any application and can be implemented in -a framework. I would like to consider this the job -of a Display class and possibly a Toolkit. Currently -there is a Display class which manages Resources and -event dispatch. A framework is not the primary goal -of the client project and will likely move into a -new project. +There is a need for higher levels of abstraction such as Window, Pixmap, and +GraphicsContext. As well as managing the creation and destruction of these +objects. There is also a need for dispatching events in a consistent way. These +abstractions will be needed for any application and can be implemented in a +framework. I would like to consider this the job of a Display class and +possibly a Toolkit. Currently there is a Display class which manages Resources +and event dispatch. A framework is not the primary goal of the client project +and will likely move into a new project. # versions @@ -330,6 +326,8 @@ Moved DisplayName, KeySym, KeySystem, ParametersCheck, and XAuthority to protoco Adding ResourceIdService which will switch to XC_MISC extension once ids run out. +Adding AtomService which will cache InternAtoms. + ## 0.2.1 Adding github actions build. The workflow will build the library and run a sonar scan. diff --git a/build.gradle b/build.gradle index 03f7cc39..5d94bcff 100644 --- a/build.gradle +++ b/build.gradle @@ -141,10 +141,8 @@ signing { sonarqube { properties { + property "sonar.projectKey", "com.github.moaxcp.x11:x11-client" property 'sonar.host.url', 'https://sonarcloud.io' property 'sonar.organization', 'moaxcp' - if(System.getenv('sonar_login')) { - property 'sonar.login', System.getenv('SONAR_LOGIN') - } } } \ No newline at end of file diff --git a/src/integrationTest/java/com/github/moaxcp/x11client/experimental/XDisplayIT.java b/src/integrationTest/java/com/github/moaxcp/x11client/experimental/XDisplayIT.java index bdffdda3..b9486316 100644 --- a/src/integrationTest/java/com/github/moaxcp/x11client/experimental/XDisplayIT.java +++ b/src/integrationTest/java/com/github/moaxcp/x11client/experimental/XDisplayIT.java @@ -13,7 +13,7 @@ void clientTestXFunctions() throws IOException { try(X11Client client = X11Client.connect()) { int wid = client.createSimpleWindow((short) 10, (short) 10, (short) 600, (short) 480, EventMask.EXPOSURE, EventMask.KEY_PRESS); client.storeName(wid, "Hello World!"); - int deleteAtom = client.internAtom("WM_DELETE_WINDOW"); + int deleteAtom = client.getAtom("WM_DELETE_WINDOW"); client.setWMProtocols(wid, deleteAtom); client.mapWindow(wid); int gc = client.createGC(0, wid); diff --git a/src/main/java/com/github/moaxcp/x11client/AtomService.java b/src/main/java/com/github/moaxcp/x11client/AtomService.java new file mode 100644 index 00000000..a78c973b --- /dev/null +++ b/src/main/java/com/github/moaxcp/x11client/AtomService.java @@ -0,0 +1,45 @@ +package com.github.moaxcp.x11client; + +import com.github.moaxcp.x11client.protocol.AtomValue; +import com.github.moaxcp.x11client.protocol.xproto.Atom; +import com.github.moaxcp.x11client.protocol.xproto.InternAtom; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static com.github.moaxcp.x11client.protocol.Utilities.stringToByteList; + +public class AtomService { + private final XProtocolService protocolService; + private Map atomIds = new HashMap<>(); + private Map atomNames = new HashMap<>(); + public AtomService(XProtocolService protocolService) { + this.protocolService = protocolService; + for(Atom atom : Atom.values()) { + add(new AtomValue(atom.getValue(), atom.toString())); + } + } + + private void add(AtomValue atom) { + atomIds.put(atom.getId(), atom); + atomNames.put(atom.getName(), atom); + } + + public AtomValue getAtom(Atom atom) { + return atomIds.get(atom.getValue()); + } + + public Optional getAtom(int id) { + return Optional.ofNullable(atomIds.get(id)); + } + + public AtomValue getAtom(String name) { + if(atomNames.containsKey(name)) { + return atomNames.get(name); + } + int id = protocolService.send(InternAtom.builder().name(stringToByteList(name)).nameLen((short) name.length()).build()).getAtom(); + AtomValue result = new AtomValue(id, name); + add(result); + return result; + } +} diff --git a/src/main/java/com/github/moaxcp/x11client/X11Client.java b/src/main/java/com/github/moaxcp/x11client/X11Client.java index 6246d06d..fc67f662 100644 --- a/src/main/java/com/github/moaxcp/x11client/X11Client.java +++ b/src/main/java/com/github/moaxcp/x11client/X11Client.java @@ -17,6 +17,7 @@ public class X11Client implements AutoCloseable { private final X11Connection connection; private final XProtocolService protocolService; private final ResourceIdService resourceIdService; + private final AtomService atomService; /** * Creates a client for the given displayName and authority. @@ -53,6 +54,7 @@ private X11Client(X11Connection connection) { this.connection = connection; protocolService = new XProtocolService(connection.getSetup(), connection.getX11Input(), connection.getX11Output()); resourceIdService = new ResourceIdService(protocolService, connection.getSetup().getResourceIdMask(), connection.getSetup().getResourceIdBase()); + atomService = new AtomService(protocolService); } public boolean loadedPlugin(String name) { @@ -161,8 +163,13 @@ public int nextResourceId() { return resourceIdService.nextResourceId(); } - public int internAtom(String name) { - return send(InternAtom.builder().name(stringToByteList(name)).nameLen((short) name.length()).build()).getAtom(); + /** + * Returns the id of the named Atom. If the atom does not exist on the x11 server an InternAtom request is made. + * @param name + * @return + */ + public int getAtom(String name) { + return atomService.getAtom(name).getId(); } //XRaiseWindow https://github.com/mirror/libX11/blob/caa71668af7fd3ebdd56353c8f0ab90824773969/src/RaiseWin.c @@ -185,7 +192,7 @@ public void storeName(int wid, String name) { } public void setWMProtocols(int wid, int atom) { - int wmProtocols = internAtom("WM_PROTOCOLS"); + int wmProtocols = getAtom("WM_PROTOCOLS"); send(ChangeProperty.builder() .window(wid) .property(wmProtocols) diff --git a/src/main/java/com/github/moaxcp/x11client/protocol/AtomValue.java b/src/main/java/com/github/moaxcp/x11client/protocol/AtomValue.java new file mode 100644 index 00000000..9a220ecf --- /dev/null +++ b/src/main/java/com/github/moaxcp/x11client/protocol/AtomValue.java @@ -0,0 +1,14 @@ +package com.github.moaxcp.x11client.protocol; + +import lombok.NonNull; +import lombok.Value; + +/** + * An atom. May be a predefined atom or an intern atom. + */ +@Value +public class AtomValue { + int id; + @NonNull + String name; +} diff --git a/src/main/java/com/github/moaxcp/x11client/protocol/XAuthority.java b/src/main/java/com/github/moaxcp/x11client/protocol/XAuthority.java index 00ec127a..ce1026ca 100644 --- a/src/main/java/com/github/moaxcp/x11client/protocol/XAuthority.java +++ b/src/main/java/com/github/moaxcp/x11client/protocol/XAuthority.java @@ -76,7 +76,7 @@ public static Family getByCode(int code) { * @param protocolName * @param protocolData * @throws NullPointerException if any parameter is null. - * @throws IllegalArgumentException if displayNumber is < 0 or protocolName is empty. + * @throws IllegalArgumentException if displayNumber is less than 0 or protocolName is empty. */ public XAuthority(@NonNull Family family, @NonNull List address, int displayNumber, @NonNull List protocolName, @NonNull List protocolData) { this.family = family; diff --git a/src/test/java/com/github/moaxcp/x11client/AtomServiceTest.java b/src/test/java/com/github/moaxcp/x11client/AtomServiceTest.java new file mode 100644 index 00000000..05af7f97 --- /dev/null +++ b/src/test/java/com/github/moaxcp/x11client/AtomServiceTest.java @@ -0,0 +1,51 @@ +package com.github.moaxcp.x11client; + +import com.github.moaxcp.x11client.protocol.AtomValue; +import com.github.moaxcp.x11client.protocol.xproto.Atom; +import com.github.moaxcp.x11client.protocol.xproto.InternAtom; +import com.github.moaxcp.x11client.protocol.xproto.InternAtomReply; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +@ExtendWith(MockitoExtension.class) +public class AtomServiceTest { + @Mock + private XProtocolService protocolService; + private AtomService service; + + @BeforeEach + void setup() { + service = new AtomService(protocolService); + } + + @Test + void getAtomId_has_predefined_atoms() { + AtomValue atom = service.getAtom(Atom.BITMAP); + assertThat(atom.getId()).isEqualTo(Atom.BITMAP.getValue()); + assertThat(atom.getName()).isEqualTo("BITMAP"); + } + + @Test + void getAtomName_has_predefined_atoms() { + AtomValue atom = service.getAtom("BITMAP"); + assertThat(atom.getId()).isEqualTo(Atom.BITMAP.getValue()); + assertThat(atom.getName()).isEqualTo("BITMAP"); + } + + @Test + void getAtomName_creates_new_atoms() { + given(protocolService.send(any(InternAtom.class))).willReturn(InternAtomReply.builder().atom(100).build()); + AtomValue atom = service.getAtom("DELETE_WINDOW"); + assertThat(atom.getId()).isEqualTo(100); + assertThat(atom.getName()).isEqualTo("DELETE_WINDOW"); + then(protocolService).should().send(any(InternAtom.class)); + } +} diff --git a/src/test/java/com/github/moaxcp/x11client/protocol/AtomValueTest.java b/src/test/java/com/github/moaxcp/x11client/protocol/AtomValueTest.java new file mode 100644 index 00000000..8b87f7c9 --- /dev/null +++ b/src/test/java/com/github/moaxcp/x11client/protocol/AtomValueTest.java @@ -0,0 +1,14 @@ +package com.github.moaxcp.x11client.protocol; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class AtomValueTest { + @Test + void null_name_fails() { + NullPointerException exception = assertThrows(NullPointerException.class, () -> new AtomValue(1, null)); + assertThat(exception).hasMessage("name is marked non-null but is null"); + } +}