1 package org.metricshub.ipmi.core.api.sol;
2
3 /*-
4 * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5 * IPMI Java Client
6 * ჻჻჻჻჻჻
7 * Copyright 2023 Verax Systems, MetricsHub
8 * ჻჻჻჻჻჻
9 * This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU Lesser General Public License as
11 * published by the Free Software Foundation, either version 3 of the
12 * License, or (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Lesser Public License for more details.
18 *
19 * You should have received a copy of the GNU General Lesser Public
20 * License along with this program. If not, see
21 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
22 * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
23 */
24
25 import org.metricshub.ipmi.core.api.async.ConnectionHandle;
26 import org.metricshub.ipmi.core.api.async.InboundSolMessageListener;
27 import org.metricshub.ipmi.core.api.sync.IpmiConnector;
28 import org.metricshub.ipmi.core.coding.PayloadCoder;
29 import org.metricshub.ipmi.core.coding.commands.IpmiVersion;
30 import org.metricshub.ipmi.core.coding.commands.PrivilegeLevel;
31 import org.metricshub.ipmi.core.coding.commands.payload.ActivateSolPayload;
32 import org.metricshub.ipmi.core.coding.commands.payload.ActivateSolPayloadResponseData;
33 import org.metricshub.ipmi.core.coding.commands.payload.DeactivatePayload;
34 import org.metricshub.ipmi.core.coding.commands.payload.GetPayloadActivationStatus;
35 import org.metricshub.ipmi.core.coding.commands.payload.GetPayloadActivationStatusResponseData;
36 import org.metricshub.ipmi.core.coding.commands.session.SetSessionPrivilegeLevel;
37 import org.metricshub.ipmi.core.coding.payload.CompletionCode;
38 import org.metricshub.ipmi.core.coding.payload.lan.IPMIException;
39 import org.metricshub.ipmi.core.coding.payload.sol.SolAckState;
40 import org.metricshub.ipmi.core.coding.payload.sol.SolMessage;
41 import org.metricshub.ipmi.core.coding.payload.sol.SolOperation;
42 import org.metricshub.ipmi.core.coding.protocol.AuthenticationType;
43 import org.metricshub.ipmi.core.coding.protocol.PayloadType;
44 import org.metricshub.ipmi.core.coding.security.CipherSuite;
45 import org.metricshub.ipmi.core.coding.sol.SolCoder;
46 import org.metricshub.ipmi.core.coding.sol.SolResponseData;
47 import org.metricshub.ipmi.core.common.Constants;
48 import org.metricshub.ipmi.core.common.TypeConverter;
49 import org.metricshub.ipmi.core.connection.Session;
50 import org.metricshub.ipmi.core.connection.SessionException;
51 import org.metricshub.ipmi.core.connection.SessionManager;
52
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 import java.io.Closeable;
57 import java.io.IOException;
58 import java.nio.charset.Charset;
59 import java.util.Arrays;
60 import java.util.HashSet;
61 import java.util.LinkedList;
62 import java.util.List;
63 import java.util.Set;
64
65 /**
66 * Entry point for the Serial Over LAN (SOL) communication. Use all SOL operations through this class.
67 */
68 public class SerialOverLan implements Closeable {
69
70 private static final Logger logger = LoggerFactory.getLogger(SerialOverLan.class);
71
72 private final IpmiConnector connector;
73 private final Session session;
74 private final InboundSolMessageListener inboundMessageListener;
75 private final List<SolEventListener> eventListeners;
76
77 private boolean isSessionInternal;
78 private int payloadInstance;
79 private int maxPayloadSize;
80 private boolean closed;
81
82 /**
83 * Creates connection with IPMI using given {@link IpmiConnector}, connected to remote machine on given address and port,
84 * and opens a new {@link Session} for SOL communication.
85 * This constructor should be used only when you have no other connection opened on this port.
86 *
87 * @param connector
88 * {@link IpmiConnector} that will be used for communication
89 * @param remoteHost
90 * IP address of the remote server
91 * @param remotePort
92 * UDP port number of the remote server
93 * @param user
94 * IPMI user name
95 * @param password
96 * IPMI password
97 * @param cipherSuiteSelectionHandler
98 * {@link CipherSuiteSelectionHandler} that will allow to select {@link CipherSuite} among available ones.
99 *
100 * @throws SOLException when any problem occur during establishing session or activating SOL payload.
101 */
102 public SerialOverLan(IpmiConnector connector, String remoteHost, int remotePort, String user, String password,
103 CipherSuiteSelectionHandler cipherSuiteSelectionHandler) throws SOLException, SessionException {
104 this(connector, SessionManager.establishSession(connector, remoteHost, remotePort, user, password, cipherSuiteSelectionHandler));
105
106 this.isSessionInternal = true;
107 }
108
109 /**
110 * Creates connection with IPMI using given {@link IpmiConnector}, connected to remote machine on given address and default IPMI port,
111 * and opens a new {@link Session} for SOL communication.
112 * This constructor should be used only when you have no other connection opened on this port.
113 *
114 * @param connector
115 * {@link IpmiConnector} that will be used for communication
116 * @param remoteHost
117 * IP address of the remote server
118 * @param user
119 * IPMI user name
120 * @param password
121 * IPMI password
122 * @param cipherSuiteSelectionHandler
123 * {@link CipherSuiteSelectionHandler} that will allow to select {@link CipherSuite} among available ones.
124 *
125 * @throws SOLException when any problem occur during establishing session or activating SOL payload.
126 */
127 public SerialOverLan(IpmiConnector connector, String remoteHost, String user, String password,
128 CipherSuiteSelectionHandler cipherSuiteSelectionHandler) throws SOLException, SessionException {
129 this(connector, remoteHost, Constants.IPMI_PORT, user, password, cipherSuiteSelectionHandler);
130 }
131
132 /**
133 * Tries to open SOL communication on given existing session. When it appears that separate session must be opened
134 * to handle SOL messages (for example SOL payload must be activated on separate port), the new connection and session
135 * are automatically established.
136 *
137 * @param connector
138 * {@link IpmiConnector} that will be used for communication
139 * @param session
140 * Existing session that should be reused (if possible) for SOL communication.
141 */
142 public SerialOverLan(IpmiConnector connector, Session session) throws SOLException, SessionException {
143 this.connector = connector;
144
145 int solPayloadPort = activatePayload(connector, session.getConnectionHandle());
146
147 this.session = resolveSession(connector, session.getConnectionHandle(), session, solPayloadPort);
148
149 this.eventListeners = new LinkedList<SolEventListener>();
150 this.inboundMessageListener = new InboundSolMessageListener(connector,
151 this.session.getConnectionHandle(), eventListeners);
152
153 connector.registerIncomingMessageListener(inboundMessageListener);
154 this.closed = false;
155 }
156
157 /**
158 * Given potential session object, connection data and port on which SOL should be activated,
159 * decides what session should be finally used to SOL communication.
160 *
161 * @param connector
162 * {@link IpmiConnector} that will be used for communication
163 * @param connectionHandle
164 * {@link ConnectionHandle} representing single connection to managed system.
165 * @param session
166 * Existing session that should be reused (if possible) for SOL communication.
167 * @param solPayloadPort
168 * UDP port on which managed system listens for SOL communication.
169 * @return Actual session (existing or newly established), that should be used for SOL communication.
170 * @throws SOLException
171 * If any unrecoverable error occurs.
172 * @throws SessionException
173 * If new session could not be established.
174 */
175 private Session resolveSession(IpmiConnector connector, ConnectionHandle connectionHandle, Session session, int solPayloadPort) throws SOLException, SessionException {
176 if (solPayloadPort != connectionHandle.getRemotePort()) {
177 Session alternativeSession = connector.getExistingSessionForCriteria(connectionHandle.getRemoteAddress(),
178 solPayloadPort, connectionHandle.getUser());
179
180 if (alternativeSession == null) {
181 CipherSuiteSelectionHandler cipherSuiteSelector = new SpecificCipherSuiteSelector(connectionHandle.getCipherSuite());
182
183 alternativeSession = SessionManager.establishSession(connector, connectionHandle.getRemoteAddress().getHostAddress(),
184 solPayloadPort, connectionHandle.getUser(), connectionHandle.getPassword(), cipherSuiteSelector);
185 this.isSessionInternal = true;
186
187 } else {
188 this.isSessionInternal = false;
189 }
190
191 activatePayload(connector, alternativeSession.getConnectionHandle());
192
193 return alternativeSession;
194 } else {
195 this.isSessionInternal = false;
196 return session;
197 }
198 }
199
200 /**
201 * Tries to activate SOL payload in the session associated to given {@link ConnectionHandle}.
202 * If first activation try fails due to insufficient privileges, raises the session privileges
203 * to maximum available and tries to activate payload once again.
204 *
205 * @param connector
206 * {@link IpmiConnector} that will be used for communication
207 * @param connectionHandle
208 * {@link ConnectionHandle} representing single connection to managed system.
209 * @return UDP port number on which managed system listenes for SOL messages.
210 * @throws SOLException
211 * when any unrecoverable error occurred.
212 */
213 private int activatePayload(IpmiConnector connector, ConnectionHandle connectionHandle) throws SOLException {
214 try {
215 this.payloadInstance = getFirstAvailablePayloadInstance(connector, connectionHandle);
216
217 ActivateSolPayload activatePayload = new ActivateSolPayload(connectionHandle.getCipherSuite(), payloadInstance);
218 ActivateSolPayloadResponseData activatePayloadResponseData = getActivatePayloadResponse(connector,
219 connectionHandle, activatePayload);
220
221 this.maxPayloadSize = activatePayloadResponseData.getInboundPayloadSize();
222
223 return activatePayloadResponseData.getPayloadUdpPortNumber();
224
225 } catch (Exception e) {
226 throw new SOLException("Cannot activate SOL payload due to exception during activation process", e);
227 }
228 }
229
230 private ActivateSolPayloadResponseData getActivatePayloadResponse(IpmiConnector connector, ConnectionHandle connectionHandle, ActivateSolPayload activatePayload) throws Exception {
231 ActivateSolPayloadResponseData activatePayloadResponseData;
232
233 try {
234 activatePayloadResponseData = (ActivateSolPayloadResponseData) connector.sendMessage(connectionHandle,
235 activatePayload);
236 } catch (IPMIException e) {
237 if (e.getCompletionCode() == CompletionCode.InsufficentPrivilege) {
238 raiseSessionPrivileges(connector, connectionHandle);
239
240 activatePayloadResponseData = (ActivateSolPayloadResponseData) connector.sendMessage(
241 connectionHandle, activatePayload);
242 } else {
243 throw e;
244 }
245 }
246
247 return activatePayloadResponseData;
248 }
249
250 /**
251 * Checks for available SOL payload instances and, if any, returns first available.
252 *
253 * @param connector
254 * {@link IpmiConnector} that will be used for communication
255 * @param connectionHandle
256 * {@link ConnectionHandle} representing single connection to managed system.
257 * @return number of first available SOL payload instance.
258 * @throws Exception
259 * If any exception occurred during communication or if no available instances were found.
260 */
261 private int getFirstAvailablePayloadInstance(IpmiConnector connector, ConnectionHandle connectionHandle) throws Exception {
262 GetPayloadActivationStatus getPayloadActivationStatus = new GetPayloadActivationStatus(connectionHandle.getCipherSuite(),
263 PayloadType.Sol);
264 GetPayloadActivationStatusResponseData getActivationResponseData = (GetPayloadActivationStatusResponseData) connector.sendMessage(
265 connectionHandle, getPayloadActivationStatus);
266
267 if (getActivationResponseData.getInstanceCapacity() <= 0 || getActivationResponseData.getAvailableInstances().isEmpty()) {
268 throw new SOLException("Cannot activate SOL payload, as there are no available payload instances.");
269 }
270
271 return TypeConverter.byteToInt(getActivationResponseData.getAvailableInstances().get(0));
272 }
273
274 /**
275 * Sends proper command to managed system in order to raise user privileges in given session to maximum available level.
276 *
277 * @param connector
278 * {@link IpmiConnector} that will be used for communication
279 * @param connectionHandle
280 * {@link ConnectionHandle} representing single connection to managed system.
281 * @throws Exception
282 * If any exception occurred during communication.
283 */
284 private void raiseSessionPrivileges(IpmiConnector connector, ConnectionHandle connectionHandle) throws Exception {
285 SetSessionPrivilegeLevel setSessionPrivilegeLevel = new SetSessionPrivilegeLevel(IpmiVersion.V20,
286 connectionHandle.getCipherSuite(), AuthenticationType.RMCPPlus, PrivilegeLevel.Administrator);
287 connector.sendMessage(connectionHandle, setSessionPrivilegeLevel);
288 }
289
290
291 /**
292 * Writes single byte to the port.
293 * This operation blocks until all data can be sent to remote server and is either accepted or rejected by the server.
294 *
295 * @param singleByte
296 * a byte to write
297 * @return true if byte was successfully sent and acknowledged by remote server, false otherwise.
298 */
299 public boolean writeByte(byte singleByte) {
300 return writeBytes(new byte[] {singleByte});
301 }
302
303 /**
304 * Writes bytes array to the port.
305 * This operation blocks until all data can be sent to remote server and is either accepted or rejected by the server.
306 *
307 * @param buffer
308 * an array of bytes to write
309 * @return true if all bytes were successfully sent and acknowledged by remote server, false otherwise.
310 */
311 public boolean writeBytes(byte[] buffer) {
312 boolean result = true;
313
314 int maxBufferSize = maxPayloadSize - SolMessage.PAYLOAD_HEADER_LENGTH;
315 byte[] remainingBytes = buffer;
316 int currentIndex = 0;
317
318 while (remainingBytes.length - currentIndex > maxBufferSize) {
319 byte[] bufferChunk = Arrays.copyOfRange(remainingBytes, currentIndex, maxBufferSize);
320 currentIndex += maxBufferSize;
321
322 result &= sendMessage(bufferChunk);
323
324 if (!result) {
325 return false;
326 }
327 }
328
329 if (remainingBytes.length - currentIndex > 0) {
330 remainingBytes = Arrays.copyOfRange(remainingBytes, currentIndex, remainingBytes.length);
331 result &= sendMessage(remainingBytes);
332 }
333
334 return result;
335 }
336
337 /**
338 * Writes single integer (in range from 0 to 255) to the port.
339 * This operation blocks until all data can be sent to remote server and is either accepted or rejected by the server.
340 *
341 * @param singleInt
342 * an integer value to write (must be in range from 0 to 255)
343 * @return true if integer was successfully sent and acknowledged by remote server, false otherwise.
344 */
345 public boolean writeInt(int singleInt) {
346 return writeBytes(new byte[] {TypeConverter.intToByte(singleInt)});
347 }
348
349 /**
350 * Writes integers (in range from 0 to 255) array to the port.
351 * This operation blocks until all data can be sent to remote server and is either accepted or rejected by the server.
352 *
353 * @param buffer
354 * an array of integer values to write (each must be in range from 0 to 255)
355 * @return true if all integers were successfully sent and acknowledged by remote server, false otherwise.
356 */
357 public boolean writeIntArray(int[] buffer) {
358 byte[] byteBuffer = new byte[buffer.length];
359
360 for (int i = 0; i < buffer.length; i++) {
361 byteBuffer[i] = TypeConverter.intToByte(buffer[i]);
362 }
363
364 return writeBytes(byteBuffer);
365 }
366
367 /**
368 * Writes {@link String} to port, using platform's default {@link Charset} when converting {@link String} to byte array.
369 * This operation blocks until all data can be sent to remote server and is either accepted or rejected by the server.
370 *
371 * @param string
372 * a string to write to the port.
373 * @return true if whole string was successfully sent and acknowledged by remote server, false otherwise.
374 */
375 public boolean writeString(String string) {
376 return writeBytes(string.getBytes());
377 }
378
379 /**
380 * Writes {@link String} to port, using given {@link Charset} when converting {@link String} to byte array.
381 * This operation blocks until all data can be sent to remote server and is either accepted or rejected by the server.
382 *
383 * @param string
384 * a string to write to port
385 * @param charset
386 * {@link Charset} that the string is encoded in
387 * @return true if whole string was successfully sent and acknowledged by remote server, false otherwise.
388 */
389 public boolean writeString(String string, Charset charset) {
390 return writeBytes(string.getBytes(charset));
391 }
392
393 /**
394 * Read all available bytes from the port.
395 * Returns immediately, without waiting for data to be available.
396 *
397 * @return all bytes that could be read or empty array if no bytes were available.
398 */
399 public byte[] readBytes() {
400 return readBytes(inboundMessageListener.getAvailableBytesCount());
401 }
402
403 /**
404 * Reads at max given number of bytes from the port.
405 * Returns immediately, without waiting for data to be available.
406 *
407 * @param byteCount
408 * maximum number of bytes that should be read
409 * @return byte array containing bytes that could be read, but no more than given byteCount.
410 * Returns empty array if no bytes were available.
411 */
412 public byte[] readBytes(int byteCount) {
413 return inboundMessageListener.readBytes(byteCount);
414 }
415
416 /**
417 * Reads at max given number of bytes from the port.
418 * This operation blocks until given number of bytes is available to be read or until given timeout is hit.
419 *
420 * @param byteCount
421 * maximum number of bytes that should be read.
422 * @param timeout
423 * maximum time in milliseconds that we want to wait for all available bytes
424 * @return byte array containing bytes that could be read, but no more than byteCount.
425 * When the timeout is hit, returns just bytes that were available or empty array if no bytes were available.
426 */
427 public byte[] readBytes(int byteCount, int timeout) {
428 waitForData(byteCount, timeout);
429
430 return readBytes(byteCount);
431 }
432
433 /**
434 * Reads all available bytes from the port as integer (in range from 0 to 255) values array.
435 * Returns immediately, without waiting for data to be available.
436 *
437 * @return all bytes that could be read as int array (each value in range from 0 to 255) or empty array if no data was available.
438 */
439 public int[] readIntArray() {
440 return readIntArray(inboundMessageListener.getAvailableBytesCount());
441 }
442
443 /**
444 * Reads at max given number of integer values (in range from 0 to 255) from the port.
445 * Returns immediately, without waiting for data to be available.
446 *
447 * @param byteCount
448 * maximum number of bytes that should be read
449 * @return integer array containing integer values that could be read, but no more than given byteCount (each value in range from 0 to 255).
450 * Returns empty array if no data was available.
451 */
452 public int[] readIntArray(int byteCount) {
453 byte[] bytesArray = readBytes(byteCount);
454 int[] intArray = new int[bytesArray.length];
455
456 for (int i = 0; i < bytesArray.length; i++) {
457 intArray[i] = TypeConverter.byteToInt(bytesArray[i]);
458 }
459
460 return intArray;
461 }
462
463 /**
464 * Reads at max given number of integer values (in range from 0 to 255) from the port.
465 * This operation blocks until given number of bytes is available to be read or until given timeout is hit.
466 *
467 * @param byteCount
468 * maximum number of bytes that should be read.
469 * @param timeout
470 * maximum time in milliseconds that we want to wait for all available data
471 * @return integer array containing integer values that could be read, but no more than byteCount (each value in range from 0 to 255).
472 * When the timeout is hit, returns just data that was available or empty array if no data was available.
473 */
474 public int[] readIntArray(int byteCount, int timeout) {
475 waitForData(byteCount, timeout);
476
477 return readIntArray(byteCount);
478 }
479
480 /**
481 * Read all available bytes from the port and converts them to {@link String} using platform's default {@link Charset}.
482 * Returns immediately, without waiting for data to be available.
483 *
484 * @return all bytes that could be read as {@link String}.
485 */
486 public String readString() {
487 return readString(inboundMessageListener.getAvailableBytesCount());
488 }
489
490 /**
491 * Reads at max given number of bytes from the port, converting them to {@link String} using platform's default {@link Charset}.
492 * Returns immediately, without waiting for data to be available.
493 *
494 * @param byteCount
495 * maximum number of bytes that should be read
496 * @return all bytes that could be read as {@link String}, but no more than given byteCount.
497 */
498 public String readString(int byteCount) {
499 return new String(readBytes(byteCount));
500 }
501
502 /**
503 * Reads at max given number of bytes from the port, converting them to {@link String} using platform's default {@link Charset}.
504 * This operation blocks until given number of bytes is available to be read or until given timeout is hit.
505 *
506 * @param byteCount
507 * maximum number of bytes that should be read.
508 * @param timeout
509 * maximum time in milliseconds that we want to wait for all available bytes
510 * @return all bytes that could be read as {@link String}, but no more than given byteCount.
511 * When the timeout is hit, returns just bytes that were available.
512 */
513 public String readString(int byteCount, int timeout) {
514 waitForData(byteCount, timeout);
515
516 return readString(byteCount);
517 }
518
519 /**
520 * Read all available bytes from the port and converts them to {@link String} using given {@link Charset}.
521 * Returns immediately, without waiting for data to be available.
522 *
523 * @param charset
524 * {@link Charset} that will be used when converting bytes to {@link String}
525 *
526 * @return all bytes that could be read as {@link String}.
527 */
528 public String readString(Charset charset) {
529 return readString(charset, inboundMessageListener.getAvailableBytesCount());
530 }
531
532 /**
533 * Reads at max given number of bytes from the port, converting them to {@link String} using given {@link Charset}.
534 * Returns immediately, without waiting for data to be available.
535 *
536 * @param charset
537 * {@link Charset} that will be used when converting bytes to {@link String}
538 * @param byteCount
539 * maximum number of bytes that should be read
540 * @return all bytes that could be read as {@link String}, but no more than given byteCount.
541 */
542 public String readString(Charset charset, int byteCount) {
543 return new String(readBytes(byteCount), charset);
544 }
545
546 /**
547 * Reads at max given number of bytes from the port, converting them to {@link String} using given {@link Charset}.
548 * This operation blocks until given number of bytes is available to be read or until given timeout is hit.
549 *
550 * @param charset
551 * {@link Charset} that will be used when converting bytes to {@link String}
552 * @param byteCount
553 * maximum number of bytes that should be read.
554 * @param timeout
555 * maximum time in milliseconds that we want to wait for all available bytes
556 * @return all bytes that could be read as {@link String}, but no more than given byteCount.
557 * When the timeout is hit, returns just bytes that were available.
558 */
559 public String readString(Charset charset, int byteCount, int timeout) {
560 waitForData(byteCount, timeout);
561
562 return readString(charset, byteCount);
563 }
564
565 private void waitForData(int wantedByteCount, int timeout) {
566 long startTime = System.currentTimeMillis();
567
568 while (isTooFewBytesAvailable(wantedByteCount) && timeoutNotHit(timeout, startTime)) {
569 // NOP, just waiting
570 }
571 }
572
573 private boolean isTooFewBytesAvailable(int wantedByteCount) {
574 return inboundMessageListener.getAvailableBytesCount() < wantedByteCount;
575 }
576
577 private boolean timeoutNotHit(int timeout, long startTime) {
578 return System.currentTimeMillis() - startTime < timeout;
579 }
580
581 /**
582 * Invokes given SOL-specific operations on remote serial port.
583 *
584 * @param operations
585 * a bunch of {@link SolOperation}s that should be invoked
586 * @return true if operations were successfully sent and acknowledged by remote server, false otherwise.
587 */
588 public boolean invokeOperations(SolOperation... operations) {
589 Set<SolOperation> operationSet = new HashSet<SolOperation>();
590
591 for (SolOperation operation : operations) {
592 operationSet.add(operation);
593 }
594
595 return sendMessage(operationSet);
596 }
597
598 /**
599 * Registers new {@link SolEventListener} that will be informed about SOL events fired by remote system.
600 *
601 * @param listener
602 * listener to be registered
603 */
604 public void registerEventListener(SolEventListener listener) {
605 eventListeners.add(listener);
606 }
607
608 /**
609 * Unregister given {@link SolEventListener}, preventing it from receiving SOL events from remote server.
610 *
611 * @param listener
612 * listener to be unregistered
613 */
614 public void unregisterEventListener(SolEventListener listener) {
615 eventListeners.remove(listener);
616 }
617
618 private boolean sendMessage(byte[] characterData) {
619 SolCoder payloadCoder = new SolCoder(characterData, session.getConnectionHandle().getCipherSuite());
620
621 SolResponseData responseData = sendPayload(payloadCoder);
622
623 notifyResponseListeners(characterData, new HashSet<SolOperation>(), responseData);
624
625 byte[] remainingCharacterData = characterData;
626
627 while (isNackForMessagePart(responseData, remainingCharacterData.length)) {
628 remainingCharacterData = Arrays.copyOfRange(remainingCharacterData,
629 responseData.getAcceptedCharactersNumber(), remainingCharacterData.length);
630
631 SolCoder partialPayloadCoder = new SolCoder(remainingCharacterData, session.getConnectionHandle().getCipherSuite());
632 responseData = sendPayload(partialPayloadCoder);
633
634 notifyResponseListeners(remainingCharacterData, new HashSet<SolOperation>(), responseData);
635 }
636
637 return responseData != null && responseData.getAcknowledgeState() == SolAckState.ACK;
638 }
639
640 private boolean sendMessage(Set<SolOperation> operations) {
641 SolCoder payloadCoder = new SolCoder(operations, session.getConnectionHandle().getCipherSuite());
642
643 SolResponseData responseData = sendPayload(payloadCoder);
644
645 notifyResponseListeners(new byte[0], operations, responseData);
646
647 return responseData != null && responseData.getAcknowledgeState() == SolAckState.ACK;
648 }
649
650 private void notifyResponseListeners(byte[] characterData, Set<SolOperation> solOperations, SolResponseData responseData) {
651 if (responseData != null && responseData.getStatuses() != null && !responseData.getStatuses().isEmpty()) {
652 for (SolEventListener listener : eventListeners) {
653 listener.processResponseEvent(responseData.getStatuses(), characterData, solOperations);
654 }
655 }
656 }
657
658 private SolResponseData sendPayload(PayloadCoder payloadCoder) {
659 ConnectionHandle connectionHandle = session.getConnectionHandle();
660
661 try {
662 SolResponseData responseData = (SolResponseData) connector.sendMessage(connectionHandle, payloadCoder);
663
664 int actualRetries = 0;
665
666 while (isNackForWholeMessage(responseData) && actualRetries < connector.getRetries()) {
667 actualRetries++;
668
669 responseData = (SolResponseData) connector.retryMessage(connectionHandle,
670 responseData.getRequestSequenceNumber(), PayloadType.Sol);
671 }
672
673 return responseData;
674 } catch (Exception e) {
675 logger.error("Error while sending message", e);
676 return null;
677 }
678 }
679
680 @Override
681 public synchronized void close() throws IOException {
682 if (!closed) {
683 try {
684 ConnectionHandle connectionHandle = session.getConnectionHandle();
685
686 DeactivatePayload deactivatePayload = new DeactivatePayload(connectionHandle.getCipherSuite(),
687 PayloadType.Sol, payloadInstance);
688
689 connector.sendMessage(connectionHandle, deactivatePayload);
690
691 if (isSessionInternal) {
692 connector.closeSession(connectionHandle);
693 connector.tearDown();
694 }
695
696 closed = true;
697 } catch (Exception e) {
698 throw new IOException("Error while closing Serial over LAN instance", e);
699 }
700
701 }
702 }
703
704 private boolean isNackForWholeMessage(SolResponseData responseData) {
705 return responseData != null
706 && responseData.getAcknowledgeState() == SolAckState.NACK
707 && responseData.getAcceptedCharactersNumber() == 0;
708 }
709
710 private boolean isNackForMessagePart(SolResponseData responseData, int previousMessageDataLength) {
711 return responseData != null
712 && responseData.getAcknowledgeState() == SolAckState.NACK
713 && responseData.getAcceptedCharactersNumber() > 0
714 && responseData.getAcceptedCharactersNumber() < previousMessageDataLength;
715 }
716
717 }