diff --git a/app/pom.xml b/app/pom.xml index 5470cbd..8c2a487 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -78,16 +78,16 @@ jsch 0.1.54 - - - - - - - - - - + + net.java.dev.jna + jna-platform + 5.3.1 + + + net.java.dev.jna + jna + 5.3.1 + @@ -128,6 +128,32 @@ 1.21 + + com.github.oshi + oshi-dist + 3.13.3 + pom + + + + com.github.oshi + oshi-core + 3.13.3 + + + + com.github.oshi + oshi-json + 3.13.3 + + + + com.github.oshi + oshi-parent + 3.13.3 + pom + + diff --git a/app/src/main/java/cloudshell/app/AppController.java b/app/src/main/java/cloudshell/app/AppController.java index 44fcdd2..571bee9 100644 --- a/app/src/main/java/cloudshell/app/AppController.java +++ b/app/src/main/java/cloudshell/app/AppController.java @@ -36,6 +36,9 @@ import cloudshell.app.files.PosixPermission; import cloudshell.app.files.copy.FileCopyProgressResponse; import cloudshell.app.files.copy.FileCopyRequest; import cloudshell.app.files.search.SearchResult; +import cloudshell.app.health.ProcessInfo; +import cloudshell.app.health.SystemHealthMonitor; +import cloudshell.app.health.SystemStats; import cloudshell.app.terminal.PtySession; /** @@ -56,6 +59,9 @@ public class AppController { @Autowired private BCryptPasswordEncoder passwordEncoder; + @Autowired + private SystemHealthMonitor healthMon; + @PostMapping("/app/terminal/{appId}/resize") public void resizePty(@PathVariable String appId, @RequestBody Map body) { @@ -314,4 +320,22 @@ public class AppController { Files.createFile(path); } + @GetMapping("/app/sys/stats") + public SystemStats getStats() { + return this.healthMon.getStats(); + } + + @GetMapping("/app/sys/procs") + public List getProcessList() { + return this.healthMon.getProcessList(); + } + + @PostMapping("/app/sys/procs") + public Map killProcesses( + @RequestBody List pidList) { + Map map = new HashMap<>(); + map.put("success", this.healthMon.killProcess(pidList)); + return map; + } + } diff --git a/app/src/main/java/cloudshell/app/files/FileTypeDetector.java b/app/src/main/java/cloudshell/app/files/FileTypeDetector.java index 3b88099..4563c31 100644 --- a/app/src/main/java/cloudshell/app/files/FileTypeDetector.java +++ b/app/src/main/java/cloudshell/app/files/FileTypeDetector.java @@ -29,7 +29,6 @@ public class FileTypeDetector { return tika.detect(file); } catch (IOException e) { // TODO Auto-generated catch block - e.printStackTrace(); return "application/octet-stream"; } } diff --git a/app/src/main/java/cloudshell/app/health/ProcessInfo.java b/app/src/main/java/cloudshell/app/health/ProcessInfo.java new file mode 100644 index 0000000..e70a5bb --- /dev/null +++ b/app/src/main/java/cloudshell/app/health/ProcessInfo.java @@ -0,0 +1,155 @@ +/** + * + */ +package cloudshell.app.health; + +/** + * @author subhro + * + */ +public class ProcessInfo { + private String name, command, user, state; + private int pid, priority; + private double cpuUsage, memoryUsage, vmUsage; + private long startTime; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the command + */ + public String getCommand() { + return command; + } + + /** + * @param command the command to set + */ + public void setCommand(String command) { + this.command = command; + } + + /** + * @return the user + */ + public String getUser() { + return user; + } + + /** + * @param user the user to set + */ + public void setUser(String user) { + this.user = user; + } + + /** + * @return the state + */ + public String getState() { + return state; + } + + /** + * @param state the state to set + */ + public void setState(String state) { + this.state = state; + } + + /** + * @return the pid + */ + public int getPid() { + return pid; + } + + /** + * @param pid the pid to set + */ + public void setPid(int pid) { + this.pid = pid; + } + + /** + * @return the cpuUsage + */ + public double getCpuUsage() { + return cpuUsage; + } + + /** + * @param cpuUsage the cpuUsage to set + */ + public void setCpuUsage(double cpuUsage) { + this.cpuUsage = cpuUsage; + } + + /** + * @return the memoryUsage + */ + public double getMemoryUsage() { + return memoryUsage; + } + + /** + * @param memoryUsage the memoryUsage to set + */ + public void setMemoryUsage(double memoryUsage) { + this.memoryUsage = memoryUsage; + } + + /** + * @return the vmUsage + */ + public double getVmUsage() { + return vmUsage; + } + + /** + * @param vmUsage the vmUsage to set + */ + public void setVmUsage(double vmUsage) { + this.vmUsage = vmUsage; + } + + /** + * @return the startTime + */ + public long getStartTime() { + return startTime; + } + + /** + * @param startTime the startTime to set + */ + public void setStartTime(long startTime) { + this.startTime = startTime; + } + + /** + * @return the priority + */ + public int getPriority() { + return priority; + } + + /** + * @param priority the priority to set + */ + public void setPriority(int priority) { + this.priority = priority; + } +} diff --git a/app/src/main/java/cloudshell/app/health/SystemHealthMonitor.java b/app/src/main/java/cloudshell/app/health/SystemHealthMonitor.java new file mode 100644 index 0000000..f12e86f --- /dev/null +++ b/app/src/main/java/cloudshell/app/health/SystemHealthMonitor.java @@ -0,0 +1,130 @@ +/** + * + */ +package cloudshell.app.health; + +import java.io.File; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Component; + +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.software.os.OSProcess; +import oshi.software.os.OperatingSystem; + +/** + * @author subhro + * + */ +@Component +public class SystemHealthMonitor { + /** + * + */ + private CentralProcessor processor; + private GlobalMemory memory; + private OperatingSystem os; + private boolean isWindows; + + private SystemInfo si; + + public SystemHealthMonitor() { + si = new SystemInfo(); + HardwareAbstractionLayer hal = si.getHardware(); + os = si.getOperatingSystem(); + processor = hal.getProcessor(); + memory = hal.getMemory(); + processor.getSystemCpuLoadBetweenTicks(); + isWindows = System.getProperty("os.name").toLowerCase() + .contains("windows"); + } + + public synchronized SystemStats getStats() { + SystemStats stats = new SystemStats(); + double cpuUsed = processor.getSystemCpuLoadBetweenTicks() * 100; + + stats.setCpuUsed(cpuUsed); + stats.setCpuFree(100 - cpuUsed); + + long avail = memory.getAvailable(); + long total = memory.getTotal(); + if (total > 0) { + double memoryUsed = ((total - avail) * 100) / total; + stats.setMemoryUsed(memoryUsed); + stats.setMemoryFree(100 - memoryUsed); + } + + File f = new File("/"); + long totalDiskSpace = f.getTotalSpace(); + long freeDiskSpace = f.getFreeSpace(); + if (totalDiskSpace > 0) { + double diskUsed = ((totalDiskSpace - freeDiskSpace) * 100) + / totalDiskSpace; + stats.setDiskUsed(diskUsed); + stats.setDiskFree(100 - diskUsed); + } + + long totalSwap = memory.getSwapTotal(); + long usedSwap = memory.getSwapUsed(); + if (totalSwap > 0) { + double swapUsed = usedSwap * 100 / totalSwap; + stats.setSwapUsed(swapUsed); + stats.setSwapFree(100 - swapUsed); + } + + return stats; + } + + public synchronized List getProcessList() { + OSProcess[] procs = os.getProcesses(0, null, false); + List list = new ArrayList<>(); + if (procs != null && procs.length > 0) { + for (OSProcess proc : procs) { + ProcessInfo info = new ProcessInfo(); + info.setPid(proc.getProcessID()); + info.setName(proc.getName()); + info.setCommand(proc.getCommandLine()); + info.setCpuUsage(proc.calculateCpuPercent()); + info.setMemoryUsage(proc.getResidentSetSize()); + info.setVmUsage(proc.getVirtualSize()); + info.setState(proc.getState().toString()); + info.setStartTime(proc.getStartTime()); + info.setUser(proc.getUser()); + info.setPriority(proc.getPriority()); + list.add(info); + } + } + return list; + } + + private boolean killProcess(int pid) { + ProcessBuilder pb = new ProcessBuilder( + isWindows ? Arrays.asList("taskkill", "/pid", pid + "", "/f") + : Arrays.asList("kill", "-9", pid + "")); + try { + Process proc = pb.start(); + int ret = proc.waitFor(); + return ret == 0; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + public synchronized boolean killProcess(List pidList) { + boolean success = true; + for (Integer pid : pidList) { + if (success) { + success = killProcess(pid); + } + } + return success; + } +} diff --git a/app/src/main/java/cloudshell/app/health/SystemStats.java b/app/src/main/java/cloudshell/app/health/SystemStats.java new file mode 100644 index 0000000..2811c1d --- /dev/null +++ b/app/src/main/java/cloudshell/app/health/SystemStats.java @@ -0,0 +1,125 @@ +/** + * + */ +package cloudshell.app.health; + +/** + * @author subhro + * + */ +public class SystemStats { + private double cpuUsed = -1, cpuFree = -1, memoryUsed = -1, memoryFree = -1, + swapUsed = -1, swapFree = -1, diskUsed = -1, diskFree = -1; + + /** + * @return the cpuUsed + */ + public double getCpuUsed() { + return cpuUsed; + } + + /** + * @param cpuUsed the cpuUsed to set + */ + public void setCpuUsed(double cpuUsed) { + this.cpuUsed = cpuUsed; + } + + /** + * @return the cpuFree + */ + public double getCpuFree() { + return cpuFree; + } + + /** + * @param cpuFree the cpuFree to set + */ + public void setCpuFree(double cpuFree) { + this.cpuFree = cpuFree; + } + + /** + * @return the memoryUsed + */ + public double getMemoryUsed() { + return memoryUsed; + } + + /** + * @param memoryUsed the memoryUsed to set + */ + public void setMemoryUsed(double memoryUsed) { + this.memoryUsed = memoryUsed; + } + + /** + * @return the memoryFree + */ + public double getMemoryFree() { + return memoryFree; + } + + /** + * @param memoryFree the memoryFree to set + */ + public void setMemoryFree(double memoryFree) { + this.memoryFree = memoryFree; + } + + /** + * @return the swapUsed + */ + public double getSwapUsed() { + return swapUsed; + } + + /** + * @param swapUsed the swapUsed to set + */ + public void setSwapUsed(double swapUsed) { + this.swapUsed = swapUsed; + } + + /** + * @return the swapFree + */ + public double getSwapFree() { + return swapFree; + } + + /** + * @param swapFree the swapFree to set + */ + public void setSwapFree(double swapFree) { + this.swapFree = swapFree; + } + + /** + * @return the diskUsed + */ + public double getDiskUsed() { + return diskUsed; + } + + /** + * @param diskUsed the diskUsed to set + */ + public void setDiskUsed(double diskUsed) { + this.diskUsed = diskUsed; + } + + /** + * @return the diskFree + */ + public double getDiskFree() { + return diskFree; + } + + /** + * @param diskFree the diskFree to set + */ + public void setDiskFree(double diskFree) { + this.diskFree = diskFree; + } +} diff --git a/app/src/main/java/cloudshell/app/terminal/PtyProcessPipe.java b/app/src/main/java/cloudshell/app/terminal/PtyProcessPipe.java new file mode 100644 index 0000000..e2f3074 --- /dev/null +++ b/app/src/main/java/cloudshell/app/terminal/PtyProcessPipe.java @@ -0,0 +1,19 @@ +/** + * + */ +package cloudshell.app.terminal; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @author subhro + * + */ +public interface PtyProcessPipe { + public int read(byte[] b) throws Exception; + + public void write(byte[] b, int off, int len) throws Exception; + + public void resizePty(int col, int row, int wp, int hp); +} diff --git a/app/src/main/java/cloudshell/app/terminal/SshPtyProcessPipe.java b/app/src/main/java/cloudshell/app/terminal/SshPtyProcessPipe.java new file mode 100644 index 0000000..b51fe93 --- /dev/null +++ b/app/src/main/java/cloudshell/app/terminal/SshPtyProcessPipe.java @@ -0,0 +1,201 @@ +/** + * + */ +package cloudshell.app.terminal; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.io.PipedReader; +import java.io.PipedWriter; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.jcraft.jsch.ChannelShell; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.Session; +import com.jcraft.jsch.UserInfo; + +/** + * @author subhro + * + */ +public class SshPtyProcessPipe implements PtyProcessPipe, UserInfo { + + private PipedInputStream _in, in; + private PipedOutputStream _out, out; + private String hostName, keyFile, user; + private int port; + private JSch jsch; + private Session session; + private ChannelShell shell; + + /** + * @throws IOException + * @throws UnsupportedEncodingException + * + */ + public SshPtyProcessPipe() { + hostName = "192.168.56.106"; + port = 22; + user = "subhro"; + } + + public void start() { + try { + _out = new PipedOutputStream(); + in = new PipedInputStream(_out, 1); + _in = new PipedInputStream(1); + out = new PipedOutputStream(_in); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + new Thread(() -> { + try { + _start(); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + }).start(); + } + + public void _start() throws Exception { + + jsch = new JSch(); + JSch.setConfig("MaxAuthTries", "5"); + + if (keyFile != null && keyFile.length() > 0) { + jsch.addIdentity(keyFile); + } + + session = jsch.getSession(user, hostName, port); + + session.setUserInfo(this); + // session.setConfig("StrictHostKeyChecking", "no"); + session.setConfig("PreferredAuthentications", + "publickey,keyboard-interactive,password"); + + System.out.println("Before connect"); + + session.connect(); + + System.out.println("Client version: " + session.getClientVersion()); + System.out.println("Server host: " + session.getHost()); + System.out.println("Server version: " + session.getServerVersion()); + System.out.println( + "Hostkey: " + session.getHostKey().getFingerPrint(jsch)); + + shell = (ChannelShell) session.openChannel("shell"); + shell.setEnv("TERM", "xterm"); + + shell.setInputStream(in); + shell.setOutputStream(out); + + shell.connect(); + } + + public void resizePty(int col, int row, int wp, int hp) { + shell.setPtySize(col, row, wp, hp); + } + + private String readLine() throws IOException { + System.out.println("Attempt readline"); + StringBuilder sb = new StringBuilder(); + while (true) { + int ch = in.read(); + System.out.println("char: " + ch); + if (ch == -1 || ch == '\n'|| ch == '\r') + break; + sb.append((char) ch); + } + return sb.toString(); + } + + @Override + public String getPassphrase() { + try { + return readLine(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + @Override + public String getPassword() { + try { + String pass = readLine(); + System.out.println("Paww: " + pass); + return pass; + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + @Override + public boolean promptPassword(String message) { + System.out.println("prompt password: " + message); + try { + out.write(message.getBytes("utf-8")); + } catch (UnsupportedEncodingException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return true; + } + + @Override + public boolean promptPassphrase(String message) { + try { + out.write(message.getBytes("utf-8")); + } catch (UnsupportedEncodingException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return true; + } + + @Override + public boolean promptYesNo(String message) { + return true; + } + + @Override + public void showMessage(String message) { + System.out.println("prompt messae: " + message); + try { + out.write(message.getBytes("utf-8")); + } catch (UnsupportedEncodingException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + @Override + public int read(byte[] b) throws Exception { + return _in.read(b); + } + + @Override + public void write(byte[] b, int off, int len) throws Exception { + this._out.write(b, off, len); + } + +} diff --git a/ui/package.json b/ui/package.json index 2f50b24..dfc6a4e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,13 +20,16 @@ "@angular/platform-browser-dynamic": "~7.2.0", "@angular/router": "~7.2.0", "@ng-bootstrap/ng-bootstrap": "^4.2.0", + "@swimlane/ngx-charts": "^12.0.1", "bootstrap": "^4.3.1", + "chart.js": "^2.8.0", "core-js": "^2.5.4", "font-awesome": "^4.7.0", "ng2-ace-editor": "^0.3.9", + "ng2-charts": "^2.2.3", "rxjs": "~6.3.3", "tslib": "^1.9.0", - "xterm": "^3.13.2", + "xterm": "^3.14.5", "zone.js": "~0.8.26" }, "devDependencies": { diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index eacc5dd..03049c8 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -1,9 +1,12 @@ import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import {NgxChartsModule} from '@swimlane/ngx-charts'; +import { ChartsModule } from 'ng2-charts'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { AppRoutingModule } from './app-routing.module'; @@ -54,7 +57,10 @@ import { NewItemComponent } from './home/files/browser/new-item/new-item.compone FormsModule, ReactiveFormsModule, NgbModule, - HttpClientModule + HttpClientModule, + BrowserAnimationsModule, + NgxChartsModule, + ChartsModule ], providers: [httpInterceptorProviders], bootstrap: [AppComponent] diff --git a/ui/src/app/data.service.ts b/ui/src/app/data.service.ts index d97a867..b9c5c65 100644 --- a/ui/src/app/data.service.ts +++ b/ui/src/app/data.service.ts @@ -78,7 +78,7 @@ export class DataService { currentViewChanger = new Subject(); - viewTextRequests=new Subject(); + viewTextRequests = new Subject(); terminalSession: TerminalSession; @@ -404,4 +404,16 @@ export class DataService { return this.http.get(environment.BASE_URL + "token/temp"); } + getSystemStats(): Observable { + return this.http.get(environment.BASE_URL + "app/sys/stats"); + } + + getProcessList(): Observable { + return this.http.get(environment.BASE_URL + "app/sys/procs"); + } + + killProcesses(pids: any[]): Observable { + return this.http.post(environment.BASE_URL + "app/sys/procs", pids); + } + } diff --git a/ui/src/app/home/home.component.html b/ui/src/app/home/home.component.html index 6d002fc..b83fe02 100644 --- a/ui/src/app/home/home.component.html +++ b/ui/src/app/home/home.component.html @@ -20,22 +20,31 @@ Settings --> - Files - Editor - Search - Settings + + Monitoring + @@ -130,14 +149,16 @@ -
Terminal
- + - +
- monitoring works! -

+
+
+
+
+ CPU USAGE {{cpuUsageSet[0][0].toFixed(1)}}% +
+
+ + +
+
+
+
+ MEMORY USAGE {{memoryUsageSet[0][0].toFixed(1)}}% +
+
+ + +
+
+
+
+ DISKSPACE USAGE {{diskUsageSet[0][0].toFixed(1)}}% +
+
+ + +
+
+
+
+ SWAP USAGE {{swapUsageSet[0][0].toFixed(1)}}% +
+
+ + +
+
+
+
+
+ Processes +
+
+ +
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NAMEPIDPRIORITYCPU USAGEMEMORYVIRTUAL MEMORYUSERSTART TIMESTATECOMMAND
{{proc.name}}{{proc.pid}}{{proc.priority}}{{proc.cpuUsage}}{{proc.memoryUsage}}{{proc.vmUsage}}{{proc.user}}{{proc.startTime}}{{proc.state}} + {{proc.command}} +
+
+
\ No newline at end of file diff --git a/ui/src/app/home/monitoring/monitoring.component.ts b/ui/src/app/home/monitoring/monitoring.component.ts index 0ed0008..0768409 100644 --- a/ui/src/app/home/monitoring/monitoring.component.ts +++ b/ui/src/app/home/monitoring/monitoring.component.ts @@ -1,15 +1,119 @@ import { Component, OnInit } from '@angular/core'; +import { ChartType, ChartOptions } from 'chart.js'; +import { MultiDataSet, Label, Colors, Color } from 'ng2-charts'; +import { DataService } from 'src/app/data.service'; @Component({ selector: 'app-monitoring', templateUrl: './monitoring.component.html', - styleUrls: ['./monitoring.component.css'] + styleUrls: ['./monitoring.component.css'], + host: { + '(window:resize)': 'onResize($event)' + } }) export class MonitoringComponent implements OnInit { - constructor() { } + public doughnutChartLabels: Label[] = ['Used', 'Free']; + public colors: Color[] = [ + { + backgroundColor: ['Orange', 'SteelBlue'], + borderColor: ['Orange', 'SteelBlue'] + } + ]; + public cpuUsageSet: MultiDataSet = [ + [0, 100] + ]; + public memoryUsageSet: MultiDataSet = [ + [0, 100] + ]; + public diskUsageSet: MultiDataSet = [ + [0, 100] + ]; + public swapUsageSet: MultiDataSet = [ + [0, 100] + ]; + public doughnutChartType: ChartType = 'doughnut'; + public options: ChartOptions = { + maintainAspectRatio: false, + legend: { + display: false + } + }; + + timer: any; + + processList: any[] = []; + + constructor(private service: DataService) { } ngOnInit() { + this.getStats(); + this.getProcStats(); + this.timer = setInterval(() => { + this.getStats(); + }, 5000); + } + + public getProcStats() { + this.service.getProcessList().subscribe((resp: any[]) => { + this.processList = resp; + for (let proc of this.processList) { + proc.selected = false; + } + }); + } + + public killSelectedProcesses() { + let pids: number[] = []; + for (let proc of this.processList) { + if (proc.selected) { + pids.push(proc.pid); + } + } + if (pids.length < 1) { + alert("Nothing selected to kill"); + return; + } + + this.service.killProcesses(pids).subscribe((resp: any) => { + if (!resp.success) { + alert("Failed to kill"); + } else { + this.getProcStats(); + } + }) + } + + setSelected(item: any, selection: boolean) { + item.selected = selection; + } + + public getStats() { + this.service.getSystemStats().subscribe((resp: any) => { + this.cpuUsageSet = [ + [resp.cpuUsed, resp.cpuFree] + ]; + this.memoryUsageSet = [ + [resp.memoryUsed, resp.memoryFree] + ]; + this.diskUsageSet = [ + [resp.diskUsed, resp.diskFree] + ]; + this.swapUsageSet = [ + [resp.swapUsed, resp.swapFree] + ]; + }); + } + + public chartClicked({ event, active }: { event: MouseEvent, active: {}[] }): void { + console.log(event, active); + } + + public chartHovered({ event, active }: { event: MouseEvent, active: {}[] }): void { + console.log(event, active); + } + + onResize(event: any) { } }