181 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			181 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
//! The `kcl` lsp server.
 | 
						|
 | 
						|
#![deny(missing_docs)]
 | 
						|
 | 
						|
use anyhow::{bail, Result};
 | 
						|
use clap::Parser;
 | 
						|
use slog::Drain;
 | 
						|
use tower_lsp::{LspService, Server as LspServer};
 | 
						|
use tracing_subscriber::{prelude::*, Layer};
 | 
						|
 | 
						|
lazy_static::lazy_static! {
 | 
						|
/// Initialize the logger.
 | 
						|
    // We need a slog::Logger for steno and when we export out the logs from re-exec-ed processes.
 | 
						|
    pub static ref LOGGER: slog::Logger = {
 | 
						|
        let decorator = slog_term::TermDecorator::new().build();
 | 
						|
        let drain = slog_term::FullFormat::new(decorator).build().fuse();
 | 
						|
        let drain = slog_async::Async::new(drain).build().fuse();
 | 
						|
        slog::Logger::root(drain, slog::slog_o!())
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
/// This doc string acts as a help message when the user runs '--help'
 | 
						|
/// as do all doc strings on fields.
 | 
						|
#[derive(Parser, Debug, Clone)]
 | 
						|
#[clap(version = clap::crate_version!(), author = clap::crate_authors!("\n"))]
 | 
						|
pub struct Opts {
 | 
						|
    /// Print debug info
 | 
						|
    #[clap(short, long)]
 | 
						|
    pub debug: bool,
 | 
						|
 | 
						|
    /// Print logs as json
 | 
						|
    #[clap(short, long)]
 | 
						|
    pub json: bool,
 | 
						|
 | 
						|
    /// The subcommand to run.
 | 
						|
    #[clap(subcommand)]
 | 
						|
    pub subcmd: SubCommand,
 | 
						|
}
 | 
						|
 | 
						|
impl Opts {
 | 
						|
    /// Setup our logger.
 | 
						|
    pub fn create_logger(&self) -> slog::Logger {
 | 
						|
        if self.json {
 | 
						|
            let drain = slog_json::Json::default(std::io::stderr()).fuse();
 | 
						|
            self.async_root_logger(drain)
 | 
						|
        } else {
 | 
						|
            let decorator = slog_term::TermDecorator::new().build();
 | 
						|
            let drain = slog_term::FullFormat::new(decorator).build().fuse();
 | 
						|
            self.async_root_logger(drain)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    fn async_root_logger<T>(&self, drain: T) -> slog::Logger
 | 
						|
    where
 | 
						|
        T: slog::Drain + Send + 'static,
 | 
						|
        <T as slog::Drain>::Err: std::fmt::Debug,
 | 
						|
    {
 | 
						|
        let level = if self.debug {
 | 
						|
            slog::Level::Debug
 | 
						|
        } else {
 | 
						|
            slog::Level::Info
 | 
						|
        };
 | 
						|
 | 
						|
        let level_drain = slog::LevelFilter(drain, level).fuse();
 | 
						|
        let async_drain = slog_async::Async::new(level_drain).build().fuse();
 | 
						|
        slog::Logger::root(async_drain, slog::o!())
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/// A subcommand for our cli.
 | 
						|
#[derive(Parser, Debug, Clone)]
 | 
						|
pub enum SubCommand {
 | 
						|
    /// Run the server.
 | 
						|
    Server(kcl_lib::KclLspServerSubCommand),
 | 
						|
}
 | 
						|
 | 
						|
#[tokio::main]
 | 
						|
async fn main() -> Result<()> {
 | 
						|
    let opts: Opts = Opts::parse();
 | 
						|
 | 
						|
    let level_filter = if opts.debug {
 | 
						|
        tracing_subscriber::filter::LevelFilter::DEBUG
 | 
						|
    } else {
 | 
						|
        tracing_subscriber::filter::LevelFilter::INFO
 | 
						|
    };
 | 
						|
 | 
						|
    // Format fields using the provided closure.
 | 
						|
    // We want to make this very concise otherwise the logs are not able to be read by humans.
 | 
						|
    let format = tracing_subscriber::fmt::format::debug_fn(|writer, field, value| {
 | 
						|
        if format!("{field}") == "message" {
 | 
						|
            write!(writer, "{field}: {value:?}")
 | 
						|
        } else {
 | 
						|
            write!(writer, "{field}")
 | 
						|
        }
 | 
						|
    })
 | 
						|
    // Separate each field with a comma.
 | 
						|
    // This method is provided by an extension trait in the
 | 
						|
    // `tracing-subscriber` prelude.
 | 
						|
    .delimited(", ");
 | 
						|
 | 
						|
    let (json, plain) = if opts.json {
 | 
						|
        // Cloud run likes json formatted logs if possible.
 | 
						|
        // See: https://cloud.google.com/run/docs/logging
 | 
						|
        // We could probably format these specifically for cloud run if we wanted,
 | 
						|
        // will save that as a TODO: https://cloud.google.com/run/docs/logging#special-fields
 | 
						|
        (
 | 
						|
            Some(tracing_subscriber::fmt::layer().json().with_filter(level_filter)),
 | 
						|
            None,
 | 
						|
        )
 | 
						|
    } else {
 | 
						|
        (
 | 
						|
            None,
 | 
						|
            Some(
 | 
						|
                tracing_subscriber::fmt::layer()
 | 
						|
                    .pretty()
 | 
						|
                    .fmt_fields(format)
 | 
						|
                    .with_filter(level_filter),
 | 
						|
            ),
 | 
						|
        )
 | 
						|
    };
 | 
						|
 | 
						|
    // Initialize the tracing.
 | 
						|
    tracing_subscriber::registry().with(json).with(plain).init();
 | 
						|
 | 
						|
    if let Err(err) = run_cmd(&opts).await {
 | 
						|
        bail!("running cmd `{:?}` failed: {:?}", &opts.subcmd, err);
 | 
						|
    }
 | 
						|
 | 
						|
    Ok(())
 | 
						|
}
 | 
						|
 | 
						|
async fn run_cmd(opts: &Opts) -> Result<()> {
 | 
						|
    match &opts.subcmd {
 | 
						|
        SubCommand::Server(s) => {
 | 
						|
            let (service, socket) = LspService::new(|client| {
 | 
						|
                kcl_lib::KclLspBackend::new(client, Default::default(), kittycad::Client::new(""), false).unwrap()
 | 
						|
            });
 | 
						|
 | 
						|
            // TODO find a way to ctrl+c on windows.
 | 
						|
            #[cfg(not(target_os = "windows"))]
 | 
						|
            {
 | 
						|
                // For Cloud run & ctrl+c, shutdown gracefully.
 | 
						|
                // "The main process inside the container will receive SIGTERM, and after a grace period,
 | 
						|
                // SIGKILL."
 | 
						|
                // Registering SIGKILL here will panic at runtime, so let's avoid that.
 | 
						|
                use signal_hook::{
 | 
						|
                    consts::{SIGINT, SIGTERM},
 | 
						|
                    iterator::Signals,
 | 
						|
                };
 | 
						|
                let mut signals = Signals::new([SIGINT, SIGTERM])?;
 | 
						|
 | 
						|
                tokio::spawn(async move {
 | 
						|
                    if let Some(sig) = signals.forever().next() {
 | 
						|
                        log::info!("received signal: {sig:?}");
 | 
						|
                        log::info!("triggering cleanup...");
 | 
						|
 | 
						|
                        // Exit the process.
 | 
						|
                        log::info!("all clean, exiting!");
 | 
						|
                        std::process::exit(0);
 | 
						|
                    }
 | 
						|
                });
 | 
						|
            }
 | 
						|
 | 
						|
            if s.stdio {
 | 
						|
                // Listen on stdin and stdout.
 | 
						|
                let stdin = tokio::io::stdin();
 | 
						|
                let stdout = tokio::io::stdout();
 | 
						|
                LspServer::new(stdin, stdout, socket).serve(service).await;
 | 
						|
            } else {
 | 
						|
                // Listen on a tcp stream.
 | 
						|
                let listener = tokio::net::TcpListener::bind(&format!("0.0.0.0:{}", s.socket)).await?;
 | 
						|
                let (stream, _) = listener.accept().await?;
 | 
						|
                let (read, write) = tokio::io::split(stream);
 | 
						|
                LspServer::new(read, write, socket).serve(service).await;
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    Ok(())
 | 
						|
}
 |