use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command as ProcessCommand;
use anyhow::{anyhow, bail, Context, Result};
use serde::Serialize;
use crate::cli::db_path::default_db_path;
use crate::db::Database;
use crate::platform::capture_snapshot;
const DIRECT_LABEL: &str = "io.openclaw.clipmem.watch";
const HOMEBREW_LABEL: &str = "homebrew.mxcl.clipmem";
const DEFAULT_INTERVAL_MS: u64 = 350;
const SERVICE_FRESHNESS_HOURS: u32 = 1;
const DIRECT_PLIST_TEMPLATE: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/extras/launchd/io.openclaw.clipmem.watch.plist.template"
));
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(super) enum ServiceProvider {
Homebrew,
Launchagent,
}
impl ServiceProvider {
pub(super) const fn as_str(self) -> &'static str {
match self {
Self::Homebrew => "homebrew",
Self::Launchagent => "launchagent",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(super) enum ServiceState {
NotInstalled,
Installed,
Loaded,
Running,
}
#[derive(Debug, Clone, Serialize)]
pub(super) struct ServiceProviderStatus {
pub(super) provider: ServiceProvider,
pub(super) label: String,
pub(super) state: ServiceState,
pub(super) installed: bool,
pub(super) loaded: bool,
pub(super) running: bool,
pub(super) pid: Option<i64>,
pub(super) plist_path: Option<String>,
pub(super) stdout_log_path: Option<String>,
pub(super) stderr_log_path: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub(super) struct ServiceStatusReport {
pub(super) binary_path: String,
pub(super) db_path: String,
pub(super) preferred_provider: String,
pub(super) preferred_provider_reason: String,
pub(super) conflict: bool,
pub(super) homebrew: ServiceProviderStatus,
pub(super) launchagent: ServiceProviderStatus,
pub(super) db_exists: bool,
pub(super) recent_capture_at: Option<String>,
pub(super) recent_capture_within_last_hour: Option<bool>,
pub(super) stale: bool,
pub(super) db_error: Option<String>,
pub(super) notes: Vec<String>,
}
#[derive(Debug, Clone)]
pub(super) struct ServiceActionReport {
pub(super) action: &'static str,
pub(super) provider: ServiceProvider,
pub(super) binary_path: PathBuf,
pub(super) db_path: PathBuf,
pub(super) label: &'static str,
pub(super) notes: Vec<String>,
}
#[derive(Debug, Clone)]
pub(super) struct SetupReport {
pub(super) seeded_capture: bool,
pub(super) action: ServiceActionReport,
}
#[derive(Debug, Clone)]
struct ServiceContext {
binary_path: PathBuf,
db_path: PathBuf,
default_db_path: PathBuf,
direct_plist_path: PathBuf,
homebrew_plist_path: PathBuf,
direct_stdout_path: PathBuf,
direct_stderr_path: PathBuf,
brew_path: Option<PathBuf>,
homebrew_prefix: Option<PathBuf>,
}
#[derive(Debug, Clone)]
struct ProviderSelection {
provider: ServiceProvider,
reason: String,
notes: Vec<String>,
}
#[derive(Debug, Clone)]
struct LaunchctlRow {
pid: Option<i64>,
}
pub(super) fn setup(db_path: &Path) -> Result<SetupReport> {
ensure_supported()?;
let context = build_context(db_path)?;
let status = status_report(db_path)?;
let selection = select_provider(&context)?;
ensure_no_conflict(&status)?;
let seeded_capture = seed_capture(db_path)?;
let action = start_with_provider(&context, &selection)?;
Ok(SetupReport {
seeded_capture,
action,
})
}
pub(super) fn start(db_path: &Path) -> Result<ServiceActionReport> {
ensure_supported()?;
let context = build_context(db_path)?;
let status = status_report(db_path)?;
let selection = select_provider(&context)?;
ensure_no_conflict(&status)?;
start_with_provider(&context, &selection)
}
pub(super) fn stop(db_path: &Path) -> Result<ServiceActionReport> {
ensure_supported()?;
let context = build_context(db_path)?;
let status = status_report(db_path)?;
if status.conflict {
bail!(conflict_message());
}
if status.homebrew.installed || status.homebrew.loaded || status.homebrew.running {
stop_homebrew_provider(&context)
} else {
stop_direct_provider(&context)
}
}
pub(super) fn uninstall(db_path: &Path) -> Result<ServiceActionReport> {
ensure_supported()?;
let context = build_context(db_path)?;
let status = status_report(db_path)?;
if status.conflict {
bail!(conflict_message());
}
if status.homebrew.installed || status.homebrew.loaded || status.homebrew.running {
uninstall_homebrew_provider(&context)
} else {
uninstall_direct_provider(&context)
}
}
pub(super) fn status_report(db_path: &Path) -> Result<ServiceStatusReport> {
ensure_supported()?;
let context = build_context(db_path)?;
let direct_row = launchctl_row(DIRECT_LABEL)?;
let homebrew_row = launchctl_row(HOMEBREW_LABEL)?;
let direct_installed = context.direct_plist_path.is_file() || direct_row.is_some();
let homebrew_installed = context.homebrew_plist_path.is_file() || homebrew_row.is_some();
let direct_status = provider_status(
ServiceProvider::Launchagent,
DIRECT_LABEL,
direct_installed,
direct_row,
Some(context.direct_plist_path.clone()),
Some(context.direct_stdout_path.clone()),
Some(context.direct_stderr_path.clone()),
);
let homebrew_status = provider_status(
ServiceProvider::Homebrew,
HOMEBREW_LABEL,
homebrew_installed,
homebrew_row,
Some(context.homebrew_plist_path.clone()),
context
.homebrew_prefix
.as_ref()
.map(|prefix| prefix.join("var/log/clipmem.log")),
context
.homebrew_prefix
.as_ref()
.map(|prefix| prefix.join("var/log/clipmem.error.log")),
);
let conflict = homebrew_status.installed && direct_status.installed;
let selection = select_provider(&context)?;
let db_exists = context.db_path.is_file();
let (recent_capture_at, recent_capture_within_last_hour, db_error) = if db_exists {
match Database::open_existing(&context.db_path) {
Ok(db) => (
db.latest_capture_observed_at()?,
Some(db.has_capture_within_hours(SERVICE_FRESHNESS_HOURS)?),
None,
),
Err(error) => (None, None, Some(error.to_string())),
}
} else {
(None, None, None)
};
let stale = matches!(recent_capture_within_last_hour, Some(false))
&& !homebrew_status.running
&& !direct_status.running;
let mut notes = selection.notes;
if conflict {
notes.push(conflict_message());
}
if !db_exists {
notes.push(format!(
"Database does not exist yet at {}. Run `clipmem setup` to initialize capture.",
context.db_path.display()
));
}
if stale {
notes.push(
"No recent captures were found and no background watcher is running.".to_string(),
);
}
Ok(ServiceStatusReport {
binary_path: context.binary_path.display().to_string(),
db_path: context.db_path.display().to_string(),
preferred_provider: selection.provider.as_str().to_string(),
preferred_provider_reason: selection.reason,
conflict,
homebrew: homebrew_status,
launchagent: direct_status,
db_exists,
recent_capture_at,
recent_capture_within_last_hour,
stale,
db_error,
notes,
})
}
pub(super) fn render_setup_text(report: &SetupReport) -> String {
let mut out = String::new();
out.push_str("clipmem setup completed\n");
out.push_str(&format!(
"provider: {}\nlabel: {}\nbinary: {}\ndatabase: {}\nseeded_capture: {}\n",
report.action.provider.as_str(),
report.action.label,
report.action.binary_path.display(),
report.action.db_path.display(),
report.seeded_capture
));
out.push_str("\nNext steps:\n");
out.push_str(" clipmem service status\n");
out.push_str(" clipmem doctor\n");
for note in &report.action.notes {
out.push_str(&format!(" note: {note}\n"));
}
out
}
pub(super) fn render_service_action_text(report: &ServiceActionReport) -> String {
let mut out = String::new();
out.push_str(&format!("clipmem service {} completed\n", report.action));
out.push_str(&format!(
"provider: {}\nlabel: {}\nbinary: {}\ndatabase: {}\n",
report.provider.as_str(),
report.label,
report.binary_path.display(),
report.db_path.display(),
));
for note in &report.notes {
out.push_str(&format!("note: {note}\n"));
}
out
}
pub(super) fn render_service_status_text(report: &ServiceStatusReport) -> String {
let mut out = String::new();
out.push_str(&format!("binary: {}\n", report.binary_path));
out.push_str(&format!("database: {}\n", report.db_path));
out.push_str(&format!(
"preferred provider: {} ({})\n",
report.preferred_provider, report.preferred_provider_reason
));
out.push('\n');
render_provider_status(&mut out, &report.homebrew);
out.push('\n');
render_provider_status(&mut out, &report.launchagent);
out.push('\n');
out.push_str(&format!("database exists: {}\n", report.db_exists));
if let Some(recent_capture_at) = &report.recent_capture_at {
out.push_str(&format!("latest capture: {recent_capture_at}\n"));
} else {
out.push_str("latest capture: none\n");
}
if let Some(fresh) = report.recent_capture_within_last_hour {
out.push_str(&format!("capture within last hour: {fresh}\n"));
} else {
out.push_str("capture within last hour: unknown\n");
}
out.push_str(&format!("stale: {}\n", report.stale));
if let Some(db_error) = &report.db_error {
out.push_str(&format!("database error: {db_error}\n"));
}
if !report.notes.is_empty() {
out.push_str("\nNotes:\n");
for note in &report.notes {
out.push_str(&format!(" - {note}\n"));
}
}
out
}
fn render_provider_status(out: &mut String, status: &ServiceProviderStatus) {
out.push_str(&format!("{} service:\n", status.provider.as_str()));
out.push_str(&format!(" label: {}\n", status.label));
out.push_str(&format!(" state: {:?}\n", status.state));
out.push_str(&format!(" installed: {}\n", status.installed));
out.push_str(&format!(" loaded: {}\n", status.loaded));
out.push_str(&format!(" running: {}\n", status.running));
if let Some(pid) = status.pid {
out.push_str(&format!(" pid: {pid}\n"));
}
if let Some(path) = &status.plist_path {
out.push_str(&format!(" plist: {path}\n"));
}
if let Some(path) = &status.stdout_log_path {
out.push_str(&format!(" stdout log: {path}\n"));
}
if let Some(path) = &status.stderr_log_path {
out.push_str(&format!(" stderr log: {path}\n"));
}
}
fn ensure_supported() -> Result<()> {
if cfg!(target_os = "macos") {
Ok(())
} else {
bail!("clipmem setup and service commands are only supported on macOS");
}
}
fn build_context(db_path: &Path) -> Result<ServiceContext> {
let home = home_dir()?;
let binary_path = active_binary_path()?;
let direct_plist_path = home
.join("Library/LaunchAgents")
.join(format!("{DIRECT_LABEL}.plist"));
let homebrew_plist_path = home
.join("Library/LaunchAgents")
.join(format!("{HOMEBREW_LABEL}.plist"));
let app_support_dir = db_path.parent().ok_or_else(|| {
anyhow!(
"database path {} has no parent directory",
db_path.display()
)
})?;
let log_dir = app_support_dir.join("logs");
let brew_path = find_executable("brew");
let homebrew_prefix = if is_homebrew_binary_path(&binary_path) {
binary_path
.parent()
.and_then(Path::parent)
.map(Path::to_path_buf)
} else {
None
};
Ok(ServiceContext {
binary_path,
db_path: db_path.to_path_buf(),
default_db_path: default_db_path(),
direct_plist_path,
homebrew_plist_path,
direct_stdout_path: log_dir.join("clipmem.stdout.log"),
direct_stderr_path: log_dir.join("clipmem.stderr.log"),
brew_path,
homebrew_prefix,
})
}
fn select_provider(context: &ServiceContext) -> Result<ProviderSelection> {
let mut notes = Vec::new();
if let Some(brew_path) = &context.brew_path {
if is_homebrew_binary_path(&context.binary_path) {
if brew_services_available(brew_path) {
return Ok(ProviderSelection {
provider: ServiceProvider::Homebrew,
reason:
"current binary is the Homebrew formula and `brew services` is available"
.to_string(),
notes,
});
}
notes.push(
"Homebrew install detected, but `brew services` was unavailable; falling back to a direct LaunchAgent.".to_string(),
);
}
}
if context.db_path != context.default_db_path {
notes.push(format!(
"Direct LaunchAgent provider will use the explicit database path {}.",
context.db_path.display()
));
}
Ok(ProviderSelection {
provider: ServiceProvider::Launchagent,
reason: "using the direct per-user LaunchAgent managed by clipmem".to_string(),
notes,
})
}
fn ensure_no_conflict(status: &ServiceStatusReport) -> Result<()> {
if status.conflict {
bail!(conflict_message());
}
Ok(())
}
fn conflict_message() -> String {
"Both the Homebrew service and the direct LaunchAgent are installed. Remove one first with `brew services stop clipmem` or `clipmem service uninstall`.".to_string()
}
fn start_with_provider(
context: &ServiceContext,
selection: &ProviderSelection,
) -> Result<ServiceActionReport> {
match selection.provider {
ServiceProvider::Homebrew => start_homebrew_provider(context, &selection.notes),
ServiceProvider::Launchagent => start_direct_provider(context, &selection.notes),
}
}
fn start_homebrew_provider(
context: &ServiceContext,
notes: &[String],
) -> Result<ServiceActionReport> {
let brew = context
.brew_path
.as_ref()
.ok_or_else(|| anyhow!("`brew` is not available on PATH"))?;
run_command_checked(
ProcessCommand::new(brew)
.args(["services", "start", "clipmem"])
.output(),
"brew services start clipmem",
)?;
Ok(ServiceActionReport {
action: "start",
provider: ServiceProvider::Homebrew,
binary_path: context.binary_path.clone(),
db_path: context.default_db_path.clone(),
label: HOMEBREW_LABEL,
notes: notes.to_vec(),
})
}
fn stop_homebrew_provider(context: &ServiceContext) -> Result<ServiceActionReport> {
let brew = context
.brew_path
.as_ref()
.ok_or_else(|| anyhow!("`brew` is not available on PATH"))?;
run_command_checked(
ProcessCommand::new(brew)
.args(["services", "stop", "clipmem"])
.output(),
"brew services stop clipmem",
)?;
Ok(ServiceActionReport {
action: "stop",
provider: ServiceProvider::Homebrew,
binary_path: context.binary_path.clone(),
db_path: context.default_db_path.clone(),
label: HOMEBREW_LABEL,
notes: vec!["The Homebrew formula remains installed.".to_string()],
})
}
fn uninstall_homebrew_provider(context: &ServiceContext) -> Result<ServiceActionReport> {
let mut report = stop_homebrew_provider(context)?;
report.action = "uninstall";
report.notes.push(
"Only the Homebrew-managed service was removed; the formula stays installed.".to_string(),
);
Ok(report)
}
fn start_direct_provider(
context: &ServiceContext,
notes: &[String],
) -> Result<ServiceActionReport> {
ensure_parent_dir(&context.direct_plist_path)?;
ensure_parent_dir(&context.direct_stdout_path)?;
touch_log_file(&context.direct_stdout_path)?;
touch_log_file(&context.direct_stderr_path)?;
write_direct_plist(context)?;
launchctl_bootout(DIRECT_LABEL)?;
launchctl_bootstrap(&context.direct_plist_path)?;
launchctl_enable(DIRECT_LABEL)?;
launchctl_kickstart(DIRECT_LABEL)?;
Ok(ServiceActionReport {
action: "start",
provider: ServiceProvider::Launchagent,
binary_path: context.binary_path.clone(),
db_path: context.db_path.clone(),
label: DIRECT_LABEL,
notes: notes.to_vec(),
})
}
fn stop_direct_provider(context: &ServiceContext) -> Result<ServiceActionReport> {
launchctl_bootout(DIRECT_LABEL)?;
launchctl_disable(DIRECT_LABEL)?;
Ok(ServiceActionReport {
action: "stop",
provider: ServiceProvider::Launchagent,
binary_path: context.binary_path.clone(),
db_path: context.db_path.clone(),
label: DIRECT_LABEL,
notes: vec!["The LaunchAgent plist remains installed.".to_string()],
})
}
fn uninstall_direct_provider(context: &ServiceContext) -> Result<ServiceActionReport> {
launchctl_bootout(DIRECT_LABEL)?;
launchctl_disable(DIRECT_LABEL)?;
let _ = fs::remove_file(&context.direct_plist_path);
Ok(ServiceActionReport {
action: "uninstall",
provider: ServiceProvider::Launchagent,
binary_path: context.binary_path.clone(),
db_path: context.db_path.clone(),
label: DIRECT_LABEL,
notes: vec!["Removed the direct LaunchAgent plist.".to_string()],
})
}
fn seed_capture(db_path: &Path) -> Result<bool> {
if env::var_os("CLIPMEM_TEST_SKIP_SETUP_CAPTURE_ONCE").as_deref() == Some("1".as_ref()) {
return Ok(false);
}
let mut db = Database::open_or_init(db_path)?;
let snapshot = capture_snapshot().context("setup clipboard read failed")?;
db.store_capture(&snapshot)
.context("setup database write failed")?;
Ok(true)
}
fn launchctl_row(label: &str) -> Result<Option<LaunchctlRow>> {
let output = ProcessCommand::new("launchctl")
.arg("list")
.output()
.context("run launchctl list")?;
if !output.status.success() {
return Ok(None);
}
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
let mut parts = line.split_whitespace();
let Some(pid) = parts.next() else {
continue;
};
let _last_exit = parts.next();
let Some(found_label) = parts.next() else {
continue;
};
if found_label != label {
continue;
}
return Ok(Some(LaunchctlRow {
pid: if pid == "-" {
None
} else {
pid.parse::<i64>().ok()
},
}));
}
Ok(None)
}
fn provider_status(
provider: ServiceProvider,
label: &str,
installed: bool,
row: Option<LaunchctlRow>,
plist_path: Option<PathBuf>,
stdout_log_path: Option<PathBuf>,
stderr_log_path: Option<PathBuf>,
) -> ServiceProviderStatus {
let (loaded, running, pid) = match row {
Some(row) if row.pid.is_some() => (true, true, row.pid),
Some(_) => (true, false, None),
None => (false, false, None),
};
let state = if running {
ServiceState::Running
} else if loaded {
ServiceState::Loaded
} else if installed {
ServiceState::Installed
} else {
ServiceState::NotInstalled
};
ServiceProviderStatus {
provider,
label: label.to_string(),
state,
installed,
loaded,
running,
pid,
plist_path: plist_path.map(|path| path.display().to_string()),
stdout_log_path: stdout_log_path.map(|path| path.display().to_string()),
stderr_log_path: stderr_log_path.map(|path| path.display().to_string()),
}
}
fn write_direct_plist(context: &ServiceContext) -> Result<()> {
let plist = DIRECT_PLIST_TEMPLATE
.replace(
"{{CLIPMEM_BIN}}",
&xml_escape(&context.binary_path.display().to_string()),
)
.replace(
"{{CLIPMEM_DB_PATH}}",
&xml_escape(&context.db_path.display().to_string()),
)
.replace("{{CLIPMEM_INTERVAL_MS}}", &DEFAULT_INTERVAL_MS.to_string())
.replace(
"{{STDOUT_PATH}}",
&xml_escape(&context.direct_stdout_path.display().to_string()),
)
.replace(
"{{STDERR_PATH}}",
&xml_escape(&context.direct_stderr_path.display().to_string()),
);
fs::write(&context.direct_plist_path, plist)
.with_context(|| format!("write {}", context.direct_plist_path.display()))?;
set_mode_600(&context.direct_plist_path)?;
Ok(())
}
fn run_command_checked(output: std::io::Result<std::process::Output>, command: &str) -> Result<()> {
let output = output.with_context(|| format!("run `{command}`"))?;
if output.status.success() {
return Ok(());
}
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
bail!(
"`{command}` failed with {}.\nstdout:\n{}\nstderr:\n{}",
output.status,
stdout.trim(),
stderr.trim()
);
}
fn launchctl_bootout(label: &str) -> Result<()> {
let _ = ProcessCommand::new("launchctl")
.args(["bootout", &format!("gui/{}/{}", uid()?, label)])
.output();
Ok(())
}
fn launchctl_bootstrap(plist_path: &Path) -> Result<()> {
run_command_checked(
ProcessCommand::new("launchctl")
.args([
"bootstrap",
&format!("gui/{}", uid()?),
&plist_path.display().to_string(),
])
.output(),
"launchctl bootstrap",
)
}
fn launchctl_enable(label: &str) -> Result<()> {
run_command_checked(
ProcessCommand::new("launchctl")
.args(["enable", &format!("gui/{}/{}", uid()?, label)])
.output(),
"launchctl enable",
)
}
fn launchctl_disable(label: &str) -> Result<()> {
let _ = ProcessCommand::new("launchctl")
.args(["disable", &format!("gui/{}/{}", uid()?, label)])
.output();
Ok(())
}
fn launchctl_kickstart(label: &str) -> Result<()> {
run_command_checked(
ProcessCommand::new("launchctl")
.args(["kickstart", "-k", &format!("gui/{}/{}", uid()?, label)])
.output(),
"launchctl kickstart",
)
}
fn uid() -> Result<String> {
if let Some(uid) = env::var_os("UID") {
return Ok(uid.to_string_lossy().into_owned());
}
let output = ProcessCommand::new("id")
.arg("-u")
.output()
.context("run `id -u`")?;
if !output.status.success() {
bail!("`id -u` failed with {}", output.status);
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn ensure_parent_dir(path: &Path) -> Result<()> {
let parent = path
.parent()
.ok_or_else(|| anyhow!("{} has no parent directory", path.display()))?;
fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
set_mode_700(parent)?;
Ok(())
}
fn touch_log_file(path: &Path) -> Result<()> {
fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.with_context(|| format!("create {}", path.display()))?;
set_mode_600(path)?;
Ok(())
}
#[cfg(unix)]
fn set_mode_700(path: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(path)?.permissions();
perms.set_mode(0o700);
fs::set_permissions(path, perms)?;
Ok(())
}
#[cfg(not(unix))]
fn set_mode_700(_path: &Path) -> Result<()> {
Ok(())
}
#[cfg(unix)]
fn set_mode_600(path: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(path)?.permissions();
perms.set_mode(0o600);
fs::set_permissions(path, perms)?;
Ok(())
}
#[cfg(not(unix))]
fn set_mode_600(_path: &Path) -> Result<()> {
Ok(())
}
fn xml_escape(value: &str) -> String {
value
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn brew_services_available(brew_path: &Path) -> bool {
ProcessCommand::new(brew_path)
.args(["services", "list"])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn active_binary_path() -> Result<PathBuf> {
if let Some(path) = env::var_os("CLIPMEM_TEST_ACTIVE_BINARY") {
return Ok(PathBuf::from(path));
}
if let Some(path) = find_executable("clipmem") {
return Ok(path);
}
env::current_exe().context("resolve current executable path")
}
fn find_executable(name: &str) -> Option<PathBuf> {
let path = env::var_os("PATH")?;
env::split_paths(&path).find_map(|dir| {
let candidate = dir.join(name);
candidate.is_file().then_some(candidate)
})
}
fn is_homebrew_binary_path(path: &Path) -> bool {
path == Path::new("/opt/homebrew/bin/clipmem") || path == Path::new("/usr/local/bin/clipmem")
}
fn home_dir() -> Result<PathBuf> {
env::var_os("HOME")
.map(PathBuf::from)
.ok_or_else(|| anyhow!("HOME is not set"))
}