Skip to content

Commit 3d8c095

Browse files
authored
Merge pull request #4 from morfien101/feature/bug-fixes
Fixes lots of bugs and stability issues. Also fixes issue #4 Updates readme files for both humans and llm agents.
2 parents 6ea6467 + 80ab281 commit 3d8c095

File tree

24 files changed

+770
-101
lines changed

24 files changed

+770
-101
lines changed

.github/workflows/release_body.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,36 @@
1-
# Adding in more supported CPU architectures
1+
# Stability and bug fixes
22

3-
This can now be used on arm64 chips. This enables use on apple silicon and other arm64 chips.
3+
This release resolves a comprehensive set of bugs identified in a static analysis and manual code review. All critical, high, medium, and low severity issues have been addressed.
4+
5+
## Critical fixes
6+
- Fixed infinite recursion in `rotateWriter.Close()` — was calling itself instead of `w.fp.Close()`, crashing on every graceful shutdown
7+
- Fixed nil map panic in `FileLogManager``filetracker` map was never initialized, making file logging completely non-functional
8+
- Fixed nil map access in `FileLogManager.RegisterConfig()` — same root cause as above
9+
- Fixed data race in signal replicator — `listen()` iterated over `signalChannels` without holding the read lock
10+
- Fixed potential deadlock in signal replicator — sends to process channels are now non-blocking (`select` with `default`)
11+
- Fixed multiple timer callbacks on repeated signals — `time.AfterFunc` is now guarded with `sync.Once`
12+
13+
## High severity fixes
14+
- Fixed STDERR logging — was logging the full unsplit buffer for every line instead of the individual line
15+
- Removed always-false nil guards on struct fields — `&field == nil` is never true; removed four dead code blocks and the unused `createLoggingConfig` helper
16+
- Fixed watchdog goroutine never being started in `newRW()` — file rotation was silently not working
17+
- Fixed panic on empty write in `BytePipe.Write()` — added length guard before accessing the last byte
18+
- Fixed implicit nil error return in `BytePipe.Write()` — now returns explicit `nil` on success
19+
20+
## Medium severity fixes
21+
- Fixed inverted logic in `Process.running()` — was returning `p.exited` (true when stopped) instead of `!p.exited`
22+
- Fixed nil file handle after rename failure in `rotate()` — error is now logged and execution falls through to create a fresh file rather than returning with `w.fp = nil`
23+
- Removed unused global variable `fileLogManager`
24+
- Fixed goroutine leak in `LogManager.Submit()` — sending to a nil channel (missing map key) now guarded with an ok check
25+
- Fixed `log.Fatalf` format string — was using string concatenation instead of a format argument (missing comma)
26+
- Fixed data race on `p.exited` — assignment now wrapped in `Lock()`/`Unlock()` to match the `RLock()` in `running()`
27+
28+
## Low severity fixes
29+
- Renamed misleading `rotateWriter.panic()` method to `logError()` — it never panicked
30+
- Added safe ok-check for `*exec.ExitError` type assertion in `RunSecretProcess`
31+
- Added comments in `Console.Submit()` explaining why write errors are intentionally not reported
32+
33+
## Tests
34+
- Added regression test for `rotateWriter.Close()` to guard against infinite recursion
35+
- Added regression test for `FileLogManager` nil map to guard against uninitialized map panics
36+
- Added concurrent regression test for signal replicator to catch the data race under `-race`

Dockerfile.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM gcr.io/distroless/base
1+
FROM gcr.io/distroless/static-debian13
22

33
ADD ./passwd /etc/passwd
44

README.md

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,68 @@
11
# Launch
22

3-
A simple runtime-agnostic process manager that eases Docker services startup and logging.
4-
Consider it a half way house between Kubernetes and Docker.
3+
A lightweight process manager designed to run as PID 1 in Docker containers.
54

6-
Launch is expected to be process 1 in a container. It allows you to watch other processes in your containers and when they are all finished it will finish allowing containers to stop correctly.
5+
Launch lets you run multiple processes in a single container with centralized logging, ordered startup hooks, and graceful signal propagation.
76

8-
## What can Launch help with
7+
## Features
98

10-
Launch is designed to be a process manager with simple but powerful logging. It borrows some ideas from kubernetes without having to deploy a kubernetes stack.
9+
- Run **multiple processes** in one container.
10+
- **Secret collection** runs first and injects output as environment variables for all later processes.
11+
- **Init processes** run sequentially after secrets — any failure aborts startup.
12+
- **Main processes** run in parallel; if one exits, Launch sends SIGTERM to all remaining ones and waits for shutdown.
13+
- Ship logs to **console, file, syslog, or /dev/null** independently per process.
14+
- **Templated YAML config** — inject environment variables and use built-in helper functions.
1115

12-
* You can run multiple processes in a single container.
13-
* You can ship logs from processes to different logging engines.
14-
* You can run init processes that run before your main processes. This allows you to collect artifacts, secrets or just setup an environment.
15-
* A single main process dying will bring down a container, gracefully shutting down the other applications.
16+
## How it works
1617

17-
## Configuration
18-
19-
More details can be found in the [README Folder](./READMEs/)
18+
```
19+
Startup sequence:
20+
1. Read and render config file (first pass — container env vars available)
21+
2. Run secret processes → capture stdout JSON and inject as env vars
22+
3. Re-render config file (second pass — secret env vars now available)
23+
4. Run init processes sequentially — any non-zero exit aborts startup
24+
5. Start main processes in parallel
25+
6. Wait — when any main process exits, send SIGTERM to all others and shut down
26+
```
2027

2128
## Processes
2229

23-
Launch has 3 processes types when running.
30+
Launch supports three types of processes, each with a distinct role.
2431

2532
### Secret processes
2633

27-
These are used to collect secret data like username and passwords at startup.
34+
Run before anything else. They collect credentials or other secrets and expose them as environment variables. Because full logging is not yet available at this stage, output goes to the console.
2835

2936
### Init processes
3037

31-
These are used to configure the state of the container or collect more resources that don't need to be exported to environment variables.
38+
Run sequentially after secrets. Use these for setup tasks — running migrations, fetching config, preparing the filesystem. All init processes must succeed before main processes start.
3239

33-
### Main Processes
40+
### Main processes
3441

35-
These are the long running processes in your containers.
42+
The long-running processes in your container. They start in parallel and are expected to run for the lifetime of the container. If any one of them exits, Launch sends SIGTERM to all the others.
3643

3744
## Configuration
3845

39-
The configuration YAML file is the driving force behind Launch. The configuration file will tell Launch what processes to run with what arguments, where to send logs.
46+
The configuration is driven by a YAML file. Run the following to generate an annotated example:
47+
48+
```bash
49+
./launch -example-config
50+
```
4051

41-
The configuration file has a templating feature that allows you to make the configuration dynamic.
52+
Full documentation:
4253

43-
The configuration file has many sections that are documented in the
44-
[README dedicated to configuration](./READMEs/ConfigurationFile.MD).
54+
- [Configuration reference](./READMEs/ConfigurationFile.md) — all sections and options
55+
- [Logging engines](./READMEs/Logging.md) — console, file, syslog, `/dev/null`
56+
- [Secrets](./READMEs/Secrets.md) — how secret collection works
4557

4658
## Contributing
4759

48-
This project is still very much in active development. Expect changes and improvements.
60+
This project is still in active development. There is a simple build script for integration testing.
4961

50-
There is a simple build script that does the current integration testing.
62+
```bash
63+
# Run tests only
64+
./build_and_test.sh
5165

52-
Use `./build_and_test.sh` to run only the tests
53-
Use `./build_and_test.sh` gobuild" to also rebuild the go project
66+
# Rebuild the binary and run tests
67+
./build_and_test.sh gobuild
68+
```
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ An example of the configuration file can be obtained by running:
1111
./launch -example-config
1212
```
1313

14-
Below is each section of the configuration with the relevant values and details. If you would like more information regarding the configuration all data is layed out in the [configfile folder](../configfile).
14+
Below is each section of the configuration with the relevant values and details. If you would like more information regarding the configuration all data is laid out in the [configfile folder](../configfile).
1515

1616
## Understanding double rendering
1717

1818
Configuration files have templating built in, see later templating section. This allows for environment variables to be used in the configuration file.
1919
However many of those environment variables will be collected during the secrets and parameters collection phase.
2020

21-
The configuration file is rendered twice when the Launch is started.
22-
The first render will be done on start up, it will produce the configuration with the available environment variables as the container starts.
23-
The Launch will then proceed to collect secrets and parameters. Once complete the configuration is rendered a second time.
21+
The configuration file is rendered twice when Launch starts.
22+
The first render runs on startup and produces the configuration with the environment variables available at that point.
23+
Launch then proceeds to collect secrets and parameters. Once complete, the configuration is rendered a second time.
2424
This allows the configuration to make use of parameters and secrets as part of the configuration. Previously blanked environment settings will now be filled in.
2525

2626
An example use case is using an override for hostname in the syslog logging configuration, or setting a environment variable.
@@ -32,7 +32,7 @@ Secrets are only collected once at startup.
3232

3333
## process_manager
3434

35-
`process_manager` configures the Launch process itself. It needs to know where to send it's logs and also if it needs to enable debug logging.
35+
`process_manager` configures Launch itself. It needs to know where to send its logs and whether to enable debug logging.
3636

3737
Example:
3838

@@ -54,7 +54,7 @@ process_manager:
5454
5555
## processes
5656
57-
`processes` tells the Launch what start and where to send the logs. There is 3 sections here: secret_processes, init_processes and main_processes
57+
`processes` tells Launch what to start and where to send the logs. There are 3 sections: `secret_processes`, `init_processes`, and `main_processes`.
5858

5959
Example:
6060

@@ -142,7 +142,7 @@ default_logger_config:
142142
```
143143
## Logging
144144

145-
Logging is used to tell the Launch to send logs to a logging engine eg. syslog, console, etc...
145+
Logging is used to tell Launch where to send logs — e.g. syslog, console, etc.
146146

147147
Below is the logging configuration. However this is a slightly different one to the rest.
148148

READMEs/Logging.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ A more detailed description of the loggers that are available is documented belo
44

55
## Startup logging
66

7-
The Launch needs to start logging before it is actually capable of logging fully. This is because some loggers require authentication that is expected to be collected by the secrets.
8-
To overcome this the Launch will log to the console until after the secrets have been collected.
7+
Launch needs to start logging before it is fully configured to do so. This is because some loggers require credentials that are expected to be collected by the secrets phase.
8+
To overcome this, Launch logs to the console until after secrets have been collected.
99
At which point it will read the configuration file and use the specified loggers.
1010

1111
## Default logging
1212

13-
Default values for logging can be set. These values will be used if there is not value present for a process or they will be merged. This allows most the configuration to be setup here, and only specifics to be set at the process level.
13+
Default values for logging can be set. These values are used when no value is present for a process, or they are merged with per-process values. This allows most of the configuration to be set here, with only the specifics overridden at the process level.
1414

1515
Processes will still need to select a logging engine.
1616

@@ -32,11 +32,11 @@ File logging is only really useful in development environments. In most producti
3232

3333
## Syslog
3434

35-
Syslog is a pretty standard linux way of sending logs. These logs are sent as lines and multiline logs are unfortunetly split.
35+
Syslog is a standard Linux way of sending logs. Messages are sent line-by-line; multiline logs are unfortunately split across multiple entries.
3636

37-
The logger allows you to override the name that you see in syslog. By default it will use the process name. If you set the `program_name` key under the syslog logging configuration it will use that in its place.
37+
The logger allows you to override the name that you see in syslog. By default it uses the process name. If you set the `program_name` key under the syslog logging configuration, that value is used instead.
3838

39-
Due to the logs being presented to Launch via stdout we are not able to know if the log is critical, warning or informational. This information might be available in the text, however the Launch will simply forward on the message with out inspecting its contents by default.
39+
Because logs are presented to Launch via stdout, it cannot determine whether a log line is critical, a warning, or informational. By default, Launch forwards the message without inspecting its contents.
4040

4141
If you set `extract_log_level: true` the logger will attempt to detect the level from your message. There are limitations here and your messages need to be structured correctly.
4242

@@ -70,6 +70,6 @@ You can combine this with the configuration file templating to make names that r
7070

7171
The normal rules for host names will apply. No special chars, no space, etc...
7272

73-
For easier tracking of the instances themselves you can append the containers hostname to either the process or to the hostname. If you do this you need to include any _ or - as it will simple tack on the hostname.
74-
The can be set at the default configuration OR the process configuration.
73+
For easier tracking you can append the container hostname to either the process tag or the syslog hostname. If you do this you need to include any `_` or `-` separator yourself, as Launch will simply append the hostname directly.
74+
This can be set at the default configuration level or the process configuration level.
7575
Use `append_container_name_to_tag` and `append_container_name_to_hostname` to control these features.

READMEs/Secrets.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,21 @@ The idea is that you can create your own binary to use to collect secrets and ha
1515
Because Launch is the parent process, child processes CAN NOT update the environment of the parent.
1616
Therefore your process can not expose environment variables for later processes to see.
1717
```
18-
To over come this, Launch will read a key value pair in JSON as the stdout of your process and expose those as environment variables for you.
18+
To overcome this, Launch reads the stdout of your process as a JSON key/value object and exposes those as environment variables.
1919

2020
Output expected:
2121

2222
```json
2323
{"key":"value", "key2":"value2"}
2424
```
2525

26-
It is also possible that you process writes to files that other processes can collect. It is expected that no output to STDOUT is given in this case.
26+
It is also possible that your process writes to files that other processes can read. In this case no output to stdout is expected.
2727

28-
With this being said here are the requirements for Secret Processes:
28+
Requirements for secret processes:
2929

30-
1. They must complete successfully to continue execution.
31-
1. They are executed sequentially as shown in the configuration file.
32-
1. output must be valid JSON and the only output on STDOUT.
30+
1. They must exit successfully (exit code 0) to allow execution to continue.
31+
1. They are executed sequentially in the order listed in the configuration file.
32+
1. If they produce stdout, it must be a single valid JSON object and nothing else.
3333

3434
Secrets might not always need to be collected. Consider if you are using in a Dev environment.
3535
Make use of the `skip` field to stop a process from running.

build_and_test.sh

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,19 @@ if [[ $1 = "gobuild" ]]; then
88
echo "Building the go project!"
99
curdir=$(pwd)
1010
echo "Building launch"
11-
CGO_ENABLED=0 go build -a -installsuffix cgo -o launch .
11+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o launch .
1212
cd testbin
1313
echo "Building testbin"
14-
CGO_ENABLED=0 go build -a -installsuffix cgo -o testbin .
14+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o testbin .
1515
cd $curdir
1616
echo "Finished building"
1717
else
1818
echo "Skipping go build"
1919
fi
2020

21-
docker build -t morfien101/launch-test:latest -f Dockerfile.test .
22-
docker build -t morfien101/launch-test:debug -f Dockerfile.debug .
21+
# It's worth noting that distroless only supports linux, so we need to make sure we build for linux/amd64.
22+
docker build --no-cache --platform linux/amd64 -t morfien101/launch-test:latest -f Dockerfile.test .
23+
docker build --no-cache --platform linux/amd64 -t morfien101/launch-test:debug -f Dockerfile.debug .
2324

2425
echo "#########################"
2526
echo "## Running full config ##"

bytepipe/bytepipe.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@ func New() *BytePipe {
1313
}
1414

1515
func (bp *BytePipe) Write(p []byte) (n int, err error) {
16+
if len(p) == 0 {
17+
return 0, nil
18+
}
1619
if p[len(p)-1] != '\n' {
1720
p = append(p, '\n')
1821
}
1922

2023
bp.Ready <- string(p)
21-
return len(p), err
24+
return len(p), nil
2225
}
2326

2427
func (bp *BytePipe) Close() {

configfile/configfile.go

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,6 @@ func (cf *Config) setDefaultSecretTimeout() {
6666
// NOTE: setDefaultLoggerConfig should be called first
6767
//
6868
func (cf *Config) setDefaultProcessLogger() {
69-
createLoggingConfig := func(proc *Process) {
70-
proc.LoggerConfig = LoggingConfig{}
71-
}
7269
setName := func(proc *Process) {
7370
proc.LoggerConfig.ProcessName = proc.Name
7471
}
@@ -77,10 +74,6 @@ func (cf *Config) setDefaultProcessLogger() {
7774
}
7875
f := func(procList []*Process) {
7976
for _, proc := range procList {
80-
if &proc.LoggerConfig == nil {
81-
// Create a logging config
82-
createLoggingConfig(proc)
83-
}
8477
if proc.LoggerConfig.ProcessName == "" {
8578
setName(proc)
8679
}
@@ -95,21 +88,12 @@ func (cf *Config) setDefaultProcessLogger() {
9588
}
9689

9790
func (cf *Config) setDefaultProcessManager() {
98-
if &cf.ProcessManager == nil {
99-
cf.ProcessManager = defaultProcessManager
100-
}
101-
if &cf.ProcessManager.LoggerConfig == nil {
102-
cf.ProcessManager.LoggerConfig = defaultProcessManager.LoggerConfig
103-
}
10491
if len(cf.ProcessManager.LoggerConfig.Engine) == 0 {
10592
cf.ProcessManager.LoggerConfig.Engine = defaultProcessManager.LoggerConfig.Engine
10693
}
10794

10895
// Set defaults for logging engines under process manager context
10996
if cf.ProcessManager.LoggerConfig.Engine == "syslog" {
110-
if &cf.ProcessManager.LoggerConfig.Syslog == nil {
111-
cf.ProcessManager.LoggerConfig.Syslog = defaultProcessManagerSyslog
112-
}
11397
if cf.ProcessManager.LoggerConfig.Syslog.ProgramName == "" {
11498
cf.ProcessManager.LoggerConfig.Syslog.ProgramName = defaultProcessManagerSyslog.ProgramName
11599
}

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ require (
77
github.com/c2h5oh/datasize v0.0.0-20171227191756-4eba002a5eae
88
github.com/silverstagtech/gotracer v0.2.0
99
github.com/silverstagtech/srslog v0.2.1
10-
gopkg.in/yaml.v2 v2.2.2
10+
gopkg.in/yaml.v2 v2.2.8
1111
)
1212

1313
require (
1414
github.com/spf13/afero v1.9.4 // indirect
15-
golang.org/x/text v0.3.4 // indirect
15+
golang.org/x/text v0.3.8 // indirect
1616
)

0 commit comments

Comments
 (0)