mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-19 13:53:01 +02:00
* fix: let ReadUDPbyUCXMessageThread own its DatagramSocket so interrupt() closes it (#115) The field `socket` was a java.net.Socket that was never assigned; the real DatagramSocket lived in a local variable of run(). interrupt() therefore closed nothing and terminateConnection() would have thrown a NullPointerException. The receive loop kept running in two-second timeout steps until the user-disconnect flag was set. The field is now the DatagramSocket, run() binds it when no pre-bound socket was handed in, the loop ends on the interrupt flag or a closed socket, and both interrupt() and terminateConnection() are null-safe. A package-private constructor accepts a pre-bound socket so loopback tests can listen on an ephemeral port; the public constructor used by ChatController is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: stop ReadUDPbyUCXMessageThread truncating datagrams after a short packet (#115) A DatagramPacket keeps the length of the last datagram it received, so a long RadioInfo packet following a short one was cut to the short one's length and failed to parse. The length is reset to the buffer size before every receive, the same way ReadUDPByWintestThread does it. The loopback test sends a short and then a long packet on port 0 and checks that both frequencies arrive. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: give PstRotatorClient's port constructor a callback, decode AirScout as UTF-8 (#115) The explicit-port constructor left the thread-status callback null, so start() and the first received packet threw an NPE; the default constructor now delegates to it. The AirScout listener decoded replies with the platform charset although the inquirer sends UTF-8. PROJECT_CONTEXT listed PSTRotator as TCP; the client speaks UDP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: decode UCX datagrams as UTF-8 and tighten the interrupt test (#115) Review follow-up. UCXLog sends UTF-8 XML, but the listener decoded with the platform charset before handing a String to the parser, so the XML header's encoding was ignored. The port-in-use fallback now sets its receive timeout before the first receive(). The interrupt test waits 1 s, below the 2 s receive timeout, so it fails if interrupt() stops closing the socket; the QRG latches now count one value each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
6b0c80f71e
commit
4679c708bc
7 files changed
+172
-16
No files matched your search
@@ -37,19 +37,16 @@ public class PstRotatorClient implements Runnable {
|
|||||||
* @param listener Callback für den Chatcontroller
|
* @param listener Callback für den Chatcontroller
|
||||||
*/
|
*/
|
||||||
public PstRotatorClient(String host, int remotePort, PstRotatorEventListener listener, ThreadStatusCallback callBack) {
|
public PstRotatorClient(String host, int remotePort, PstRotatorEventListener listener, ThreadStatusCallback callBack) {
|
||||||
this.callBackToController = callBack;
|
|
||||||
this.host = host;
|
|
||||||
this.remotePort = remotePort;
|
|
||||||
// Laut Manual antwortet PSTRotator oft auf Port+1.
|
// Laut Manual antwortet PSTRotator oft auf Port+1.
|
||||||
// Wir binden uns also standardmäßig auf remotePort + 1.
|
// Wir binden uns also standardmäßig auf remotePort + 1.
|
||||||
this.localPort = remotePort + 1;
|
this(host, remotePort, remotePort + 1, listener, callBack);
|
||||||
this.listener = listener;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* alternative constructor for seting the remote port explicitely
|
* alternative constructor for seting the remote port explicitely
|
||||||
*/
|
*/
|
||||||
public PstRotatorClient(String host, int remotePort, int localPort, PstRotatorEventListener listener) {
|
public PstRotatorClient(String host, int remotePort, int localPort, PstRotatorEventListener listener, ThreadStatusCallback callBack) {
|
||||||
|
this.callBackToController = callBack;
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.remotePort = remotePort;
|
this.remotePort = remotePort;
|
||||||
this.localPort = localPort;
|
this.localPort = localPort;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.net.DatagramSocket;
|
|||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -136,7 +137,8 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
|
|||||||
String received = new String(
|
String received = new String(
|
||||||
packet.getData(),
|
packet.getData(),
|
||||||
packet.getOffset(),
|
packet.getOffset(),
|
||||||
packet.getLength()
|
packet.getLength(),
|
||||||
|
StandardCharsets.UTF_8
|
||||||
).trim();
|
).trim();
|
||||||
|
|
||||||
if (received.contains(
|
if (received.contains(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package kst4contest.controller;
|
|||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.*;
|
import java.net.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
@@ -32,7 +33,12 @@ import kst4contest.model.Band;
|
|||||||
*/
|
*/
|
||||||
public class ReadUDPbyUCXMessageThread extends Thread {
|
public class ReadUDPbyUCXMessageThread extends Thread {
|
||||||
private BufferedReader reader;
|
private BufferedReader reader;
|
||||||
private Socket socket;
|
/**
|
||||||
|
* The listening socket. It is owned by this thread: {@link #run()} binds it
|
||||||
|
* (unless a pre-bound socket was handed in) and {@link #interrupt()} closes
|
||||||
|
* it, which is the only way to wake a blocking {@code receive()}.
|
||||||
|
*/
|
||||||
|
private volatile DatagramSocket socket;
|
||||||
private ChatController client;
|
private ChatController client;
|
||||||
private int udpPortNr = 12060;
|
private int udpPortNr = 12060;
|
||||||
private ThreadStatusCallback callBackToController;
|
private ThreadStatusCallback callBackToController;
|
||||||
@@ -49,6 +55,22 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
|||||||
this.callBackToController = callback;
|
this.callBackToController = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a listener on a socket that is already bound. Tests use this to
|
||||||
|
* listen on an ephemeral loopback port; the application binds the configured
|
||||||
|
* port itself through the public constructor.
|
||||||
|
*
|
||||||
|
* @param boundSocket bound datagram socket this thread takes ownership of
|
||||||
|
* @param client controller that receives the parsed packets
|
||||||
|
* @param callback thread-status sink
|
||||||
|
*/
|
||||||
|
ReadUDPbyUCXMessageThread(DatagramSocket boundSocket, ChatController client, ThreadStatusCallback callback) {
|
||||||
|
this.socket = boundSocket;
|
||||||
|
this.udpPortNr = boundSocket.getLocalPort();
|
||||||
|
this.client = client;
|
||||||
|
this.callBackToController = callback;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void interrupt() {
|
public void interrupt() {
|
||||||
super.interrupt();
|
super.interrupt();
|
||||||
@@ -138,15 +160,15 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
|||||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
|
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
|
||||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||||
|
|
||||||
DatagramSocket socket = null;
|
|
||||||
|
|
||||||
boolean running;
|
boolean running;
|
||||||
byte[] buf = new byte[1777];
|
byte[] buf = new byte[1777];
|
||||||
DatagramPacket packet = new DatagramPacket(buf, buf.length);
|
DatagramPacket packet = new DatagramPacket(buf, buf.length);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// socket = new DatagramSocket(12060);
|
// socket = new DatagramSocket(12060);
|
||||||
socket = new DatagramSocket(udpPortNr);
|
if (socket == null) {
|
||||||
|
socket = new DatagramSocket(udpPortNr);
|
||||||
|
}
|
||||||
socket.setSoTimeout(2000); //TODO try for end properly
|
socket.setSoTimeout(2000); //TODO try for end properly
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,13 +177,23 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
|||||||
// e.printStackTrace();
|
// e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
/*
|
||||||
|
* interrupt() sets the flag and closes the socket; the closed socket makes
|
||||||
|
* the blocking receive() throw, and the flag ends the loop.
|
||||||
|
*/
|
||||||
|
while (!isInterrupted()) {
|
||||||
|
|
||||||
boolean timeOutIndicator = false;
|
boolean timeOutIndicator = false;
|
||||||
|
|
||||||
// packet = new DatagramPacket(buf, buf.length); //TODO: Changed that due to memory leak, check if all works (seems like that)
|
// packet = new DatagramPacket(buf, buf.length); //TODO: Changed that due to memory leak, check if all works (seems like that)
|
||||||
// DatagramPacket packet = new DatagramPacket(SRPDefinitions.BYTE_BUFFER_MAX_LENGTH); //TODO: Changed that due to memory leak, check if all works (seems like that)
|
// DatagramPacket packet = new DatagramPacket(SRPDefinitions.BYTE_BUFFER_MAX_LENGTH); //TODO: Changed that due to memory leak, check if all works (seems like that)
|
||||||
try {
|
try {
|
||||||
|
/*
|
||||||
|
* A DatagramPacket remembers the length of the last datagram it
|
||||||
|
* received; without this reset a long packet after a short one is
|
||||||
|
* truncated to the short one's length (#115).
|
||||||
|
*/
|
||||||
|
packet.setLength(packet.getData().length);
|
||||||
socket.receive(packet);
|
socket.receive(packet);
|
||||||
|
|
||||||
} catch (SocketTimeoutException e2) {
|
} catch (SocketTimeoutException e2) {
|
||||||
@@ -171,6 +203,9 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
|||||||
// e2.printStackTrace();
|
// e2.printStackTrace();
|
||||||
}
|
}
|
||||||
catch (IOException e) {
|
catch (IOException e) {
|
||||||
|
if (isInterrupted() || socket == null || socket.isClosed()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
// TODO Auto-generated catch block
|
// TODO Auto-generated catch block
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
} catch (NullPointerException nE) {
|
} catch (NullPointerException nE) {
|
||||||
@@ -189,8 +224,9 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
|||||||
socket = new DatagramSocket(null);
|
socket = new DatagramSocket(null);
|
||||||
socket.setReuseAddress(true);
|
socket.setReuseAddress(true);
|
||||||
socket.bind(new InetSocketAddress(client.getChatPreferences().getLogsynch_ucxUDPWkdCallListenerPort()));
|
socket.bind(new InetSocketAddress(client.getChatPreferences().getLogsynch_ucxUDPWkdCallListenerPort()));
|
||||||
socket.receive(packet);
|
packet.setLength(packet.getData().length);
|
||||||
socket.setSoTimeout(3000);
|
socket.setSoTimeout(3000);
|
||||||
|
socket.receive(packet);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
System.out.println("ReadUDPByUCXMsgTh: Could not solve that. Program Restart needed.");
|
System.out.println("ReadUDPByUCXMsgTh: Could not solve that. Program Restart needed.");
|
||||||
throw new RuntimeException(ex);
|
throw new RuntimeException(ex);
|
||||||
@@ -203,7 +239,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
|||||||
InetAddress address = packet.getAddress();
|
InetAddress address = packet.getAddress();
|
||||||
int port = packet.getPort();
|
int port = packet.getPort();
|
||||||
// packet = new DatagramPacket(buf, buf.length, address, port);
|
// packet = new DatagramPacket(buf, buf.length, address, port);
|
||||||
String received = new String(packet.getData(), packet.getOffset(), packet.getLength());
|
String received = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
|
||||||
received = received.trim();
|
received = received.trim();
|
||||||
|
|
||||||
|
|
||||||
@@ -236,6 +272,12 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Covers an interrupt() that arrived before the socket was bound.
|
||||||
|
DatagramSocket listeningSocket = socket;
|
||||||
|
if (listeningSocket != null && !listeningSocket.isClosed()) {
|
||||||
|
listeningSocket.close();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String processUCXUDPMessage(String udpPacketToProcess) {
|
public String processUCXUDPMessage(String udpPacketToProcess) {
|
||||||
@@ -563,7 +605,10 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
|||||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, false, "terminated", false);
|
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, false, "terminated", false);
|
||||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||||
|
|
||||||
this.socket.close();
|
DatagramSocket listeningSocket = this.socket;
|
||||||
|
if (listeningSocket != null && !listeningSocket.isClosed()) {
|
||||||
|
listeningSocket.close();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package kst4contest.controller;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.net.DatagramPacket;
|
||||||
|
import java.net.DatagramSocket;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import kst4contest.model.ChatPreferences;
|
||||||
|
import kst4contest.observe.DirectUiDispatcher;
|
||||||
|
import kst4contest.observe.SimpleValue;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loopback tests for the UCXLog/DXLog UDP listener. Every socket is bound to
|
||||||
|
* port 0 and every datagram goes to the loopback address, so the tests neither
|
||||||
|
* collide with a running logger nor send anything to a broadcast address.
|
||||||
|
*/
|
||||||
|
class ReadUDPbyUCXMessageThreadLoopbackTest {
|
||||||
|
|
||||||
|
private final ChatController controller = mock(ChatController.class);
|
||||||
|
private final ChatPreferences preferences = mock(ChatPreferences.class);
|
||||||
|
private final ThreadStatusCallback statusCallback = (threadName, message) -> { };
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void interruptEndsTheReceiveLoopBeforeTheReceiveTimeout() throws Exception {
|
||||||
|
when(controller.getChatPreferences()).thenReturn(preferences);
|
||||||
|
ReadUDPbyUCXMessageThread listener = new ReadUDPbyUCXMessageThread(0, controller, statusCallback);
|
||||||
|
listener.setDaemon(true);
|
||||||
|
listener.start();
|
||||||
|
// Let the thread bind its socket and block in receive() before interrupting it.
|
||||||
|
Thread.sleep(200);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The receive timeout is 2 s. Waiting only 1 s proves that interrupt()
|
||||||
|
* closed the socket, rather than the loop ending on the next timeout.
|
||||||
|
*/
|
||||||
|
listener.interrupt();
|
||||||
|
listener.join(1_000);
|
||||||
|
|
||||||
|
assertFalse(listener.isAlive(), "listener must stop once interrupted");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A DatagramPacket remembers the length of the last datagram it received.
|
||||||
|
* Without resetting it before every receive(), a long RadioInfo packet that
|
||||||
|
* follows a short one is cut to the short one's length and fails to parse.
|
||||||
|
* The QRG probe needs no database: RadioInfo only sets the first-category
|
||||||
|
* frequency through the UI dispatcher.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void longDatagramAfterShortOneIsProcessedCompletely() throws Exception {
|
||||||
|
when(controller.getChatPreferences()).thenReturn(preferences);
|
||||||
|
when(controller.uiDispatcher()).thenReturn(new DirectUiDispatcher());
|
||||||
|
when(preferences.isTrxSynch_ucxLogUDPListenerEnabled()).thenReturn(true);
|
||||||
|
SimpleValue<String> qrg = new SimpleValue<>("");
|
||||||
|
when(preferences.getMYQRGFirstCat()).thenReturn(qrg);
|
||||||
|
CountDownLatch firstQrg = new CountDownLatch(1);
|
||||||
|
CountDownLatch secondQrg = new CountDownLatch(1);
|
||||||
|
qrg.addListener(value -> {
|
||||||
|
if (value.equals("144.200.00")) firstQrg.countDown();
|
||||||
|
if (value.equals("432.200.00")) secondQrg.countDown();
|
||||||
|
});
|
||||||
|
|
||||||
|
String shortPacket = "<RadioInfo><Freq>14420000</Freq><Mode>CW</Mode></RadioInfo>";
|
||||||
|
String longPacket = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
|
||||||
|
+ "<RadioInfo><StationName>CONTEST-PC-WITH-A-LONG-NAME</StationName>"
|
||||||
|
+ "<RadioNr>1</RadioNr><Freq>43220000</Freq><TXFreq>43220000</TXFreq>"
|
||||||
|
+ "<Mode>SSB</Mode><OpCall>DL0TEST</OpCall><IsRunning>False</IsRunning>"
|
||||||
|
+ "<FocusEntry>0</FocusEntry><Antenna>1</Antenna><Rotors>1</Rotors>"
|
||||||
|
+ "<FocusRadioNr>1</FocusRadioNr><IsStereo>False</IsStereo>"
|
||||||
|
+ "<ActiveRadioNr>1</ActiveRadioNr></RadioInfo>";
|
||||||
|
assertTrue(longPacket.length() > shortPacket.length());
|
||||||
|
|
||||||
|
InetAddress loopback = InetAddress.getLoopbackAddress();
|
||||||
|
DatagramSocket bound = new DatagramSocket(new InetSocketAddress(loopback, 0));
|
||||||
|
ReadUDPbyUCXMessageThread listener = new ReadUDPbyUCXMessageThread(bound, controller, statusCallback);
|
||||||
|
listener.setDaemon(true);
|
||||||
|
listener.start();
|
||||||
|
try (DatagramSocket sender = new DatagramSocket(new InetSocketAddress(loopback, 0))) {
|
||||||
|
InetSocketAddress target = new InetSocketAddress(loopback, bound.getLocalPort());
|
||||||
|
|
||||||
|
send(sender, target, shortPacket);
|
||||||
|
assertTrue(firstQrg.await(5, TimeUnit.SECONDS), "short packet must be processed");
|
||||||
|
assertEquals("144.200.00", qrg.get());
|
||||||
|
|
||||||
|
send(sender, target, longPacket);
|
||||||
|
assertTrue(secondQrg.await(5, TimeUnit.SECONDS), "long packet must be processed");
|
||||||
|
assertEquals("432.200.00", qrg.get());
|
||||||
|
} finally {
|
||||||
|
listener.interrupt();
|
||||||
|
listener.join(2_000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void send(DatagramSocket sender, InetSocketAddress target, String payload) throws Exception {
|
||||||
|
byte[] bytes = payload.getBytes(StandardCharsets.UTF_8);
|
||||||
|
sender.send(new DatagramPacket(bytes, bytes.length, target));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,7 +99,7 @@ Known integration areas include:
|
|||||||
- AirScout;
|
- AirScout;
|
||||||
- UCXLog / DXLog UDP XML (`contactinfo`, `contactreplace`);
|
- UCXLog / DXLog UDP XML (`contactinfo`, `contactreplace`);
|
||||||
- Win-Test UDP;
|
- Win-Test UDP;
|
||||||
- PSTRotator TCP;
|
- PSTRotator UDP;
|
||||||
- DXCluster;
|
- DXCluster;
|
||||||
- local SQLite persistence.
|
- local SQLite persistence.
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ Mehrere Operateure an einem Rechner können jetzt eigene Rufzeichen, Locators un
|
|||||||
- **Kein Locator für die zweite Kategorie mehr:** Intern gab es eine Einstellung für einen eigenen Locator der zweiten Chat-Kategorie, die weder in der Oberfläche noch in der `preferences.xml` erschien und deshalb immer auf ihrem eingebauten Standardwert stand. Beim Verbindungsaufbau wurde dieser Standardwert mit dem eigenen Locator verglichen und eine Warnung ausgegeben, dass der zweite Locator ignoriert werde. ON4KST verwendet ohnehin einen Locator pro TCP-Sitzung; die Einstellung und die Warnung sind entfernt. Damit ist [Issue #104](https://github.com/praktimarc/kst4contest/issues/104) behoben.
|
- **Kein Locator für die zweite Kategorie mehr:** Intern gab es eine Einstellung für einen eigenen Locator der zweiten Chat-Kategorie, die weder in der Oberfläche noch in der `preferences.xml` erschien und deshalb immer auf ihrem eingebauten Standardwert stand. Beim Verbindungsaufbau wurde dieser Standardwert mit dem eigenen Locator verglichen und eine Warnung ausgegeben, dass der zweite Locator ignoriert werde. ON4KST verwendet ohnehin einen Locator pro TCP-Sitzung; die Einstellung und die Warnung sind entfernt. Damit ist [Issue #104](https://github.com/praktimarc/kst4contest/issues/104) behoben.
|
||||||
- **Linux-Beta-Pakete lassen sich auf das stabile Paket aktualisieren:** DEB- und RPM-Pakete eines Beta-Tags trugen dieselbe Versionsnummer wie das folgende stabile Paket, sodass `dnf` und `dpkg` die Aktualisierung als gleiche Version ablehnten. Beta-Pakete erhalten jetzt eine Vorab-Release-Nummer (`1.50.0-0.beta.001`) unter der des stabilen Pakets (`1.50.0-1`), und der Release-Lauf bricht ab, wenn der Tag nicht zur `composePackageVersion` passt. Damit ist [Issue #113](https://github.com/praktimarc/kst4contest/issues/113) behoben.
|
- **Linux-Beta-Pakete lassen sich auf das stabile Paket aktualisieren:** DEB- und RPM-Pakete eines Beta-Tags trugen dieselbe Versionsnummer wie das folgende stabile Paket, sodass `dnf` und `dpkg` die Aktualisierung als gleiche Version ablehnten. Beta-Pakete erhalten jetzt eine Vorab-Release-Nummer (`1.50.0-0.beta.001`) unter der des stabilen Pakets (`1.50.0-1`), und der Release-Lauf bricht ab, wenn der Tag nicht zur `composePackageVersion` passt. Damit ist [Issue #113](https://github.com/praktimarc/kst4contest/issues/113) behoben.
|
||||||
|
|
||||||
|
- **UCXLog-Pakete werden nicht mehr abgeschnitten:** Nach einem kurzen UDP-Paket las der Empfänger für UCXLog-kompatible Logprogramme jedes längere Folgepaket nur bis zur Länge des kurzen, sodass Logeinträge unvollständig ankommen konnten. Außerdem schließt das Trennen den Socket des Empfängers jetzt sofort, statt bis zum nächsten Timeout zu warten. UCXLog-Pakete und Antworten von AirScout werden jetzt immer als UTF-8 dekodiert statt im Standardzeichensatz des Systems, sodass Umlaute in Stationsnamen und Operator-Rufzeichen unter Windows nicht mehr verstümmelt ankommen. Damit ist [Issue #115](https://github.com/praktimarc/kst4contest/issues/115) behoben.
|
||||||
|
|
||||||
### Hinweise zur Aktualisierung
|
### Hinweise zur Aktualisierung
|
||||||
|
|
||||||
- Bestehende Installationen werden **nicht** verändert: `preferences.xml` und `praktiKST.db` bleiben genau dort liegen, wo sie sind, und werden zum Profil **Default**. Es wird keine Datei verschoben oder kopiert.
|
- Bestehende Installationen werden **nicht** verändert: `preferences.xml` und `praktiKST.db` bleiben genau dort liegen, wo sie sind, und werden zum Profil **Default**. Es wird keine Datei verschoben oder kopiert.
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ Several operators sharing one computer can now use their own callsigns, locators
|
|||||||
- **No second-category locator any more:** internally there was a setting for a separate locator of the second chat category that appeared neither in the user interface nor in `preferences.xml`, so it always kept its compiled-in default. During login that default was compared with the station's locator and a warning was emitted that the second locator is ignored. ON4KST uses one locator per TCP session anyway; the setting and the warning are removed. This fixes [Issue #104](https://github.com/praktimarc/kst4contest/issues/104).
|
- **No second-category locator any more:** internally there was a setting for a separate locator of the second chat category that appeared neither in the user interface nor in `preferences.xml`, so it always kept its compiled-in default. During login that default was compared with the station's locator and a warning was emitted that the second locator is ignored. ON4KST uses one locator per TCP session anyway; the setting and the warning are removed. This fixes [Issue #104](https://github.com/praktimarc/kst4contest/issues/104).
|
||||||
- **Linux beta packages upgrade to the stable package:** DEB and RPM packages of a beta tag carried the same version number as the following stable package, so `dnf` and `dpkg` refused the upgrade as an equal version. Beta packages now carry a pre-release number (`1.50.0-0.beta.001`) below the stable package (`1.50.0-1`), and the release run aborts when the tag does not match `composePackageVersion`. This fixes [Issue #113](https://github.com/praktimarc/kst4contest/issues/113).
|
- **Linux beta packages upgrade to the stable package:** DEB and RPM packages of a beta tag carried the same version number as the following stable package, so `dnf` and `dpkg` refused the upgrade as an equal version. Beta packages now carry a pre-release number (`1.50.0-0.beta.001`) below the stable package (`1.50.0-1`), and the release run aborts when the tag does not match `composePackageVersion`. This fixes [Issue #113](https://github.com/praktimarc/kst4contest/issues/113).
|
||||||
|
|
||||||
|
- **UCXLog packets are no longer cut off:** after a short UDP packet, the listener for UCXLog-compatible loggers read every longer following packet only up to the length of the short one, so log entries could arrive incomplete. In addition, disconnecting now closes the listener's socket immediately instead of waiting for the next timeout. UCXLog packets and AirScout replies are now always decoded as UTF-8 instead of the system default, so umlauts in station names and operator callsigns no longer turn into garbage on Windows. This fixes [Issue #115](https://github.com/praktimarc/kst4contest/issues/115).
|
||||||
|
|
||||||
### Upgrade notes
|
### Upgrade notes
|
||||||
|
|
||||||
- Existing installations are **not** modified: `preferences.xml` and `praktiKST.db` stay exactly where they are and become the **Default** profile. No file is moved or copied.
|
- Existing installations are **not** modified: `preferences.xml` and `praktiKST.db` stay exactly where they are and become the **Default** profile. No file is moved or copied.
|
||||||
|
|||||||
Reference in new issue
Block a user