Skip to content

LLT-6736: Add command 'reload' and support SIGHUP#1844

Open
tomasz-kumor wants to merge 6 commits into
mainfrom
kumor/LLT6736_reload
Open

LLT-6736: Add command 'reload' and support SIGHUP#1844
tomasz-kumor wants to merge 6 commits into
mainfrom
kumor/LLT6736_reload

Conversation

@tomasz-kumor

@tomasz-kumor tomasz-kumor commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Problem

Currently to change the nordvpnlite configuration while the deamon is running we need to stop it, change the config file and restart the daemon.

./nordvpnlite stop
// change config.json file
./nordvpnlite start -c ~/config.json

The packets may leak during this operation.

Solution

This PR doesn't address the packets leak problem yet but prepares a base for future changes.
It introduces:

  • CLI reload command support
  • SIGHUP signal support

Both triggers causes the the daemon to restart with a new configuration.

Verification

Happy path verification

  • CLI reload
nordvpnlite start -c ~/config.json
nordvpnlite status
// change anything in the config.json (i.e. country code/log level)
nordvpnlite reload
nordvpnliste status

  • SIGHUP signal
nordvpnlite start -c ~/config.json
nordvpnlite status
// change anything in the config.json
kill -HUP $(pidof nordvpnlite)
nordvpnliste status

Daemon does not restart when config.json not changed

nordvpnlite start -c ~/config.json
kill -HUP $(pidof nordvpnlite) // nordvpnlite reload
// check /var/log/nordvpnlite.log if telio daemon did not restart

☑️ Definition of Done checklist

  • Commit history is clean (requirements)
  • README.md is updated
  • Functionality is covered by unit or integration tests

@tomasz-kumor tomasz-kumor force-pushed the kumor/LLT6736_reload branch from 5271577 to c052054 Compare June 26, 2026 07:55
@tomasz-kumor tomasz-kumor changed the title Kumor/llt6736 reload LLT-6736: Add command 'reload' and support SIGHUP Jun 26, 2026
@tomasz-kumor tomasz-kumor force-pushed the kumor/LLT6736_reload branch from c052054 to 4d76ce4 Compare June 29, 2026 07:31
@tomasz-kumor tomasz-kumor force-pushed the kumor/LLT6736_reload branch from 4d76ce4 to 3541688 Compare June 29, 2026 07:56
@tomasz-kumor tomasz-kumor marked this pull request as ready for review June 29, 2026 08:15
@tomasz-kumor tomasz-kumor requested a review from a team as a code owner June 29, 2026 08:15
Comment thread clis/nordvpnlite/src/command_listener.rs
Comment thread clis/nordvpnlite/src/daemon.rs
@tomaszpatejko

Copy link
Copy Markdown
Contributor

+1

tomaszpatejko
tomaszpatejko previously approved these changes Jun 30, 2026

@stalowyjez stalowyjez left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm, just a few minor comments/questions

Comment thread clis/nordvpnlite/src/logging.rs Outdated
Comment thread clis/nordvpnlite/src/logging.rs Outdated
Comment thread clis/nordvpnlite/src/daemon.rs Outdated
Comment thread clis/nordvpnlite/src/config.rs
@tomasz-kumor tomasz-kumor requested a review from stalowyjez June 30, 2026 15:31
Needed for config reload so that reload is triggered only when
the file actually changes.
Adds CLI Reload command support. Allows the daemon event loop to receive
an already-parsed config from the Reload handler.
Adds support for CLI reload command and SIGHUP signal.
Logging reload requires a persistent handle to the subscriber
reload layers. This handle is returned from setup_logging so the
daemon loop can swap the log writer and filter level on reload.
@tomasz-kumor

Copy link
Copy Markdown
Contributor Author

@stalowyjez do you have any other comments?

@tomasz-grz tomasz-grz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling this, nice work!
Though I'll add some more critical thoughts from my side here..

Also would be nice to see some nat-lab test cases for the reload scenarios (at least the happy path) 🙏

Comment on lines +398 to +399
mut config: NordVpnLiteConfig,
config_file: &ConfigFile,

@tomasz-grz tomasz-grz Jul 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could these two be unified? It's not very obvious what are the differences between the two parameters

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about that but wasn't sure if I should. What is in the ConfigFile describes the config file, path to it and it's hash. NordVpnLiteConfig contains parsed content of a config file. That's why I wasn't sure if I should mix them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would personally prefer something like RunningConfig { parsed, path, hash }, but I'm not sure if it would make it more complicated with the borrow checker..

Comment thread clis/nordvpnlite/src/daemon.rs

#[derive(Clone)]
pub struct ConfigFile {
pub path: String,

@tomasz-grz tomasz-grz Jul 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about PathBuf for a more ergonomic way of working with owned paths?

rt.block_on(daemon::daemon_event_loop(config))
rt.block_on(daemon::daemon_event_loop(
config,
opts.config_path.clone(),

@tomasz-grz tomasz-grz Jul 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this work with relative config and log paths when we detach? (since the daemon changes working directory, either to / or opts.working_directory)

Do we need to convert to canonical paths before detaching?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For log_file_path there is already a statement in readme:

log_file_path - a path to store the daemon's logs, needs be absolute, otherwise will be relative to working-directory when daemonized

I assumed similar rule for config-file.

IMO it make sense that relative path is determined towards the working directory.

@tomasz-grz tomasz-grz Jul 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes but the working directory of the daemon changes after it's "detached".
So if you launch nordvpnlite start -c config.json from /home/user/, the daemon later tries to read /config.json on reload, instead of /home/user/config.json.
We probably need to call canonicalize() before passing the config_path into the daemon_event_loop

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are correct. But is such case, when config file is relative, shouldn't the user launch the app with --working-directory /home/user/? I assumed that we should follow similar logic as for log_file_path

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually log path is canonicalized when reading the config, but canonicalizing again after detach would result a different path.

@tomasz-grz tomasz-grz Jul 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before the changes, the config and log path were resolved only once before "detaching" the daemon.

So if you nordvpnlite start -c config.json from /home/user/, the config is read and parsed from /home/user/config.json.

After nordvpnlite reload
The daemon will try to read the config from /config.json, but since it doesn't exist, it will:

  1. Create a new default config
  2. Since the default config does not contain a valid API token, the daemon will exit with error breaking the VPN connection.

As you mentioned nordvpnlite start -c config.json --working-directory /home/user/ would solve this, provided the user gave the correct --working-directory, IMHO this is not a very smooth and polished user experience.

I'm just pointing this out, If you are already aware and this was a conscious design decision, please document the expected behavior in the read me.

}
Some(Err(e)) => {
error!("Config file changed but failed to parse: {e}");
Err(e)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would Ok(CommandResponse::Err(e)) here return the error to the client caller?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here is what we get in log file:

2026-07-03T10:59:21.854830Z ERROR nordvpnlite::command_listener: 201: Config file changed but failed to parse: invalid type: unit variant, expected newtype variant at line 12 column 13

@tomasz-grz tomasz-grz Jul 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but what do you get in the cli calling nordvpnlite reload? And does the daemon keep running the old config?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On CLI I am getting

Error: InvalidResponse("EOF while parsing a value at line 1 column 0")

daemon is not restarted and continues with the old configuration

Comment on lines +61 to +67
fn hash_bytes(bytes: &[u8]) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
bytes.hash(&mut hasher);
hasher.finish()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder how would this approach handle whitespace or formatting changes in the config file?
Would it trigger reload causing a reconnect?

Maybe we could just use PartialEq to compare the pending config against the current config?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With PartialEq the problem is that changes to AdapterType::Custom will not be catch. #1844 (comment)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a whitespace to a file should trigger a reload. At least that was my team intention during this task refinement.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but that means it will trigger a reconnect -> packet leak, even though nothing changed in the config functionally.
If you are aware of that and the risk is accepted, then it's fine by me.

Comment thread clis/nordvpnlite/src/daemon.rs
rt.block_on(daemon::daemon_event_loop(
config,
opts.config_path.clone(),
logging_handle,

@tomasz-grz tomasz-grz Jul 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logging_handle will be dropped inside daemon_event_loop since it's moved here. Could this lead to unflushed logs during teardown?

config_path: String,
mut logging_handle: logging::LoggingHandle,
) -> Result<(), NordVpnLiteError> {
let mut config_file = ConfigFile::new(config_path);

@tomasz-grz tomasz-grz Jul 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this again, this is called after possible detaching of the daemon.

In case of a relative config_path like we already discussed, I suspect the std::fs::read(&path) could already fail when calculating the hash, even before reload is called.

Do you think it would be worth to at least add a log to?

        let hash = std::fs::read(&path)
            .map(|b| Self::hash_bytes(&b))
            .unwrap_or(0);

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.

4 participants