Merge branch 'feature/heartbeat-online-users' of https://git.alice.ufsj.edu.br/fls/pitiupi into feature/help-and-documentation
This commit is contained in:
commit
5d650df8eb
|
|
@ -0,0 +1,10 @@
|
||||||
|
# NetBeans
|
||||||
|
/build/
|
||||||
|
/dist/
|
||||||
|
/nbproject/private/
|
||||||
|
|
||||||
|
# Java compilado
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# JARs gerados
|
||||||
|
*.jar
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
#Mon, 20 Jul 2026 12:23:12 -0300
|
|
||||||
|
|
||||||
|
|
||||||
C\:\\Users\\extre\\OneDrive\\Documentos\\Faculdade\\2026_02\\Monitoria\\Pitiupi\\pitiupi=
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -11,10 +11,12 @@ import java.util.List;
|
||||||
import javax.swing.JMenu;
|
import javax.swing.JMenu;
|
||||||
import pitiupi.control.PluginLoader;
|
import pitiupi.control.PluginLoader;
|
||||||
import pitiupi.plugin.Plugin;
|
import pitiupi.plugin.Plugin;
|
||||||
|
import pitiupi.control.HeartbeatManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author flavio
|
* @author flavio
|
||||||
|
* @author tony
|
||||||
*/
|
*/
|
||||||
public class MainWindow extends javax.swing.JFrame {
|
public class MainWindow extends javax.swing.JFrame {
|
||||||
|
|
||||||
|
|
@ -24,6 +26,10 @@ public class MainWindow extends javax.swing.JFrame {
|
||||||
private javax.swing.JPanel statusPanel;
|
private javax.swing.JPanel statusPanel;
|
||||||
private javax.swing.JTabbedPane tabbedPane;
|
private javax.swing.JTabbedPane tabbedPane;
|
||||||
|
|
||||||
|
private HeartbeatManager heartbeatManager;
|
||||||
|
private OnlineUsersPanel onlineUsersPanel;
|
||||||
|
|
||||||
|
|
||||||
private Socket socket;
|
private Socket socket;
|
||||||
|
|
||||||
private final String userName;
|
private final String userName;
|
||||||
|
|
@ -64,6 +70,12 @@ public class MainWindow extends javax.swing.JFrame {
|
||||||
this.socket = new Socket(this);
|
this.socket = new Socket(this);
|
||||||
this.socket.start();
|
this.socket.start();
|
||||||
|
|
||||||
|
this.heartbeatManager = new HeartbeatManager(this);
|
||||||
|
this.onlineUsersPanel = new OnlineUsersPanel();
|
||||||
|
this.heartbeatManager.addPeerListener(this.onlineUsersPanel);
|
||||||
|
this.getContentPane().add(this.onlineUsersPanel, java.awt.BorderLayout.LINE_END);
|
||||||
|
this.heartbeatManager.start();
|
||||||
|
|
||||||
this.setSize(new java.awt.Dimension(1024, 768));
|
this.setSize(new java.awt.Dimension(1024, 768));
|
||||||
this.pluginManager = new PluginLoader(this);
|
this.pluginManager = new PluginLoader(this);
|
||||||
this.pluginManager.loadPlugins();
|
this.pluginManager.loadPlugins();
|
||||||
|
|
@ -76,6 +88,10 @@ public class MainWindow extends javax.swing.JFrame {
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public HeartbeatManager getHeartbeatManager() {
|
||||||
|
return heartbeatManager;
|
||||||
|
}
|
||||||
|
|
||||||
public String getUserName(){
|
public String getUserName(){
|
||||||
return this.userName;
|
return this.userName;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package pitiupi.GUI;
|
||||||
|
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.DefaultListModel;
|
||||||
|
import javax.swing.JList;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JScrollPane;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
|
import pitiupi.control.PeerInfo;
|
||||||
|
import pitiupi.control.PeerListener;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author tony
|
||||||
|
*/
|
||||||
|
public class OnlineUsersPanel extends JPanel implements PeerListener {
|
||||||
|
|
||||||
|
private final DefaultListModel<String> listModel;
|
||||||
|
private final JList<String> userList;
|
||||||
|
|
||||||
|
public OnlineUsersPanel() {
|
||||||
|
setLayout(new BorderLayout());
|
||||||
|
setBorder(BorderFactory.createTitledBorder("Online"));
|
||||||
|
setPreferredSize(new Dimension(160, 0));
|
||||||
|
|
||||||
|
listModel = new DefaultListModel<>();
|
||||||
|
userList = new JList<>(listModel);
|
||||||
|
add(new JScrollPane(userList), BorderLayout.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPeersChanged(List<PeerInfo> peers) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
listModel.clear();
|
||||||
|
for (PeerInfo peer : peers) {
|
||||||
|
listModel.addElement(peer.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
package pitiupi.control;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import pitiupi.GUI.MainWindow;
|
||||||
|
import pitiupi.net.HeartbeatMessage;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author tony
|
||||||
|
*/
|
||||||
|
public class HeartbeatManager {
|
||||||
|
|
||||||
|
private static final long HEARTBEAT_INTERVAL_MS = 5000;
|
||||||
|
private static final long PEER_TIMEOUT_MS = 15000; // ~3 batimentos perdidos
|
||||||
|
|
||||||
|
private final MainWindow mainWindow;
|
||||||
|
private final Map<String, PeerInfo> peers = new ConcurrentHashMap<>();
|
||||||
|
private final List<PeerListener> listeners = new CopyOnWriteArrayList<>();
|
||||||
|
private ScheduledExecutorService scheduler;
|
||||||
|
|
||||||
|
public HeartbeatManager(MainWindow mainWindow) {
|
||||||
|
this.mainWindow = mainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
scheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
scheduler.scheduleAtFixedRate(this::announce, 0, HEARTBEAT_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
||||||
|
scheduler.scheduleAtFixedRate(this::checkTimeouts, HEARTBEAT_INTERVAL_MS, HEARTBEAT_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
if (scheduler != null) {
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void announce() {
|
||||||
|
try {
|
||||||
|
HeartbeatMessage msg = new HeartbeatMessage(mainWindow.getUserName());
|
||||||
|
mainWindow.getSocket().send(msg.toByteArray());
|
||||||
|
} catch (IOException ex) {
|
||||||
|
Logger.getLogger(HeartbeatManager.class.getName()).log(Level.WARNING, "Falha ao enviar heartbeat", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chamado pelo Socket quando um pacote é identificado como heartbeat
|
||||||
|
public void receiveHeartbeat(HeartbeatMessage msg, InetAddress from) {
|
||||||
|
// ignora o próprio heartbeat
|
||||||
|
if (msg.getUserName().equals(mainWindow.getUserName())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = from.getHostAddress() + "#" + msg.getUserName();
|
||||||
|
boolean isNew = !peers.containsKey(key);
|
||||||
|
|
||||||
|
PeerInfo peer = peers.computeIfAbsent(key, k -> new PeerInfo(msg.getUserName(), from, msg.getTimestamp()));
|
||||||
|
peer.touch(System.currentTimeMillis());
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkTimeouts() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
boolean changed = peers.values().removeIf(p -> now - p.getLastSeen() > PEER_TIMEOUT_MS);
|
||||||
|
if (changed) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyListeners() {
|
||||||
|
List<PeerInfo> snapshot = new ArrayList<>(peers.values());
|
||||||
|
for (PeerListener l : listeners) {
|
||||||
|
l.onPeersChanged(snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addPeerListener(PeerListener l) {
|
||||||
|
listeners.add(l);
|
||||||
|
l.onPeersChanged(new ArrayList<>(peers.values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removePeerListener(PeerListener l) {
|
||||||
|
listeners.remove(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PeerInfo> getPeers() {
|
||||||
|
return new ArrayList<>(peers.values());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package pitiupi.control;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author tony
|
||||||
|
*/
|
||||||
|
public class PeerInfo {
|
||||||
|
|
||||||
|
private final String userName;
|
||||||
|
private final InetAddress address;
|
||||||
|
private volatile long lastSeen;
|
||||||
|
|
||||||
|
public PeerInfo(String userName, InetAddress address, long lastSeen) {
|
||||||
|
this.userName = userName;
|
||||||
|
this.address = address;
|
||||||
|
this.lastSeen = lastSeen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserName() {
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InetAddress getAddress() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastSeen() {
|
||||||
|
return lastSeen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void touch(long timestamp) {
|
||||||
|
this.lastSeen = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return userName + " (" + address.getHostAddress() + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package pitiupi.control;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author tony
|
||||||
|
*/
|
||||||
|
public interface PeerListener {
|
||||||
|
void onPeersChanged(List<PeerInfo> peers);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package pitiupi.net;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author tony
|
||||||
|
*/
|
||||||
|
public class HeartbeatMessage extends Message {
|
||||||
|
|
||||||
|
private final String userName;
|
||||||
|
private final long timestamp;
|
||||||
|
|
||||||
|
public HeartbeatMessage(String userName) {
|
||||||
|
this.userName = userName;
|
||||||
|
this.timestamp = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserName() {
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,10 +17,12 @@ import java.net.UnknownHostException;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import pitiupi.plugin.Plugin;
|
import pitiupi.plugin.Plugin;
|
||||||
|
import pitiupi.net.HeartbeatMessage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author flavio
|
* @author flavio
|
||||||
|
* @author tony
|
||||||
*/
|
*/
|
||||||
public class Socket extends Thread {
|
public class Socket extends Thread {
|
||||||
|
|
||||||
|
|
@ -53,19 +55,36 @@ public class Socket extends Thread {
|
||||||
msgPacket = new DatagramPacket(msg, msg.length, this.address, this.main.getPort());
|
msgPacket = new DatagramPacket(msg, msg.length, this.address, this.main.getPort());
|
||||||
multicastSocket.send(msgPacket);
|
multicastSocket.send(msgPacket);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void receive() throws UnknownHostException, IOException, ClassNotFoundException {
|
public void receive() throws UnknownHostException, IOException, ClassNotFoundException {
|
||||||
byte[] buf = new byte[256000];
|
byte[] buf = new byte[256000];
|
||||||
while (true) {
|
while (true) {
|
||||||
// Receive the information and print it.
|
|
||||||
DatagramPacket msgPacket = new DatagramPacket(buf, buf.length);
|
DatagramPacket msgPacket = new DatagramPacket(buf, buf.length);
|
||||||
multicastSocket.receive(msgPacket);
|
multicastSocket.receive(msgPacket);
|
||||||
|
|
||||||
|
HeartbeatMessage heartbeat = tryParseHeartbeat(msgPacket.getData());
|
||||||
|
if (heartbeat != null) {
|
||||||
|
main.getHeartbeatManager().receiveHeartbeat(heartbeat, msgPacket.getAddress());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for (Plugin plugin : main.getPlugins()) {
|
for (Plugin plugin : main.getPlugins()) {
|
||||||
System.out.println(msgPacket.getAddress().toString());
|
System.out.println(msgPacket.getAddress().toString());
|
||||||
plugin.receiveMessage(msgPacket.getData());
|
plugin.receiveMessage(msgPacket.getData());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HeartbeatMessage tryParseHeartbeat(byte[] data) {
|
||||||
|
try {
|
||||||
|
ByteArrayInputStream bais = new ByteArrayInputStream(data);
|
||||||
|
ObjectInput in = new ObjectInputStream(bais);
|
||||||
|
Object obj = in.readObject();
|
||||||
|
return (obj instanceof HeartbeatMessage) ? (HeartbeatMessage) obj : null;
|
||||||
|
} catch (IOException | ClassNotFoundException | ClassCastException ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue