Compare commits
10 Commits
14012dabd7
...
trunk
Author | SHA1 | Date | |
---|---|---|---|
8bd09a4983 | |||
6c38363694 | |||
9b6a90fbce | |||
b9c1b9e320 | |||
1d16d30cb6 | |||
5d534221a4 | |||
6d76a88c9c | |||
e7e8953ec3 | |||
fd41e0c03e | |||
30593d1372 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,4 +24,3 @@ go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
|
98
README.md
98
README.md
@ -1,3 +1,101 @@
|
||||
# sfcs
|
||||
Sudoku-Funpark Cluster Software: same as [Sudoku-funpark](https://gitea.ligthert.net/golang/sudoku-funpark/), but networked.
|
||||
|
||||
## Notes
|
||||
|
||||
### Network Protocol
|
||||
While network and transport is done via WebSocket, traffic is sent using Python.
|
||||
( 🤔 not sure if this is the smartest idea, as Golang isn't really that strong with JSON. 🫠 )
|
||||
|
||||
Agent => Manager:
|
||||
- register: register an agent with the manager
|
||||
- update: agents sends CPU and Mem metrics (does this every second)
|
||||
- solution: solution of a task given by the manager
|
||||
- deregister: deregister an agent from the manager
|
||||
|
||||
Manager => Agent:
|
||||
- task: A task for the agent to chew on
|
||||
|
||||
### Commands
|
||||
|
||||
#### Register
|
||||
1. Hostname
|
||||
2. CPU cores
|
||||
3. Memory
|
||||
|
||||
#### Update
|
||||
1. cpu usage
|
||||
2. memory usage
|
||||
3. assigned task [none,taskID]
|
||||
|
||||
#### Solution
|
||||
1. taskId
|
||||
2. solution ([][9]string)
|
||||
|
||||
#### deregister
|
||||
1. Hostname
|
||||
|
||||
#### task
|
||||
1. taskId
|
||||
2. split
|
||||
3. part
|
||||
4. puzzle ([][9]string)
|
||||
|
||||
|
||||
### Protocol
|
||||
For ease of programming I would like start of easy and use `;`-separated string to push values between manager and its agents. In this setup the format is predefined, and parsing will be based on the first field after the string is split on `;`.
|
||||
|
||||
* register;string;int;memory int
|
||||
* update;float;float;int
|
||||
* solution;string;string;string;string;string;string;string;string;string;string
|
||||
* deregister;string
|
||||
* task;int;int;int;string;string;string;string;string;string;string;string;string
|
||||
|
||||
I still have the impression that Go and JSON is hard to do as the rigidness of Go does not go well with the free spirited nature of JSON. I might revise this in the future.
|
||||
|
||||
### Imports
|
||||
I intend use `golang/sudoku-funpark` for the code to determine workload and calculate the puzzles that need to be solved. And while this is all modular, it unfortunately isn't flexible enough to actually use it in the tool (or I am not trying hard enough).
|
||||
|
||||
### Other notes
|
||||
#### Load ordering
|
||||
Outputter
|
||||
Controller
|
||||
Export
|
||||
Flags
|
||||
Solver
|
||||
|
||||
```Go
|
||||
controller := controller.Controller{}
|
||||
outp := outputter.Outputter{}
|
||||
export := export.Export{Controller: &controller}
|
||||
flags := flags.Flags{Controller: &controller}
|
||||
solver := solver.Solver{Controller: &controller, Outp: &outp}
|
||||
```
|
||||
### Manager & Agent channel setup
|
||||
Both the manager and the agent will have similar setups:
|
||||
* `*websocket.Conn` is stored in _types.go_.
|
||||
* Any go routine can write to the socket.
|
||||
* All reads go to a `readProcessor()` which in turn parses the message, and in turn does the appropriate logic (read, calling go routine/functions)
|
||||
|
||||
This does however presents me with a problem. Since all communication is reactive, state-less, and all over the same connection; so tracking things is a bit hard. And this is something for maybe the next phase. So for now, reactive content only. This said, I need figure out how on earth I can prevent an agent from taking two jobs at the same time. I guess I will need to build in some agent state tracking for this.
|
||||
|
||||
#### Server setup
|
||||
_(This piece assumes that the network is super fast, and turbo stable.)_
|
||||
|
||||
The current setup of the server is only the thing that its connected to. There is a writer that spams something, a reader that prints everything it receives to the console. And that is it. Pretty much split-brain and it currently lacks the ability to manage workloads on agents.
|
||||
|
||||
For this exercise it needs to be able to handle multiple agents, but start at one. There needs to be an interface that should tell me how much CPU/MEMs an agent has. And finally tell the agent to solve something and accept any answers it returns.
|
||||
|
||||
Another issue is that it needs a manager interfaces to keep track of Agents, agents metrics, status. But also allow users to issue commands. NFC how I am going to do that. But something tells me it will require me to find a way to communicate all this with the user, and I can do this with a simple REST API, or create a terminal interface, or do web things.
|
||||
|
||||
|
||||
1. Incoming connection gets caught by `handleConnections()`
|
||||
2. `handleConnections()` verifies an agent registers
|
||||
3. After registeration it puts this into a map, along with the `conn` and stuffs this into a slice of maps
|
||||
4. The manager pics up the new agent, and offers info to anyone who wants to see it.
|
||||
5. A client can ask the manager to solve a puzzle, manager calculates which agents to use, sends of the work.
|
||||
6. Solution is returned and stored.
|
||||
|
||||
So from there.
|
||||
|
||||
I think the biggest issue now is find a way to have a terminal or web interface with the user.
|
||||
|
@ -11,6 +11,14 @@ tasks:
|
||||
cmds:
|
||||
- go run . --help
|
||||
silent: true
|
||||
server:
|
||||
cmds:
|
||||
- go run . -role scheduler
|
||||
silent: true
|
||||
agent:
|
||||
cmds:
|
||||
- go run . -role agent
|
||||
silent: true
|
||||
precommit:
|
||||
cmds:
|
||||
- pre-commit autoupdate
|
||||
|
94
client/Start.go
Normal file
94
client/Start.go
Normal file
@ -0,0 +1,94 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/inhies/go-bytesize"
|
||||
"github.com/mackerelio/go-osstat/memory"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
)
|
||||
|
||||
func (client *Client) Start() {
|
||||
|
||||
// Setup interrupt handling
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, os.Interrupt)
|
||||
|
||||
// Connect to the server
|
||||
err := client.connectToServer()
|
||||
if err != nil {
|
||||
log.Fatal("ERROR: Failed connecting to server - ", err)
|
||||
}
|
||||
defer client.conn.Close()
|
||||
|
||||
// Agent => Manager:
|
||||
// - register: register an agent with the manager
|
||||
// - update: agents sends CPU and Mem metrics (does this every second)
|
||||
// - solution: solution of a task given by the manager
|
||||
// - deregister: deregister an agent from the manager
|
||||
|
||||
// Register
|
||||
// One-time at the start
|
||||
|
||||
// Fetch the hostname
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
log.Fatal("ERROR: Failed to register - cannot get hostname - ", err)
|
||||
}
|
||||
|
||||
// Fetch the number of logical cores
|
||||
cpustats, err := cpu.Counts(true)
|
||||
if err != nil {
|
||||
log.Fatal("ERROR: Failed to register - unable to fetch number of cores - ", err)
|
||||
}
|
||||
|
||||
// Fetch memory stats in bytes
|
||||
mem, err := memory.Get()
|
||||
if err != nil {
|
||||
log.Println("ERROR: Failed to register - fetching memory info - ", err)
|
||||
}
|
||||
// Use the ByteSize package to allow for memory calculations
|
||||
b := bytesize.New(float64(mem.Total))
|
||||
|
||||
// Register this agent with the scheduler
|
||||
client.writeToServer("register;" + hostname + ";" + strconv.Itoa(cpustats) + ";" + b.String())
|
||||
|
||||
// Setup updater logic
|
||||
// Runs continuesly in the background
|
||||
go client.statusUpdater()
|
||||
|
||||
// Start handler for incoming messages
|
||||
done := make(chan struct{})
|
||||
go client.handleIncoming(done)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-interrupt:
|
||||
log.Println("Interrupt received -- Stopping")
|
||||
|
||||
// Dereg the agent before dying off
|
||||
client.writeToServer("deregister;" + hostname)
|
||||
|
||||
// Cleanly close the connection by sending a close message and then
|
||||
// waiting (with timeout) for the server to close the connection.
|
||||
err := client.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||
if err != nil {
|
||||
log.Println("ERROR: Close connection - ", err)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
362880
client/blocks.csv
Normal file
362880
client/blocks.csv
Normal file
File diff suppressed because it is too large
Load Diff
22
client/connectToServer.go
Normal file
22
client/connectToServer.go
Normal file
@ -0,0 +1,22 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func (client *Client) connectToServer() (err error) {
|
||||
u := url.URL{Scheme: "ws", Host: client.ServerAddress + ":" + strconv.Itoa(client.ServerPort), Path: "/ws"}
|
||||
log.Printf("Connecting to %s", u.String())
|
||||
|
||||
// sigh
|
||||
client.conn, _, err = websocket.DefaultDialer.Dial(u.String(), nil)
|
||||
if err != nil {
|
||||
log.Fatal("ERROR: dialing - :", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
26
client/handleIncoming.go
Normal file
26
client/handleIncoming.go
Normal file
@ -0,0 +1,26 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (client *Client) handleIncoming(done chan struct{}) {
|
||||
defer close(done)
|
||||
for {
|
||||
_, message, err := client.conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("ERROR: read message - :", err)
|
||||
return
|
||||
}
|
||||
// Ignore printing keep alives
|
||||
if string(message) == "keep alive - staying alive" {
|
||||
continue
|
||||
}
|
||||
log.Printf("recv: %s", message)
|
||||
inputs := strings.Split(string(message), ";")
|
||||
if inputs[0] == "task" {
|
||||
client.task(inputs)
|
||||
}
|
||||
}
|
||||
}
|
38
client/statusUpdater.go
Normal file
38
client/statusUpdater.go
Normal file
@ -0,0 +1,38 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/inhies/go-bytesize"
|
||||
"github.com/mackerelio/go-osstat/memory"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
)
|
||||
|
||||
// Look into using a ticker instead of a for-loop
|
||||
func (client *Client) statusUpdater() {
|
||||
for {
|
||||
// Fetch CPU usage in percentages
|
||||
cpustats, err := cpu.Percent(time.Second, false)
|
||||
if err != nil {
|
||||
log.Println("ERROR: fetching CPU usage infor - ", err)
|
||||
}
|
||||
|
||||
// Fetch memory stats in bytes
|
||||
mem, err := memory.Get()
|
||||
if err != nil {
|
||||
log.Println("ERROR: fetching memory info - ", err)
|
||||
}
|
||||
|
||||
// Use the ByteSize package to allow for memory calculations
|
||||
b := bytesize.New(float64(mem.Used))
|
||||
|
||||
// Prepare the message.
|
||||
update := "update;" + strconv.Itoa(int(cpustats[0])) + ";" + b.String() + ";" + strconv.Itoa(client.taskId)
|
||||
|
||||
// Write the message to the server
|
||||
client.writeToServer(update)
|
||||
|
||||
}
|
||||
}
|
7
client/task.go
Normal file
7
client/task.go
Normal file
@ -0,0 +1,7 @@
|
||||
package client
|
||||
|
||||
import "log"
|
||||
|
||||
func (client *Client) task(inputs []string) {
|
||||
log.Println(inputs)
|
||||
}
|
@ -1,6 +1,10 @@
|
||||
package client
|
||||
|
||||
import "github.com/gorilla/websocket"
|
||||
|
||||
type Client struct {
|
||||
serverAddress string
|
||||
serverPort int
|
||||
ServerAddress string
|
||||
ServerPort int
|
||||
taskId int
|
||||
conn *websocket.Conn
|
||||
}
|
||||
|
16
client/writeToServer.go
Normal file
16
client/writeToServer.go
Normal file
@ -0,0 +1,16 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func (client *Client) writeToServer(msg string) {
|
||||
// Send the message towards the server.
|
||||
err := client.conn.WriteMessage(websocket.TextMessage, []byte(msg))
|
||||
if err != nil {
|
||||
log.Println("ERROR: writing - ", err)
|
||||
return
|
||||
}
|
||||
}
|
16
go.mod
16
go.mod
@ -4,4 +4,18 @@ go 1.23.4
|
||||
|
||||
require gitea.ligthert.net/golang/sudoku-funpark v0.0.0-20250129164508-c1f2a28ac155
|
||||
|
||||
require github.com/gorilla/websocket v1.5.3
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf
|
||||
github.com/mackerelio/go-osstat v0.2.5
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
)
|
||||
|
27
go.sum
27
go.sum
@ -1,4 +1,31 @@
|
||||
gitea.ligthert.net/golang/sudoku-funpark v0.0.0-20250129164508-c1f2a28ac155 h1:o3MLpDn26r/9DOCdATINPck2W0pNdXi6WiJcpKQwAYQ=
|
||||
gitea.ligthert.net/golang/sudoku-funpark v0.0.0-20250129164508-c1f2a28ac155/go.mod h1:GdZ2otAB+NMA19Noe7UFeP3gJtHCU4h8N158Oj1jNVY=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s=
|
||||
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
|
||||
github.com/mackerelio/go-osstat v0.2.5 h1:+MqTbZUhoIt4m8qzkVoXUJg1EuifwlAJSk4Yl2GXh+o=
|
||||
github.com/mackerelio/go-osstat v0.2.5/go.mod h1:atxwWF+POUZcdtR1wnsUcQxTytoHG4uhl2AKKzrOajY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
6
main.go
6
main.go
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gitea.ligthert.net/golang/sfcs/client"
|
||||
"gitea.ligthert.net/golang/sfcs/flags"
|
||||
"gitea.ligthert.net/golang/sfcs/server"
|
||||
"gitea.ligthert.net/golang/sfcs/vars"
|
||||
@ -25,10 +26,11 @@ func main() {
|
||||
// Switch on the role
|
||||
switch vars.Role {
|
||||
case "scheduler":
|
||||
server := server.Server{ListenAddress: vars.Address, ListenPort: vars.Port}
|
||||
server := server.Server{ListenAddress: vars.Address, ListenPort: vars.Port, Agents: make(map[string]*server.Agent)}
|
||||
vars.Operator = &server
|
||||
case "agent":
|
||||
// operator := client.Client{}
|
||||
client := client.Client{ServerAddress: vars.Address, ServerPort: vars.Port}
|
||||
vars.Operator = &client
|
||||
}
|
||||
|
||||
vars.Operator.Start()
|
||||
|
@ -4,65 +4,21 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func (server *Server) Start() error {
|
||||
// Start the server
|
||||
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
handleConnections(w, r)
|
||||
})
|
||||
// Start the HTTP and WebSocket server
|
||||
func (server *Server) Start() {
|
||||
// Declare ourselves up and running to the console.
|
||||
log.Println("Started sudoku-funpark server")
|
||||
|
||||
// Run the agent manager
|
||||
server.agentManager()
|
||||
|
||||
// Start the http server
|
||||
http.HandleFunc("GET /json", server.jsonDumper)
|
||||
http.HandleFunc("POST /addTask", server.addTask)
|
||||
|
||||
// Start the websocket server
|
||||
http.HandleFunc("/ws", server.handleConnections)
|
||||
log.Fatal(http.ListenAndServe(server.ListenAddress+":"+strconv.Itoa(server.ListenPort), nil))
|
||||
|
||||
time.Sleep(600 * time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
func handleConnections(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
log.Println("Starting server")
|
||||
go readMessages(conn)
|
||||
writeMessages(conn)
|
||||
|
||||
}
|
||||
|
||||
func readMessages(conn *websocket.Conn) {
|
||||
for {
|
||||
messageType, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("Error reading message:", err)
|
||||
break
|
||||
}
|
||||
log.Printf("Received(%d): %s\n", messageType, message)
|
||||
|
||||
// This sets the time-out for any incoming messages.
|
||||
// conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
}
|
||||
}
|
||||
|
||||
func writeMessages(conn *websocket.Conn) {
|
||||
for {
|
||||
err := conn.WriteMessage(websocket.TextMessage, []byte("Hello World!"))
|
||||
if err != nil {
|
||||
log.Println("Error writing message:", err)
|
||||
break
|
||||
}
|
||||
// This sets the time-out for any outgoing messages.
|
||||
// conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
61
server/addTask.go
Normal file
61
server/addTask.go
Normal file
@ -0,0 +1,61 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (server *Server) addTask(w http.ResponseWriter, r *http.Request) {
|
||||
var valid bool = true
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
fmt.Fprintf(w, "ERROR: Failed ParseForm() err: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
//fmt.Println(r.PostForm)
|
||||
// Validate r.PostForm["rows"]
|
||||
// Look at flags.validateRow()
|
||||
// 1. Make sure the rows is 9 in length
|
||||
if len(r.PostForm["rows"]) != 9 {
|
||||
fmt.Fprintf(w, "ERROR: There aren't 9 rows")
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Validate the row
|
||||
for _, value := range r.PostForm["rows"] {
|
||||
if !server.validateRow(value) {
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
|
||||
// Go/No-Go moment
|
||||
if !valid {
|
||||
w.Write([]byte("ERROR found"))
|
||||
return
|
||||
}
|
||||
|
||||
// Add the task
|
||||
var puzzle [9]string
|
||||
puzzle[0] = r.PostForm["rows"][0]
|
||||
puzzle[1] = r.PostForm["rows"][1]
|
||||
puzzle[2] = r.PostForm["rows"][2]
|
||||
puzzle[3] = r.PostForm["rows"][3]
|
||||
puzzle[4] = r.PostForm["rows"][4]
|
||||
puzzle[5] = r.PostForm["rows"][5]
|
||||
puzzle[6] = r.PostForm["rows"][6]
|
||||
puzzle[7] = r.PostForm["rows"][7]
|
||||
puzzle[8] = r.PostForm["rows"][8]
|
||||
|
||||
// Create task and chuck it in the server struct
|
||||
task := Task{Puzzle: puzzle}
|
||||
server.Tasks = append(server.Tasks, &task)
|
||||
|
||||
// Calling it
|
||||
w.Write([]byte("Ok"))
|
||||
|
||||
}
|
23
server/agentManager.go
Normal file
23
server/agentManager.go
Normal file
@ -0,0 +1,23 @@
|
||||
package server
|
||||
|
||||
import "time"
|
||||
|
||||
func (server *Server) agentManager() {
|
||||
// Start the great for-loop
|
||||
for {
|
||||
// Count the number of agents without a runnig task
|
||||
|
||||
// Check if there are any new Tasks up for grabs
|
||||
|
||||
// See if the task can be divided by the number of agents
|
||||
|
||||
// If so, split
|
||||
// if not, split-1
|
||||
// Try again
|
||||
|
||||
// Give agents task, maybe even look for the ones with the most CPUs
|
||||
|
||||
// 😴
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
}
|
80
server/agents.html
Normal file
80
server/agents.html
Normal file
@ -0,0 +1,80 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>JSON Data Display</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
}
|
||||
#data-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.data-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>JSON Data Display</h1>
|
||||
<div id="data-container">
|
||||
<!-- Data will be inserted here -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const url = 'http://localhost:8080/json'; // Replace with your actual URL
|
||||
const dataContainer = document.getElementById('data-container');
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const data = await response.json();
|
||||
updateData(data);
|
||||
} catch (error) {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateData(data) {
|
||||
const agent = data.Agents.blackbox;
|
||||
dataContainer.innerHTML = `
|
||||
<div class="data-item"><strong>Name:</strong> ${agent.Name}</div>
|
||||
<div class="data-item"><strong>Registration Time:</strong> ${agent.Reg}</div>
|
||||
<div class="data-item"><strong>Cores:</strong> ${agent.Cores}</div>
|
||||
<div class="data-item"><strong>CPU Usage:</strong> ${agent.Cpu.join(', ')}</div>
|
||||
<div class="data-item"><strong>Max Memory:</strong> ${agent.Mem_max}</div>
|
||||
<div class="data-item"><strong>Memory Usage:</strong> ${agent.Mem_usg.join(', ')}</div>
|
||||
<div class="data-item"><strong>Task ID:</strong> ${agent.TaskId}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Fetch data initially
|
||||
fetchData();
|
||||
|
||||
// Fetch data every second
|
||||
setInterval(fetchData, 1000);
|
||||
</script>
|
||||
<hr/>
|
||||
<form action="http://localhost:8080/addTask" method="post">
|
||||
<table border="0">
|
||||
<tr><td>Row1</td><td>:</td><td><input type="text" value="000000000" name="rows"></td></tr>
|
||||
<tr><td>Row2</td><td>:</td><td><input type="text" value="000000000" name="rows"></td></tr>
|
||||
<tr><td>Row3</td><td>:</td><td><input type="text" value="000000000" name="rows"></td></tr>
|
||||
<tr><td>Row4</td><td>:</td><td><input type="text" value="000000000" name="rows"></td></tr>
|
||||
<tr><td>Row5</td><td>:</td><td><input type="text" value="000000000" name="rows"></td></tr>
|
||||
<tr><td>Row6</td><td>:</td><td><input type="text" value="000000000" name="rows"></td></tr>
|
||||
<tr><td>Row7</td><td>:</td><td><input type="text" value="000000000" name="rows"></td></tr>
|
||||
<tr><td>Row8</td><td>:</td><td><input type="text" value="000000000" name="rows"></td></tr>
|
||||
<tr><td>Row9</td><td>:</td><td><input type="text" value="000000000" name="rows"></td></tr>
|
||||
<tr><td colspan="3" align="right"><input type="submit"></td></tr>
|
||||
|
||||
</table>
|
||||
</form>
|
||||
<hr/>
|
||||
</body>
|
||||
</html>
|
6
server/closeConnection.go
Normal file
6
server/closeConnection.go
Normal file
@ -0,0 +1,6 @@
|
||||
package server
|
||||
|
||||
func (server *Server) closeConnection(agentName string) {
|
||||
server.Agents[agentName].conn.Close()
|
||||
delete(server.Agents, agentName)
|
||||
}
|
66
server/handleConnections.go
Normal file
66
server/handleConnections.go
Normal file
@ -0,0 +1,66 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Handle incoming websocket connections
|
||||
func (server *Server) handleConnections(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
// defer conn.Close()
|
||||
|
||||
log.Println("Incoming connection")
|
||||
|
||||
// Check for register
|
||||
var registered bool
|
||||
var msgs []string
|
||||
for !registered {
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("Error reading message:", err)
|
||||
break
|
||||
}
|
||||
|
||||
msgs = server.parseMessage(message)
|
||||
if string(msgs[0]) == "register" {
|
||||
registered = true
|
||||
}
|
||||
}
|
||||
log.Println("Connected to agent")
|
||||
|
||||
// Create var of type Agent
|
||||
cores_int, err := strconv.Atoi(msgs[2])
|
||||
if err != nil {
|
||||
log.Fatal("ERROR: failed strconv.Atoi(cpu cores):", err)
|
||||
}
|
||||
mem_max_string := strings.ReplaceAll(msgs[3], "GB", "")
|
||||
mem_max_float, err := strconv.ParseFloat(mem_max_string, 64)
|
||||
if err != nil {
|
||||
log.Fatal("ERROR: failed strconv.ParseFloat():", err)
|
||||
}
|
||||
agent := Agent{
|
||||
Name: msgs[1],
|
||||
Reg: time.Now(),
|
||||
Cores: cores_int,
|
||||
Mem_max: mem_max_float,
|
||||
TaskId: 0,
|
||||
conn: conn,
|
||||
}
|
||||
|
||||
// Dump it into server.Agents
|
||||
server.Agents[msgs[1]] = &agent
|
||||
|
||||
defer server.closeConnection(msgs[1])
|
||||
|
||||
go server.readMessages(agent)
|
||||
server.writeMessages(conn)
|
||||
|
||||
}
|
20
server/jsonDumper.go
Normal file
20
server/jsonDumper.go
Normal file
@ -0,0 +1,20 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (server *Server) jsonDumper(w http.ResponseWriter, r *http.Request) {
|
||||
json, err := json.Marshal(server)
|
||||
if err != nil {
|
||||
fmt.Println("JSON Marshaling error:", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("This is my Website"))
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(json)
|
||||
}
|
7
server/parseMessage.go
Normal file
7
server/parseMessage.go
Normal file
@ -0,0 +1,7 @@
|
||||
package server
|
||||
|
||||
import "strings"
|
||||
|
||||
func (server *Server) parseMessage(message []byte) []string {
|
||||
return strings.Split(string(message), ";")
|
||||
}
|
29
server/pruneSlice.go
Normal file
29
server/pruneSlice.go
Normal file
@ -0,0 +1,29 @@
|
||||
package server
|
||||
|
||||
import "slices"
|
||||
|
||||
func (server *Server) pruneIntSlice(mySlice []int) []int {
|
||||
if len(mySlice) > 10 {
|
||||
for key := range mySlice {
|
||||
if key == 10 {
|
||||
mySlice = slices.Delete(mySlice, 9, 10)
|
||||
} else {
|
||||
mySlice[key] = mySlice[key+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return mySlice
|
||||
}
|
||||
|
||||
func (server *Server) pruneFloat64Slice(mySlice []float64) []float64 {
|
||||
if len(mySlice) > 10 {
|
||||
for key := range mySlice {
|
||||
if key == 10 {
|
||||
mySlice = slices.Delete(mySlice, 9, 10)
|
||||
} else {
|
||||
mySlice[key] = mySlice[key+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return mySlice
|
||||
}
|
52
server/readMessages.go
Normal file
52
server/readMessages.go
Normal file
@ -0,0 +1,52 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Rewrite to become a general input handler
|
||||
func (server *Server) readMessages(agent Agent) {
|
||||
for {
|
||||
_, message, err := agent.conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("Error reading message:", err)
|
||||
break
|
||||
}
|
||||
// log.Printf("Received(%d): %s\n", messageType, message)
|
||||
|
||||
msgs := server.parseMessage(message)
|
||||
|
||||
if msgs[0] == "update" {
|
||||
cpu_usg, err := strconv.Atoi(msgs[1])
|
||||
if err != nil {
|
||||
fmt.Println("ERROR: converting string to int", err)
|
||||
}
|
||||
server.Agents[agent.Name].Cpu = append(server.Agents[agent.Name].Cpu, cpu_usg)
|
||||
server.Agents[agent.Name].Cpu = server.pruneIntSlice(server.Agents[agent.Name].Cpu)
|
||||
|
||||
mem_usg_string := strings.ReplaceAll(msgs[2], "GB", "")
|
||||
mem_usg_float, err := strconv.ParseFloat(mem_usg_string, 64)
|
||||
if err != nil {
|
||||
log.Fatal("ERROR: failed strconv.ParseFloat():", err)
|
||||
}
|
||||
server.Agents[agent.Name].Mem_usg = append(server.Agents[agent.Name].Mem_usg, mem_usg_float)
|
||||
server.Agents[agent.Name].Mem_usg = server.pruneFloat64Slice(server.Agents[agent.Name].Mem_usg)
|
||||
|
||||
taskId, err := strconv.Atoi(msgs[3])
|
||||
if err != nil {
|
||||
log.Println("ERROR: cannot convert taskId:", err)
|
||||
}
|
||||
server.Agents[agent.Name].TaskId = taskId
|
||||
}
|
||||
|
||||
if msgs[0] == "deregister" {
|
||||
server.closeConnection(agent.Name)
|
||||
}
|
||||
|
||||
// This sets the time-out for any incoming messages.
|
||||
// conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
}
|
||||
}
|
@ -1,6 +1,35 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Agent struct {
|
||||
Name string
|
||||
Reg time.Time
|
||||
Cores int
|
||||
Cpu []int
|
||||
Mem_max float64
|
||||
Mem_usg []float64
|
||||
TaskId int
|
||||
conn *websocket.Conn
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
Puzzle [9]string
|
||||
Solutions [][]string
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
ListenAddress string
|
||||
ListenPort int
|
||||
Agents map[string]*Agent
|
||||
Tasks []*Task
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
13
server/validChar.go
Normal file
13
server/validChar.go
Normal file
@ -0,0 +1,13 @@
|
||||
package server
|
||||
|
||||
func (server *Server) validChar(char rune) (valid bool) {
|
||||
decvals := [10]int{48, 49, 50, 51, 52, 53, 54, 55, 56, 57}
|
||||
|
||||
for _, value := range decvals {
|
||||
if char == rune(value) {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
40
server/validateRow.go
Normal file
40
server/validateRow.go
Normal file
@ -0,0 +1,40 @@
|
||||
package server
|
||||
|
||||
func (server *Server) validateRow(row string) (valid bool) {
|
||||
// Declarations baby!
|
||||
valid = true
|
||||
var found bool
|
||||
var double bool
|
||||
count := make(map[rune]int)
|
||||
|
||||
// 1. Make sure row is 9 in length
|
||||
if len(row) != 9 {
|
||||
valid = false
|
||||
}
|
||||
|
||||
// 2. Ensure all digits are numbers
|
||||
for _, value := range row {
|
||||
found = server.validChar(value)
|
||||
}
|
||||
|
||||
if !found {
|
||||
valid = false
|
||||
}
|
||||
|
||||
// 3. Ensure all digits (except zero) are only present once
|
||||
for _, digits := range row {
|
||||
count[digits] = count[digits] + 1
|
||||
}
|
||||
|
||||
for key, value := range count {
|
||||
if value > 1 && key != 48 {
|
||||
double = true
|
||||
}
|
||||
}
|
||||
|
||||
if double {
|
||||
valid = false
|
||||
}
|
||||
|
||||
return
|
||||
}
|
19
server/writeMessages.go
Normal file
19
server/writeMessages.go
Normal file
@ -0,0 +1,19 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func (server *Server) writeMessages(conn *websocket.Conn) {
|
||||
for {
|
||||
err := conn.WriteMessage(websocket.TextMessage, []byte("keep alive - staying alive"))
|
||||
if err != nil {
|
||||
log.Println("Error writing message:", err)
|
||||
break
|
||||
}
|
||||
// This sets the time-out for any outgoing messages.
|
||||
// conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import "gitea.ligthert.net/golang/sudoku-funpark/outputter"
|
||||
|
||||
type Operator interface {
|
||||
// Start the operator
|
||||
Start() error
|
||||
Start()
|
||||
}
|
||||
|
||||
type Vars struct {
|
||||
|
Reference in New Issue
Block a user