No terminal window on windows
This commit is contained in:
5
Makefile
5
Makefile
@@ -6,6 +6,7 @@ DIST_DIR := dist
|
|||||||
NATIVE_GOOS := $(shell go env GOOS)
|
NATIVE_GOOS := $(shell go env GOOS)
|
||||||
NATIVE_GOARCH := $(shell go env GOARCH)
|
NATIVE_GOARCH := $(shell go env GOARCH)
|
||||||
NATIVE_EXT := $(if $(filter windows,$(NATIVE_GOOS)),.exe,)
|
NATIVE_EXT := $(if $(filter windows,$(NATIVE_GOOS)),.exe,)
|
||||||
|
WINDOWS_GUI_LDFLAGS := $(if $(filter windows,$(NATIVE_GOOS)),-ldflags "-H=windowsgui",)
|
||||||
|
|
||||||
# Native (current platform)
|
# Native (current platform)
|
||||||
NATIVE_EXE := $(APP_NAME)$(if $(filter windows,$(OS)),.exe,)
|
NATIVE_EXE := $(APP_NAME)$(if $(filter windows,$(OS)),.exe,)
|
||||||
@@ -23,7 +24,7 @@ $(DIST_DIR):
|
|||||||
|
|
||||||
build: $(DIST_DIR)
|
build: $(DIST_DIR)
|
||||||
@echo "Building $(APP_NAME) for current platform..."
|
@echo "Building $(APP_NAME) for current platform..."
|
||||||
go build -trimpath -o $(DIST_DIR)/$(APP_NAME)$(NATIVE_EXT) .
|
go build -trimpath $(WINDOWS_GUI_LDFLAGS) -o $(DIST_DIR)/$(APP_NAME)$(NATIVE_EXT) .
|
||||||
|
|
||||||
build-linux: $(DIST_DIR)
|
build-linux: $(DIST_DIR)
|
||||||
@echo "Cross-compiling $(APP_NAME) for linux/amd64..."
|
@echo "Cross-compiling $(APP_NAME) for linux/amd64..."
|
||||||
@@ -31,7 +32,7 @@ build-linux: $(DIST_DIR)
|
|||||||
|
|
||||||
build-windows: $(DIST_DIR)
|
build-windows: $(DIST_DIR)
|
||||||
@echo "Cross-compiling $(APP_NAME) for windows/amd64..."
|
@echo "Cross-compiling $(APP_NAME) for windows/amd64..."
|
||||||
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -o $(DIST_DIR)/$(APP_NAME)-windows-amd64.exe .
|
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags "-H=windowsgui" -o $(DIST_DIR)/$(APP_NAME)-windows-amd64.exe .
|
||||||
|
|
||||||
build-all: build-linux build-windows
|
build-all: build-linux build-windows
|
||||||
|
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -8,6 +8,7 @@ This program runs on the user’s machine and listens on a loopback address only
|
|||||||
|
|
||||||
- Exposes `GET /health` for a simple health check.
|
- Exposes `GET /health` for a simple health check.
|
||||||
- Exposes `GET /open?path=...` and `POST /open` to open a folder.
|
- Exposes `GET /open?path=...` and `POST /open` to open a folder.
|
||||||
|
- Exposes `GET /control` (HTML) and `GET /logs` (text) to view recent logs.
|
||||||
- Normalizes and validates the requested path:
|
- Normalizes and validates the requested path:
|
||||||
- Accepts absolute paths only.
|
- Accepts absolute paths only.
|
||||||
- If a file path is provided, it opens the containing directory.
|
- If a file path is provided, it opens the containing directory.
|
||||||
@@ -150,6 +151,8 @@ Notes:
|
|||||||
- Installs to `%LOCALAPPDATA%\luxtools-client\luxtools-client.exe`
|
- Installs to `%LOCALAPPDATA%\luxtools-client\luxtools-client.exe`
|
||||||
- Stores config in `%LOCALAPPDATA%\luxtools-client\config.json`
|
- Stores config in `%LOCALAPPDATA%\luxtools-client\config.json`
|
||||||
- Re-running `install` updates the EXE in place and refreshes the task.
|
- Re-running `install` updates the EXE in place and refreshes the task.
|
||||||
|
- Windows builds use the GUI subsystem, so the app starts without a console window.
|
||||||
|
- Logs are written next to `config.json` and can be viewed at `http://127.0.0.1:8765/control` or `http://127.0.0.1:8765/logs`.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
@@ -195,6 +198,14 @@ curl -i \
|
|||||||
'http://127.0.0.1:8765/open?path=/tmp'
|
'http://127.0.0.1:8765/open?path=/tmp'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `GET /logs`
|
||||||
|
|
||||||
|
Returns recent log output as plain text.
|
||||||
|
|
||||||
|
### `GET /control`
|
||||||
|
|
||||||
|
Shows a simple HTML page with recent log output.
|
||||||
|
|
||||||
## OS support
|
## OS support
|
||||||
|
|
||||||
- Linux: uses `xdg-open`.
|
- Linux: uses `xdg-open`.
|
||||||
|
|||||||
105
internal/logging/logging.go
Normal file
105
internal/logging/logging.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"luxtools-client/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
logBufferMaxBytes = 256 * 1024
|
||||||
|
logFileMaxBytes = 5 * 1024 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
// Buffer stores recent log output for control pages.
|
||||||
|
type Buffer struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
buf []byte
|
||||||
|
max int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBuffer(max int) *Buffer {
|
||||||
|
return &Buffer{max: max}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) Write(p []byte) (int, error) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if len(p) > b.max {
|
||||||
|
p = p[len(p)-b.max:]
|
||||||
|
}
|
||||||
|
b.buf = append(b.buf, p...)
|
||||||
|
if len(b.buf) > b.max {
|
||||||
|
b.buf = b.buf[len(b.buf)-b.max:]
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns a copy of the buffered log content.
|
||||||
|
func (b *Buffer) Bytes() []byte {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
if len(b.buf) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
copyBuf := make([]byte, len(b.buf))
|
||||||
|
copy(copyBuf, b.buf)
|
||||||
|
return copyBuf
|
||||||
|
}
|
||||||
|
|
||||||
|
func logFilePath() (string, error) {
|
||||||
|
configPath, err := config.ConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(filepath.Dir(configPath), "luxtools-client.log"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareLogFile(path string, maxBytes int64) (*os.File, error) {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if info, err := os.Stat(path); err == nil && info.Size() > maxBytes {
|
||||||
|
_ = os.Remove(path + ".old")
|
||||||
|
_ = os.Rename(path, path+".old")
|
||||||
|
}
|
||||||
|
return os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup builds loggers, a buffer, and optional file logging.
|
||||||
|
func Setup() (*log.Logger, *log.Logger, *Buffer, string, func()) {
|
||||||
|
logStore := newBuffer(logBufferMaxBytes)
|
||||||
|
logPath, logErr := logFilePath()
|
||||||
|
var logFile *os.File
|
||||||
|
if logErr == nil {
|
||||||
|
if f, err := prepareLogFile(logPath, logFileMaxBytes); err == nil {
|
||||||
|
logFile = f
|
||||||
|
} else {
|
||||||
|
logPath = ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logPath = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
infoWriters := []io.Writer{logStore, os.Stdout}
|
||||||
|
errWriters := []io.Writer{logStore, os.Stderr}
|
||||||
|
if logFile != nil {
|
||||||
|
infoWriters = append(infoWriters, logFile)
|
||||||
|
errWriters = append(errWriters, logFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
infoLog := log.New(io.MultiWriter(infoWriters...), "", log.LstdFlags)
|
||||||
|
errLog := log.New(io.MultiWriter(errWriters...), "ERROR: ", log.LstdFlags)
|
||||||
|
|
||||||
|
cleanup := func() {}
|
||||||
|
if logFile != nil {
|
||||||
|
cleanup = func() { _ = logFile.Close() }
|
||||||
|
}
|
||||||
|
|
||||||
|
return infoLog, errLog, logStore, logPath, cleanup
|
||||||
|
}
|
||||||
@@ -179,6 +179,32 @@ var settingsTemplate = template.Must(template.New("settings").Parse(`<!doctype h
|
|||||||
</html>
|
</html>
|
||||||
`))
|
`))
|
||||||
|
|
||||||
|
var logsTemplate = template.Must(template.New("logs").Parse(`<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>luxtools-client Logs</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui, sans-serif; margin: 1.25rem; }
|
||||||
|
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||||||
|
pre { background: #f7f7f7; padding: 0.75rem; border: 1px solid #ddd; overflow: auto; max-height: 70vh; }
|
||||||
|
.small { color: #666; font-size: 0.9rem; }
|
||||||
|
button { padding: 0.35rem 0.7rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Logs</h1>
|
||||||
|
<p class="small">Log file: <code>{{ .LogPath }}</code></p>
|
||||||
|
<p class="small">Updated: <code>{{ .Updated }}</code></p>
|
||||||
|
<p>
|
||||||
|
<a href="/logs">View raw text</a>
|
||||||
|
<button type="button" onclick="location.reload()">Refresh</button>
|
||||||
|
</p>
|
||||||
|
<pre>{{ .LogText }}</pre>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`))
|
||||||
|
|
||||||
// RenderIndex renders the main index page.
|
// RenderIndex renders the main index page.
|
||||||
func RenderIndex(w io.Writer, data any) error {
|
func RenderIndex(w io.Writer, data any) error {
|
||||||
return indexTemplate.Execute(w, data)
|
return indexTemplate.Execute(w, data)
|
||||||
@@ -188,3 +214,8 @@ func RenderIndex(w io.Writer, data any) error {
|
|||||||
func RenderSettings(w io.Writer) error {
|
func RenderSettings(w io.Writer) error {
|
||||||
return settingsTemplate.Execute(w, nil)
|
return settingsTemplate.Execute(w, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenderLogs renders the logs control page.
|
||||||
|
func RenderLogs(w io.Writer, data any) error {
|
||||||
|
return logsTemplate.Execute(w, data)
|
||||||
|
}
|
||||||
|
|||||||
53
main.go
53
main.go
@@ -21,6 +21,7 @@ import (
|
|||||||
|
|
||||||
"luxtools-client/internal/config"
|
"luxtools-client/internal/config"
|
||||||
"luxtools-client/internal/installer"
|
"luxtools-client/internal/installer"
|
||||||
|
"luxtools-client/internal/logging"
|
||||||
"luxtools-client/internal/notify"
|
"luxtools-client/internal/notify"
|
||||||
"luxtools-client/internal/openfolder"
|
"luxtools-client/internal/openfolder"
|
||||||
"luxtools-client/internal/web"
|
"luxtools-client/internal/web"
|
||||||
@@ -129,8 +130,8 @@ func buildInfoPayload() map[string]any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
infoLog := log.New(os.Stdout, "", log.LstdFlags)
|
infoLog, errLog, logStore, logPath, cleanup := logging.Setup()
|
||||||
errLog := log.New(os.Stderr, "ERROR: ", log.LstdFlags)
|
defer cleanup()
|
||||||
|
|
||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
@@ -232,6 +233,54 @@ func main() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
register(mux, &endpointDocs, "/control", "GET, OPTIONS", "Logs control page", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
withCORS(w, r)
|
||||||
|
if r.Method == http.MethodOptions {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
displayPath := logPath
|
||||||
|
if displayPath == "" {
|
||||||
|
displayPath = "unavailable"
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
LogPath string
|
||||||
|
LogText template.HTML
|
||||||
|
Updated string
|
||||||
|
}{
|
||||||
|
LogPath: displayPath,
|
||||||
|
LogText: template.HTML(html.EscapeString(string(logStore.Bytes()))),
|
||||||
|
Updated: time.Now().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
if err := web.RenderLogs(w, data); err != nil {
|
||||||
|
errLog.Printf("/control template error=%v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
register(mux, &endpointDocs, "/logs", "GET, OPTIONS", "Recent log output (text)", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
withCORS(w, r)
|
||||||
|
if r.Method == http.MethodOptions {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
_, _ = w.Write(logStore.Bytes())
|
||||||
|
})
|
||||||
|
|
||||||
register(mux, &endpointDocs, "/settings/config", "GET, POST, PUT, OPTIONS", "Read/update path alias config (JSON)", func(w http.ResponseWriter, r *http.Request) {
|
register(mux, &endpointDocs, "/settings/config", "GET, POST, PUT, OPTIONS", "Read/update path alias config (JSON)", func(w http.ResponseWriter, r *http.Request) {
|
||||||
withCORS(w, r)
|
withCORS(w, r)
|
||||||
if r.Method == http.MethodOptions {
|
if r.Method == http.MethodOptions {
|
||||||
|
|||||||
Reference in New Issue
Block a user