Skip to content

Commit 3ff2f16

Browse files
committed
chore: Compile DLL as static instead of dynamic
1 parent 9115d26 commit 3ff2f16

9 files changed

Lines changed: 270 additions & 39 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@ jobs:
100100
go-version: '1.23'
101101
cache: true
102102

103+
# Match Makefile / build.bat: static MinGW runtime (no libwinpthread-1.dll beside exe)
103104
- name: Build pgrollback
105+
env:
106+
CGO_LDFLAGS: "-static"
104107
run: go build -o bin/pgrollback.exe ./cmd/pgrollback
105108

106109
- name: Upload binary artifact
@@ -135,6 +138,7 @@ jobs:
135138
GOOS: windows
136139
GOARCH: amd64
137140
CC: x86_64-w64-mingw32-gcc
141+
CGO_LDFLAGS: "-static"
138142
run: |
139143
mkdir -p bin
140144
go build -o bin/pgrollback.exe ./cmd/pgrollback

Makefile

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
11
.PHONY: build test test-unit test-integration run clean
22

3+
# When building for Windows (host GOOS), default to static MinGW CGO so the
4+
# binary does not need libwinpthread-1.dll beside the exe. Faster dev links:
5+
# make build DYNAMIC=1
6+
GOOS := $(shell go env GOOS)
7+
ifeq ($(GOOS),windows)
8+
ifeq ($(DYNAMIC),1)
9+
WINDOWS_CGO_PREFIX :=
10+
else
11+
WINDOWS_CGO_PREFIX := CGO_LDFLAGS=-static
12+
endif
13+
else
14+
WINDOWS_CGO_PREFIX :=
15+
endif
16+
317
build:
4-
go build -o bin/pgrollback ./cmd/pgrollback
18+
$(WINDOWS_CGO_PREFIX) go build -o bin/pgrollback ./cmd/pgrollback
519

620
test: test-unit test-integration
721

822
test-unit:
9-
go test -v ./pkg/... ./internal/...
23+
$(WINDOWS_CGO_PREFIX) go test -v ./pkg/... ./internal/...
1024

1125
test-integration:
12-
go test -v ./test/integration/... -tags=integration
26+
$(WINDOWS_CGO_PREFIX) go test -v ./test/integration/... -tags=integration
1327

1428
run: build
1529
./bin/pgrollback

README.md

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pgrollback keeps **one real PostgreSQL transaction** on the server side and expo
4343
- Connection pools and multiple connections per test ID.
4444
- Automatic rollback when the session ends; no extra cleanup scripts.
4545
- Real PostgreSQL execution (not mocked SQL).
46-
- Optional web GUI for queries on the same listen port.
46+
- Web GUI to view the running queries and query history.
4747

4848
---
4949

@@ -68,10 +68,44 @@ When the test finishes (or you issue a full rollback command), the sandbox is di
6868

6969
## Quick start
7070

71-
1. **Install Go** (1.23+) and, on Windows for this repo, a **64-bit MinGW** toolchain for CGO (see [Build](#build)).
72-
2. **Config** — Copy `config/pgrollback.yaml` and set `postgres.*` and `proxy.listen_*` for your machine.
73-
3. **Run the proxy**`make run` or `./bin/pgrollback --config config/pgrollback.yaml` (Windows: `build.bat` / `run.bat` after `setEnvironments.bat`).
74-
4. **Point tests at the proxy** — Use the proxy host/port as the DB endpoint and set `application_name` to `pgrollback_<testID>` so sessions are isolated (see below).
71+
This is how you **use** pgrollback: run the proxy, aim your app at it, and keep each test run in its own sandbox.
72+
73+
### From a release (binary)
74+
75+
1. **Download** the pgrollback for your platform from **[pgrollback releases](https://github.com/asfixia/pgrollback/releases/)**, plus the **sample config** (or use [`config/pgrollback.yaml`](config/pgrollback.yaml) from this repository).
76+
77+
2. **Point the proxy at your database** — In the YAML, set the `postgres` block to the credentials of the PostgreSQL instance you use for **testing** (the real server the proxy will open the long-lived transaction on):
78+
79+
```yaml
80+
postgres:
81+
host: localhost
82+
port: 5432
83+
database: postgres
84+
user: postgres
85+
password: postgres
86+
```
87+
88+
Replace `host`, `port`, `database`, `user`, and `password` with your testing database. Your application does **not** connect here; only the proxy does.
89+
90+
3. **Change where the proxy listens** — Under `proxy`, keep or adjust `listen_host` and `listen_port`. For example:
91+
92+
```yaml
93+
proxy:
94+
listen_host: localhost
95+
listen_port: 5433
96+
```
97+
98+
Your app and tests will use **this** host and port as the “database” address.
99+
100+
4. **Start pgrollback** — Run the binary with your config, e.g. `./bin/pgrollback --config /path/to/pgrollback.yaml`.
101+
102+
5. **Configure your connection through the proxy** — Configure your PostgreSQL client to use `listen_host` and `listen_port` (e.g. `localhost` and `5433`). Traffic goes: **app → pgrollback → Postgres**. Work for that sandbox runs inside **one** server-side transaction; when the sandbox ends, it **rolls back**—no durable commits from that path.
103+
104+
6. **Separate sandboxes with `application_name`** — Set `application_name` to a stable value per logical test or worker, e.g. `pgrollback_<testId>`. The proxy treats each **distinct** `application_name` as a **different** sandbox (a **different** base transaction). Connections that share the same `application_name` share one sandbox; different names are isolated from each other. See [Connect from tests](#connect-from-tests).
105+
106+
### Building from source
107+
108+
To compile locally (Go, and on Windows MinGW for CGO), see [Build](#build). Then run the proxy the same way as in step 4, pointing `--config` at your edited YAML.
75109

76110
---
77111

@@ -126,12 +160,20 @@ Example: `db.Exec("pgrollback rollback")` in Go, or the equivalent in your stack
126160

127161
**Windows:** install a **64-bit MinGW** GCC (e.g. MSYS2: `pacman -S mingw-w64-x86_64-gcc`), then run **`setEnvironments.bat`** so `PATH`, `CC`, `CXX`, and `CGO_ENABLED=1` are set. See `.vscode/settings.json` for terminal integration.
128162

163+
**Single-file Windows executable (default):** On Windows, **`build.bat`**, **`make build`**, **`make test`**, and **`test.bat`** all use **`CGO_LDFLAGS=-static`** so MinGW links the pthread/GCC runtime into the binary. The resulting `pgrollback.exe` typically only depends on system DLLs (`KERNEL32.dll`, `msvcrt.dll`)—no `libwinpthread-1.dll` next to the exe. Test binaries under `%TEMP%` also avoid needing MinGW on `PATH` at run time.
164+
165+
**Dynamic MinGW link (one flag):** **`build.bat dynamic`** or **`make build DYNAMIC=1`** (and **`make test DYNAMIC=1`** if you use Make for tests) skip `-static` for faster links; then keep MinGW’s `bin` on `PATH` at run time, or copy `libwinpthread-1.dll` / `libgcc_s_seh-1.dll` next to the exe (`build.bat dynamic` copies them into `bin\`). A plain **`go build`** outside these scripts does not set `-static`; pass **`CGO_LDFLAGS=-static`** yourself if you want a single-file exe.
166+
129167
```bash
130168
go build -o bin/pgrollback ./cmd/pgrollback
131-
# or
169+
# or (Windows: static CGO by default)
132170
make build
171+
# or (Windows: dynamic CGO, faster link)
172+
make build DYNAMIC=1
133173
```
134174

175+
**CI:** On tag pushes, [`.github/workflows/ci.yml`](.github/workflows/ci.yml) builds Windows artifacts with **`CGO_LDFLAGS=-static`** (native `windows-latest` job and the Linux cross-compile job), consistent with the default local Windows build.
176+
135177
Without a proper 64-bit `gcc`, you may see errors like `sorry, unimplemented: 64-bit mode not compiled in`.
136178

137179
---

cmd/pgrollback/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func main() {
6060
log.Printf("GUI: %s", guiURL)
6161

6262
// System tray icon blocks the main goroutine until the user clicks Quit.
63-
tray.Run(guiURL, func() {
63+
tray.Run(guiURL, config.PostgresConnStringMasked(&cfg.Postgres), func() {
6464
log.Println("Shutting down server...")
6565
if err := server.Stop(); err != nil {
6666
log.Printf("Error stopping server: %v", err)

internal/config/config.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,18 @@ func validateConfig(config *Config) error {
270270
return nil
271271
}
272272

273-
// PasswordMask is the value returned for password in config API responses.
274-
const PasswordMask = "****"
273+
// PasswordMask is the value returned for password in config API responses and in masked connection strings.
274+
const PasswordMask = "******"
275+
276+
// PostgresConnStringMasked returns a libpq-style keyword connection string for the real database
277+
// with the password replaced by PasswordMask (literal asterisks, not percent-encoded).
278+
func PostgresConnStringMasked(p *PostgresConfig) string {
279+
if p == nil {
280+
return ""
281+
}
282+
return fmt.Sprintf("host=\"%s\" port=\"%d\" dbname=\"%s\" user=\"%s\" password=\"%s\"",
283+
p.Host, p.Port, p.Database, p.User, PasswordMask)
284+
}
275285

276286
// ConfigForAPI returns a copy of the config with password masked as PasswordMask for API/UI display.
277287
func ConfigForAPI(c *Config) *Config {
@@ -286,7 +296,7 @@ func ConfigForAPI(c *Config) *Config {
286296
return &out
287297
}
288298

289-
// UpdateAndSave merges updated into the current config (keeping existing password if updated sends "" or "****"),
299+
// UpdateAndSave merges updated into the current config (keeping existing password if updated sends "" or PasswordMask),
290300
// validates, writes to the config file, and updates in-memory config. Returns error if path is empty or write fails.
291301
func UpdateAndSave(updated *Config) error {
292302
if updated == nil {
@@ -301,7 +311,7 @@ func UpdateAndSave(updated *Config) error {
301311
return fmt.Errorf("config not initialized")
302312
}
303313
merged := *updated
304-
if updated.Postgres.Password == "" || updated.Postgres.Password == PasswordMask {
314+
if updated.Postgres.Password == "" || updated.Postgres.Password == PasswordMask || updated.Postgres.Password == "****" {
305315
merged.Postgres.Password = current.Postgres.Password
306316
}
307317
if err := validateConfig(&merged); err != nil {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package config
2+
3+
import "testing"
4+
5+
func TestPostgresConnStringMasked(t *testing.T) {
6+
p := &PostgresConfig{
7+
Host: "db.example.com",
8+
Port: 5432,
9+
Database: "appdb",
10+
User: "appuser",
11+
Password: "real-secret",
12+
}
13+
got := PostgresConnStringMasked(p)
14+
want := "host=db.example.com port=5432 dbname=appdb user=appuser password=" + PasswordMask
15+
if got != want {
16+
t.Fatalf("PostgresConnStringMasked() = %q, want %q", got, want)
17+
}
18+
}
19+
20+
func TestPostgresConnStringMasked_IPv6Host(t *testing.T) {
21+
p := &PostgresConfig{
22+
Host: "::1",
23+
Port: 5432,
24+
Database: "postgres",
25+
User: "u",
26+
Password: "x",
27+
}
28+
got := PostgresConnStringMasked(p)
29+
want := "host=::1 port=5432 dbname=postgres user=u password=" + PasswordMask
30+
if got != want {
31+
t.Fatalf("PostgresConnStringMasked() = %q, want %q", got, want)
32+
}
33+
}
34+
35+
func TestPostgresConnStringMasked_nil(t *testing.T) {
36+
if s := PostgresConnStringMasked(nil); s != "" {
37+
t.Fatalf("want empty, got %q", s)
38+
}
39+
}

internal/proxy/gui/handlers.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import (
88
"pgrollback/internal/config"
99
)
1010

11-
// ConfigResponse is the config returned by GET /api/config (includes masked password and config_path).
11+
// ConfigResponse is the config returned by GET /api/config.
12+
// PostgresConnectionStringMasked is always derived from the same in-memory postgres settings as the proxy (via config.PostgresConnStringMasked), not stored separately.
1213
type ConfigResponse struct {
13-
ConfigPath string `json:"config_path"`
14-
Config *config.Config `json:"config"`
14+
ConfigPath string `json:"config_path"`
15+
Config *config.Config `json:"config"`
16+
PostgresConnectionStringMasked string `json:"postgres_connection_string_masked"`
1517
}
1618

1719
func handleAPISessions(provider SessionProvider) http.HandlerFunc {
@@ -143,8 +145,9 @@ func handleAPIConfigGet(w http.ResponseWriter, r *http.Request) {
143145
_ = json.NewEncoder(w).Encode(ConfigResponse{
144146
// Use EffectiveConfigPath so the UI always sees the path it will
145147
// use when saving (even if the file didn't exist at startup).
146-
ConfigPath: config.EffectiveConfigPath(),
147-
Config: config.ConfigForAPI(cfg),
148+
ConfigPath: config.EffectiveConfigPath(),
149+
Config: config.ConfigForAPI(cfg),
150+
PostgresConnectionStringMasked: config.PostgresConnStringMasked(&cfg.Postgres),
148151
})
149152
}
150153

0 commit comments

Comments
 (0)