View Javadoc
1   package org.sentrysoftware.ipmi.core.api.sol;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * IPMI Java Client
6    * ჻჻჻჻჻჻
7    * Copyright 2023 Verax Systems, Sentry Software
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.sentrysoftware.ipmi.core.api.async.ConnectionHandle;
26  import org.sentrysoftware.ipmi.core.api.async.InboundSolMessageListener;
27  import org.sentrysoftware.ipmi.core.api.sync.IpmiConnector;
28  import org.sentrysoftware.ipmi.core.coding.PayloadCoder;
29  import org.sentrysoftware.ipmi.core.coding.commands.IpmiVersion;
30  import org.sentrysoftware.ipmi.core.coding.commands.PrivilegeLevel;
31  import org.sentrysoftware.ipmi.core.coding.commands.payload.ActivateSolPayload;
32  import org.sentrysoftware.ipmi.core.coding.commands.payload.ActivateSolPayloadResponseData;
33  import org.sentrysoftware.ipmi.core.coding.commands.payload.DeactivatePayload;
34  import org.sentrysoftware.ipmi.core.coding.commands.payload.GetPayloadActivationStatus;
35  import org.sentrysoftware.ipmi.core.coding.commands.payload.GetPayloadActivationStatusResponseData;
36  import org.sentrysoftware.ipmi.core.coding.commands.session.SetSessionPrivilegeLevel;
37  import org.sentrysoftware.ipmi.core.coding.payload.CompletionCode;
38  import org.sentrysoftware.ipmi.core.coding.payload.lan.IPMIException;
39  import org.sentrysoftware.ipmi.core.coding.payload.sol.SolAckState;
40  import org.sentrysoftware.ipmi.core.coding.payload.sol.SolMessage;
41  import org.sentrysoftware.ipmi.core.coding.payload.sol.SolOperation;
42  import org.sentrysoftware.ipmi.core.coding.protocol.AuthenticationType;
43  import org.sentrysoftware.ipmi.core.coding.protocol.PayloadType;
44  import org.sentrysoftware.ipmi.core.coding.security.CipherSuite;
45  import org.sentrysoftware.ipmi.core.coding.sol.SolCoder;
46  import org.sentrysoftware.ipmi.core.coding.sol.SolResponseData;
47  import org.sentrysoftware.ipmi.core.common.Constants;
48  import org.sentrysoftware.ipmi.core.common.TypeConverter;
49  import org.sentrysoftware.ipmi.core.connection.Session;
50  import org.sentrysoftware.ipmi.core.connection.SessionException;
51  import org.sentrysoftware.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 }