feat(core): improve exit code handling and investigate improvement of shell handling on Windows
This commit is contained in:
		
							parent
							
								
									6eb3fdfcf3
								
							
						
					
					
						commit
						798cbfee45
					
				
							
								
								
									
										6
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -319,10 +319,12 @@ dependencies = [
 | 
			
		|||
 "log-panics",
 | 
			
		||||
 "maplit",
 | 
			
		||||
 "markdown",
 | 
			
		||||
 "named_pipe",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "simplelog",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "winapi",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
| 
						 | 
				
			
			@ -612,9 +614,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
 | 
			
		|||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "libc"
 | 
			
		||||
version = "0.2.85"
 | 
			
		||||
version = "0.2.94"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3"
 | 
			
		||||
checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "libdbus-sys"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,3 +40,7 @@ markdown = "0.3.0"
 | 
			
		|||
html2text = "0.2.1"
 | 
			
		||||
log-panics = "2.0.0"
 | 
			
		||||
fs2 = "0.4.3"
 | 
			
		||||
 | 
			
		||||
[target.'cfg(windows)'.dependencies]
 | 
			
		||||
named_pipe = "0.4.1"
 | 
			
		||||
winapi = { version = "0.3.9", features = ["wincon"] }
 | 
			
		||||
							
								
								
									
										56
									
								
								espanso/src/cli/daemon/ipc.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								espanso/src/cli/daemon/ipc.rs
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,56 @@
 | 
			
		|||
/*
 | 
			
		||||
 * This file is part of espanso.
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (C) 2019-2021 Federico Terzi
 | 
			
		||||
 *
 | 
			
		||||
 * espanso is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * espanso is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use crossbeam::channel::{Sender};
 | 
			
		||||
use log::{error, warn};
 | 
			
		||||
 | 
			
		||||
use crate::ipc::IPCEvent;
 | 
			
		||||
 | 
			
		||||
use super::ExitCode;
 | 
			
		||||
 | 
			
		||||
pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender<i32>) -> Result<()> {
 | 
			
		||||
  let receiver = crate::ipc::spawn_daemon_ipc_server(runtime_dir)?;
 | 
			
		||||
 | 
			
		||||
  std::thread::Builder::new()
 | 
			
		||||
    .name("daemon-ipc-handler".to_string())
 | 
			
		||||
    .spawn(move || loop {
 | 
			
		||||
      match receiver.recv() {
 | 
			
		||||
        Ok(event) => {
 | 
			
		||||
          match event {
 | 
			
		||||
            IPCEvent::Exit => {
 | 
			
		||||
              if let Err(err) = exit_notify.send(ExitCode::Success as i32) {
 | 
			
		||||
                error!("experienced error while sending exit signal from daemon ipc handler: {}", err);
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
            unexpected_event => {
 | 
			
		||||
              warn!("received unexpected event in daemon ipc handler: {:?}", unexpected_event);
 | 
			
		||||
            },
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        Err(err) => {
 | 
			
		||||
          error!("experienced error while receiving ipc event from daemon handler: {}", err);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
  Ok(())
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -19,20 +19,35 @@
 | 
			
		|||
 | 
			
		||||
use std::{path::Path, process::Command, time::Instant};
 | 
			
		||||
 | 
			
		||||
use crossbeam::{
 | 
			
		||||
  channel::{unbounded, Sender},
 | 
			
		||||
  select,
 | 
			
		||||
};
 | 
			
		||||
use espanso_ipc::IPCClient;
 | 
			
		||||
use espanso_path::Paths;
 | 
			
		||||
use log::{error, info, warn};
 | 
			
		||||
 | 
			
		||||
use crate::{ipc::{IPCEvent, create_ipc_client_to_worker}, lock::{acquire_daemon_lock, acquire_worker_lock}};
 | 
			
		||||
use crate::{
 | 
			
		||||
  ipc::{create_ipc_client_to_worker, IPCEvent},
 | 
			
		||||
  lock::{acquire_daemon_lock, acquire_worker_lock},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use super::{CliModule, CliModuleArgs};
 | 
			
		||||
 | 
			
		||||
mod ipc;
 | 
			
		||||
 | 
			
		||||
pub enum ExitCode {
 | 
			
		||||
  Success = 0,
 | 
			
		||||
  ExitCodeUnwrapError = 100,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn new() -> CliModule {
 | 
			
		||||
  #[allow(clippy::needless_update)]
 | 
			
		||||
  CliModule {
 | 
			
		||||
    requires_paths: true,
 | 
			
		||||
    requires_config: true,
 | 
			
		||||
    enable_logs: true,
 | 
			
		||||
    log_mode: super::LogMode::Write,
 | 
			
		||||
    log_mode: super::LogMode::CleanAndAppend,
 | 
			
		||||
    subcommand: "daemon".to_string(),
 | 
			
		||||
    entry: daemon_main,
 | 
			
		||||
    ..Default::default()
 | 
			
		||||
| 
						 | 
				
			
			@ -41,16 +56,18 @@ pub fn new() -> CliModule {
 | 
			
		|||
 | 
			
		||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
 | 
			
		||||
 | 
			
		||||
fn daemon_main(args: CliModuleArgs) {
 | 
			
		||||
fn daemon_main(args: CliModuleArgs) -> i32 {
 | 
			
		||||
  let paths = args.paths.expect("missing paths in daemon main");
 | 
			
		||||
 | 
			
		||||
  // Make sure only one instance of the daemon is running
 | 
			
		||||
  let lock_file = acquire_daemon_lock(&paths.runtime);
 | 
			
		||||
  if lock_file.is_none() {
 | 
			
		||||
    error!("daemon is already running!");
 | 
			
		||||
    std::process::exit(1);
 | 
			
		||||
    return 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO: we might need to check preconditions: accessibility on macOS, presence of binaries on Linux, etc
 | 
			
		||||
 | 
			
		||||
  info!("espanso version: {}", VERSION);
 | 
			
		||||
  // TODO: print os system and version? (with os_info crate)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -59,13 +76,74 @@ fn daemon_main(args: CliModuleArgs) {
 | 
			
		|||
 | 
			
		||||
  terminate_worker_if_already_running(&paths.runtime, worker_ipc);
 | 
			
		||||
 | 
			
		||||
  let (exit_notify, exit_signal) = unbounded::<i32>();
 | 
			
		||||
 | 
			
		||||
  // TODO: register signals to terminate the worker if the daemon terminates
 | 
			
		||||
 | 
			
		||||
  spawn_worker(&paths, exit_notify.clone());
 | 
			
		||||
 | 
			
		||||
  ipc::initialize_and_spawn(&paths.runtime, exit_notify)
 | 
			
		||||
    .expect("unable to initialize ipc server for daemon");
 | 
			
		||||
 | 
			
		||||
  // TODO: start file watcher thread
 | 
			
		||||
 | 
			
		||||
  let mut exit_code: i32 = ExitCode::Success as i32;
 | 
			
		||||
 | 
			
		||||
  loop {
 | 
			
		||||
    select! {
 | 
			
		||||
      recv(exit_signal) -> code => {
 | 
			
		||||
        match code {
 | 
			
		||||
          Ok(code) => {
 | 
			
		||||
            exit_code = code
 | 
			
		||||
          },
 | 
			
		||||
          Err(err) => {
 | 
			
		||||
            error!("received error when unwrapping exit_code: {}", err);
 | 
			
		||||
            exit_code = ExitCode::ExitCodeUnwrapError as i32;
 | 
			
		||||
          },
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  exit_code
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn terminate_worker_if_already_running(runtime_dir: &Path, worker_ipc: impl IPCClient<IPCEvent>) {
 | 
			
		||||
  let lock_file = acquire_worker_lock(&runtime_dir);
 | 
			
		||||
  if lock_file.is_some() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  warn!("a worker process is already running, sending termination signal...");
 | 
			
		||||
  if let Err(err) = worker_ipc.send(IPCEvent::Exit) {
 | 
			
		||||
    error!(
 | 
			
		||||
      "unable to send termination signal to worker process: {}",
 | 
			
		||||
      err
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let now = Instant::now();
 | 
			
		||||
  while now.elapsed() < std::time::Duration::from_secs(3) {
 | 
			
		||||
    let lock_file = acquire_worker_lock(runtime_dir);
 | 
			
		||||
    if lock_file.is_some() {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::thread::sleep(std::time::Duration::from_millis(200));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  panic!(
 | 
			
		||||
    "could not terminate worker process, please kill it manually, otherwise espanso won't start"
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn spawn_worker(paths: &Paths, exit_notify: Sender<i32>) {
 | 
			
		||||
  info!("spawning the worker process...");
 | 
			
		||||
 | 
			
		||||
  let espanso_exe_path =
 | 
			
		||||
    std::env::current_exe().expect("unable to obtain espanso executable location");
 | 
			
		||||
 | 
			
		||||
  info!("spawning the worker process...");
 | 
			
		||||
 | 
			
		||||
  let mut command = Command::new(&espanso_exe_path.to_string_lossy().to_string());
 | 
			
		||||
  command.args(&["worker"]);
 | 
			
		||||
  command.env(
 | 
			
		||||
| 
						 | 
				
			
			@ -81,44 +159,35 @@ fn daemon_main(args: CliModuleArgs) {
 | 
			
		|||
    paths.runtime.to_string_lossy().to_string(),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // On windows, we need to spawn the process as "Detached"
 | 
			
		||||
  #[cfg(target_os = "windows")]
 | 
			
		||||
  {
 | 
			
		||||
    use std::os::windows::process::CommandExt;
 | 
			
		||||
    command.creation_flags(0x08000008); // Detached process without window
 | 
			
		||||
  }
 | 
			
		||||
  // TODO: investigate if this is needed here, especially when invoking a form
 | 
			
		||||
  // // On windows, we need to spawn the process as "Detached"
 | 
			
		||||
  // #[cfg(target_os = "windows")]
 | 
			
		||||
  // {
 | 
			
		||||
  //   use std::os::windows::process::CommandExt;
 | 
			
		||||
  //   //command.creation_flags(0x08000008); // CREATE_NO_WINDOW + DETACHED_PROCESS
 | 
			
		||||
  // }
 | 
			
		||||
 | 
			
		||||
  command.spawn().expect("unable to spawn worker process");
 | 
			
		||||
  let mut child = command.spawn().expect("unable to spawn worker process");
 | 
			
		||||
 | 
			
		||||
  // TODO: start IPC server
 | 
			
		||||
 | 
			
		||||
  // TODO: start file watcher thread
 | 
			
		||||
 | 
			
		||||
  loop {
 | 
			
		||||
    std::thread::sleep(std::time::Duration::from_millis(1000));
 | 
			
		||||
  // Create a monitor thread that will exit with the same non-zero code if
 | 
			
		||||
  // the worker thread exits
 | 
			
		||||
  std::thread::Builder::new()
 | 
			
		||||
    .name("worker-status-monitor".to_string())
 | 
			
		||||
    .spawn(move || {
 | 
			
		||||
      let result = child.wait();
 | 
			
		||||
      if let Ok(status) = result {
 | 
			
		||||
        if let Some(code) = status.code() {
 | 
			
		||||
          if code != 0 {
 | 
			
		||||
            error!(
 | 
			
		||||
              "worker process exited with non-zero code: {}, exiting",
 | 
			
		||||
              code
 | 
			
		||||
            );
 | 
			
		||||
            exit_notify
 | 
			
		||||
              .send(code)
 | 
			
		||||
              .expect("unable to forward worker exit code");
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
fn terminate_worker_if_already_running(runtime_dir: &Path, worker_ipc: impl IPCClient<IPCEvent>) {
 | 
			
		||||
  let lock_file = acquire_worker_lock(&runtime_dir);
 | 
			
		||||
  if lock_file.is_some() {
 | 
			
		||||
    return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
  warn!("a worker process is already running, sending termination signal...");
 | 
			
		||||
  if let Err(err) = worker_ipc.send(IPCEvent::Exit) {
 | 
			
		||||
    error!("unable to send termination signal to worker process: {}", err);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let now = Instant::now();
 | 
			
		||||
  while now.elapsed() < std::time::Duration::from_secs(3) {
 | 
			
		||||
    let lock_file = acquire_worker_lock(runtime_dir);
 | 
			
		||||
    if lock_file.is_some() {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::thread::sleep(std::time::Duration::from_millis(200));
 | 
			
		||||
  } 
 | 
			
		||||
 | 
			
		||||
  panic!("could not terminate worker process, please kill it manually, otherwise espanso won't start")
 | 
			
		||||
    })
 | 
			
		||||
    .expect("Unable to spawn worker monitor thread");
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -31,13 +31,13 @@ pub fn new() -> CliModule {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn log_main(args: CliModuleArgs) {
 | 
			
		||||
fn log_main(args: CliModuleArgs) -> i32 {
 | 
			
		||||
  let paths = args.paths.expect("missing paths argument");
 | 
			
		||||
  let log_file = paths.runtime.join(crate::LOG_FILE_NAME);
 | 
			
		||||
 | 
			
		||||
  if !log_file.exists() {
 | 
			
		||||
    eprintln!("No log file found.");
 | 
			
		||||
    std::process::exit(2);
 | 
			
		||||
    return 2;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let log_file = File::open(log_file);
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +50,8 @@ fn log_main(args: CliModuleArgs) {
 | 
			
		|||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    eprintln!("Error reading log file");
 | 
			
		||||
    std::process::exit(1);
 | 
			
		||||
    return 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  0
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,7 +32,7 @@ pub struct CliModule {
 | 
			
		|||
  pub requires_paths: bool,
 | 
			
		||||
  pub requires_config: bool,
 | 
			
		||||
  pub subcommand: String,
 | 
			
		||||
  pub entry: fn(CliModuleArgs),
 | 
			
		||||
  pub entry: fn(CliModuleArgs)->i32,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for CliModule {
 | 
			
		||||
| 
						 | 
				
			
			@ -43,7 +43,7 @@ impl Default for CliModule {
 | 
			
		|||
      requires_paths: false, 
 | 
			
		||||
      requires_config: false, 
 | 
			
		||||
      subcommand: "".to_string(), 
 | 
			
		||||
      entry: |_| {},
 | 
			
		||||
      entry: |_| {0},
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ pub fn new() -> CliModule {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn path_main(args: CliModuleArgs) {
 | 
			
		||||
fn path_main(args: CliModuleArgs) -> i32 {
 | 
			
		||||
  let paths = args.paths.expect("missing paths argument");
 | 
			
		||||
  let cli_args = args.cli_args.expect("missing cli_args argument");
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -56,4 +56,6 @@ fn path_main(args: CliModuleArgs) {
 | 
			
		|||
    println!("Packages: {}", paths.packages.to_string_lossy());
 | 
			
		||||
    println!("Runtime: {}", paths.runtime.to_string_lossy());
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  0
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,21 +37,21 @@ pub fn new() -> CliModule {
 | 
			
		|||
    requires_paths: true,
 | 
			
		||||
    requires_config: true,
 | 
			
		||||
    enable_logs: true,
 | 
			
		||||
    log_mode: super::LogMode::Append,
 | 
			
		||||
    log_mode: super::LogMode::AppendOnly,
 | 
			
		||||
    subcommand: "worker".to_string(),
 | 
			
		||||
    entry: worker_main,
 | 
			
		||||
    ..Default::default()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn worker_main(args: CliModuleArgs) {
 | 
			
		||||
fn worker_main(args: CliModuleArgs) -> i32 {
 | 
			
		||||
  let paths = args.paths.expect("missing paths in worker main");
 | 
			
		||||
 | 
			
		||||
  // Avoid running multiple worker instances
 | 
			
		||||
  let lock_file = acquire_worker_lock(&paths.runtime);
 | 
			
		||||
  if lock_file.is_none() {
 | 
			
		||||
    error!("worker is already running!");
 | 
			
		||||
    std::process::exit(1);
 | 
			
		||||
    return 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let config_store = args
 | 
			
		||||
| 
						 | 
				
			
			@ -101,4 +101,6 @@ fn worker_main(args: CliModuleArgs) {
 | 
			
		|||
  }));
 | 
			
		||||
 | 
			
		||||
  info!("exiting worker process...");
 | 
			
		||||
   
 | 
			
		||||
  0
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,7 @@
 | 
			
		|||
 */
 | 
			
		||||
 | 
			
		||||
// This is needed to avoid showing a console window when starting espanso on Windows
 | 
			
		||||
// TODO #![windows_subsystem = "windows"]
 | 
			
		||||
#![windows_subsystem = "windows"]
 | 
			
		||||
 | 
			
		||||
#[macro_use]
 | 
			
		||||
extern crate lazy_static;
 | 
			
		||||
| 
						 | 
				
			
			@ -56,7 +56,7 @@ lazy_static! {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
  // TODO: attach console
 | 
			
		||||
  util::attach_console();
 | 
			
		||||
 | 
			
		||||
  let install_subcommand = SubCommand::with_name("install")
 | 
			
		||||
    .about("Install a package. Equivalent to 'espanso package install'")
 | 
			
		||||
| 
						 | 
				
			
			@ -325,7 +325,9 @@ fn main() {
 | 
			
		|||
      cli_args.cli_args = Some(args.clone());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    (handler.entry)(cli_args)
 | 
			
		||||
    let exit_code = (handler.entry)(cli_args);
 | 
			
		||||
 | 
			
		||||
    std::process::exit(exit_code);
 | 
			
		||||
  } else {
 | 
			
		||||
    clap_instance
 | 
			
		||||
      .print_long_help()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,3 +31,15 @@ pub fn set_command_flags(command: &mut Command) {
 | 
			
		|||
pub fn set_command_flags(_: &mut Command) {
 | 
			
		||||
  // NOOP on Linux and macOS
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(target_os = "windows")]
 | 
			
		||||
pub fn attach_console() {
 | 
			
		||||
  // When using the windows subsystem we loose the terminal output.
 | 
			
		||||
  // Therefore we try to attach to the current console if available.
 | 
			
		||||
  unsafe { winapi::um::wincon::AttachConsole(0xFFFFFFFF) };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(not(target_os = "windows"))]
 | 
			
		||||
pub fn attach_console() {
 | 
			
		||||
  // Not necessary on Linux and macOS
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								packager/win/espanso.cmd
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packager/win/espanso.cmd
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
@"%~dp0espansow.exe" %*
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user