Skip to content

Commit

Permalink
Merge pull request #1 from JacquesBeets/feature/adding-front-end
Browse files Browse the repository at this point in the history
UI creation for service management
  • Loading branch information
JacquesBeets authored Oct 5, 2024
2 parents 5a11410 + 17af43b commit 14d09bf
Show file tree
Hide file tree
Showing 35 changed files with 11,690 additions and 26 deletions.
64 changes: 64 additions & 0 deletions .github/workflows/build_and_release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Build and Release

on:
push:
branches:
- master

jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2

- name: Set version
run: echo "VERSION=0.0.${{ github.run_number }}" >> $env:GITHUB_ENV

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.21

- name: Build Go binary
run: |
cd backend
go build -o ../WinSenseConnect.exe
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '18'

- name: Build Nuxt3 frontend
run: |
cd frontend
npm ci
npm run generate
- name: Package release
run: |
mkdir release
copy WinSenseConnect.exe release\
copy config.template.json release\config.json
xcopy /E /I frontend\.output\public release\frontend
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ env.VERSION }}
release_name: Release v${{ env.VERSION }}
draft: false
prerelease: false

- name: Upload Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./release
asset_name: WinSenseConnect-v${{ env.VERSION }}.zip
asset_content_type: application/zip
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# /frontend
/scripts/*.ps1
*.exe
*.log
Expand Down
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"files.associations": {
"*.css": "tailwindcss"
},
"editor.quickSuggestions": {
"strings": true
}
}
6 changes: 6 additions & 0 deletions config.go → backend/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@ type Config struct {
LogLevel string `json:"log_level"`
ScriptTimeout int `json:"script_timeout"`
Commands map[string]ScriptConfig `json:"commands"`
SensorConfig SensorConfig `json:"sensor_config"`
}

type ScriptConfig struct {
ScriptPath string `json:"script_path"`
RunAsUser bool `json:"run_as_user"`
}
type SensorConfig struct {
Enabled bool `json:"enabled"`
Interval int `json:"interval"`
SensorTopic string `json:"sensor_topic"`
}

func (p *program) loadConfig() error {
p.logger.Debug("Starting to load config...")
Expand Down
72 changes: 72 additions & 0 deletions backend/http_server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package main

import (
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"

"github.com/rs/cors"
)

func (p *program) startHTTPServer() {
p.logger.Debug("Starting HTTP server")
r := p.router

exePath, err := os.Executable()
if err != nil {
p.logger.Error(fmt.Sprintf("Failed to get executable path: %v", err))
}
staticPath := filepath.Join(filepath.Dir(exePath), "frontend/.output/public")

// API endpoints
r.HandleFunc("/api/config", p.handleGetConfig).Methods("GET")
r.HandleFunc("/api/config", p.handleUpdateConfig).Methods("POST")
r.HandleFunc("/api/scripts", p.handleListScripts).Methods("GET")
r.HandleFunc("/api/scripts", p.handleAddScript).Methods("POST")
// Serve static files (our UI) - this will be added at build time from our Nuxt frontend
r.PathPrefix("/").Handler(http.FileServer(http.Dir(staticPath)))

// CORS Middleware
corsHandler := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
ExposedHeaders: []string{"Link"},
AllowCredentials: true,
MaxAge: 300, // Maximum value not ignored by any of major browsers
}).Handler(r)

p.logger.Debug("Listening on port 8077")
http.ListenAndServe("0.0.0.0:8077", corsHandler)
}

func (p *program) handleGetConfig(w http.ResponseWriter, r *http.Request) {
p.logger.Debug("Handling /api/config GET request")
err := json.NewEncoder(w).Encode(p.config)
if err != nil {
p.logger.Error(fmt.Sprintf("Failed to encode config: %v", err))
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}

func (p *program) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
p.logger.Debug("Handling /api/config POST request")
var newConfig Config
json.NewDecoder(r.Body).Decode(&newConfig)
// Write new config to file
// This will later be saved in a db
}

func (p *program) handleListScripts(w http.ResponseWriter, r *http.Request) {
p.logger.Debug("Handling /api/scripts GET request")
files, _ := filepath.Glob(filepath.Join(p.scriptDir, "*.ps1"))
json.NewEncoder(w).Encode(files)
}

func (p *program) handleAddScript(w http.ResponseWriter, r *http.Request) {
p.logger.Debug("Handling /api/scripts POST request")
// Logic to add new powershell scripts
}
File renamed without changes.
2 changes: 1 addition & 1 deletion main.go → backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

func main() {
svcConfig := &service.Config{
Name: "MQTTPowershellService",
Name: "WinSenseConnect",
DisplayName: "MQTT Powershell Automation Service",
Description: "Listens for MQTT messages and runs PowerShell scripts",
}
Expand Down
31 changes: 31 additions & 0 deletions mqtt.go → backend/mqtt.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/json"
"fmt"
"path/filepath"
"runtime/debug"
Expand Down Expand Up @@ -81,6 +82,32 @@ func (p *program) publishResponse(client mqtt.Client, message string) {
}
}

func (p *program) publishSensorData() {
ticker := time.NewTicker(time.Duration(p.config.SensorConfig.Interval) * time.Second)
defer ticker.Stop()

for range ticker.C {
sensorData, err := collectSensorData()
if err != nil {
p.logger.Error(fmt.Sprintf("Failed to collect sensor data: %v", err))
continue
}

jsonData, err := json.Marshal(sensorData)
if err != nil {
p.logger.Error(fmt.Sprintf("Failed to marshal sensor data: %v", err))
continue
}

token := p.mqttClient.Publish(p.config.SensorConfig.SensorTopic, 0, false, jsonData)
if token.Wait() && token.Error() != nil {
p.logger.Error(fmt.Sprintf("Failed to publish sensor data: %v", token.Error()))
} else {
p.logger.Debug("Successfully published sensor data")
}
}
}

func (p *program) setupMQTTClient() {
opts := mqtt.NewClientOptions().AddBroker(p.config.BrokerAddress)
opts.SetClientID(p.config.ClientID)
Expand All @@ -95,4 +122,8 @@ func (p *program) setupMQTTClient() {
opts.SetConnectRetryInterval(time.Second * 10)

p.mqttClient = mqtt.NewClient(opts)

if p.config.SensorConfig.Enabled {
go p.publishSensorData()
}
}
133 changes: 133 additions & 0 deletions backend/sensors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package main

import (
"context"
"encoding/json"
"fmt"
"os/exec"
"time"

"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/host"
"github.com/shirou/gopsutil/v4/mem"
"github.com/shirou/gopsutil/v4/net"
"github.com/shirou/gopsutil/v4/sensors"
)

type SensorData struct {
Timestamp time.Time `json:"timestamp"`
CPUUsage float64 `json:"cpu_usage"`
CPUInfo cpu.InfoStat `json:"cpu_info"`
MemoryUsage float64 `json:"memory_usage"`
DiskUsage float64 `json:"disk_usage"`
Uptime uint64 `json:"uptime"`
DiskPartitions []disk.PartitionStat `json:"disk_partitions"`
NetConnections []net.ConnectionStat `json:"net_connections"`
Users []host.UserStat `json:"users"`
Sensors []sensors.TemperatureStat `json:"sensors"`
CPUTemperature float64 `json:"cpu_temperature"`
}

type TemperatureData struct {
Temperature float64 `json:"Temperature"`
}

func collectSensorData() (SensorData, error) {
data := SensorData{
Timestamp: time.Now(),
}

// CPU Usage
cpuPercent, err := cpu.Percent(time.Second, false)
if err == nil && len(cpuPercent) > 0 {
data.CPUUsage = cpuPercent[0]
}

// CPU Info
cpuInfo, err := cpu.Info()
if err == nil && len(cpuInfo) > 0 {
data.CPUInfo = cpuInfo[0]
}

// Memory Usage
memInfo, err := mem.VirtualMemory()
if err == nil {
data.MemoryUsage = memInfo.UsedPercent
}

// Disk Usage
diskInfo, err := disk.Usage("C:")
if err == nil {
data.DiskUsage = diskInfo.UsedPercent
}

// Disk Partitions
partitions, err := disk.Partitions(false)
if err == nil {
data.DiskPartitions = partitions
}

// Net
netConnections, err := net.Connections("all")
if err == nil {
data.NetConnections = netConnections
}

// Uptime
hostInfo, err := host.Info()
if err == nil {
data.Uptime = hostInfo.Uptime
}

// Users
users, err := host.Users()
if err == nil {
data.Users = users
}

// Sensors
sensors, err := sensors.TemperaturesWithContext(context.Background())
if err == nil {
data.Sensors = sensors
}

// CPU Temperature
temp, err := getCPUTemperature()
if err == nil {
data.CPUTemperature = temp
} else {
fmt.Printf("Failed to get CPU temperature: %v\n", err)
}

return data, nil
}

func getCPUTemperature() (float64, error) {
cmd := exec.Command("powershell", "-Command", `
$temp = Get-WmiObject MSAcpi_ThermalZoneTemperature -Namespace "root/wmi" | Select-Object -First 1
if ($temp) {
$celsius = ($temp.CurrentTemperature / 10) - 273.15
ConvertTo-Json @{ Temperature = [math]::Round($celsius, 2) }
} else {
ConvertTo-Json @{ Temperature = $null }
}
`)

output, err := cmd.Output()
if err != nil {
return 0, fmt.Errorf("failed to execute PowerShell command: %v", err)
}

var tempData TemperatureData
err = json.Unmarshal(output, &tempData)
if err != nil {
return 0, fmt.Errorf("failed to parse temperature data: %v", err)
}

if tempData.Temperature == 0 {
return 0, fmt.Errorf("temperature data not available")
}

return tempData.Temperature, nil
}
Loading

0 comments on commit 14d09bf

Please sign in to comment.