client io architecture
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package util_test
|
||||
|
||||
const (
|
||||
nonWritableDir = "/usr/lib"
|
||||
)
|
||||
Reference in New Issue
Block a user