Skip to content

Commit

Permalink
Merge pull request #92 from JNU-econovation/restart_process
Browse files Browse the repository at this point in the history
[FEAT] mdns, montior writer에 프로세스 복구 로직 추가
  • Loading branch information
inferior3x authored Dec 15, 2024
2 parents ab2c8d4 + 0d8b87c commit 6d7b28e
Showing 18 changed files with 260 additions and 200 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package com.whoz_in.log_writer.system_validator;
package com.whoz_in.log_writer.common;

import com.whoz_in.log_writer.common.NetworkInterface;
import com.whoz_in.log_writer.common.process.TransientProcess;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

//iwconfig의 출력을 파싱하여 네트워크 인터페이스들을 반환함
//prod 환경에선 iwconfig가 설치되어있을 것을 SystemValidator를 통해 보장한다.
@Profile("prod")
@Component
public class SystemNetworkInterfaces {
public final class IwconfigNetworkInterfaces implements SystemNetworkInterfaces {

//최신 정보를 가져온다.
public List<NetworkInterface> getLatest() {
List<NetworkInterface> interfaces = new ArrayList<>();
@@ -22,15 +25,17 @@ public List<NetworkInterface> getLatest() {
for (String line : iwconfigOutput) {
line = line.trim();
// 인터페이스 이름 감지 (인터페이스 정보 나오기 시작)
if (!line.startsWith(" ") && (line.contains("IEEE 802.11") || line.contains("unassociated"))) {
if (!line.startsWith(" ") && (line.contains("IEEE 802.11") || line.contains(
"unassociated"))) {
if (currentName != null) {
// 첫 인터페이스가 아니면 모아둔 이전 인터페이스의 정보 저장
interfaces.add(new NetworkInterface(currentName, currentEssid, currentMode));
}
// 새 인터페이스 정보 모으기 & 초기화
currentName = line.split("\\s+")[0];
if (line.contains("ESSID:"))
currentEssid = line.split("ESSID:")[1].split("\\s+")[0].replace("\"", "").trim();
currentEssid = line.split("ESSID:")[1].split("\\s+")[0].replace("\"", "")
.trim();
else
currentEssid = "";
currentMode = null; // 초기화
@@ -47,7 +52,7 @@ public List<NetworkInterface> getLatest() {
}
return interfaces;
}

}

/*
//샘플 iwconfig
@@ -84,4 +89,3 @@ public List<NetworkInterface> getLatest() {
" Tx excessive retries:0 Invalid misc:0 Missed beacon:0"
);
*/
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.whoz_in.log_writer.common;

import com.whoz_in.log_writer.config.NetworkConfig;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;


/*
기본적으로 맥에서 iwconfig를 못쓴다.
그래서 local에선 설정한 네트워크 인터페이스를 출력하는 이 가짜 객체를 만든 것이다.
*/
@Profile("local")
@Component
@RequiredArgsConstructor
public final class StubNetworkInterfaces implements SystemNetworkInterfaces{
private final NetworkConfig networkConfig;

@Override
public List<NetworkInterface> getLatest() {
return networkConfig.getNetworkInterfaces();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.whoz_in.log_writer.common;

import java.util.List;

//실시간으로 시스템에 존재하는 네트워크 인터페이스들을 얻을 수 있는 기능을 제공해야 한다.
public interface SystemNetworkInterfaces {
List<NetworkInterface> getLatest();
default boolean exists(NetworkInterface ni){
return getLatest().contains(ni);
}
}
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;

//실행 후 종료되지 않는 프로세스
//꾸준히 출력을 읽을 수 있어야 한다.
@@ -50,6 +52,19 @@ public String readLine(){
}
}

public List<String> readLines() {
List<String> lines = new ArrayList<>();
try {
String line;
while((line=this.br.readLine()) != null) {
lines.add(line);
}
return lines;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

/**
* @return 프로세스의 에러 출력에서 한 줄을 읽어들인다.
* 읽을 줄이 없을경우 null을 출력한다.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.whoz_in.log_writer.common.process;

import jakarta.annotation.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -20,22 +19,31 @@ public class TransientProcess {

public TransientProcess() {}

// sudo 없이 실행할 커맨드
// command 예시: "ifconfig"
public TransientProcess(String command){
this(command, null);
}
public TransientProcess(String command, @Nullable String sudoPassword) {
try {
this.process = new ProcessBuilder(command.split(" "))
.redirectErrorStream(true)
.start();
this.br = new BufferedReader(new InputStreamReader(process.getInputStream()));
this.ebr = new BufferedReader(new InputStreamReader(process.getErrorStream()));
if (sudoPassword==null) return;
Writer writer = new OutputStreamWriter(this.process.getOutputStream());
} catch (IOException e) {
throw new RuntimeException(command+" - 실행 실패");
}
this.br = new BufferedReader(new InputStreamReader(process.getInputStream()));
this.ebr = new BufferedReader(new InputStreamReader(process.getErrorStream()));
}

// sudo로 실행할 커맨드
// sudoCommand 예시: "sudo iwconfig"
// sudo 없이 실행한 커맨드일 경우 writer에 flush하기도 전에 끝날 수 있으므로 나눠진 것
public TransientProcess(String sudoCommand, String sudoPassword) {
this(sudoCommand);
Writer writer = new OutputStreamWriter(this.process.getOutputStream());
try {
writer.write(sudoPassword + System.lineSeparator());
writer.flush();
} catch (IOException e) {
throw new RuntimeException("TransientProcess 실행 실패 -", e);
throw new RuntimeException(sudoCommand + " - sudo 명령어 입력 중 오류 발생");
}
}

Original file line number Diff line number Diff line change
@@ -47,20 +47,24 @@ public NetworkConfig(@Value("${spring.profiles.active:default}") String profile,
//monitor
Map<String, String> monitorMap = (Map<String, String>) map.get("monitor");
this.monitorInfo = new MonitorInfo(
generateCommand(monitorMap.get("command"), monitorMap.get("interface")));
this.networkInterfaces.stream()
.filter(ni->ni.getName().equals(monitorMap.get("interface")))
.findAny()
.orElseThrow(()->new IllegalStateException(monitorMap.get("interface")+"은 설정된 network_interfaces에 존재하지 않습니다.")),
generateCommand(monitorMap.get("command"), monitorMap.get("interface"))
);
// managed
Map<String, Object> managedMap = (Map<String, Object>) map.get("managed");
// mdns
Map<String, Object> mdnsMap = (Map<String, Object>) managedMap.get("mdns");
String mdnsCommand = (String) mdnsMap.get("command");
this.mdnsList = ((List<String>) mdnsMap.get("interfaces")).stream()
.map(interfaceName -> {
NetworkInterface mdnsNI = networkInterfaces.stream().filter(
ni -> ni.getName().equals(interfaceName)
).findAny().orElseThrow(()->new IllegalStateException(interfaceName+"은 설정된 network_interfaces에 존재하지 않습니다."));
return new ManagedInfo(interfaceName,
mdnsNI.getEssid(),
generateCommand(mdnsCommand, interfaceName));
NetworkInterface mdnsNI = networkInterfaces.stream()
.filter(ni -> ni.getName().equals(interfaceName))
.findAny()
.orElseThrow(()->new IllegalStateException(interfaceName+"은 설정된 network_interfaces에 존재하지 않습니다."));
return new ManagedInfo(mdnsNI, generateCommand(mdnsCommand, interfaceName));
})
.toList();
// arp
@@ -71,9 +75,7 @@ public NetworkConfig(@Value("${spring.profiles.active:default}") String profile,
NetworkInterface arpNI = networkInterfaces.stream().filter(
ni -> ni.getName().equals(interfaceName)
).findAny().orElseThrow(()->new IllegalStateException(interfaceName+"은 설정된 network_interfaces에 존재하지 않습니다."));
return new ManagedInfo(interfaceName,
arpNI.getEssid(),
generateCommand(arpCommand, interfaceName));
return new ManagedInfo(arpNI, generateCommand(arpCommand, interfaceName));
})
.toList();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package com.whoz_in.log_writer.managed;

import com.whoz_in.log_writer.common.NetworkInterface;

//Managed 프로세스를 생성하고 처리하는 과정에서 필요한 정보를 담는다.
public record ManagedInfo(String interfaceName, String ssid, String command) {}
public record ManagedInfo(NetworkInterface ni, String command) {}
Original file line number Diff line number Diff line change
@@ -2,10 +2,14 @@

import com.whoz_in.log_writer.common.process.TransientProcess;
import com.whoz_in.log_writer.managed.ManagedInfo;
import lombok.Getter;

@Getter
public class ArpLogProcess extends TransientProcess {
private final ManagedInfo info;
public ArpLogProcess(ManagedInfo info, String password) {
super(info.command(), password);
this.info = info;
}

}
Original file line number Diff line number Diff line change
@@ -5,16 +5,16 @@
import com.whoz_in.log_writer.managed.ManagedInfo;
import com.whoz_in.log_writer.managed.ManagedLog;
import com.whoz_in.log_writer.managed.ManagedLogDAO;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

//TODO: 에러 로그 어떻게 관리할지 생각. 일단 TransientProcess라서 구현 안함

@Slf4j
@Component
public class ArpLogWriter {
@@ -34,36 +34,27 @@ public ArpLogWriter(ManagedLogDAO dao,
this.sudoPassword = sudoPassword;
}

//주기적으로 arp 명령어를 실행하여 로그를 저장함
@Scheduled(initialDelay = 10000, fixedDelay = 5000)
private void scan() {
List<ManagedLog> logs= arpList.stream()
.flatMap(arpInfo-> {
ArpLogProcess proc = new ArpLogProcess(arpInfo, sudoPassword); //프로세스 실행
List<String> lines = proc.resultList(); //프로세스의 모든 출력 가져오기
Set<ManagedLog> procLogs = lines.stream() //출력 라인들을 ManagedLog 변환하며 ssid도 넣어줌
.filter(parser::validate)
.map(line->{
ManagedLog log = parser.parse(line);
log.setSsid(arpInfo.ssid());
return log;
})
.collect(Collectors.toSet()); //Set으로 중복 제거

/*
Arp-scan은 단발성인데
Process의 isAlive()는 실행 중일 때도 false일 수 있고, 종료 중일 때도 true일 수 있으므로 오류의 판단이 힘들다.
따라서 Arp-scan의 경우 무조건 1개 이상의 결과가 나오므로 0개라면 실행 실패라고 판단한다.
*/
if (procLogs.isEmpty()) {
//SystemValidator가 시스템의 네트워크 인터페이스가 올바른지 검증하기 때문에 여기서는 warn으로 로깅
log.warn("[managed - arp({})] 실행 실패 : ERROR", arpInfo.ssid());
return Stream.empty();
}
log.info("[managed - arp({})] log to save : {}", arpInfo.ssid(), procLogs.size());
return procLogs.stream();
})
List<ManagedLog> logs = arpList.stream() //실행할 arp들을 스트림화
.map(info -> new ArpLogProcess(info, sudoPassword)) //arp 실행
.map(this::getLogsFromProcess) //arp 출력을 로그 Set으로 변환
.flatMap(Collection::stream) //Set끼리 합침
.toList();

dao.upsertAll(logs);
}

//프로세스의 출력들을 로그로 변환한다.
private Set<ManagedLog> getLogsFromProcess(ArpLogProcess process){
Set<ManagedLog> logs = process.resultList().stream()
.filter(parser::validate)
.map(parser::parse)
.collect(Collectors.toSet());//Set으로 중복 제거

String ssid = process.getInfo().ni().getEssid();
logs.forEach(log->log.setSsid(ssid));
log.info("[managed - arp({})] log to save : {}", ssid, logs.size());
return logs;
}
}
Original file line number Diff line number Diff line change
@@ -2,10 +2,13 @@

import com.whoz_in.log_writer.common.process.ContinuousProcess;
import com.whoz_in.log_writer.managed.ManagedInfo;
import lombok.Getter;

@Getter
public class MdnsLogProcess extends ContinuousProcess {

private final ManagedInfo info;
public MdnsLogProcess(ManagedInfo info, String sudoPassword) {
super(info.command(), sudoPassword);
this.info = info;
}
}
Loading

0 comments on commit 6d7b28e

Please sign in to comment.