View Javadoc
1   package org.metricshub.ipmi.core.sm;
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 java.io.IOException;
26  import java.net.InetAddress;
27  import java.util.ArrayList;
28  import java.util.List;
29  import java.util.concurrent.CopyOnWriteArrayList;
30  
31  import org.metricshub.ipmi.core.coding.rmcp.RmcpClassOfMessage;
32  import org.metricshub.ipmi.core.coding.rmcp.RmcpDecoder;
33  import org.metricshub.ipmi.core.coding.rmcp.RmcpMessage;
34  import org.metricshub.ipmi.core.common.Constants;
35  import org.metricshub.ipmi.core.sm.actions.MessageAction;
36  import org.metricshub.ipmi.core.sm.actions.StateMachineAction;
37  import org.metricshub.ipmi.core.sm.events.StateMachineEvent;
38  import org.metricshub.ipmi.core.sm.states.State;
39  import org.metricshub.ipmi.core.sm.states.Uninitialized;
40  import org.metricshub.ipmi.core.transport.Messenger;
41  import org.metricshub.ipmi.core.transport.UdpListener;
42  import org.metricshub.ipmi.core.transport.UdpMessage;
43  
44  /**
45   * State machine for connecting and acquiring session with the remote host via
46   * IPMI v.2.0.
47   */
48  public class StateMachine implements UdpListener {
49  
50  	private final List<MachineObserver> observers = new CopyOnWriteArrayList<MachineObserver>();
51  
52  	/**
53  	 * In-session messages received while the lock is held, dispatched by {@link #doTransition(StateMachineEvent)}
54  	 * and {@link #notifyMessage(UdpMessage)} once they release it, so that no application listener runs under the
55  	 * lock. The other actions (a handshake reply, an error, the session key) are published under the lock: the
56  	 * caller then sees the reply together with the state it produced, and cannot time the request out in between.
57  	 */
58  	private final List<StateMachineAction> pendingActions = new ArrayList<StateMachineAction>();
59  
60  	private volatile State current;
61  
62  	private Messenger messenger;
63  	private volatile InetAddress remoteMachineAddress;
64  	private volatile int remoteMachinePort;
65  
66  	private volatile boolean initialized;
67  
68  	public State getCurrent() {
69  		return current;
70  	}
71  
72  	public void setCurrent(State current) {
73  		this.current = current;
74  		current.onEnter(this);
75  	}
76  
77  	/**
78  	 * Initializes the State Machine
79  	 *
80  	 * @param messenger
81  	 *        - {@link Messenger} connected to the
82  	 *        {@link Constants#IPMI_PORT}
83  	 */
84  	public StateMachine(Messenger messenger) {
85  		this.messenger = messenger;
86  		initialized = false;
87  	}
88  
89  	/**
90  	 * Sends message via {@link #messenger} to the managed system.
91  	 *
92  	 * @param message
93  	 *        - the encoded message
94  	 * @throws IOException
95  	 *         - when sending of the message fails
96  	 */
97  	public void sendMessage(byte[] message) throws IOException {
98  		UdpMessage udpMessage = new UdpMessage();
99  		udpMessage.setAddress(getRemoteMachineAddress());
100 		udpMessage.setPort(getRemoteMachinePort());
101 		udpMessage.setMessage(message);
102 		messenger.send(udpMessage);
103 	}
104 
105 	public InetAddress getRemoteMachineAddress() {
106 		return remoteMachineAddress;
107 	}
108 
109 	public int getRemoteMachinePort() {
110 		return remoteMachinePort;
111 	}
112 
113 	/**
114 	 * Sends a notification of an action to all {@link MachineObserver}s. A {@link MessageAction} emitted by a state
115 	 * while the lock is held is deferred until the transition releases it; the other actions are published at once.
116 	 *
117 	 * @param action
118 	 *        - a {@link StateMachineAction} to perform
119 	 */
120 	public void doExternalAction(StateMachineAction action) {
121 		if (action instanceof MessageAction && Thread.holdsLock(this)) {
122 			pendingActions.add(action);
123 		} else {
124 			notifyObservers(action);
125 		}
126 	}
127 
128 	private void notifyObservers(StateMachineAction action) {
129 		for (MachineObserver observer : observers) {
130 			if (observer != null) {
131 				observer.notify(action);
132 			}
133 		}
134 	}
135 
136 	private void dispatch(List<StateMachineAction> actions) {
137 		for (StateMachineAction action : actions) {
138 			notifyObservers(action);
139 		}
140 	}
141 
142 	/** Returns the actions emitted so far and clears them; called under the lock. */
143 	private List<StateMachineAction> drainPendingActions() {
144 		List<StateMachineAction> actions = new ArrayList<StateMachineAction>(pendingActions);
145 		pendingActions.clear();
146 		return actions;
147 	}
148 
149 	/**
150 	 * Sets the State Machine in the initial state.
151 	 *
152 	 * @param address
153 	 *        - IP address of the remote machine.
154 	 * @param port
155 	 *        - UDP remoteMachinePort of the remote machine
156 	 * @see #stop()
157 	 */
158 	public void start(InetAddress address, int port) {
159 		messenger.register(this);
160 		remoteMachineAddress = address;
161 		this.remoteMachinePort = port;
162 		setCurrent(new Uninitialized());
163 		initialized = true;
164 	}
165 
166 	/**
167 	 * Cleans up the machine resources and leaves the current state: a stopped machine no longer reports a valid
168 	 * session.
169 	 *
170 	 * @see #start(InetAddress, int)
171 	 */
172 	public void stop() {
173 		messenger.unregister(this);
174 		initialized = false;
175 		current = new Uninitialized();
176 	}
177 
178 	/**
179 	 * @return true if {@link StateMachine} is initialized, false otherwise.
180 	 * @see #start(InetAddress, int)
181 	 * @see #stop()
182 	 */
183 	public boolean isActive() {
184 		return initialized;
185 	}
186 
187 	/**
188 	 * Performs a {@link State} transition according to the event and
189 	 * {@link #current} state. Transitions and received messages are serialized, so a late reply cannot interleave
190 	 * with the timeout or close of the request it answers; the in-session messages are dispatched once the lock is
191 	 * released.
192 	 *
193 	 * @param event
194 	 *        - {@link StateMachineEvent} invoking the transition
195 	 * @throws NullPointerException
196 	 *         - when machine was not yet started
197 	 * @see #start(InetAddress, int)
198 	 */
199 	public void doTransition(StateMachineEvent event) {
200 		List<StateMachineAction> actions;
201 		synchronized (this) {
202 			if (!initialized) {
203 				throw new NullPointerException("State machine not started");
204 			}
205 			current.doTransition(this, event);
206 			actions = drainPendingActions();
207 		}
208 		dispatch(actions);
209 	}
210 
211 	@Override
212 	public void notifyMessage(UdpMessage message) {
213 		if (!message.getAddress().equals(getRemoteMachineAddress()) || message.getPort() != getRemoteMachinePort()) {
214 			return;
215 		}
216 		List<StateMachineAction> actions;
217 		synchronized (this) {
218 			try {
219 				RmcpMessage rmcpMessage = RmcpDecoder.decode(message.getMessage());
220 				if (rmcpMessage.getClassOfMessage() == RmcpClassOfMessage.Ipmi) {
221 					current.doAction(this, rmcpMessage);
222 				}
223 			} catch (RuntimeException e) {
224 				// A datagram too short or malformed for the states to decode: drop it, as the BMC could not have sent
225 				// it, rather than let the exception skip the other listeners of the messenger
226 			}
227 			actions = drainPendingActions();
228 		}
229 		dispatch(actions);
230 	}
231 
232 	/**
233 	 * Registers the listener in the {@link StateMachine} so it will be notified
234 	 * of the {@link StateMachineAction}s performed via
235 	 * {@link #doExternalAction(StateMachineAction)}
236 	 *
237 	 * @param observer
238 	 *        - {@link MachineObserver} to register
239 	 */
240 	public void register(MachineObserver observer) {
241 		observers.add(observer);
242 	}
243 }