client io architecture

This commit is contained in:
Ilia Denisov
2026-03-12 18:45:46 +02:00
committed by GitHub
parent 2dafa69b93
commit 079b9facb0
36 changed files with 1810 additions and 460 deletions
+47
View File
@@ -0,0 +1,47 @@
package client
import (
"galaxy/model/order"
"galaxy/model/report"
)
type Client interface {
// Run initializes necessary UI layout an settings, and activates client's main window.
// This is a blocking operation until client's main window is closed.
Run() error
// Shutdown closes client's main window and performing all necessary data persistence.
Shutdown()
// OnConnection receives an event when connection with client's server may be established (true) or connectivity lost (false).
OnConnection(bool)
}
type GameID string
func (i GameID) String() string {
return string(i)
}
type State struct {
// TODO: store user login key
GameState []GameState `json:"gameState"`
ActiveGameID GameID `json:"activeGameId"`
}
type GameState struct {
ID GameID `json:"id"`
LastTurn uint `json:"lastTurn"`
ActiveTurn uint `json:"activeTurn"`
}
type GameData struct {
Turn uint `json:"turn"`
Report report.Report `json:"report"`
Order *order.Order `json:"order,omitempty"`
}
type Settings struct {
// TODO: use fyne.Storage for initializing and storing data
StoragePath string
}
+3 -1
View File
@@ -5,7 +5,9 @@ import (
)
type Order struct {
Commands []DecodableCommand `json:"cmd"`
// TODO: check with already stored order, if any, and generate an error, if newer order exists
UpdatedAt int `json:"updatedAt"`
Commands []DecodableCommand `json:"cmd"`
}
func (o Order) MarshalBinary() (data []byte, err error) {
+4
View File
@@ -14,6 +14,10 @@ func F(v float64) Float {
return Float(u.Fixed3(v))
}
func (f Float) F() float64 {
return float64(f)
}
type Report struct {
Version uint `json:"version"`
Turn uint `json:"turn"`
+23
View File
@@ -1,6 +1,7 @@
package util
import (
"fmt"
"os"
"testing"
)
@@ -17,3 +18,25 @@ func CreateWorkDir(t *testing.T) (string, func()) {
}
}
}
func DirExists(path string) (bool, error) {
return PathExists(path, true)
}
func FileExists(path string) (bool, error) {
return PathExists(path, false)
}
func PathExists(path string, isDir bool) (bool, error) {
if fi, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
} else {
if isDir != fi.IsDir() {
return false, fmt.Errorf("wrong type: "+path+" mode=%s isDir=%t", fi.Mode(), isDir)
}
return true, nil
}
}
+66
View File
@@ -0,0 +1,66 @@
package util_test
import (
"galaxy/util"
"os"
"path/filepath"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestPathExists(t *testing.T) {
root, cleanup := util.CreateWorkDir(t)
defer cleanup()
testDirExistsFunc(t, root, func(s string) (bool, error) { return util.PathExists(s, true) })
testFileExistsFunc(t, root, func(s string) (bool, error) { return util.PathExists(s, false) })
}
func TestDirExists(t *testing.T) {
root, cleanup := util.CreateWorkDir(t)
defer cleanup()
testDirExistsFunc(t, root, util.DirExists)
}
func TestFileExists(t *testing.T) {
root, cleanup := util.CreateWorkDir(t)
defer cleanup()
testFileExistsFunc(t, root, util.FileExists)
}
func testDirExistsFunc(t *testing.T, root string, dirCheck func(string) (bool, error)) {
exists, err := dirCheck(root)
assert.NoError(t, err, "directory existence check")
assert.True(t, exists, "directory should exist")
nonExistentDir := filepath.Join(root, uuid.New().String())
exists, err = dirCheck(nonExistentDir)
assert.NoError(t, err, "non-existent directory existence check")
assert.False(t, exists, "non-existent directory should not exist")
}
func testFileExistsFunc(t *testing.T, root string, fileCheck func(string) (bool, error)) {
fpath := createTempFile(t, root)
exists, err := fileCheck(fpath)
assert.NoError(t, err, "file existence check")
assert.True(t, exists, "file should exist")
nonExistentFile := filepath.Join(root, uuid.New().String())
exists, err = fileCheck(nonExistentFile)
assert.NoError(t, err, "non-existent file existence check")
assert.False(t, exists, "non-existent file should not exist")
}
func createTempFile(t *testing.T, root string) string {
t.Helper()
fd, err := os.CreateTemp(root, "a-file")
if err != nil {
assert.FailNow(t, "create temporary file", err)
return ""
}
if err := fd.Close(); err != nil {
assert.FailNow(t, "close temporary file", err)
return ""
}
return fd.Name()
}
+15
View File
@@ -0,0 +1,15 @@
//go:build unix || (js && wasm) || wasip1
package util
import "golang.org/x/sys/unix"
// Writable reports whether path is Writable on Windows.
//
// Semantics:
// - for an existing regular file, it tries to open it for writing;
// - for an existing directory, it tries to create and remove a temp file inside it;
// - for other file types, it returns false with no error.
func Writable(filepath string) (bool, error) {
return unix.Access(filepath, unix.W_OK) == nil, nil
}
+22
View File
@@ -0,0 +1,22 @@
//go:build unix || (js && wasm) || wasip1
package util_test
import (
"galaxy/util"
"testing"
"github.com/stretchr/testify/assert"
)
func TestWritable(t *testing.T) {
root := t.ArtifactDir()
ok, err := util.Writable(root)
assert.NoError(t, err, "directory writable check")
assert.True(t, ok, "directory should be writable")
ok, err = util.Writable(nonWritableDir)
assert.NoError(t, err, "system directory writable check")
assert.False(t, ok, "system directory should not be writable")
}
+94
View File
@@ -0,0 +1,94 @@
package util
import (
"errors"
"os"
"path/filepath"
"syscall"
)
// Writable reports whether path is Writable on Windows.
//
// Semantics:
// - for an existing regular file, it tries to open it for writing;
// - for an existing directory, it tries to create and remove a temp file inside it;
// - for other file types, it returns false with no error.
//
// This is intentionally an operational check, not a mode-bit check, because
// on Windows effective writability is determined by ACLs and file attributes,
// not by POSIX-like permission bits from os.FileMode.
func Writable(path string) (bool, error) {
info, err := os.Stat(path)
if err != nil {
return false, err
}
if info.IsDir() {
return writableDir(path)
}
if !info.Mode().IsRegular() {
return false, nil
}
return writableFile(path)
}
// writableFile checks whether an existing regular file can be opened for writing.
func writableFile(path string) (bool, error) {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0)
if err == nil {
_ = f.Close()
return true, nil
}
if isPermissionLikeError(err) {
return false, nil
}
return false, err
}
// writableDir checks whether a directory allows creating a child file.
// That is usually the most useful definition of "directory is writable".
func writableDir(path string) (bool, error) {
pattern := filepath.Join(path, ".writable-check-*")
f, err := os.CreateTemp(path, filepath.Base(pattern))
if err == nil {
name := f.Name()
_ = f.Close()
_ = os.Remove(name)
return true, nil
}
if isPermissionLikeError(err) {
return false, nil
}
return false, err
}
// isPermissionLikeError normalizes the common Windows "access denied" style
// failures to a simple false result instead of surfacing them as hard errors.
func isPermissionLikeError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, os.ErrPermission) {
return true
}
var errno syscall.Errno
if errors.As(err, &errno) {
// ERROR_ACCESS_DENIED
if errno == syscall.ERROR_ACCESS_DENIED {
return true
}
}
var pathErr *os.PathError
if errors.As(err, &pathErr) && errors.Is(pathErr.Err, os.ErrPermission) {
return true
}
return false
}
+50
View File
@@ -0,0 +1,50 @@
//go:build windows
package util
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
// TestWritable_NewFile verifies that a freshly created regular file is writable.
func TestWritable_NewFile(t *testing.T) {
t.Parallel()
dir := t.TempDir()
path := filepath.Join(dir, "file.txt")
err := os.WriteFile(path, []byte("x"), 0o600)
require.NoError(t, err)
ok, err := Writable(path)
require.NoError(t, err)
require.True(t, ok)
}
// TestWritable_NewDirectory verifies that a freshly created directory is writable
// by checking that a temp file can be created inside it.
func TestWritable_NewDirectory(t *testing.T) {
t.Parallel()
dir := t.TempDir()
ok, err := Writable(dir)
require.NoError(t, err)
require.True(t, ok)
}
// TestWritable_MissingPath verifies that a missing path returns an error from Stat.
func TestWritable_MissingPath(t *testing.T) {
t.Parallel()
dir := t.TempDir()
path := filepath.Join(dir, "missing")
ok, err := Writable(path)
require.Error(t, err)
require.False(t, ok)
}
+5 -1
View File
@@ -2,7 +2,11 @@ module galaxy/util
go 1.26.0
require github.com/stretchr/testify v1.11.1
require (
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.11.1
golang.org/x/sys v0.41.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
+2
View File
@@ -1,8 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+5
View File
@@ -0,0 +1,5 @@
package util_test
const (
nonWritableDir = "/usr/lib"
)