Skip to content

feat(profile): add --profile flag for isolated instances - #128

Merged
ErikBjare merged 3 commits into
ActivityWatch:masterfrom
TimeToBuildBob:feat/profile-flag
Aug 25, 2026
Merged

ErikBjare merged 3 commits into
ActivityWatch:masterfrom
TimeToBuildBob:feat/profile-flag

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Part of ActivityWatch/activitywatch#1399 (isolated profiles per edition/mode).

Lets prod, --testing and e.g. a Research Edition build run side by side on one machine. --profile NAME picks the instance; --testing is now an alias for --profile testing, so existing behaviour is unchanged — with no flag, and with --testing, every path is byte-identical to today.

aw-qt only launches things, so its part is deliberately small:

  • Resolve + validate the profile (aw_qt/profile.py) — same rule as aw-server-rust's validate_profile: lowercase alphanumeric plus -/_, max 32 chars, so a profile is always a safe path segment.
  • Export AW_PROFILE for spawned modules, so the profile propagates without threading a flag through every module's CLI.
  • Profile-suffixed single-instance lockfile (aw-qt-research.lock) — otherwise the second profile is refused as "already running".
  • Config section [aw-qt-<profile>], falling back to [aw-qt] when the profile has no section of its own.
  • Port lookup reads the profile's own server config (config-<profile>.toml for aw-server-rust, [server-<profile>] for aw-server), mirroring aw-server-rust's config path. A custom profile with no port configured logs a warning, since it would collide with 5600.
  • Tray tooltip and menu show the profile (ActivityWatch (research), "Running in profile: research"). testing keeps its existing "Running in testing mode" wording.

Not in this PR

Full data/config isolation lives in the dirs implementations — ActivityWatch/aw-server-rust#652 and its follow-up do the Rust half, the aw-core half is separate. Until those land, a custom profile isolates what aw-qt owns (lockfile, config section, port) but still shares aw-core's data dir. Nothing regresses in the meantime: without --profile, no behaviour changes at all.

Verification

  • pytest tests/ — 97 passed (36 new, covering resolution/validation, suffixes, env propagation, per-profile port lookup, config-section fallback)
  • mypy aw_qt — 8 errors, identical to the pre-existing master baseline (all PyQt6 QAction | None stubs in trayicon.py)
  • flake8 — clean on changed files
  • Manual: --help shows both flags; --profile Research and --profile research --testing fail with clear usage errors

Lets prod, --testing and e.g. a Research Edition build run side by side
on one machine. `--profile NAME` picks the instance; `--testing` is now an
alias for `--profile testing`, so existing behaviour is unchanged.

aw-qt only launches things, so its part is small:

- resolve + validate the profile (same rule as aw-server-rust:
  lowercase alnum with -/_, max 32 chars)
- export AW_PROFILE so spawned modules inherit it without threading a
  flag through every module CLI
- profile-suffixed single-instance lockfile (aw-qt-research.lock), so a
  second profile is not refused as "already running"
- config section [aw-qt-<profile>], falling back to [aw-qt]
- port lookup reads the profile's own server config (config-<p>.toml /
  [server-<p>]), mirroring aw-server-rust's config path; a custom profile
  with no port configured warns, since it would collide with 5600
- tray tooltip and menu show the profile name

Part of ActivityWatch/activitywatch#1399.
TimeToBuildBob added a commit to TimeToBuildBob/aw-server-rust that referenced this pull request Aug 24, 2026
aw-qt exports AW_PROFILE for the modules it spawns (ActivityWatch/aw-qt#128),
so a profile set on the launcher reaches aw-server-rust without every module
growing its own flag. --profile still wins when given.
@greptile-apps

greptile-apps Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds named ActivityWatch profiles so separate editions or modes can run concurrently while preserving default and testing behavior.

  • Resolves and validates profile names, exports AW_PROFILE, and assigns profile-specific lockfiles.
  • Loads profile-specific aw-qt and server configuration with legacy fallbacks.
  • Displays the active profile in the tray and adds focused profile/configuration tests.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
aw_qt/profile.py Introduces profile validation, CLI resolution helpers, environment propagation, and filename suffix generation.
aw_qt/main.py Integrates profile resolution into startup, locking, configuration, child-module launch, and tray initialization.
aw_qt/config.py Adds profile-specific configuration sections and server-port lookup while preserving default and testing fallbacks.
aw_qt/manager.py Resolves server probe ports from the inherited profile when starting or reusing server modules.
aw_qt/trayicon.py Displays the active profile in the tooltip and tray menu while retaining testing-mode wording.
tests/test_profile.py Covers profile validation, resolution, suffixes, testing detection, and environment propagation.
tests/test_profile_config.py Covers profile-specific server ports, server implementation filtering, and aw-qt configuration fallback.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    CLI[CLI profile options] --> Resolve[Resolve and validate profile]
    Resolve --> Env[Export AW_PROFILE]
    Resolve --> Lock[Acquire profile lock]
    Resolve --> Config[Load profile configuration]
    Config --> Port[Resolve server port]
    Env --> Manager[Start selected modules]
    Port --> Tray[Profile-aware tray and links]
    Manager --> Tray
Loading

Reviews (3): Last reviewed commit: "fix(profile): keep default profile on le..." | Re-trigger Greptile

Comment thread aw_qt/config.py Outdated
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Greptile 4/5, one P1 addressed in-thread: the Rust-first port lookup vs. module-specific probe divergence is pre-existing on master and unchanged here, so it's split out as #129 rather than folded into a flag-addition PR. All CI green (ubuntu/macos/windows).

…ay/manager divergence

When only aw-server (Python) is in autostart_modules but an aw-server-rust
config file exists with a custom port, the old Rust-first logic would make
the tray open the wrong URL while the manager probed the correct server.

Pass autostart_modules to _read_server_port so port selection matches the
server type that will actually run. Backward-compat: callers that omit the
arg still get the original Rust-first behaviour.

Fixes Greptile's 4/5 finding on PR ActivityWatch#128.
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread aw_qt/config.py
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Fixed the default-profile root regression in 584d626. export_profile("default") now removes AW_PROFILE instead of exporting the literal value default, so aw-core keeps the legacy bare activitywatch root. The regression test starts from AW_PROFILE=research and verifies default startup clears it. Local verification: 30 profile tests passed, mypy clean, scoped flake8 clean; Ubuntu, macOS, and Windows CI are green on the new head.

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@ErikBjare
ErikBjare merged commit 455ad3e into ActivityWatch:master Aug 25, 2026
4 checks passed
TimeToBuildBob added a commit to TimeToBuildBob/aw-server-rust that referenced this pull request Aug 31, 2026
aw-qt exports AW_PROFILE for the modules it spawns (ActivityWatch/aw-qt#128),
so a profile set on the launcher reaches aw-server-rust without every module
growing its own flag. --profile still wins when given.
ErikBjare pushed a commit to ActivityWatch/aw-server-rust that referenced this pull request Aug 31, 2026
* feat(profile): named instance profiles via --profile flag

Replace the two-valued `testing: bool` with a named profile string so that
more than two parallel ActivityWatch instances can coexist on one machine.

- `dirs.rs`: `db_path(profile)` → `sqlite.db` / `sqlite-<profile>.db`; add
  `validate_profile()` (lowercase alnum + `-_`, max 32 chars, starts with
  alnum); tests for suffix rule and validation
- `config.rs`: replace `static mut TESTING: bool` with `OnceLock<String>
  PROFILE`; `set_profile()` is idempotent for same value, panics on
  conflict; `get_profile()` / `is_testing()` derived from it; config file
  is `config.toml` / `config-<profile>.toml`
- `logging.rs`: `setup_logger(module, profile, verbose)` — logfile suffix
  is `<module>-<profile>_<ts>.log` for non-default profiles
- `main.rs`: add `--profile NAME`; `--testing` remains as alias for
  `--profile testing`; debug builds still default to "testing"; profile is
  validated before use
- `android/mod.rs`: update two call-sites to pass `"default"`

Backwards compatibility:
- `--testing` still works (alias for `--profile testing`)
- `default` profile maps to existing unsuffixed paths (sqlite.db,
  config.toml) — no migration required
- `testing` profile maps to existing -testing suffix paths

Part of ActivityWatch/activitywatch#1399.

* fix(aw-sync): update setup_logger call to pass profile string

setup_logger signature changed to accept profile: &str instead of testing: bool.
Convert opts.testing bool to "testing"/"default" profile string at the call site.

Also run cargo fmt to fix long assert! lines in dirs.rs tests.

* fix(config): eliminate TOCTOU race in set_profile via atomic OnceLock::set result

Previously the function checked PROFILE.get() then PROFILE.set() in two separate
steps. Two threads with different profile values could both see get()==None before
either set, causing the loser's set() error to be discarded and the loser to
silently proceed under the wrong profile.

Fix: use the atomic OnceLock::set return value directly. If Ok(()), we won the
race. If Err(_), the lock was already set by a concurrent caller; check the existing
value and panic only if it differs.

* fix(config): set_testing delegates to set_profile to propagate conflict panics

Greptile P1: bare PROFILE.set() in set_testing silently discarded conflicts
even when the existing profile differed, allowing a losing caller to proceed
under the wrong instance. Delegating to set_profile() reuses its idempotent
same-value check and conflict panic, matching the documented semantics.

* fix(tests): mark in-process server as testing

* feat(profile): isolate dir roots per profile and report profile in server info

--profile only suffixed the DB filename, so a non-default instance still
shared config.toml, the cache dir and the log dir with prod — the one thing
profile isolation is for.

Move the profile into the platformdirs appname instead
("activitywatch-<profile>"), which isolates config/data/cache/logs and
everything nested under them in one place, with no per-module path changes.

default and testing deliberately keep the bare "activitywatch" root: their
legacy per-file suffixes (sqlite-testing.db, config-testing.toml, port 5666)
already separate them, and moving their root would orphan existing installs.

Also add Info.profile so clients (webui badge) can tell concurrent instances
apart; it deserializes with a "default" fallback so a new client still parses
an older server's /api/0/info.

* feat(profile): fall back to AW_PROFILE env when --profile is absent

aw-qt exports AW_PROFILE for the modules it spawns (ActivityWatch/aw-qt#128),
so a profile set on the launcher reaches aw-server-rust without every module
growing its own flag. --profile still wins when given.

* feat(profile): isolate aw-sync config dirs via shared appname

aw-sync hard-coded activitywatch/aw-sync, so a research instance would
read prod's sync config. Use aw_server::dirs::appname() and the same
config-{profile}.toml filename rule as the server.

aw-sync now resolves --profile / AW_PROFILE / --testing and calls
set_profile so appname() is the named profile, not always default.

* feat(profile): testing-root fallback with legacy artifacts

Adopt the activitywatch#1399 rule so rust matches aw-core#152:

1. activitywatch-testing/ exists → use it
2. else legacy testing files in activitywatch/ → stay on the shared root
   (sqlite-testing.db, config-testing.toml)
3. else fresh setup → create and use activitywatch-testing/

Isolated profile roots use bare sqlite.db / config.toml / log names.
set_profile now runs before setup_logger so named profiles log into
their own cache dir.

* fix(aw-sync): keep Android filesDir config path after profile rebase

Master's #666 reads the embedded server's config from filesDir for
the API key. After rebasing onto that, get_server_config_path must
not switch Android onto desktop XDG + appname_for (those helpers
are cfg-gated off Android). Desktop still uses the isolated
activitywatch-<profile> root.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants