Stan hai 2 semanas
achega
3c9b37f4d1
Modificáronse 15 ficheiros con 800 adicións e 0 borrados
  1. 105 0
      application.go
  2. 133 0
      commandline.go
  3. 3 0
      go.mod
  4. 0 0
      go.sum
  5. 3 0
      http.go
  6. 113 0
      http_client.go
  7. 20 0
      http_const.go
  8. 240 0
      http_server.go
  9. 57 0
      logging_log.go
  10. 5 0
      logging_log_formatter.go
  11. 28 0
      logging_log_formatter_json.go
  12. 28 0
      logging_log_formatter_plain.go
  13. 19 0
      logging_log_line.go
  14. 22 0
      logging_log_parameter.go
  15. 24 0
      logging_log_severity.go

+ 105 - 0
application.go

@@ -0,0 +1,105 @@
+package fairwind
+
+import (
+	"context"
+	"os"
+	"os/signal"
+	"sync"
+	"syscall"
+)
+
+type ApplicationModule interface {
+	Name() string
+	Start() error
+	Stop() error
+}
+
+type ApplicationModuleBuilder func(ctx context.Context, log *Log) (ApplicationModule, error)
+
+type Application struct {
+	ctx     context.Context
+	cancel  func()
+	log     *Log
+	modules []ApplicationModule
+}
+
+func Run(builders ...ApplicationModuleBuilder) {
+	ctx, cancel := context.WithCancel(context.Background())
+	log := NewLog(NewLogFormatterJSON())
+
+	modules := []ApplicationModule{}
+	for _, builder := range builders {
+		module, err := builder(ctx, log)
+		if err != nil {
+			log.Error("can't build module", LogValue("name", module.Name()), LogError(err))
+			cancel()
+			return
+		}
+
+		modules = append(modules, module)
+	}
+
+	this := &Application{
+		ctx:     ctx,
+		cancel:  cancel,
+		log:     log,
+		modules: modules,
+	}
+
+	this.log.Information("starting application")
+	this.startup()
+
+	channel := make(chan os.Signal, 1)
+	signal.Notify(channel, syscall.SIGTERM, syscall.SIGINT)
+	<-channel
+
+	this.log.Information("stopping application")
+	this.cancel()
+	this.shutdown()
+}
+
+func (this *Application) startup() {
+	waitGroup := sync.WaitGroup{}
+	waitGroup.Add(len(this.modules))
+
+	start := func(log *Log, module ApplicationModule) {
+		log.Information("starting module", LogValue("module", module.Name()))
+
+		err := module.Start()
+		if err != nil {
+			log.Error("error starting module", LogValue("module", module.Name()), LogError(err))
+		} else {
+			log.Information("module started", LogValue("module", module.Name()))
+		}
+
+		waitGroup.Done()
+	}
+	for _, module := range this.modules {
+		go start(this.log, module)
+	}
+
+	waitGroup.Wait()
+}
+
+func (this *Application) shutdown() {
+	waitGroup := sync.WaitGroup{}
+	waitGroup.Add(len(this.modules))
+
+	stop := func(log *Log, module ApplicationModule) {
+		log.Information("stopping module", LogValue("module", module.Name()))
+
+		err := module.Stop()
+		if err != nil {
+			log.Error("error stopping module", LogValue("module", module.Name()), LogError(err))
+		} else {
+			log.Information("module stopped", LogValue("module", module.Name()))
+		}
+
+		waitGroup.Done()
+	}
+	for _, module := range this.modules {
+		go stop(this.log, module)
+	}
+
+	waitGroup.Wait()
+}

+ 133 - 0
commandline.go

@@ -0,0 +1,133 @@
+package fairwind
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"reflect"
+	"strings"
+)
+
+type CommandHandler func(Parameters) error
+
+type Parameter struct {
+	Kind     reflect.Kind
+	Required bool
+	Value    reflect.Value `json:"-"`
+}
+
+type Parameters map[string]*Parameter
+
+type Command struct {
+	Name          string
+	Documentation string
+	Parameters    Parameters
+	Handler       CommandHandler `json:"-"`
+	Commands      []Command
+}
+
+func (this *Command) Execute() {
+	parts := os.Args[1:]
+
+	if len(parts) == 0 {
+		this.manual()
+		return
+	}
+
+	currentCommand := this
+	index := -1
+	for i, part := range parts {
+		if strings.Contains(part, "--") {
+			index = i
+			break
+		}
+
+		found := false
+		for _, iteratedCommand := range currentCommand.Commands {
+			if iteratedCommand.Name != part {
+				continue
+			}
+
+			currentCommand = &iteratedCommand
+			found = true
+			break
+		}
+
+		if !found {
+			fmt.Println("Command not found")
+			os.Exit(1)
+		}
+	}
+
+	parameters := Parameters{}
+	fail := false
+
+	if index >= 0 {
+		for i := index; i < len(parts); i++ {
+			part := parts[i]
+			keyValue := strings.Split(part, "=")
+			if len(keyValue) != 2 {
+				fmt.Printf("Invalid argument: %s\n", part)
+				os.Exit(1)
+			}
+
+			key := keyValue[0]
+			value := keyValue[1]
+
+			parameter, ok := currentCommand.Parameters[key]
+			if !ok {
+				fmt.Printf("Invalid argument: %s=%s\n", key, value)
+				fail = true
+				continue
+			}
+
+			switch parameter.Kind {
+			case reflect.String:
+				parameters[key[2:]] = &Parameter{
+					Value: reflect.ValueOf(value),
+				}
+			}
+		}
+	}
+
+	if fail {
+		os.Exit(1)
+	}
+
+	for key, parameter := range currentCommand.Parameters {
+		if !parameter.Required {
+			continue
+		}
+
+		_, ok := parameters[key[2:]]
+		if !ok {
+			fmt.Printf("Argument required: %s\n", key)
+			fail = true
+		}
+	}
+
+	if fail {
+		os.Exit(1)
+	}
+
+	if currentCommand.Handler == nil {
+		fmt.Println("Handler for command not set")
+		os.Exit(1)
+	}
+
+	err := currentCommand.Handler(parameters)
+	if err != nil {
+		fmt.Printf("Execution error: %v\n", err)
+		os.Exit(1)
+	}
+}
+
+func (this *Command) manual() {
+	buffer, err := json.Marshal(this)
+	if err != nil {
+		fmt.Printf("Execution error: %v\n", err)
+		os.Exit(1)
+	}
+
+	fmt.Println(string(buffer))
+}

+ 3 - 0
go.mod

@@ -0,0 +1,3 @@
+module git.buran.team/fairwind
+
+go 1.26.0

+ 0 - 0
go.sum


+ 3 - 0
http.go

@@ -0,0 +1,3 @@
+package fairwind
+
+type HTTPHeaders map[string]string

+ 113 - 0
http_client.go

@@ -0,0 +1,113 @@
+package fairwind
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+)
+
+type HTTPClientRequest struct {
+	Data     string
+	Object   any
+	Encoding int
+}
+
+type HTTPCLientResponse struct {
+	Raw      []byte
+	Object   any
+	Encoding int
+}
+
+type HTTPClient struct {
+	scheme string
+	host   string
+	port   uint
+}
+
+func NewHTTPClient(scheme string, host string, port uint) *HTTPClient {
+	return &HTTPClient{
+		scheme: scheme,
+		host:   host,
+		port:   port,
+	}
+}
+
+func (this *HTTPClient) Request(method int, path string, headers HTTPHeaders, request *HTTPClientRequest, response *HTTPCLientResponse) error {
+	buffer, err := json.Marshal(request.Object)
+	if err != nil {
+		return err
+	}
+
+	var requestInternal *http.Request
+	if method == METHOD_GET {
+		requestInternal, err = http.NewRequest(
+			"GET",
+			fmt.Sprintf("%s://%s:%d/%s", this.scheme, this.host, this.port, path),
+			nil,
+		)
+	}
+
+	if method == METHOD_POST {
+		requestInternal, err = http.NewRequest(
+			"POST",
+			fmt.Sprintf("%s://%s:%d/%s", this.scheme, this.host, this.port, path),
+			bytes.NewBuffer(buffer),
+		)
+	}
+
+	if err != nil {
+		return err
+	}
+
+	for key, value := range headers {
+		requestInternal.Header.Add(key, value)
+	}
+
+	client := &http.Client{}
+	responseInternal, err := client.Do(requestInternal)
+	if err != nil {
+		return err
+	}
+	if responseInternal.StatusCode != http.StatusOK {
+		return fmt.Errorf("invalid status = %d", responseInternal.StatusCode)
+	}
+
+	buffer, err = io.ReadAll(responseInternal.Body)
+	if err != nil {
+		return err
+	}
+
+	contentTypeHeader := responseInternal.Header.Get("Content-Type")
+	if contentTypeHeader == "" {
+		return errors.New("content-type not specified")
+	}
+
+	var contentType string
+	if contentTypeHeader == "text/plain" || contentTypeHeader == "application/octet-stream" {
+		contentType = contentTypeHeader
+	} else {
+		parts := strings.Split(contentTypeHeader, ";")
+		if len(parts) != 2 {
+			return errors.New("content-type invalid")
+		}
+
+		contentType = parts[0]
+	}
+
+	switch contentType {
+	case "text/plain":
+	case "application/octet-stream":
+		response.Raw = buffer
+	case "application/json":
+		err = json.Unmarshal(buffer, response.Object)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}

+ 20 - 0
http_const.go

@@ -0,0 +1,20 @@
+package fairwind
+
+// Method
+const METHOD_NONE = 0
+const METHOD_GET = 1
+const METHOD_POST = 2
+
+// Status
+const OK = 0
+const ERROR_JSON_PARSE = 10
+const ERROR_VALIDATE = 20
+const ERROR_ACCESS_CONTROL = 30
+const ERROR_CACHE = 40
+const ERROR_DATABASE = 50
+const ERROR_QUEUE = 60
+const ERROR_GENERAL = 100
+
+// Encoding
+const ENCODING_PLAIN = 0
+const ENCODING_JSON = 1

+ 240 - 0
http_server.go

@@ -0,0 +1,240 @@
+package fairwind
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"time"
+)
+
+type HTTPServerRequest struct {
+	Headers HTTPHeaders
+	Data    any
+}
+
+type HTTPServerResponse struct {
+	Headers  HTTPHeaders
+	Status   int
+	Error    error
+	Encoding int
+	Plain    []byte
+	Data     any
+}
+
+func ResponsePlain(headers HTTPHeaders, plain []byte) *HTTPServerResponse {
+	return &HTTPServerResponse{
+		Headers:  headers,
+		Plain:    plain,
+		Encoding: ENCODING_PLAIN,
+	}
+}
+
+func ResponseJSON(headers HTTPHeaders, data any) *HTTPServerResponse {
+	return &HTTPServerResponse{
+		Headers:  headers,
+		Data:     data,
+		Encoding: ENCODING_JSON,
+	}
+}
+
+func ResponseErr(status int, err error) *HTTPServerResponse {
+	return &HTTPServerResponse{
+		Status: status,
+		Error:  err,
+	}
+}
+
+type HTTPServerCallback func(log *Log, request *HTTPServerRequest) *HTTPServerResponse
+
+type HTTPServerHandler struct {
+	Callback HTTPServerCallback
+	Data     any
+	Buffer   int
+}
+
+type HTTPServerAction struct {
+	Method int
+	Path   string
+}
+
+type HTTPServer struct {
+	log      *Log
+	host     string
+	port     string
+	handlers map[HTTPServerAction]HTTPServerHandler
+}
+
+func NewHTTPServer(log *Log, host string, port string, handlers map[HTTPServerAction]HTTPServerHandler) *HTTPServer {
+	return &HTTPServer{
+		log:      log,
+		host:     host,
+		port:     port,
+		handlers: handlers,
+	}
+}
+
+func (this *HTTPServer) Start() error {
+	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", this.host, this.port))
+	if err != nil {
+		return err
+	}
+
+	channel := make(chan error)
+	go func(listener net.Listener, channel chan<- error) {
+		server := &http.Server{
+			Handler: this,
+		}
+		channel <- server.Serve(listener)
+
+	}(listener, channel)
+
+	err = <-channel
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (this *HTTPServer) ServeHTTP(responseStream http.ResponseWriter, requestStream *http.Request) {
+	// Find handler
+	method := METHOD_NONE
+	switch requestStream.Method {
+	case "GET":
+		method = METHOD_GET
+	case "POST":
+		method = METHOD_POST
+	}
+
+	if method == METHOD_NONE {
+		this.log.Error("invalid method", LogValue("method", requestStream.Method))
+		responseStream.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	handler, ok := this.handlers[HTTPServerAction{Method: method, Path: requestStream.URL.Path}]
+	if !ok {
+		this.log.Error("handler not found", LogValue("path", requestStream.URL.Path))
+		responseStream.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	// Parse GET query
+	if method == METHOD_GET {
+		query := map[string]string{}
+		for key, values := range requestStream.URL.Query() {
+			for _, value := range values {
+				query[key] = value
+			}
+		}
+
+		buffer, err := json.Marshal(query)
+		if err != nil {
+			this.log.Error("malformed request query", LogValue("path", requestStream.URL.Path), LogError(err))
+			responseStream.WriteHeader(http.StatusInternalServerError)
+			return
+		}
+
+		err = json.Unmarshal(buffer, handler.Data)
+		if err != nil {
+			this.log.Error("malformed request query", LogValue("path", requestStream.URL.Path), LogError(err))
+			responseStream.WriteHeader(http.StatusInternalServerError)
+			return
+		}
+	}
+
+	// Parse POST body
+	if method == METHOD_POST {
+		size := 1
+		if handler.Buffer > 0 {
+			size = handler.Buffer
+		}
+
+		buffer := make([]byte, 1024*size)
+		n, err := requestStream.Body.Read(buffer)
+		if err != io.EOF {
+			this.log.Error("malformed request body", LogValue("path", requestStream.URL.Path), LogError(err))
+			responseStream.WriteHeader(http.StatusInternalServerError)
+			return
+		}
+
+		err = json.Unmarshal(buffer[:n], handler.Data)
+		if err != nil {
+			this.log.Error("malformed request body", LogValue("path", requestStream.URL.Path), LogError(err))
+			responseStream.WriteHeader(http.StatusInternalServerError)
+			return
+		}
+	}
+
+	// Log query
+	start := time.Now()
+	defer func() {
+		this.log.Debug(
+			"request",
+			LogValue("method", requestStream.Method),
+			LogValue("path", requestStream.URL.Path),
+			LogValue("duration", time.Since(start)),
+		)
+	}()
+
+	// Handle
+	requestHeaders := HTTPHeaders{}
+	for key, values := range requestStream.Header {
+		for _, value := range values {
+			requestHeaders[key] = value
+		}
+	}
+
+	response := handler.Callback(
+		this.log,
+		&HTTPServerRequest{
+			Headers: requestHeaders,
+			Data:    handler.Data,
+		},
+	)
+
+	// Headers
+	for key, value := range response.Headers {
+		responseStream.Header().Add(key, value)
+	}
+
+	if response.Encoding == ENCODING_PLAIN {
+		responseStream.Header().Add("Content-Type", "text/plain")
+	}
+
+	if response.Encoding == ENCODING_JSON {
+		responseStream.Header().Add("Content-Type", "application/json")
+	}
+
+	// Status
+	if response.Status == 0 {
+		responseStream.WriteHeader(http.StatusOK)
+	} else {
+		responseStream.WriteHeader(response.Status)
+	}
+
+	// Body
+	if response.Encoding == ENCODING_PLAIN {
+		_, err := responseStream.Write(response.Plain)
+		if err != nil {
+			this.log.Error("malformed response body", LogValue("path", requestStream.URL.Path), LogError(err))
+			return
+		}
+	}
+
+	if response.Encoding == ENCODING_JSON {
+		buffer, err := json.Marshal(response.Data)
+		if err != nil {
+			this.log.Error("malformed response body", LogValue("path", requestStream.URL.Path), LogError(err))
+			return
+		}
+
+		_, err = responseStream.Write(buffer)
+		if err != nil {
+			this.log.Error("malformed response body", LogValue("path", requestStream.URL.Path), LogError(err))
+			return
+		}
+	}
+}

+ 57 - 0
logging_log.go

@@ -0,0 +1,57 @@
+package fairwind
+
+import (
+	"sync"
+	"syscall/js"
+)
+
+type Log struct {
+	mutex     sync.Mutex
+	formatter LogFormatter
+	lines     []LogLine
+}
+
+func NewLog(formatter LogFormatter) *Log {
+	return &Log{
+		formatter: formatter,
+		lines:     []LogLine{},
+	}
+}
+
+func (this *Log) Debug(message string, parameters ...LoggingParameter) {
+	this.log(SEVERITY_DEBUG, message, parameters...)
+}
+
+func (this *Log) Information(message string, parameters ...LoggingParameter) {
+	this.log(SEVERITY_INFORMATION, message, parameters...)
+}
+
+func (this *Log) Warning(message string, parameters ...LoggingParameter) {
+	this.log(SEVERITY_WARNING, message, parameters...)
+}
+
+func (this *Log) Error(message string, parameters ...LoggingParameter) {
+	this.log(SEVERITY_ERROR, message, parameters...)
+}
+
+func (this *Log) Critical(message string, parameters ...LoggingParameter) {
+	this.log(SEVERITY_CRITICAL, message, parameters...)
+}
+
+func (this *Log) log(severity int, message string, parameters ...LoggingParameter) {
+	this.mutex.Lock()
+	defer this.mutex.Unlock()
+
+	// TODO: detect OS
+	console := js.Global().Get("console")
+
+	line, err := this.formatter.Format(
+		*NewLogLine(severity, message, parameters),
+	)
+	if err != nil {
+		// TODO: log error OS-specific
+		return
+	}
+
+	console.Call("log", line)
+}

+ 5 - 0
logging_log_formatter.go

@@ -0,0 +1,5 @@
+package fairwind
+
+type LogFormatter interface {
+	Format(LogLine) (string, error)
+}

+ 28 - 0
logging_log_formatter_json.go

@@ -0,0 +1,28 @@
+package fairwind
+
+import "encoding/json"
+
+type LogFormatterJSON struct {
+}
+
+func NewLogFormatterJSON() *LogFormatterJSON {
+	return &LogFormatterJSON{}
+}
+
+func (this *LogFormatterJSON) Format(line LogLine) (string, error) {
+	vector := map[string]any{
+		"severity":  SeverityToString(line.Severity),
+		"timestamp": line.Timestamp,
+		"message":   line.Message,
+	}
+	for _, parameter := range line.Parameters {
+		vector[parameter.Key] = parameter.Value
+	}
+
+	buffer, err := json.Marshal(vector)
+	if err != nil {
+		return "", err
+	}
+
+	return string(buffer), nil
+}

+ 28 - 0
logging_log_formatter_plain.go

@@ -0,0 +1,28 @@
+package fairwind
+
+import (
+	"fmt"
+	"strings"
+)
+
+type LogFormatterPlain struct {
+}
+
+func NewLogFormatterPlain() *LogFormatterPlain {
+	return &LogFormatterPlain{}
+}
+
+func (this *LogFormatterPlain) Format(line LogLine) (string, error) {
+	pairs := []string{}
+	for _, parameter := range line.Parameters {
+		pairs = append(pairs, fmt.Sprintf("%s=%v", parameter.Key, parameter.Value))
+	}
+
+	return fmt.Sprintf(
+		"%s %d %s %s",
+		SeverityToString(line.Severity),
+		line.Timestamp,
+		line.Message,
+		strings.Join(pairs, ", "),
+	), nil
+}

+ 19 - 0
logging_log_line.go

@@ -0,0 +1,19 @@
+package fairwind
+
+import "time"
+
+type LogLine struct {
+	Severity   int
+	Timestamp  int64
+	Message    string
+	Parameters []LoggingParameter
+}
+
+func NewLogLine(severity int, message string, parameters []LoggingParameter) *LogLine {
+	return &LogLine{
+		Severity:   severity,
+		Timestamp:  time.Now().UnixNano(),
+		Message:    message,
+		Parameters: parameters,
+	}
+}

+ 22 - 0
logging_log_parameter.go

@@ -0,0 +1,22 @@
+package fairwind
+
+import "fmt"
+
+type LoggingParameter struct {
+	Key   string
+	Value any
+}
+
+func LogValue(key string, value any) LoggingParameter {
+	return LoggingParameter{
+		Key:   key,
+		Value: value,
+	}
+}
+
+func LogError(value any) LoggingParameter {
+	return LoggingParameter{
+		Key:   "error",
+		Value: fmt.Sprintf("%v", value),
+	}
+}

+ 24 - 0
logging_log_severity.go

@@ -0,0 +1,24 @@
+package fairwind
+
+const SEVERITY_DEBUG = 0
+const SEVERITY_INFORMATION = 1
+const SEVERITY_WARNING = 2
+const SEVERITY_ERROR = 3
+const SEVERITY_CRITICAL = 4
+
+func SeverityToString(severity int) string {
+	switch severity {
+	case SEVERITY_DEBUG:
+		return "debug"
+	case SEVERITY_INFORMATION:
+		return "information"
+	case SEVERITY_WARNING:
+		return "warning"
+	case SEVERITY_ERROR:
+		return "error"
+	case SEVERITY_CRITICAL:
+		return "critical"
+	}
+
+	return "unknown"
+}