Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TS-2073] Server implementation for tooling interop #11

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ mod error;
mod game;
mod package;
mod project;
mod server;
mod ts;
mod ui;
mod util;
Expand Down
31 changes: 31 additions & 0 deletions src/server/lock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::fs::{self, File};
use std::path::{Path, PathBuf};

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("The lock at {0:?} is already acquired by a process with PID {1}.")]
InUse(PathBuf, u32),
}

/// Only one server process can "own" a project at a single time.
/// We enforce this exclusive access through the creation and deletion of this lockfile.
const LOCKFILE: &'static str = ".server-lock";

Check warning

Code scanning / clippy

constants have by default a 'static lifetime Warning

constants have by default a 'static lifetime

pub struct ProjectLock<'a> {
file: File,
path: &'a Path,
}

impl<'a> ProjectLock<'_> {
/// Attempt to acquire a lock on the provided directory.
pub fn lock(project_path: &'a Path) -> Result<Self, Error> {
todo!()
}
}

impl Drop for ProjectLock<'_> {
fn drop(&mut self) {
// The file handle is dropped before this, so we can safely delete it.
fs::remove_file(self.path);
}
}
43 changes: 43 additions & 0 deletions src/server/method/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
pub mod package;
pub mod project;

use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};

use self::package::PackageMethod;
use self::project::ProjectMethod;
use super::Error;

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub enum Method {
Exit,
Project(ProjectMethod),
Package(PackageMethod),
}

/// Method namespace registration.
impl Method {
pub fn from_value(method: &str, value: serde_json::Value) -> Result<Self, Error> {
let mut split = method.split('/');
let (namespace, name) = (
split
.next()
.ok_or_else(|| Error::InvalidMethod(method.into()))?,
split
.next()
.ok_or_else(|| Error::InvalidMethod(method.into()))?,
);

// Route namespaces to the appropriate enum variants for construction.
Ok(match namespace {
"exit" => Self::Exit,
"project" => Self::Project(ProjectMethod::from_value(name, value)?),
"package" => Self::Package(PackageMethod::from_value(name, value)?),
x => Err(Error::InvalidMethod(x.into()))?,
})
}
}

pub fn parse_value<T: DeserializeOwned>(value: serde_json::Value) -> Result<T, serde_json::Error> {
serde_json::from_value(value)
}
21 changes: 21 additions & 0 deletions src/server/method/package.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use serde::{Deserialize, Serialize};

use super::Error;

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub enum PackageMethod {
/// Get metadata about this package.
GetMetadata,
/// Determine if the package exists within the cache.
IsCached,
}

impl PackageMethod {
pub fn from_value(method: &str, value: serde_json::Value) -> Result<Self, Error> {

Check warning

Code scanning / clippy

unused variable: value Warning

unused variable: value
Ok(match method {
"get_metadata" => Self::GetMetadata,
"is_cached" => Self::IsCached,
x => Err(Error::InvalidMethod(x.into()))?,
})
}
}
55 changes: 55 additions & 0 deletions src/server/method/project.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

use crate::ts::package_reference::PackageReference;

use super::Error;

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub enum ProjectMethod {
/// Set the project directory context. This locks the project, if it exists.
SetContext(SetContext),
/// Release the project directory context. This unlocks the project, if a lock exists.
ReleaseContext,
/// Get project metadata.
GetMetadata,
/// Add one or more packages to the project.
AddPackages(AddPackages),
/// Remove one or more packages from the project.
RemovePackages(RemovePackages),
/// Get a list of currently installed packages.
GetPackages,
/// Determine if the current context is a valid project.
IsValid,
}

impl ProjectMethod {
pub fn from_value(method: &str, value: serde_json::Value) -> Result<Self, Error> {
Ok(match method {
"set_context" => Self::SetContext(super::parse_value(value)?),
"release_context" => Self::ReleaseContext,
"get_metadata" => Self::GetMetadata,
"add_packages" => Self::AddPackages(super::parse_value(value)?),
"remove_packages" => Self::RemovePackages(super::parse_value(value)?),
"get_packages" => Self::GetPackages,
"is_valid" => Self::IsValid,
x => Err(Error::InvalidMethod(x.into()))?,
})
}
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct SetContext {
path: PathBuf,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct AddPackages {
packages: Vec<PackageReference>,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct RemovePackages {
packages: Vec<PackageReference>,
}
90 changes: 90 additions & 0 deletions src/server/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use std::io::Write;
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::RwLock;
use std::{io, thread};

use self::proto::{Message, Request, Response};

Check warning

Code scanning / clippy

unused import: Request Warning

unused import: Request
Fixed Show fixed Hide fixed

mod method;
mod proto;
mod route;

trait ToJson {
fn to_json(&self) -> Result<String, serde_json::Error>;
}

/// This error type exists to wrap library errors into a single easy-to-use package.
#[derive(thiserror::Error, Debug)]
#[repr(isize)]
pub enum Error {
/// A partial implementation of the error variants described by the JRPC spec.
#[error("Failed to serialize JSON: {0:?}")]
InvalidJson(#[from] serde_json::Error) = -32700,

#[error("The method {0} is not valid.")]
InvalidMethod(String) = -32601,

#[error("Recieved invalid params for method {0}: {1}")]
InvalidParams(String, String) = -32602,

/// Wrapper error types and codes.
#[error("${0:?}")]
ProjectError(String) = 1000,

Check warning

Code scanning / clippy

variant name ends with the enum's name Warning

variant name ends with the enum's name

#[error("{0:?}")]
PackageError(String) = 2000,

Check warning

Code scanning / clippy

variant name ends with the enum's name Warning

variant name ends with the enum's name
}

impl Error {
pub fn discriminant(&self) -> isize {
// SAFETY: `Self` is `repr(isize)` with layout `repr(C)`, with each variant having an isize
// as its first field, so we can access this value without a pointer offset.
unsafe { *<*const _>::from(self).cast::<isize>() }
}
}

impl ToJson for Result<Response, Error> {
fn to_json(&self) -> Result<String, serde_json::Error> {
todo!()
}
}

/// The daemon's entrypoint. This is a psuedo event loop which does the following in step:
/// 1. Read JSON-RPC input(s) from stdin.
/// 2. Route each input.
/// 3. Serialize the output and write to stdout.
async fn start() {
let stdin = io::stdin();
let mut line = String::new();
let (tx, rx) = mpsc::channel::<Result<Response, Error>>();

let cancel = RwLock::new(false);

// Responses are published through the tx send channel.
thread::spawn(move || respond_msg(rx, cancel));

loop {
// Block the main thread until we have an input line available to be read.
// This is ok because, in theory, tasks will be processed on background threads.
if let Err(e) = stdin.read_line(&mut line) {

Check warning

Code scanning / clippy

unused variable: e Warning

unused variable: e
panic!("")
}
let res = route(&line, tx.clone()).await;
res.to_json().unwrap();
}
}

fn respond_msg(rx: Receiver<Result<Response, Error>>, cancel: RwLock<bool>) {

Check warning

Code scanning / clippy

unused variable: cancel Warning

unused variable: cancel
let mut stdout = io::stdout();
while let Ok(res) = rx.recv() {
let msg = res.map(|x| serde_json::to_string(&x).unwrap());
stdout.write_all(msg.unwrap().as_bytes());

Check warning

Code scanning / clippy

unused std::result::Result that must be used Warning

unused std::result::Result that must be used
stdout.write_all("\n".as_bytes());

Check warning

Code scanning / clippy

unused std::result::Result that must be used Warning

unused std::result::Result that must be used
}
}

/// Route and execute the request, returning the result.
async fn route(line: &str, tx: Sender<Result<Response, Error>>) -> Result<Response, Error> {

Check warning

Code scanning / clippy

unused variable: tx Warning

unused variable: tx
let req = Message::from_json(line);

Check warning

Code scanning / clippy

unused variable: req Warning

unused variable: req
todo!()
}
Loading
Loading