Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1058661
refactor(handlers): refactor prepareOutput to prepareOutputAndRunCommand
dkarczmarski Feb 24, 2026
872049d
refactor(handlers): prepareOutputAndRunCommand
dkarczmarski Feb 25, 2026
849c0b9
refactor(handlers): refactor ExecutionHandler to translate errors
dkarczmarski Feb 25, 2026
acac998
refactor(handlers): refactor tests to use X-Error-Message instead of …
dkarczmarski Feb 25, 2026
cf1f5ca
refactor(handlers): refactor error handling
dkarczmarski Feb 25, 2026
77810d7
refactor(handlers): refactor logging
dkarczmarski Feb 25, 2026
362cade
refactor(handlers): extract executeCommand logic into processrunner p…
dkarczmarski Feb 25, 2026
e3c7fda
refactor(processrunner): refactor WaitAsync()
dkarczmarski Feb 25, 2026
51ac46b
refactor(processrunner): refactor WaitAsync()
dkarczmarski Feb 25, 2026
95646cf
refactor(handlers): extract runCommand logic into gateexec package
dkarczmarski Feb 25, 2026
78c1b3e
refactor(gateexec): refactor error handling
dkarczmarski Feb 25, 2026
91189cd
fix(gateexec): release lock after process completion in async mode
dkarczmarski Feb 25, 2026
9aa4ace
fix(handlers): release lock after process completion in async mode
dkarczmarski Feb 25, 2026
480ac26
refactor(processrunner): remove logging and request ID handling
dkarczmarski Feb 25, 2026
b076e52
feat(processrunner): treat non-zero exit codes as normal process comp…
dkarczmarski Feb 25, 2026
69356bf
fix(handlers): fix context cancellation in async mode
dkarczmarski Feb 27, 2026
3a2dbf2
refactor(handlers): simplify error handling
dkarczmarski Feb 27, 2026
f695027
refactor(handlers): extract createGateAction from runCommand
dkarczmarski Feb 27, 2026
a5017c0
feat(handlers): add command execution status headers to response
dkarczmarski Feb 27, 2026
c1ff384
refactor(gateexec): simplify error handling
dkarczmarski Feb 28, 2026
31f0b58
docs(gateexec): add godoc
dkarczmarski Feb 28, 2026
efd7eef
test(gateexec): add unit tests
dkarczmarski Feb 28, 2026
a527cd3
fix(callgate): Sequence serves waiters in FIFO
dkarczmarski Mar 2, 2026
a1067d5
test(gateexec): add integration tests
dkarczmarski Mar 2, 2026
3491069
fix(processrunner): prioritize Wait() result over context cancellation
dkarczmarski Mar 3, 2026
7ae3ccb
fix(cmdrunner): send signal to provided PID without implicit PGID neg…
dkarczmarski Mar 3, 2026
61c3fac
test(cmdrunner): add integration tests
dkarczmarski Mar 4, 2026
262f763
refactor(processrunner): rename file
dkarczmarski Mar 4, 2026
8464619
feat(processrunner): expose process PID and PGID
dkarczmarski Mar 4, 2026
6c8ebf7
feat(processrunner): add WithSignalObserver option for observing sent…
dkarczmarski Mar 4, 2026
b2ab10e
test(processrunner): add intergration tests
dkarczmarski Mar 4, 2026
1f2457a
refactor(processrunner): add ProcessRunner wrapper to decouple comman…
dkarczmarski Mar 5, 2026
97afd65
refactor(tests): move handler tests to server integration tests
dkarczmarski Mar 5, 2026
7e133c0
refactor(handlers): decouple ExecutionHandler from processrunner usin…
dkarczmarski Mar 5, 2026
10e668f
refactor(handlers): decouple ExecutionHandler from gateexec using Gat…
dkarczmarski Mar 6, 2026
14742e3
test(handlers): remove obsolete gomock-generated mocks
dkarczmarski Mar 6, 2026
56d1d30
docs(handlers): document ExecutionHandler HTTP status semantics
dkarczmarski Mar 6, 2026
86aac3c
chore: README.md
dkarczmarski Mar 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ webcmd

key.pem
cert.pem

*notes*.txt
*notes*.md

31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,35 @@ The `commandTemplate` uses Go's `text/template` syntax to inject data from the H
Header names are normalized by replacing hyphens (`-`) with underscores (`_`).
Example: `{{.headers.X_Api_Key}}` or `{{.headers.User_Agent}}` .

## HTTP Response and Error Handling

The server returns different HTTP status codes depending on the outcome of the request and the command execution:

- **200 OK**
Returned when the command starts successfully, regardless of whether the command later exits with code 0 or non-zero, or fails while executing.
In this case, the handler sets the following response headers:
- `X-Success`: `"true"` if the process exit code is 0, otherwise `"false"`.
- `X-Exit-Code`: The process exit code (if available).
- `X-Error-Message`: Empty on success, or contains the execution error message if the command fails (only if `server.withErrorHeader` is enabled in the configuration).

- **429 Too Many Requests**
Returned when command execution cannot start because the call gate rejects the request as busy (e.g., when `mode: single` is used).

- **404 Not Found**
Returned when the URL command is missing or the endpoint is not configured.

- **400 Bad Request**
Returned when `bodyAsJson` is enabled but the request body is not a valid JSON object.

- **500 Internal Server Error**
Returned when the command cannot be prepared or started at all, for example:
- Streaming was requested but the `ResponseWriter` does not support flushing.
- Command template rendering/building failed.
- Gate or pre-action setup failed before the process was started.
- Handler configuration is invalid.

**Important distinction:** A command that starts successfully but later fails (e.g., returns a non-zero exit code) is still treated as an HTTP-level success and returns **200 OK**. Detailed information about the process outcome is available in the `X-Success` and `X-Exit-Code` headers. The `X-Error-Message` header is also provided if `server.withErrorHeader` is set to `true` in the configuration.

## Configuration (`config.yaml`)

### `server`
Expand All @@ -240,6 +269,8 @@ The `commandTemplate` uses Go's `text/template` syntax to inject data from the H

* `shutdownGracePeriod` *(optional)* - the time to wait for active requests to finish before the server shuts down (e.g., `5s`, `30s`). Format: [Go Duration](https://pkg.go.dev/time#ParseDuration). Default: `5s`.

* `withErrorHeader` *(optional)* - if set to `true`, the `X-Error-Message` header will be included in the HTTP response when a command execution fails. Default: `false`.

* `https` *(optional)* - HTTPS configuration:
* `enabled` - enable or disable HTTPS. Default: `false`.
* `certFile` - path to the SSL certificate file.
Expand Down
1 change: 1 addition & 0 deletions config.sample-ssl.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
server:
# address: "127.0.0.1:8443"
withErrorHeader: true
shutdownGracePeriod: 5s
https:
enabled: true
Expand Down
1 change: 1 addition & 0 deletions config.sample.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
server:
# address: "127.0.0.1:8080"
withErrorHeader: true
shutdownGracePeriod: 5s
authorization:
- name: auth-name1
Expand Down
87 changes: 73 additions & 14 deletions pkg/callgate/callgate_queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,61 @@ import (
)

type Sequence struct {
token chan struct{}
mu sync.Mutex
busy bool
queue []chan struct{} // FIFO of waiters
}

func NewSequence() *Sequence {
l := &Sequence{
token: make(chan struct{}, 1),
}
l.token <- struct{}{}

return l
return &Sequence{} //nolint:exhaustruct
}

// Acquire blocks until the execution slot is available.
//
// This gate allows only one execution at a time. Calls are processed in sequence:
// if one is running, the next call waits until it completes.
// This gate allows only one execution at a time.
// Waiters are served in strict FIFO order.
//
// When the slot is acquired, Acquire returns a release function. The caller must
// call release() when the work is done.
// When the slot is acquired, Acquire returns a release function.
// The caller must call release() when the work is done.
//
// If the context is canceled before acquiring the slot, Acquire returns ctx.Err().
func (cg *Sequence) Acquire(ctx context.Context) (func(), error) {
// Fast path: not busy and no queue => acquire immediately.
cg.mu.Lock()
if !cg.busy && len(cg.queue) == 0 {
cg.busy = true

cg.mu.Unlock()

return cg.releaseFunc(), nil
}

// Otherwise, enqueue and wait for our turn.
waiter := make(chan struct{})

cg.queue = append(cg.queue, waiter)
cg.mu.Unlock()

select {
case <-ctx.Done():
return nil, fmt.Errorf("acquire sequence: %w", ctx.Err())
case <-cg.token:
// Remove ourselves from the queue if we haven't been granted the slot yet.
cg.mu.Lock()
removed := cg.removeWaiterLocked(waiter)
cg.mu.Unlock()

// If we were NOT removed, it means we were already granted (channel closed)
// roughly concurrently. In that case, we must return success, not ctx.Err().
// We detect "granted" by checking removed==false; but there is a race:
// - if channel closed just before we locked, removeWaiterLocked won't find it.
// In that case, proceed as acquired.
if removed {
return nil, fmt.Errorf("acquire sequence: %w", ctx.Err())
}

return cg.releaseFunc(), nil

case <-waiter:
// Granted in FIFO order.
return cg.releaseFunc(), nil
}
}
Expand All @@ -42,7 +71,37 @@ func (cg *Sequence) releaseFunc() func() {

return func() {
once.Do(func() {
cg.token <- struct{}{}
cg.mu.Lock()
defer cg.mu.Unlock()

// If someone is waiting, grant the next one in FIFO order.
if len(cg.queue) > 0 {
next := cg.queue[0]
// pop front
copy(cg.queue[0:], cg.queue[1:])
cg.queue = cg.queue[:len(cg.queue)-1]

// Keep busy=true, transfer ownership to the next waiter.
close(next)

return
}

// No waiters => free the slot.
cg.busy = false
})
}
}

func (cg *Sequence) removeWaiterLocked(waiter chan struct{}) bool {
for i := range cg.queue {
if cg.queue[i] == waiter {
copy(cg.queue[i:], cg.queue[i+1:])
cg.queue = cg.queue[:len(cg.queue)-1]

return true
}
}

return false
}
Loading