py-ib-controller 0.1.4__tar.gz → 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/.github/workflows/ci.yml +4 -1
  2. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/Makefile +15 -0
  3. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/PKG-INFO +99 -22
  4. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/README.md +98 -21
  5. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/agent/src/main/java/ibcontroller/agent/AgentMain.java +67 -70
  6. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/agent/src/main/java/ibcontroller/agent/ComponentLookup.java +37 -83
  7. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/agent/src/main/java/ibcontroller/agent/EventBridge.java +74 -41
  8. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/agent/src/main/java/ibcontroller/agent/Protocol.java +25 -12
  9. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/agent/src/main/java/ibcontroller/agent/Security.java +3 -4
  10. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/agent/src/main/java/ibcontroller/agent/WriteOps.java +51 -99
  11. py_ib_controller-0.1.5/agent/src/test/java/ibcontroller/agent/AgentTests.java +243 -0
  12. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/docs/Configuration.md +62 -6
  13. py_ib_controller-0.1.5/docs/development.md +469 -0
  14. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/actions.py +97 -81
  15. py_ib_controller-0.1.5/ibcontroller/agent_client.py +605 -0
  16. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/cli.py +19 -55
  17. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/config.py +52 -34
  18. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/control_loop.py +247 -227
  19. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/data/builtin_settings.toml +1 -1
  20. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/data/ibcontroller.toml.example +43 -13
  21. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/data/ibkr_settings.toml.example +30 -0
  22. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/data/labels.json +51 -3
  23. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/data/labels.json.example +10 -11
  24. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/diagnostics.py +53 -18
  25. py_ib_controller-0.1.5/ibcontroller/dispatch.py +391 -0
  26. py_ib_controller-0.1.5/ibcontroller/errors.py +33 -0
  27. py_ib_controller-0.1.5/ibcontroller/ibcontroller-agent.jar +0 -0
  28. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/labels.py +183 -32
  29. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/launcher.py +283 -215
  30. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/logging_setup.py +10 -13
  31. py_ib_controller-0.1.5/ibcontroller/login.py +660 -0
  32. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/main.py +22 -13
  33. py_ib_controller-0.1.5/ibcontroller/management.py +124 -0
  34. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/recognisers.py +313 -111
  35. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/schedule.py +2 -1
  36. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/settings.py +158 -95
  37. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller.env +19 -7
  38. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/py_ib_controller.egg-info/PKG-INFO +99 -22
  39. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/py_ib_controller.egg-info/SOURCES.txt +7 -0
  40. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/py_ib_controller.egg-info/scm_file_list.json +7 -0
  41. py_ib_controller-0.1.5/py_ib_controller.egg-info/scm_version.json +8 -0
  42. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/pyproject.toml +1 -0
  43. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/conftest.py +12 -4
  44. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/fakes.py +42 -0
  45. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/test_actions.py +214 -196
  46. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/test_agent_client.py +456 -33
  47. py_ib_controller-0.1.5/tests/test_agent_jar.py +136 -0
  48. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/test_cli.py +74 -10
  49. py_ib_controller-0.1.5/tests/test_command_wire.py +162 -0
  50. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/test_config.py +34 -21
  51. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/test_control_loop.py +507 -295
  52. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/test_diagnostics.py +101 -4
  53. py_ib_controller-0.1.5/tests/test_dispatch.py +710 -0
  54. py_ib_controller-0.1.5/tests/test_errors.py +73 -0
  55. py_ib_controller-0.1.5/tests/test_labels.py +240 -0
  56. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/test_launcher.py +545 -350
  57. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/test_logging_setup.py +4 -4
  58. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/test_login.py +855 -323
  59. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/test_main.py +7 -8
  60. py_ib_controller-0.1.5/tests/test_management.py +306 -0
  61. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/test_recognisers.py +571 -35
  62. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/test_schedule.py +5 -3
  63. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/test_settings.py +505 -60
  64. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/test_trace_integration.py +12 -11
  65. py_ib_controller-0.1.4/docs/development.md +0 -252
  66. py_ib_controller-0.1.4/ibcontroller/agent_client.py +0 -466
  67. py_ib_controller-0.1.4/ibcontroller/dispatch.py +0 -288
  68. py_ib_controller-0.1.4/ibcontroller/ibcontroller-agent.jar +0 -0
  69. py_ib_controller-0.1.4/ibcontroller/login.py +0 -602
  70. py_ib_controller-0.1.4/py_ib_controller.egg-info/scm_version.json +0 -8
  71. py_ib_controller-0.1.4/tests/test_dispatch.py +0 -360
  72. py_ib_controller-0.1.4/tests/test_labels.py +0 -94
  73. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/.github/ISSUE_TEMPLATE.md +0 -0
  74. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/.github/dependabot.yml +0 -0
  75. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/.github/workflows/publish.yml +0 -0
  76. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/.gitignore +0 -0
  77. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/.pre-commit-config.yaml +0 -0
  78. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/.sdkmanrc +0 -0
  79. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/CONTRIBUTING.md +0 -0
  80. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/LICENSE.txt +0 -0
  81. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/docs/Architecture.svg +0 -0
  82. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/__init__.py +0 -0
  83. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/app_dirs.py +0 -0
  84. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/ibcontroller/py.typed +0 -0
  85. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/py_ib_controller.egg-info/dependency_links.txt +0 -0
  86. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/py_ib_controller.egg-info/entry_points.txt +0 -0
  87. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/py_ib_controller.egg-info/requires.txt +0 -0
  88. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/py_ib_controller.egg-info/top_level.txt +0 -0
  89. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/setup.cfg +0 -0
  90. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/__init__.py +0 -0
  91. {py_ib_controller-0.1.4 → py_ib_controller-0.1.5}/tests/test_app_dirs.py +0 -0
@@ -40,8 +40,11 @@ jobs:
40
40
  - name: Build Java agent jar
41
41
  run: make build
42
42
 
43
+ - name: Test Java agent
44
+ run: make test-java
45
+
43
46
  - name: Test
44
- run: uv run pytest
47
+ run: uv run pytest -n auto
45
48
 
46
49
  - name: Build
47
50
  run: uv build
@@ -19,6 +19,21 @@ build/ibcontroller-agent.jar: $(SRC)
19
19
  .PHONY: build
20
20
  build: build/ibcontroller-agent.jar
21
21
 
22
+ # Dependency-free Java unit tests (plain javac + java, same toolchain as the jar).
23
+ TEST_SRC := $(wildcard agent/src/test/java/ibcontroller/agent/*.java)
24
+
25
+ .PHONY: test-java
26
+ test-java: $(SRC) $(TEST_SRC)
27
+ mkdir -p build/test-classes
28
+ $(JAVAC) -d build/test-classes $(SRC) $(TEST_SRC)
29
+ $(JDK)/bin/java -Djava.awt.headless=true -cp build/test-classes ibcontroller.agent.AgentTests
30
+
31
+ # Everything: build the jar, run the Java unit tests, then the Python suite (which
32
+ # includes the tests that drive the built jar).
33
+ .PHONY: test
34
+ test: build test-java
35
+ uv run pytest -n auto
36
+
22
37
  # The Python package can't be built standalone -- `ibcontroller/ibcontroller-agent.jar`
23
38
  # must exist before `uv build` runs, since it's shipped via [tool.setuptools.package-data]
24
39
  # the same way ibcontroller/data/*.json is (see pyproject.toml). This target makes that
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-ib-controller
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Login/launch controller for TWS and IBKR Gateway, via an in-process Java agent
5
5
  Author-email: gnzsnz <8376642+gnzsnz@users.noreply.github.com>
6
6
  License-Expression: Apache-2.0
@@ -125,7 +125,7 @@ And the `ibcontroller run` parameters:
125
125
  │ --trading-mode <live|paper> Override Config.trading_mode ('live'/'paper') for this invocation. │
126
126
  │ --tws-path <path> Override Config.tws_path (the TWS/Gateway install-path inference) for this invocation. │
127
127
  │ --tws-channel <str> Override Config.tws_channel ('stable'/'latest', filters install auto-detection) for this invocation. │
128
- │ --program <str> Override Config.program ('gateway'/'tws') for this invocation. │
128
+ │ --program <gateway|tws> Override Config.program ('gateway'/'tws') for this invocation. │
129
129
  │ --tws-settings-path <path> Override Config.tws_settings_path (where TWS/Gateway stores its own settings) for this invocation. │
130
130
  │ --instance <str> Override Config.instance (per-instance log/trace/socket names) for this invocation. Defaults to '{program}-{trading_mode}', so │
131
131
  │ --trading-mode alone is usually enough to keep paper/live instances apart. │
@@ -197,7 +197,7 @@ When environment variable `IBC_APP_DIR` is not set, then
197
197
  | --- | --- | --- |
198
198
  | `config_dir` | `~/.config/ibcontroller` | Linux |
199
199
  | `config_dir` | `~/Library/Application Support/ibcontroller` | macOS |
200
- | `log_dir` | `~/.local/share/ibcontroller` | Linux |
200
+ | `log_dir` | `~/.local/state/ibcontroller/log` | Linux |
201
201
  | `log_dir` | `~/Library/Logs/ibcontroller` | macOS |
202
202
  | `socket_dir` | `/run/user/<uid>/ibcontroller` | Linux |
203
203
  | `socket_dir` | `~/Library/Caches/TemporaryItems/ibcontroller` | macOS |
@@ -208,6 +208,13 @@ When `IBC_APP_DIR` is set then:
208
208
  - `log_dir`-> `IBC_APP_DIR/log`
209
209
  - `socket_dir` -> `IBC_APP_DIR/run`
210
210
 
211
+ Socket paths (`socket_dir/ibcontroller-agent-<instance>-events.sock`) must fit
212
+ the Unix socket limit: 102 bytes on macOS, 106 on Linux. On macOS the default
213
+ `socket_dir` leaves room for an instance name of about 15 characters with a
214
+ 5-letter user name (each extra user-name character costs one). Over the limit,
215
+ `ibcontroller run` stops with an error naming the path; use a shorter instance
216
+ name or set `IBC_APP_DIR` to a shorter directory.
217
+
211
218
  ### Settings
212
219
 
213
220
  `ibcontroller`ships with sensible defaults. **Only the two credential env vars below are actually mandatory**
@@ -223,7 +230,7 @@ allowing `None`. `Config`'s schema is closed — an unknown key in
223
230
  | TOML key | Env var or CLI flag | Description | Default |
224
231
  | --- | --- | --- | --- |
225
232
  | `instance` | `IBC_INSTANCE`, `--instance` | Instance name; separate log/trace files per instance | `"{program}-{trading_mode}"` (e.g. `"gateway-paper"`) |
226
- | `program` | `IBC_PROGRAM`, `--program` | `"gateway"` or `"tws"` | `"gateway"` |
233
+ | `program` | `IBC_PROGRAM`, `--program` | `"gateway"` or `"tws"` (any letter case); anything else is a config error | `"gateway"` |
227
234
  | `tws_version` | `IBC_TWS_VERSION` | Installed TWS/Gateway version, e.g. `"10.50"` | `None` — auto-detected (see below) |
228
235
  | `tws_channel` | `IBC_TWS_CHANNEL`, `--tws-channel` | `"stable"`/`"latest"`; narrows auto-detection to one update channel | `"stable"` |
229
236
  | `tws_path` | `IBC_TWS_PATH`, `--tws-path` | Override TWS/gateway install-path inference | `None` |
@@ -237,6 +244,7 @@ allowing `None`. `Config`'s schema is closed — an unknown key in
237
244
  | `log_backup_count` | `IBC_LOG_BACKUP_COUNT` | Number of rotated backups to keep. Only respected when `log_sink="file"` | `5` |
238
245
  | `diagnostic_scope` | `IBC_DIAGNOSTIC_SCOPE` | `"known"`/`"unknown"`/`"all"` — which windows get a structure dump logged (see `docs/Configuration.md`'s "Diagnostics") | `"known"` |
239
246
  | `diagnostic_when` | `IBC_DIAGNOSTIC_WHEN` | `"open"`/`"openclose"`/`"never"` — when to log a structure dump | `"never"` |
247
+ | `diagnostic_settings_sweep` | `IBC_DIAGNOSTIC_SETTINGS_SWEEP` | Log a dump of each Global Configuration tree node in `labels.settings.sweep_paths` (see `docs/Configuration.md`'s "Diagnostics") | `false` |
240
248
  | (env only) | `IBC_APP_DIR`, `--app-dir` | Override file locations (config/log/run) for container mode; `--app-dir`/`init`/`run` set the env var for the invocation, same effect |
241
249
 
242
250
  TWS/ibgateway settings
@@ -244,15 +252,16 @@ TWS/ibgateway settings
244
252
  | TOML key | Env var or CLI flag | Description | Default |
245
253
  | --- | --- | --- | --- |
246
254
  | `trading_mode` | `IBC_TRADING_MODE`, `--trading-mode` | `"live"` or `"paper"`; which account to authenticate as | `"paper"` |
247
- | `read_only_login` | `IBC_READ_ONLY_LOGIN` | TWS-only; **not yet wired to any behavior** (loaded, unused — see TODO.md) | `false` |
255
+ | `read_only_login` | `IBC_READ_ONLY_LOGIN` | TWS-only (Gateway warns and logs in normally); clicks "Enter Read Only" on the 2FA frame instead of waiting for the push, no effect without 2FA (paper) | `false` |
248
256
  | `read_only_api` | `IBC_READ_ONLY_API` | `true`/`false` sets it; omit to leave unchanged. Applied to Gateway/TWS automatically -- see "Declarative configuration" below | `None` |
249
257
  | `accept_incoming_connections` | `IBC_ACCEPT_INCOMING_CONNECTIONS` | `manual`/`accept`/`reject` | `"manual"` |
250
258
  | `existing_session_action` | `IBC_EXISTING_SESSION_ACTION` | `manual`/`primary`/`primaryoverride`/`secondary` | `"manual"` |
251
259
  | `login_dialog_display_timeout` | `IBC_LOGIN_DIALOG_DISPLAY_TIMEOUT` | Seconds to wait for the login dialog | `60.0` |
252
- | `mfa_timeout` | `IBC_MFA_TIMEOUT` | IB's 2FA timeout in seconds | `180.0` |
253
- | `relogin_after_mfa_timeout` | `IBC_RELOGIN_AFTER_MFA_TIMEOUT` | Restart login if 2FA times out | `false` |
254
- | `mfa_exit_interval` | `IBC_MFA_EXIT_INTERVAL` | Bounds the post-2FA wait when relogin enabled | `60.0` |
255
- | `mfa_timeout_action` | `IBC_MFA_TIMEOUT_ACTION` | `exit` (exit 75, see "Exit codes" above) or `restart` (relaunch with a fresh login) when the 2FA push goes unanswered until `mfa_timeout` or `mfa_exit_interval` fires | `"exit"` |
260
+ | `mfa_timeout` | `IBC_MFA_TIMEOUT` | IB's 2FA limit in seconds; a dialog closing at or after it counts as timed out | `180.0` |
261
+ | `relogin_after_mfa_timeout` | `IBC_RELOGIN_AFTER_MFA_TIMEOUT` | Log in again after a timed-out push, and enforce `mfa_exit_interval` after an approved one | `false` |
262
+ | `mfa_exit_interval` | `IBC_MFA_EXIT_INTERVAL` | Post-approval wait for login to complete when relogin is on | `60.0` |
263
+ | `mfa_timeout_action` | `IBC_MFA_TIMEOUT_ACTION` | `exit` (exit 75, see "Exit codes" above) or `restart` (relaunch with a fresh login) when `mfa_exit_interval` expires after an approved push | `"exit"` |
264
+ | `restart_cap` | `IBC_RESTART_CAP` | Max consecutive relaunches that never reach READY (login frame timeout, transient login failure, logout, crash during login); then exit 70 | `3` |
256
265
  | `auto_restart_time` | `IBC_AUTO_RESTART_TIME` | `"hh:mm AM/PM"` daily auto-restart time. Applied to Gateway/TWS automatically -- see "Declarative configuration" below | `None` |
257
266
  | `auto_logoff_time` | `IBC_AUTO_LOGOFF_TIME` | `"hh:mm AM/PM"` daily auto-logoff time; same "Lock and Exit" radio-button pair as `auto_restart_time` -- if both are set, `auto_restart_time` wins | `None` |
258
267
  | `time_zone` | `IBC_TIME_ZONE` | IANA zone name, e.g. `"Europe/Zurich"`, written to `jts.ini`'s `TimeZone`. Auto-detected from `TZ`, then `/etc/localtime` -- no setup needed in the common case; never overwrites an existing `TimeZone` line | auto-detected |
@@ -313,10 +322,19 @@ more fields of the bundled default, field by field — you only need to include
313
322
  you're changing, not the whole file. Use it when a TWS/Gateway release renames a button
314
323
  or dialog title ibcontroller matches against.
315
324
 
325
+ A malformed file is rejected at startup (exit code 78) with a message naming the file and
326
+ the field: a string where a list is expected, an empty list of titles or buttons, invalid
327
+ JSON, or a `dismiss_rules` entry without `match_title`/`match_text`. A misspelled key is
328
+ logged as a warning and ignored, so check the log if an override seems to have no effect.
329
+
330
+ `generic.dismiss_buttons` lists the buttons tried, in order, to close a pop-up no specific
331
+ handler recognises (default `OK`, `Close`, `Dismiss`).
332
+
316
333
  The one part of `labels.json` that isn't just text is `dismiss_rules` — a list of
317
334
  "if a window matches, click this button" entries, checked on every window that opens.
318
335
  Each entry needs at least one of `match_title`/`match_text` (both, if given, must
319
- match); `click` is the button's label.
336
+ match); `click` is the button's label. `match_text` also matches message text with HTML
337
+ tags removed and entities decoded, so copy the phrase as it shows on screen.
320
338
 
321
339
  ```json
322
340
  {
@@ -330,13 +348,18 @@ match); `click` is the button's label.
330
348
  }
331
349
  ```
332
350
 
333
- `dismiss_rules` is a list, not a map — an override *replaces the whole list*, so if you
334
- want to keep the bundled entries (the non-brokerage-account warning, the auto-restart
335
- confirmation notice) alongside your own, copy them into your override file too rather
336
- than starting from an empty list. Good for any pop-up that's always handled the same
351
+ `dismiss_rules` entries are merged by `name`: an entry with a bundled rule's name replaces
352
+ that rule, an entry with a new name is added, and bundled rules (the non-brokerage-account
353
+ warning, the auto-restart confirmation notice) you don't name are kept. Removing a bundled
354
+ rule is not supported. Good for any pop-up that's always handled the same
337
355
  simple way; a dialog needing real logic (parsing a value out of its text, retrying,
338
356
  raising an error) isn't expressible this way and needs actual code.
339
357
 
358
+ Login-outcome dialogs ("Login failed", "Unrecognized Username or Password", the connection
359
+ and too-many-attempts warnings) and the "Shutdown progress" / "Restart in progress" windows
360
+ are always handled first. A `dismiss_rules` entry that matches one of them never runs, so a
361
+ credential rejection still exits with code 77 and a restart is still recognised as one.
362
+
340
363
  ### `ibkr_settings.toml` — Global Configuration settings
341
364
 
342
365
  Global Configuration settings (the "Configure" / "Settings" dialog inside Gateway/TWS)
@@ -351,7 +374,13 @@ are applied once, right after login, from two layers combined into one list:
351
374
  - **Your own `ibkr_settings.toml`** — anything else: any other Global Configuration
352
375
  checkbox or text field, activated by pointing `[settings] file = "..."` (or
353
376
  `IBC_SETTINGS_FILE`) at a real file (rename `ibkr_settings.toml.example` to
354
- start from the bundled worked examples, e.g. the `api_precautions_*` toggles).
377
+ start from the bundled worked examples: the `api_precautions_*` toggles, plus commented
378
+ entries for the API socket port, the master API client ID and "Send market data in lots
379
+ for US stocks" under `API/Settings`).
380
+
381
+ When no entry resolves to a value (the default), the dialog is not opened. Only the TWS Mosaic
382
+ layout is supported: on TWS Classic no setting is applied and the attempt waits for
383
+ `mfa_timeout` (default 180s) before failing. Gateway is not affected.
355
384
 
356
385
  An entry looks like this:
357
386
 
@@ -504,6 +533,42 @@ or both.
504
533
  instances (e.g. `paper` and `live`) can run in parallel and each tail its own files
505
534
  without interleaving, even if copied or globbed out of their directory.
506
535
 
536
+ ## Finding the TWS / Gateway installation
537
+
538
+ ibcontroller does not install anything: it launches the TWS or Gateway you already
539
+ installed with IBKR's own installer, using that install's jars and bundled JRE. It
540
+ resolves the installation in three steps.
541
+
542
+ 1. **Install root** (`tws_path`). Unless set, it defaults to:
543
+
544
+ | OS | Default root |
545
+ | --- | --- |
546
+ | macOS | `~/Applications` |
547
+ | Linux | `~/Jts` |
548
+
549
+ 2. **Version** (`tws_version`). A pinned `tws_version` is used as-is. Otherwise
550
+ ibcontroller scans the root, picks the greatest installed version and logs the
551
+ choice; `tws_channel` narrows the scan first. See "`tws_version` auto-detection"
552
+ above.
553
+
554
+ 3. **Install directory.** Version and `program` select the directory below the root:
555
+
556
+ | OS | Gateway | TWS |
557
+ | --- | --- | --- |
558
+ | macOS | `IB Gateway <version>/` | `Trader Workstation <version>/` |
559
+ | Linux | `ibgateway/<version>/` | `<version>/` |
560
+
561
+ A directory counts as an installation only if it has a `jars/` subfolder. The
562
+ classpath is built from those jars plus the install's `.install4j/i4jruntime.jar`,
563
+ and the JVM options come from its `ibgateway.vmoptions` / `tws.vmoptions`.
564
+
565
+ If nothing matches, ibcontroller exits with an error naming `tws_path` and
566
+ `tws_channel`. A `program = "tws"` request with no TWS install falls back to a
567
+ Gateway install; the reverse does not (details above).
568
+
569
+ Settings (`tws_settings_path`) are separate from the install root: each instance
570
+ defaults to its own `~/Jts/<instance>`, so `paper` and `live` do not share state.
571
+
507
572
  ## Exit codes
508
573
 
509
574
  `ibcontroller run` exits with a [BSD sysexits](https://man.freebsd.org/cgi/man.cgi?query=sysexits)
@@ -513,22 +578,34 @@ code, grouped by what a supervisor (a container's `restart:` policy, systemd's
513
578
 
514
579
  | Code | Meaning | Supervisor action |
515
580
  | --- | --- | --- |
516
- | `0` | Intentional stop (Ctrl-C, `closedown_at`, Gateway/TWS closed normally, e.g. File>Close) | Do not restart |
581
+ | `0` | Intentional stop (Ctrl-C, `closedown_at`, Gateway/TWS closed normally, e.g. File>Close, or this session yielded to another via `existing_session_action`: `stopped: SESSION_CEDED`) | Do not restart |
517
582
  | `1` | Unhandled exception (a bug) | Restart, report |
518
583
  | `2` | CLI usage error | Fix the command |
519
584
  | `78` (`EX_CONFIG`) | Deployment error (bad config, install not found) | Do not retry |
520
585
  | `77` (`EX_NOPERM`) | Credentials rejected | Do not retry (lockout risk) |
521
- | `75` (`EX_TEMPFAIL`) | 2FA not approved in time, `mfa_timeout_action=exit` | Retry per policy |
522
- | `69` (`EX_UNAVAILABLE`) | Transient runtime failure (process crashed or killed, connection lost, JVM exited before becoming ready, login timed out with no 2FA dialog) | Retry |
586
+ | `76` (`EX_PROTOCOL`) | A dialog `existing_session_action` rules out (Trading Login Handoff with `secondary`/`primaryoverride`) | Do not retry |
587
+ | `75` (`EX_TEMPFAIL`) | Login did not complete after 2FA, `mfa_timeout_action=exit` | Retry per policy |
588
+ | `70` (`EX_SOFTWARE`) | `restart_cap` consecutive relaunches never reached READY | Do not retry |
589
+ | `69` (`EX_UNAVAILABLE`) | Transient runtime failure (process crashed or killed, connection lost, JVM exited before becoming ready) | Retry |
523
590
 
524
- `77`/`78` work with systemd's `RestartPreventExitStatus=`.
591
+ `70`/`76`/`77`/`78` work with systemd's `RestartPreventExitStatus=`.
525
592
 
526
593
  Some login-blocking dialogs IB reports as a server-side condition -- not a credential
527
- rejection -- never reach this table at all: a "Login failed"/"Login Error" dialog, or a
594
+ rejection -- never reach this table at all: a "Login failed"/"Login Error"/"not currently available" dialog, or a
528
595
  Gateway "Connection to server failed" reason other than a known credential/account one
529
596
  (e.g. a stale restart token), make ibcontroller relaunch in-process with a full fresh
530
- login, unbounded, the same way a scheduled `cold_restart_time` does -- the container
531
- itself never exits or restarts for these.
597
+ login, the same way a scheduled `cold_restart_time` does -- the container
598
+ itself does not exit for these, until `restart_cap` consecutive relaunches never reach READY
599
+ (exit 70).
600
+
601
+ ## Stopping under Docker
602
+
603
+ On SIGTERM (`docker stop`, Ctrl-C) a logged-in instance closes through File > Exit/Close so
604
+ TWS/Gateway can save its session and upload settings. Measured over 88 shutdowns: mean 1.3 s,
605
+ p99 4.3 s, max 4.9 s. A slow path (a dialog open, a stuck menu) can take longer: the menu retry
606
+ waits up to 5 s, the exit up to 8 s, then SIGTERM gets 5 s. Docker sends SIGKILL after 10 s by
607
+ default, which loses the graceful close. Set `stop_grace_period: 30s` (Compose) or
608
+ `docker stop -t 30` to keep it.
532
609
 
533
610
  ## License
534
611
 
@@ -103,7 +103,7 @@ And the `ibcontroller run` parameters:
103
103
  │ --trading-mode <live|paper> Override Config.trading_mode ('live'/'paper') for this invocation. │
104
104
  │ --tws-path <path> Override Config.tws_path (the TWS/Gateway install-path inference) for this invocation. │
105
105
  │ --tws-channel <str> Override Config.tws_channel ('stable'/'latest', filters install auto-detection) for this invocation. │
106
- │ --program <str> Override Config.program ('gateway'/'tws') for this invocation. │
106
+ │ --program <gateway|tws> Override Config.program ('gateway'/'tws') for this invocation. │
107
107
  │ --tws-settings-path <path> Override Config.tws_settings_path (where TWS/Gateway stores its own settings) for this invocation. │
108
108
  │ --instance <str> Override Config.instance (per-instance log/trace/socket names) for this invocation. Defaults to '{program}-{trading_mode}', so │
109
109
  │ --trading-mode alone is usually enough to keep paper/live instances apart. │
@@ -175,7 +175,7 @@ When environment variable `IBC_APP_DIR` is not set, then
175
175
  | --- | --- | --- |
176
176
  | `config_dir` | `~/.config/ibcontroller` | Linux |
177
177
  | `config_dir` | `~/Library/Application Support/ibcontroller` | macOS |
178
- | `log_dir` | `~/.local/share/ibcontroller` | Linux |
178
+ | `log_dir` | `~/.local/state/ibcontroller/log` | Linux |
179
179
  | `log_dir` | `~/Library/Logs/ibcontroller` | macOS |
180
180
  | `socket_dir` | `/run/user/<uid>/ibcontroller` | Linux |
181
181
  | `socket_dir` | `~/Library/Caches/TemporaryItems/ibcontroller` | macOS |
@@ -186,6 +186,13 @@ When `IBC_APP_DIR` is set then:
186
186
  - `log_dir`-> `IBC_APP_DIR/log`
187
187
  - `socket_dir` -> `IBC_APP_DIR/run`
188
188
 
189
+ Socket paths (`socket_dir/ibcontroller-agent-<instance>-events.sock`) must fit
190
+ the Unix socket limit: 102 bytes on macOS, 106 on Linux. On macOS the default
191
+ `socket_dir` leaves room for an instance name of about 15 characters with a
192
+ 5-letter user name (each extra user-name character costs one). Over the limit,
193
+ `ibcontroller run` stops with an error naming the path; use a shorter instance
194
+ name or set `IBC_APP_DIR` to a shorter directory.
195
+
189
196
  ### Settings
190
197
 
191
198
  `ibcontroller`ships with sensible defaults. **Only the two credential env vars below are actually mandatory**
@@ -201,7 +208,7 @@ allowing `None`. `Config`'s schema is closed — an unknown key in
201
208
  | TOML key | Env var or CLI flag | Description | Default |
202
209
  | --- | --- | --- | --- |
203
210
  | `instance` | `IBC_INSTANCE`, `--instance` | Instance name; separate log/trace files per instance | `"{program}-{trading_mode}"` (e.g. `"gateway-paper"`) |
204
- | `program` | `IBC_PROGRAM`, `--program` | `"gateway"` or `"tws"` | `"gateway"` |
211
+ | `program` | `IBC_PROGRAM`, `--program` | `"gateway"` or `"tws"` (any letter case); anything else is a config error | `"gateway"` |
205
212
  | `tws_version` | `IBC_TWS_VERSION` | Installed TWS/Gateway version, e.g. `"10.50"` | `None` — auto-detected (see below) |
206
213
  | `tws_channel` | `IBC_TWS_CHANNEL`, `--tws-channel` | `"stable"`/`"latest"`; narrows auto-detection to one update channel | `"stable"` |
207
214
  | `tws_path` | `IBC_TWS_PATH`, `--tws-path` | Override TWS/gateway install-path inference | `None` |
@@ -215,6 +222,7 @@ allowing `None`. `Config`'s schema is closed — an unknown key in
215
222
  | `log_backup_count` | `IBC_LOG_BACKUP_COUNT` | Number of rotated backups to keep. Only respected when `log_sink="file"` | `5` |
216
223
  | `diagnostic_scope` | `IBC_DIAGNOSTIC_SCOPE` | `"known"`/`"unknown"`/`"all"` — which windows get a structure dump logged (see `docs/Configuration.md`'s "Diagnostics") | `"known"` |
217
224
  | `diagnostic_when` | `IBC_DIAGNOSTIC_WHEN` | `"open"`/`"openclose"`/`"never"` — when to log a structure dump | `"never"` |
225
+ | `diagnostic_settings_sweep` | `IBC_DIAGNOSTIC_SETTINGS_SWEEP` | Log a dump of each Global Configuration tree node in `labels.settings.sweep_paths` (see `docs/Configuration.md`'s "Diagnostics") | `false` |
218
226
  | (env only) | `IBC_APP_DIR`, `--app-dir` | Override file locations (config/log/run) for container mode; `--app-dir`/`init`/`run` set the env var for the invocation, same effect |
219
227
 
220
228
  TWS/ibgateway settings
@@ -222,15 +230,16 @@ TWS/ibgateway settings
222
230
  | TOML key | Env var or CLI flag | Description | Default |
223
231
  | --- | --- | --- | --- |
224
232
  | `trading_mode` | `IBC_TRADING_MODE`, `--trading-mode` | `"live"` or `"paper"`; which account to authenticate as | `"paper"` |
225
- | `read_only_login` | `IBC_READ_ONLY_LOGIN` | TWS-only; **not yet wired to any behavior** (loaded, unused — see TODO.md) | `false` |
233
+ | `read_only_login` | `IBC_READ_ONLY_LOGIN` | TWS-only (Gateway warns and logs in normally); clicks "Enter Read Only" on the 2FA frame instead of waiting for the push, no effect without 2FA (paper) | `false` |
226
234
  | `read_only_api` | `IBC_READ_ONLY_API` | `true`/`false` sets it; omit to leave unchanged. Applied to Gateway/TWS automatically -- see "Declarative configuration" below | `None` |
227
235
  | `accept_incoming_connections` | `IBC_ACCEPT_INCOMING_CONNECTIONS` | `manual`/`accept`/`reject` | `"manual"` |
228
236
  | `existing_session_action` | `IBC_EXISTING_SESSION_ACTION` | `manual`/`primary`/`primaryoverride`/`secondary` | `"manual"` |
229
237
  | `login_dialog_display_timeout` | `IBC_LOGIN_DIALOG_DISPLAY_TIMEOUT` | Seconds to wait for the login dialog | `60.0` |
230
- | `mfa_timeout` | `IBC_MFA_TIMEOUT` | IB's 2FA timeout in seconds | `180.0` |
231
- | `relogin_after_mfa_timeout` | `IBC_RELOGIN_AFTER_MFA_TIMEOUT` | Restart login if 2FA times out | `false` |
232
- | `mfa_exit_interval` | `IBC_MFA_EXIT_INTERVAL` | Bounds the post-2FA wait when relogin enabled | `60.0` |
233
- | `mfa_timeout_action` | `IBC_MFA_TIMEOUT_ACTION` | `exit` (exit 75, see "Exit codes" above) or `restart` (relaunch with a fresh login) when the 2FA push goes unanswered until `mfa_timeout` or `mfa_exit_interval` fires | `"exit"` |
238
+ | `mfa_timeout` | `IBC_MFA_TIMEOUT` | IB's 2FA limit in seconds; a dialog closing at or after it counts as timed out | `180.0` |
239
+ | `relogin_after_mfa_timeout` | `IBC_RELOGIN_AFTER_MFA_TIMEOUT` | Log in again after a timed-out push, and enforce `mfa_exit_interval` after an approved one | `false` |
240
+ | `mfa_exit_interval` | `IBC_MFA_EXIT_INTERVAL` | Post-approval wait for login to complete when relogin is on | `60.0` |
241
+ | `mfa_timeout_action` | `IBC_MFA_TIMEOUT_ACTION` | `exit` (exit 75, see "Exit codes" above) or `restart` (relaunch with a fresh login) when `mfa_exit_interval` expires after an approved push | `"exit"` |
242
+ | `restart_cap` | `IBC_RESTART_CAP` | Max consecutive relaunches that never reach READY (login frame timeout, transient login failure, logout, crash during login); then exit 70 | `3` |
234
243
  | `auto_restart_time` | `IBC_AUTO_RESTART_TIME` | `"hh:mm AM/PM"` daily auto-restart time. Applied to Gateway/TWS automatically -- see "Declarative configuration" below | `None` |
235
244
  | `auto_logoff_time` | `IBC_AUTO_LOGOFF_TIME` | `"hh:mm AM/PM"` daily auto-logoff time; same "Lock and Exit" radio-button pair as `auto_restart_time` -- if both are set, `auto_restart_time` wins | `None` |
236
245
  | `time_zone` | `IBC_TIME_ZONE` | IANA zone name, e.g. `"Europe/Zurich"`, written to `jts.ini`'s `TimeZone`. Auto-detected from `TZ`, then `/etc/localtime` -- no setup needed in the common case; never overwrites an existing `TimeZone` line | auto-detected |
@@ -291,10 +300,19 @@ more fields of the bundled default, field by field — you only need to include
291
300
  you're changing, not the whole file. Use it when a TWS/Gateway release renames a button
292
301
  or dialog title ibcontroller matches against.
293
302
 
303
+ A malformed file is rejected at startup (exit code 78) with a message naming the file and
304
+ the field: a string where a list is expected, an empty list of titles or buttons, invalid
305
+ JSON, or a `dismiss_rules` entry without `match_title`/`match_text`. A misspelled key is
306
+ logged as a warning and ignored, so check the log if an override seems to have no effect.
307
+
308
+ `generic.dismiss_buttons` lists the buttons tried, in order, to close a pop-up no specific
309
+ handler recognises (default `OK`, `Close`, `Dismiss`).
310
+
294
311
  The one part of `labels.json` that isn't just text is `dismiss_rules` — a list of
295
312
  "if a window matches, click this button" entries, checked on every window that opens.
296
313
  Each entry needs at least one of `match_title`/`match_text` (both, if given, must
297
- match); `click` is the button's label.
314
+ match); `click` is the button's label. `match_text` also matches message text with HTML
315
+ tags removed and entities decoded, so copy the phrase as it shows on screen.
298
316
 
299
317
  ```json
300
318
  {
@@ -308,13 +326,18 @@ match); `click` is the button's label.
308
326
  }
309
327
  ```
310
328
 
311
- `dismiss_rules` is a list, not a map — an override *replaces the whole list*, so if you
312
- want to keep the bundled entries (the non-brokerage-account warning, the auto-restart
313
- confirmation notice) alongside your own, copy them into your override file too rather
314
- than starting from an empty list. Good for any pop-up that's always handled the same
329
+ `dismiss_rules` entries are merged by `name`: an entry with a bundled rule's name replaces
330
+ that rule, an entry with a new name is added, and bundled rules (the non-brokerage-account
331
+ warning, the auto-restart confirmation notice) you don't name are kept. Removing a bundled
332
+ rule is not supported. Good for any pop-up that's always handled the same
315
333
  simple way; a dialog needing real logic (parsing a value out of its text, retrying,
316
334
  raising an error) isn't expressible this way and needs actual code.
317
335
 
336
+ Login-outcome dialogs ("Login failed", "Unrecognized Username or Password", the connection
337
+ and too-many-attempts warnings) and the "Shutdown progress" / "Restart in progress" windows
338
+ are always handled first. A `dismiss_rules` entry that matches one of them never runs, so a
339
+ credential rejection still exits with code 77 and a restart is still recognised as one.
340
+
318
341
  ### `ibkr_settings.toml` — Global Configuration settings
319
342
 
320
343
  Global Configuration settings (the "Configure" / "Settings" dialog inside Gateway/TWS)
@@ -329,7 +352,13 @@ are applied once, right after login, from two layers combined into one list:
329
352
  - **Your own `ibkr_settings.toml`** — anything else: any other Global Configuration
330
353
  checkbox or text field, activated by pointing `[settings] file = "..."` (or
331
354
  `IBC_SETTINGS_FILE`) at a real file (rename `ibkr_settings.toml.example` to
332
- start from the bundled worked examples, e.g. the `api_precautions_*` toggles).
355
+ start from the bundled worked examples: the `api_precautions_*` toggles, plus commented
356
+ entries for the API socket port, the master API client ID and "Send market data in lots
357
+ for US stocks" under `API/Settings`).
358
+
359
+ When no entry resolves to a value (the default), the dialog is not opened. Only the TWS Mosaic
360
+ layout is supported: on TWS Classic no setting is applied and the attempt waits for
361
+ `mfa_timeout` (default 180s) before failing. Gateway is not affected.
333
362
 
334
363
  An entry looks like this:
335
364
 
@@ -482,6 +511,42 @@ or both.
482
511
  instances (e.g. `paper` and `live`) can run in parallel and each tail its own files
483
512
  without interleaving, even if copied or globbed out of their directory.
484
513
 
514
+ ## Finding the TWS / Gateway installation
515
+
516
+ ibcontroller does not install anything: it launches the TWS or Gateway you already
517
+ installed with IBKR's own installer, using that install's jars and bundled JRE. It
518
+ resolves the installation in three steps.
519
+
520
+ 1. **Install root** (`tws_path`). Unless set, it defaults to:
521
+
522
+ | OS | Default root |
523
+ | --- | --- |
524
+ | macOS | `~/Applications` |
525
+ | Linux | `~/Jts` |
526
+
527
+ 2. **Version** (`tws_version`). A pinned `tws_version` is used as-is. Otherwise
528
+ ibcontroller scans the root, picks the greatest installed version and logs the
529
+ choice; `tws_channel` narrows the scan first. See "`tws_version` auto-detection"
530
+ above.
531
+
532
+ 3. **Install directory.** Version and `program` select the directory below the root:
533
+
534
+ | OS | Gateway | TWS |
535
+ | --- | --- | --- |
536
+ | macOS | `IB Gateway <version>/` | `Trader Workstation <version>/` |
537
+ | Linux | `ibgateway/<version>/` | `<version>/` |
538
+
539
+ A directory counts as an installation only if it has a `jars/` subfolder. The
540
+ classpath is built from those jars plus the install's `.install4j/i4jruntime.jar`,
541
+ and the JVM options come from its `ibgateway.vmoptions` / `tws.vmoptions`.
542
+
543
+ If nothing matches, ibcontroller exits with an error naming `tws_path` and
544
+ `tws_channel`. A `program = "tws"` request with no TWS install falls back to a
545
+ Gateway install; the reverse does not (details above).
546
+
547
+ Settings (`tws_settings_path`) are separate from the install root: each instance
548
+ defaults to its own `~/Jts/<instance>`, so `paper` and `live` do not share state.
549
+
485
550
  ## Exit codes
486
551
 
487
552
  `ibcontroller run` exits with a [BSD sysexits](https://man.freebsd.org/cgi/man.cgi?query=sysexits)
@@ -491,22 +556,34 @@ code, grouped by what a supervisor (a container's `restart:` policy, systemd's
491
556
 
492
557
  | Code | Meaning | Supervisor action |
493
558
  | --- | --- | --- |
494
- | `0` | Intentional stop (Ctrl-C, `closedown_at`, Gateway/TWS closed normally, e.g. File>Close) | Do not restart |
559
+ | `0` | Intentional stop (Ctrl-C, `closedown_at`, Gateway/TWS closed normally, e.g. File>Close, or this session yielded to another via `existing_session_action`: `stopped: SESSION_CEDED`) | Do not restart |
495
560
  | `1` | Unhandled exception (a bug) | Restart, report |
496
561
  | `2` | CLI usage error | Fix the command |
497
562
  | `78` (`EX_CONFIG`) | Deployment error (bad config, install not found) | Do not retry |
498
563
  | `77` (`EX_NOPERM`) | Credentials rejected | Do not retry (lockout risk) |
499
- | `75` (`EX_TEMPFAIL`) | 2FA not approved in time, `mfa_timeout_action=exit` | Retry per policy |
500
- | `69` (`EX_UNAVAILABLE`) | Transient runtime failure (process crashed or killed, connection lost, JVM exited before becoming ready, login timed out with no 2FA dialog) | Retry |
564
+ | `76` (`EX_PROTOCOL`) | A dialog `existing_session_action` rules out (Trading Login Handoff with `secondary`/`primaryoverride`) | Do not retry |
565
+ | `75` (`EX_TEMPFAIL`) | Login did not complete after 2FA, `mfa_timeout_action=exit` | Retry per policy |
566
+ | `70` (`EX_SOFTWARE`) | `restart_cap` consecutive relaunches never reached READY | Do not retry |
567
+ | `69` (`EX_UNAVAILABLE`) | Transient runtime failure (process crashed or killed, connection lost, JVM exited before becoming ready) | Retry |
501
568
 
502
- `77`/`78` work with systemd's `RestartPreventExitStatus=`.
569
+ `70`/`76`/`77`/`78` work with systemd's `RestartPreventExitStatus=`.
503
570
 
504
571
  Some login-blocking dialogs IB reports as a server-side condition -- not a credential
505
- rejection -- never reach this table at all: a "Login failed"/"Login Error" dialog, or a
572
+ rejection -- never reach this table at all: a "Login failed"/"Login Error"/"not currently available" dialog, or a
506
573
  Gateway "Connection to server failed" reason other than a known credential/account one
507
574
  (e.g. a stale restart token), make ibcontroller relaunch in-process with a full fresh
508
- login, unbounded, the same way a scheduled `cold_restart_time` does -- the container
509
- itself never exits or restarts for these.
575
+ login, the same way a scheduled `cold_restart_time` does -- the container
576
+ itself does not exit for these, until `restart_cap` consecutive relaunches never reach READY
577
+ (exit 70).
578
+
579
+ ## Stopping under Docker
580
+
581
+ On SIGTERM (`docker stop`, Ctrl-C) a logged-in instance closes through File > Exit/Close so
582
+ TWS/Gateway can save its session and upload settings. Measured over 88 shutdowns: mean 1.3 s,
583
+ p99 4.3 s, max 4.9 s. A slow path (a dialog open, a stuck menu) can take longer: the menu retry
584
+ waits up to 5 s, the exit up to 8 s, then SIGTERM gets 5 s. Docker sends SIGKILL after 10 s by
585
+ default, which loses the graceful close. Set `stop_grace_period: 30s` (Compose) or
586
+ `docker stop -t 30` to keep it.
510
587
 
511
588
  ## License
512
589