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:
GustavoHMDS 2026-07-21 11:09:23 -03:00
commit 5d650df8eb
20 changed files with 263 additions and 6 deletions

10
pitiupi/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# NetBeans
/build/
/dist/
/nbproject/private/
# Java compilado
*.class
# JARs gerados
*.jar

View File

@ -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.

View File

@ -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;
} }

View File

@ -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());
}
});
}
}

View File

@ -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());
}
}

View File

@ -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() + ")";
}
}

View File

@ -0,0 +1,10 @@
package pitiupi.control;
import java.util.List;
/**
*
* @author tony
*/
public interface PeerListener {
void onPeersChanged(List<PeerInfo> peers);
}

View File

@ -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;
}
}

View File

@ -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() {