(
settings_path: &Path,
raw: &serde_json::Map<String, serde_json::Value>,
)
| 87 | } |
| 88 | |
| 89 | pub(crate) fn write_settings( |
| 90 | settings_path: &Path, |
| 91 | raw: &serde_json::Map<String, serde_json::Value>, |
| 92 | ) -> AgentResult<()> { |
| 93 | if let Some(parent) = settings_path.parent() { |
| 94 | std::fs::create_dir_all(parent).map_err(|e| AgentError::ConfigError { |
| 95 | operation: "create directory".to_string(), |
| 96 | path: parent.to_path_buf(), |
| 97 | reason: e.to_string(), |
| 98 | })?; |
| 99 | } |
| 100 | |
| 101 | let mut output = serde_json::to_string_pretty(raw).map_err(|e| AgentError::ConfigError { |
| 102 | operation: "serialize".to_string(), |
| 103 | path: settings_path.to_path_buf(), |
| 104 | reason: e.to_string(), |
| 105 | })?; |
| 106 | if !output.ends_with('\n') { |
| 107 | output.push('\n'); |
| 108 | } |
| 109 | |
| 110 | // Atomic write (temp + rename) so a settings watcher never reads a |
| 111 | // half-written file (`std::fs::write` truncates the target first). |
| 112 | use std::io::Write as _; |
| 113 | |
| 114 | let parent = settings_path.parent().unwrap_or_else(|| Path::new(".")); |
| 115 | let file_name = settings_path |
| 116 | .file_name() |
| 117 | .map(|n| n.to_string_lossy().into_owned()) |
| 118 | .unwrap_or_else(|| "settings.json".to_string()); |
| 119 | // Same-dir, pid-suffixed temp: rename stays atomic, no clobber between writers. |
| 120 | let tmp_path = parent.join(format!(".{}.{}.tmp", file_name, std::process::id())); |
| 121 | |
| 122 | // Preserve existing permissions. |
| 123 | let existing_perms = std::fs::metadata(settings_path) |
| 124 | .ok() |
| 125 | .map(|m| m.permissions()); |
| 126 | |
| 127 | if let Err(e) = (|| -> std::io::Result<()> { |
| 128 | let mut f = std::fs::File::create(&tmp_path)?; |
| 129 | f.write_all(output.as_bytes())?; |
| 130 | f.sync_all()?; |
| 131 | Ok(()) |
| 132 | })() { |
| 133 | let _ = std::fs::remove_file(&tmp_path); |
| 134 | return Err(AgentError::ConfigError { |
| 135 | operation: "write temp file".to_string(), |
| 136 | path: tmp_path, |
| 137 | reason: e.to_string(), |
| 138 | }); |
| 139 | } |
| 140 | |
| 141 | if let Some(perms) = existing_perms { |
| 142 | let _ = std::fs::set_permissions(&tmp_path, perms); |
| 143 | } |
| 144 | |
| 145 | std::fs::rename(&tmp_path, settings_path).map_err(|e| { |
| 146 | let _ = std::fs::remove_file(&tmp_path); |
no test coverage detected