Skip to content

Commit 9145b68

Browse files
authored
[jrubyscripting] Fix karaf console command newline (#20227)
* [jrubyscripting] Fix karaf console command newline Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
1 parent 7a66a1d commit 9145b68

2 files changed

Lines changed: 159 additions & 1 deletion

File tree

bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyConsoleCommandExtension.java

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import java.io.File;
1616
import java.io.IOException;
17+
import java.io.Writer;
1718
import java.net.URI;
1819
import java.nio.file.Files;
1920
import java.nio.file.Path;
@@ -26,6 +27,7 @@
2627
import java.util.UUID;
2728
import java.util.stream.Stream;
2829

30+
import javax.script.ScriptContext;
2931
import javax.script.ScriptEngine;
3032
import javax.script.ScriptException;
3133

@@ -499,10 +501,23 @@ private void printLoadingMessage(Console console, boolean show) {
499501
}
500502
}
501503

504+
/*
505+
* Configure the engine to redirect output to the provided console.
506+
*/
507+
private void configureEngineConsoleOutput(ScriptEngine engine, @Nullable Console console) {
508+
if (console != null) {
509+
ScriptContext context = engine.getContext();
510+
Writer errorWriter = context.getErrorWriter();
511+
if (errorWriter != null) {
512+
context.setErrorWriter(new ConsoleWriter(errorWriter));
513+
}
514+
}
515+
}
516+
502517
/*
503518
* Create a full openHAB-managed JRuby engine with openHAB scoped variables
504519
* including any injected required gems.
505-
*
520+
*
506521
* This will run the script with the helper library if configured.
507522
*/
508523
private @Nullable Object executeWithFullJRuby(Console console, EngineEvalFunction process) {
@@ -516,6 +531,7 @@ private void printLoadingMessage(Console console, boolean show) {
516531
}
517532
ScriptEngine engine = container.getScriptEngine();
518533
try {
534+
configureEngineConsoleOutput(engine, console);
519535
printLoadingMessage(console, false);
520536
return process.apply(engine);
521537
} catch (ScriptException e) {
@@ -535,6 +551,7 @@ private void printLoadingMessage(Console console, boolean show) {
535551
if (engine == null) {
536552
throw new ScriptException("Unable to create JRuby script engine.");
537553
}
554+
configureEngineConsoleOutput(engine, console);
538555
return process.apply(engine);
539556
} catch (ScriptException e) {
540557
if (console != null) {
@@ -546,6 +563,70 @@ private void printLoadingMessage(Console console, boolean show) {
546563
}
547564
}
548565

566+
// ============================================================================
567+
// Inner Classes
568+
// ============================================================================
569+
570+
/**
571+
* A Writer wrapper that normalizes LF line endings to CRLF while preserving all other characters.
572+
*/
573+
static class ConsoleWriter extends Writer {
574+
private final Writer delegate;
575+
private boolean previousWasCarriageReturn = false;
576+
private boolean closed = false;
577+
578+
ConsoleWriter(Writer delegate) {
579+
this.delegate = delegate;
580+
}
581+
582+
@Override
583+
public void write(char @Nullable [] c, int off, int len) throws IOException {
584+
if (closed) {
585+
throw new IOException("Writer is closed");
586+
}
587+
if (c != null) {
588+
for (int index = off; index < off + len; index++) {
589+
char ch = c[index];
590+
if (previousWasCarriageReturn) {
591+
if (ch == '\n') {
592+
delegate.write('\r');
593+
delegate.write('\n');
594+
previousWasCarriageReturn = false;
595+
continue;
596+
}
597+
delegate.write('\r');
598+
previousWasCarriageReturn = false;
599+
}
600+
601+
if (ch == '\r') {
602+
previousWasCarriageReturn = true;
603+
} else if (ch == '\n') {
604+
delegate.write('\r');
605+
delegate.write('\n');
606+
} else {
607+
delegate.write(ch);
608+
}
609+
}
610+
}
611+
}
612+
613+
@Override
614+
public void flush() throws IOException {
615+
if (previousWasCarriageReturn) {
616+
delegate.write('\r');
617+
previousWasCarriageReturn = false;
618+
}
619+
delegate.flush();
620+
}
621+
622+
@Override
623+
public void close() throws IOException {
624+
flush();
625+
closed = true;
626+
delegate.close();
627+
}
628+
}
629+
549630
@FunctionalInterface
550631
public interface EngineEvalFunction {
551632
@Nullable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) 2010-2026 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.automation.jrubyscripting.internal;
14+
15+
import static org.junit.jupiter.api.Assertions.assertEquals;
16+
17+
import java.io.Writer;
18+
19+
import org.eclipse.jdt.annotation.NonNullByDefault;
20+
import org.eclipse.jdt.annotation.Nullable;
21+
import org.junit.jupiter.api.Test;
22+
23+
/**
24+
* Unit tests for JRuby console output writer behavior.
25+
*
26+
* @author Jimmy Tanagra - Initial contribution
27+
*/
28+
@NonNullByDefault
29+
class JRubyConsoleCommandExtensionConsoleWriterTest {
30+
31+
@Test
32+
void preservesAnsiAndNormalizesLfToCrLf() throws Exception {
33+
RecordingWriter delegate = new RecordingWriter();
34+
Writer writer = new JRubyConsoleCommandExtension.ConsoleWriter(delegate);
35+
36+
String output = "\u001B[32mgreen\u001B[0m\nplain";
37+
writer.write(output.toCharArray(), 0, output.length());
38+
writer.flush();
39+
40+
assertEquals("\u001B[32mgreen\u001B[0m\r\nplain", delegate.output());
41+
}
42+
43+
@Test
44+
void preservesExistingCrLfAndHandlesChunkBoundary() throws Exception {
45+
RecordingWriter delegate = new RecordingWriter();
46+
Writer writer = new JRubyConsoleCommandExtension.ConsoleWriter(delegate);
47+
48+
writer.write("left\r".toCharArray(), 0, 5);
49+
writer.write("\nright\n".toCharArray(), 0, 7);
50+
writer.flush();
51+
52+
assertEquals("left\r\nright\r\n", delegate.output());
53+
}
54+
55+
private static final class RecordingWriter extends Writer {
56+
private final StringBuilder output = new StringBuilder();
57+
58+
@Override
59+
public void write(char @Nullable [] cbuf, int off, int len) {
60+
if (cbuf != null) {
61+
output.append(cbuf, off, len);
62+
}
63+
}
64+
65+
@Override
66+
public void flush() {
67+
}
68+
69+
@Override
70+
public void close() {
71+
}
72+
73+
String output() {
74+
return output.toString();
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)