Migrated from Uniffi+JNA to Kotars
This commit is contained in:
parent
bdb4bcb702
commit
a542e38ae2
@ -164,3 +164,9 @@ Example for windows (you may want to edit `C:\Program Files\Git\etc\gitconfig`):
|
||||
[credential]
|
||||
helper = C:/Program Files/Git/mingw64/bin/git-credential-manager-core.exe
|
||||
```
|
||||
|
||||
|
||||
## Sponsors
|
||||
|
||||
Thank you to all the sponsors for helping improve Gitnuro and JetBrains for providing the necessary tooling.
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import org.gradle.jvm.tasks.Jar
|
||||
import org.jetbrains.compose.compose
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.file.Files
|
||||
|
||||
@ -172,11 +171,11 @@ task("fatJarLinux", type = Jar::class) {
|
||||
with(tasks.jar.get() as CopySpec)
|
||||
}
|
||||
|
||||
|
||||
task("rust_generateKotlinFromUdl") {
|
||||
println("Generate Kotlin")
|
||||
generateKotlinFromUdl()
|
||||
}
|
||||
//
|
||||
//task("rust_generateKotlinFromUdl") {
|
||||
// println("Generate Kotlin")
|
||||
// generateKotlinFromUdl()
|
||||
//}
|
||||
|
||||
task("rust_build") {
|
||||
buildRust()
|
||||
@ -186,14 +185,14 @@ tasks.getByName("compileKotlin").doLast {
|
||||
println("compileKotlin called")
|
||||
buildRust()
|
||||
copyRustBuild()
|
||||
generateKotlinFromUdl()
|
||||
generateKotlinFromRs()
|
||||
}
|
||||
|
||||
tasks.getByName("compileTestKotlin").doLast {
|
||||
println("compileTestKotlin called")
|
||||
buildRust()
|
||||
copyRustBuild()
|
||||
generateKotlinFromUdl()
|
||||
generateKotlinFromRs()
|
||||
}
|
||||
|
||||
|
||||
@ -207,23 +206,15 @@ task("tasksList") {
|
||||
task("rustTasks") {
|
||||
buildRust()
|
||||
copyRustBuild()
|
||||
generateKotlinFromUdl()
|
||||
// generateKotlinFromUdl()
|
||||
}
|
||||
|
||||
task("rust_copyBuild") {
|
||||
copyRustBuild()
|
||||
}
|
||||
|
||||
fun generateKotlinFromUdl() {
|
||||
exec {
|
||||
workingDir = File(project.projectDir, "rs")
|
||||
commandLine = listOf(
|
||||
"cargo", "run", "--features=uniffi/cli",
|
||||
"--bin", "uniffi-bindgen", "generate", "src/gitnuro.udl",
|
||||
"--language", "kotlin",
|
||||
"--out-dir", rustGeneratedSource
|
||||
)
|
||||
}
|
||||
fun generateKotlinFromRs() {
|
||||
|
||||
}
|
||||
|
||||
fun buildRust() {
|
||||
@ -236,7 +227,7 @@ fun buildRust() {
|
||||
}
|
||||
|
||||
val params = mutableListOf(
|
||||
binary, "build", "--release", "--features=uniffi/cli",
|
||||
binary, "build", "--release",
|
||||
)
|
||||
|
||||
if (currentOs() == OS.LINUX && useCross) {
|
||||
@ -270,23 +261,16 @@ fun copyRustBuild() {
|
||||
val directory = File(outputDir)
|
||||
directory.mkdirs()
|
||||
|
||||
val originLib = when (currentOs()) {
|
||||
val lib = when (currentOs()) {
|
||||
OS.LINUX -> "libgitnuro_rs.so"
|
||||
OS.WINDOWS -> "gitnuro_rs.dll"
|
||||
OS.MAC -> "libgitnuro_rs.dylib"
|
||||
}
|
||||
|
||||
val destinyLib = when (currentOs()) {
|
||||
OS.LINUX -> "libuniffi_gitnuro.so"
|
||||
OS.WINDOWS -> "uniffi_gitnuro.dll"
|
||||
OS.MAC -> "libuniffi_gitnuro.dylib"
|
||||
}
|
||||
|
||||
val originFile = File(workingDir, originLib)
|
||||
val destinyFile = File(directory, destinyLib)
|
||||
val originFile = File(workingDir, lib)
|
||||
val destinyFile = File(directory, lib)
|
||||
|
||||
Files.copy(originFile.toPath(), FileOutputStream(destinyFile))
|
||||
// com.google.common.io.Files.copy(originFile, destinyFile)
|
||||
|
||||
println("Copy rs build completed")
|
||||
}
|
||||
|
@ -1,2 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
@ -8,14 +8,8 @@ crate-type = ["cdylib"]
|
||||
name = "gitnuro_rs"
|
||||
|
||||
[dependencies]
|
||||
uniffi = { version = "0.27.1" }
|
||||
notify = "6.1.1"
|
||||
thiserror = "1.0.56"
|
||||
libssh-rs = { version = "0.2.2", features = ["vendored", "vendored-openssl"] }
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { version = "0.27.1", features = ["build"] }
|
||||
|
||||
[[bin]]
|
||||
name = "uniffi-bindgen"
|
||||
path = "uniffi-bindgen.rs"
|
||||
libssh-rs = { version = "0.3.3", features = ["vendored", "vendored-openssl"] }
|
||||
kotars = { path = "/home/abde/Projects/Rust/kotars/kotars" } #{ git = "https://github.com/JetpackDuba/kotars.git" }
|
||||
jni = "0.21.1"
|
||||
|
@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
uniffi::generate_scaffolding("src/gitnuro.udl").unwrap();
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
namespace gitnuro {
|
||||
[Throws=WatcherInitError]
|
||||
void watch_directory(string path, string git_dir_path, WatchDirectoryNotifier checker);
|
||||
};
|
||||
|
||||
callback interface WatchDirectoryNotifier {
|
||||
boolean should_keep_looping();
|
||||
|
||||
void detected_change(sequence<string> paths);
|
||||
};
|
||||
|
||||
[Error]
|
||||
interface WatcherInitError {
|
||||
Generic(string error);
|
||||
Io(string error);
|
||||
PathNotFound();
|
||||
WatchNotFound();
|
||||
InvalidConfig();
|
||||
MaxFilesWatch();
|
||||
};
|
||||
|
||||
enum AuthStatus {
|
||||
"Success",
|
||||
"Denied",
|
||||
"Partial",
|
||||
"Info",
|
||||
"Again",
|
||||
};
|
||||
|
||||
|
||||
interface Session {
|
||||
constructor();
|
||||
void setup(string host, string? user, u16? port);
|
||||
AuthStatus public_key_auth(string password);
|
||||
AuthStatus password_auth(string password);
|
||||
void disconnect();
|
||||
};
|
||||
|
||||
interface Channel {
|
||||
constructor(Session session);
|
||||
void open_session();
|
||||
boolean is_open();
|
||||
void close_channel();
|
||||
void request_exec(string command);
|
||||
boolean poll_has_bytes(boolean is_stderr);
|
||||
ReadResult read(boolean is_stderr, u64 len);
|
||||
void write_byte(i32 byte);
|
||||
void write_bytes(bytes data);
|
||||
};
|
||||
|
||||
dictionary ReadResult {
|
||||
u64 read_count;
|
||||
bytes data;
|
||||
};
|
314
rs/src/lib.rs
314
rs/src/lib.rs
@ -1,10 +1,312 @@
|
||||
#[allow(unused_imports)] // Needed to map it to the enum in the UDL file
|
||||
extern crate notify;
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::{channel, RecvTimeoutError};
|
||||
use std::sync::RwLock;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use kotars::{jni_class, jni_data_class, jni_interface, jni_struct_impl};
|
||||
use kotars::jni_init;
|
||||
use libssh_rs::{PollStatus, SshOption};
|
||||
#[allow(unused_imports)]
|
||||
use libssh_rs::AuthStatus;
|
||||
use notify::{Config, Error, Event, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
|
||||
use ssh::{*};
|
||||
use watch_directory::{*};
|
||||
jni_init!("");
|
||||
|
||||
mod ssh;
|
||||
mod watch_directory;
|
||||
#[jni_class]
|
||||
struct FileWatcher {}
|
||||
|
||||
uniffi::include_scaffolding!("gitnuro");
|
||||
#[jni_data_class]
|
||||
struct FileChanged {
|
||||
path: String,
|
||||
}
|
||||
|
||||
impl From<String> for FileChanged {
|
||||
fn from(value: String) -> Self {
|
||||
FileChanged { path: value }
|
||||
}
|
||||
}
|
||||
|
||||
#[jni_struct_impl]
|
||||
impl FileWatcher {
|
||||
fn watch(
|
||||
&self,
|
||||
path: String,
|
||||
git_dir_path: String,
|
||||
notifier: &impl WatchDirectoryNotifier,
|
||||
) {
|
||||
println!("Starting to watch directory {path}");
|
||||
watch_directory(path, git_dir_path, notifier);
|
||||
}
|
||||
|
||||
fn new() -> FileWatcher {
|
||||
FileWatcher {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const MIN_TIME_IN_MS_BETWEEN_REFRESHES: u128 = 500;
|
||||
const WATCH_TIMEOUT: u64 = 500;
|
||||
|
||||
|
||||
pub fn watch_directory(
|
||||
path: String,
|
||||
git_dir_path: String,
|
||||
notifier: &impl WatchDirectoryNotifier,
|
||||
) {
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
|
||||
// Create a watcher object, delivering debounced events.
|
||||
// The notification back-end is selected based on the platform.
|
||||
let config = Config::default();
|
||||
config.with_poll_interval(Duration::from_secs(3600));
|
||||
|
||||
let mut watcher =
|
||||
RecommendedWatcher::new(tx, config).expect("Init watcher failed");
|
||||
|
||||
// Add a path to be watched. All files and directories at that path and
|
||||
// below will be monitored for changes.
|
||||
watcher
|
||||
.watch(Path::new(path.as_str()), RecursiveMode::Recursive)
|
||||
.expect("Start watching failed");
|
||||
|
||||
let mut paths_cached: Vec<String> = Vec::new();
|
||||
|
||||
let mut last_update: u128 = 0;
|
||||
|
||||
while true {
|
||||
match rx.recv_timeout(Duration::from_millis(WATCH_TIMEOUT)) {
|
||||
Ok(e) => {
|
||||
if let Some(paths) = get_paths_from_event_result(&e, &git_dir_path) {
|
||||
let mut paths_without_dirs: Vec<String> = paths
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let first_path = paths_without_dirs.first();
|
||||
|
||||
if let Some(path) = first_path {
|
||||
notifier.detected_change(path.clone().into());
|
||||
}
|
||||
|
||||
|
||||
last_update = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("We need a TARDIS to fix this")
|
||||
.as_millis();
|
||||
|
||||
println!("Event: {e:?}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if e != RecvTimeoutError::Timeout {
|
||||
println!("Watch error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watcher
|
||||
.unwatch(Path::new(path.as_str()))
|
||||
.expect("Unwatch failed");
|
||||
|
||||
// Ok(())
|
||||
}
|
||||
|
||||
pub fn get_paths_from_event_result(event_result: &Result<Event, Error>, git_dir_path: &str) -> Option<Vec<String>> {
|
||||
match event_result {
|
||||
Ok(event) => {
|
||||
let events: Vec<String> = event
|
||||
.paths
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter_map(|path| {
|
||||
// Directories are not tracked by Git so we don't care about them (just about their content)
|
||||
// We won't be able to check if it's a dir if it has been deleted but that's good enough
|
||||
if path.is_dir() {
|
||||
println!("Ignoring directory {path:#?}");
|
||||
None
|
||||
} else {
|
||||
let path_str = path.into_os_string()
|
||||
.into_string()
|
||||
.ok()?;
|
||||
|
||||
// JGit may create .probe-UUID files for its internal stuff, we don't care about it
|
||||
let probe_prefix = format!("{git_dir_path}.probe-");
|
||||
if path_str.starts_with(probe_prefix.as_str()) {
|
||||
None
|
||||
} else {
|
||||
Some(path_str)
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if events.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(events)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("{:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[jni_interface]
|
||||
pub trait WatchDirectoryNotifier {
|
||||
// fn should_keep_looping(&self) -> bool;
|
||||
fn detected_change(&self, path: FileChanged);
|
||||
}
|
||||
|
||||
const ACCEPTED_SSH_TYPES: &str = "ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-rsa,rsa-sha2-512,rsa-sha2-256,ssh-dss";
|
||||
|
||||
#[jni_class]
|
||||
pub struct Session {
|
||||
pub session: RwLock<libssh_rs::Session>,
|
||||
}
|
||||
|
||||
#[jni_struct_impl]
|
||||
impl Session {
|
||||
pub fn new() -> Session {
|
||||
let session = libssh_rs::Session::new().unwrap();
|
||||
|
||||
Session {
|
||||
session: RwLock::new(session)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup(&self, host: String, user: String, port: Option<i32>) {
|
||||
let session = self.session.write().unwrap();
|
||||
session.set_option(SshOption::Hostname(host)).unwrap();
|
||||
|
||||
if !user.is_empty() {
|
||||
session.set_option(SshOption::User(Some(user))).unwrap();
|
||||
}
|
||||
|
||||
if let Some(port) = port {
|
||||
session.set_option(SshOption::Port(port as u16)).unwrap();
|
||||
}
|
||||
|
||||
session.set_option(SshOption::PublicKeyAcceptedTypes(ACCEPTED_SSH_TYPES.to_string())).unwrap();
|
||||
session.options_parse_config(None).unwrap();
|
||||
session.connect().unwrap();
|
||||
}
|
||||
//
|
||||
pub fn public_key_auth(&self, password: String) -> i32 { //AuthStatus {
|
||||
println!("Public key auth");
|
||||
|
||||
let session = self.session.write().unwrap();
|
||||
|
||||
let status = session.userauth_public_key_auto(None, Some(&password)).unwrap();
|
||||
|
||||
println!("Status is {status:?}");
|
||||
|
||||
to_int(status) // TODO remove this cast
|
||||
}
|
||||
//
|
||||
pub fn password_auth(&self, password: String) -> i32 { //AuthStatus {
|
||||
let session = self.session.write().unwrap();
|
||||
let status = session.userauth_password(None, Some(&password)).unwrap();
|
||||
to_int(status) // TODO remove this cast
|
||||
}
|
||||
|
||||
pub fn disconnect(&self) {
|
||||
let session = self.session.write().unwrap();
|
||||
session.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
#[jni_class]
|
||||
pub struct Channel {
|
||||
channel: RwLock<libssh_rs::Channel>,
|
||||
}
|
||||
|
||||
#[jni_struct_impl]
|
||||
impl Channel {
|
||||
pub fn new(session: &mut Session) -> Channel {
|
||||
let session = session.session.write().unwrap();
|
||||
let channel = session.new_channel().unwrap();
|
||||
|
||||
Channel {
|
||||
channel: RwLock::new(channel)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_session(&self) {
|
||||
let channel = self.channel.write().unwrap();
|
||||
channel.open_session().unwrap();
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
let channel = self.channel.write().unwrap();
|
||||
channel.is_open()
|
||||
}
|
||||
|
||||
pub fn close_channel(&self) {
|
||||
let channel = self.channel.write().unwrap();
|
||||
channel.close().unwrap();
|
||||
}
|
||||
|
||||
pub fn request_exec(&self, command: String) {
|
||||
let channel = self.channel.write().unwrap();
|
||||
channel.request_exec(&command).unwrap();
|
||||
}
|
||||
|
||||
pub fn poll_has_bytes(&self, is_stderr: bool) -> bool {
|
||||
let channel = self.channel.write().unwrap();
|
||||
let poll_timeout = channel.poll_timeout(is_stderr, None).unwrap();
|
||||
|
||||
match poll_timeout {
|
||||
PollStatus::AvailableBytes(count) => count > 0,
|
||||
PollStatus::EndOfFile => false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self, is_stderr: bool, len: u64) -> ReadResult {
|
||||
let ulen = len as usize;
|
||||
|
||||
let channel = self.channel.write().unwrap();
|
||||
|
||||
let mut buffer = vec![0; ulen];
|
||||
let read = channel.read_timeout(&mut buffer, is_stderr, None).unwrap();
|
||||
|
||||
ReadResult {
|
||||
read_count: read as u64,
|
||||
data: buffer,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_byte(&self, byte: i32) {
|
||||
println!("Byte is {byte}");
|
||||
|
||||
let channel = self.channel.write().unwrap();
|
||||
channel.stdin().write_all(&byte.to_ne_bytes()).unwrap();
|
||||
}
|
||||
|
||||
pub fn write_bytes(&self, data: &Vec<u8>) {
|
||||
let channel = self.channel.write().unwrap();
|
||||
channel.stdin().write_all(data).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn to_int(auth_status: AuthStatus) -> i32 {
|
||||
match auth_status {
|
||||
AuthStatus::Success => 1,
|
||||
AuthStatus::Denied => 2,
|
||||
AuthStatus::Partial => 3,
|
||||
AuthStatus::Info => 4,
|
||||
AuthStatus::Again => 5,
|
||||
}
|
||||
}
|
||||
|
||||
#[jni_data_class]
|
||||
pub struct ReadResult {
|
||||
pub read_count: u64,
|
||||
pub data: Vec<u8>,
|
||||
}
|
138
rs/src/ssh.rs
138
rs/src/ssh.rs
@ -1,138 +0,0 @@
|
||||
use std::io::Write;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use libssh_rs::{AuthStatus, PollStatus, SshOption};
|
||||
|
||||
const ACCEPTED_SSH_TYPES: &str = "ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-rsa,rsa-sha2-512,rsa-sha2-256,ssh-dss";
|
||||
|
||||
pub struct Session {
|
||||
pub session: RwLock<libssh_rs::Session>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new() -> Self {
|
||||
let session = libssh_rs::Session::new().unwrap();
|
||||
|
||||
Session {
|
||||
session: RwLock::new(session)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup(&self, host: String, user: Option<String>, port: Option<u16>) {
|
||||
let session = self.session.write().unwrap();
|
||||
session.set_option(SshOption::Hostname(host)).unwrap();
|
||||
|
||||
if let Some(user) = user {
|
||||
session.set_option(SshOption::User(Some(user))).unwrap();
|
||||
}
|
||||
|
||||
if let Some(port) = port {
|
||||
session.set_option(SshOption::Port(port)).unwrap();
|
||||
}
|
||||
|
||||
session.set_option(SshOption::PublicKeyAcceptedTypes(ACCEPTED_SSH_TYPES.to_string())).unwrap();
|
||||
session.options_parse_config(None).unwrap();
|
||||
session.connect().unwrap();
|
||||
}
|
||||
|
||||
pub fn public_key_auth(&self, password: String) -> AuthStatus {
|
||||
println!("Public key auth");
|
||||
|
||||
let session = self.session.write().unwrap();
|
||||
|
||||
let status = session.userauth_public_key_auto(None, Some(&password)).unwrap();
|
||||
|
||||
println!("Status is {status:?}");
|
||||
|
||||
status
|
||||
}
|
||||
|
||||
pub fn password_auth(&self, password: String) -> AuthStatus {
|
||||
let session = self.session.write().unwrap();
|
||||
session.userauth_password(None, Some(&password)).unwrap()
|
||||
}
|
||||
|
||||
pub fn disconnect(&self) {
|
||||
let session = self.session.write().unwrap();
|
||||
session.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct Channel {
|
||||
channel: RwLock<libssh_rs::Channel>,
|
||||
}
|
||||
|
||||
unsafe impl Send for Channel {}
|
||||
|
||||
unsafe impl Sync for Channel {}
|
||||
|
||||
impl Channel {
|
||||
pub fn new(session: Arc<Session>) -> Self {
|
||||
let session = session.session.write().unwrap();
|
||||
let channel = session.new_channel().unwrap();
|
||||
|
||||
Channel {
|
||||
channel: RwLock::new(channel)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_session(&self) {
|
||||
let channel = self.channel.write().unwrap();
|
||||
channel.open_session().unwrap();
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
let channel = self.channel.write().unwrap();
|
||||
channel.is_open()
|
||||
}
|
||||
|
||||
pub fn close_channel(&self) {
|
||||
let channel = self.channel.write().unwrap();
|
||||
channel.close().unwrap();
|
||||
}
|
||||
|
||||
pub fn request_exec(&self, command: String) {
|
||||
let channel = self.channel.write().unwrap();
|
||||
channel.request_exec(&command).unwrap();
|
||||
}
|
||||
|
||||
pub fn poll_has_bytes(&self, is_stderr: bool) -> bool {
|
||||
let channel = self.channel.write().unwrap();
|
||||
let poll_timeout = channel.poll_timeout(is_stderr, None).unwrap();
|
||||
|
||||
match poll_timeout {
|
||||
PollStatus::AvailableBytes(count) => count > 0,
|
||||
PollStatus::EndOfFile => false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self, is_stderr: bool, len: u64) -> ReadResult {
|
||||
let ulen = len as usize;
|
||||
|
||||
let channel = self.channel.write().unwrap();
|
||||
|
||||
let mut buffer = vec![0; ulen];
|
||||
let read = channel.read_timeout(&mut buffer, is_stderr, None).unwrap();
|
||||
|
||||
ReadResult {
|
||||
read_count: read as u64,
|
||||
data: buffer,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_byte(&self, byte: i32) {
|
||||
let channel = self.channel.write().unwrap();
|
||||
channel.stdin().write_all(&byte.to_ne_bytes()).unwrap();
|
||||
}
|
||||
|
||||
pub fn write_bytes(&self, data: Vec<u8>) {
|
||||
let channel = self.channel.write().unwrap();
|
||||
channel.stdin().write_all(&data).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ReadResult {
|
||||
pub read_count: u64,
|
||||
pub data: Vec<u8>,
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
extern crate notify;
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::{channel, RecvTimeoutError};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use notify::{Config, Error, ErrorKind, Event, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
|
||||
const MIN_TIME_IN_MS_BETWEEN_REFRESHES: u128 = 500;
|
||||
const WATCH_TIMEOUT: u64 = 500;
|
||||
|
||||
|
||||
pub fn watch_directory(
|
||||
path: String,
|
||||
git_dir_path: String,
|
||||
notifier: Box<dyn WatchDirectoryNotifier>,
|
||||
) -> Result<(), WatcherInitError> {
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
|
||||
// Create a watcher object, delivering debounced events.
|
||||
// The notification back-end is selected based on the platform.
|
||||
let config = Config::default();
|
||||
config.with_poll_interval(Duration::from_secs(3600));
|
||||
|
||||
let mut watcher =
|
||||
RecommendedWatcher::new(tx, config).map_err(|err| err.kind.into_watcher_init_error())?;
|
||||
|
||||
// Add a path to be watched. All files and directories at that path and
|
||||
// below will be monitored for changes.
|
||||
watcher
|
||||
.watch(Path::new(path.as_str()), RecursiveMode::Recursive)
|
||||
.map_err(|err| err.kind.into_watcher_init_error())?;
|
||||
|
||||
let mut paths_cached: Vec<String> = Vec::new();
|
||||
|
||||
let mut last_update: u128 = 0;
|
||||
|
||||
while notifier.should_keep_looping() {
|
||||
let current_time = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("We need a TARDIS to fix this")
|
||||
.as_millis();
|
||||
|
||||
// Updates are batched to prevent excessive communication between Kotlin and Rust, as the
|
||||
// bridge has overhead
|
||||
if last_update != 0 && current_time > (last_update + MIN_TIME_IN_MS_BETWEEN_REFRESHES) {
|
||||
last_update = 0;
|
||||
|
||||
if paths_cached.len() == 1 {
|
||||
let first_path = paths_cached.first().expect("First path not found!");
|
||||
let is_dir = PathBuf::from(first_path).is_dir();
|
||||
|
||||
if !is_dir {
|
||||
notifier.detected_change(paths_cached.to_vec());
|
||||
}
|
||||
} else if !paths_cached.is_empty() {
|
||||
paths_cached.sort();
|
||||
paths_cached.dedup();
|
||||
|
||||
println!("Sending {} batched events to Kotlin side", paths_cached.len());
|
||||
notifier.detected_change(paths_cached.to_vec());
|
||||
}
|
||||
|
||||
paths_cached.clear();
|
||||
}
|
||||
|
||||
match rx.recv_timeout(Duration::from_millis(WATCH_TIMEOUT)) {
|
||||
Ok(e) => {
|
||||
if let Some(paths) = get_paths_from_event_result(&e, &git_dir_path) {
|
||||
let mut paths_without_dirs: Vec<String> = paths
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
paths_cached.append(&mut paths_without_dirs);
|
||||
|
||||
last_update = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("We need a TARDIS to fix this")
|
||||
.as_millis();
|
||||
|
||||
println!("Event: {e:?}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if e != RecvTimeoutError::Timeout {
|
||||
println!("Watch error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watcher
|
||||
.unwatch(Path::new(path.as_str()))
|
||||
.map_err(|err| err.kind.into_watcher_init_error())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_paths_from_event_result(event_result: &Result<Event, Error>, git_dir_path: &str) -> Option<Vec<String>> {
|
||||
match event_result {
|
||||
Ok(event) => {
|
||||
let events: Vec<String> = event
|
||||
.paths
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter_map(|path| {
|
||||
// Directories are not tracked by Git so we don't care about them (just about their content)
|
||||
// We won't be able to check if it's a dir if it has been deleted but that's good enough
|
||||
if path.is_dir() {
|
||||
println!("Ignoring directory {path:#?}");
|
||||
None
|
||||
} else {
|
||||
let path_str = path.into_os_string()
|
||||
.into_string()
|
||||
.ok()?;
|
||||
|
||||
// JGit may create .probe-UUID files for its internal stuff, we don't care about it
|
||||
let probe_prefix = format!("{git_dir_path}.probe-");
|
||||
if path_str.starts_with(probe_prefix.as_str()) {
|
||||
None
|
||||
} else {
|
||||
Some(path_str)
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if events.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(events)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("{:?}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WatchDirectoryNotifier: Send + Sync + Debug {
|
||||
fn should_keep_looping(&self) -> bool;
|
||||
fn detected_change(&self, paths: Vec<String>);
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum WatcherInitError {
|
||||
#[error("{error}")]
|
||||
Generic { error: String },
|
||||
#[error("IO Error")]
|
||||
Io { error: String },
|
||||
#[error("Path not found")]
|
||||
PathNotFound,
|
||||
#[error("Can not remove watch, it has not been found")]
|
||||
WatchNotFound,
|
||||
#[error("Invalid configuration")]
|
||||
InvalidConfig,
|
||||
#[error("Max files reached. Check the inotify limit")]
|
||||
MaxFilesWatch,
|
||||
}
|
||||
|
||||
trait WatcherInitErrorConverter {
|
||||
fn into_watcher_init_error(self) -> WatcherInitError;
|
||||
}
|
||||
|
||||
impl WatcherInitErrorConverter for ErrorKind {
|
||||
fn into_watcher_init_error(self) -> WatcherInitError {
|
||||
match self {
|
||||
ErrorKind::Generic(err) => WatcherInitError::Generic { error: err },
|
||||
ErrorKind::Io(err) => WatcherInitError::Generic {
|
||||
error: err.to_string(),
|
||||
},
|
||||
ErrorKind::PathNotFound => WatcherInitError::PathNotFound,
|
||||
ErrorKind::WatchNotFound => WatcherInitError::WatchNotFound,
|
||||
ErrorKind::InvalidConfig(_) => WatcherInitError::InvalidConfig,
|
||||
ErrorKind::MaxFilesWatch => WatcherInitError::MaxFilesWatch,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
uniffi::uniffi_bindgen_main()
|
||||
}
|
2
src/main/kotlin/com/jetpackduba/gitnuro/autogenerated/.gitignore
vendored
Normal file
2
src/main/kotlin/com/jetpackduba/gitnuro/autogenerated/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
@ -1,7 +1,7 @@
|
||||
package com.jetpackduba.gitnuro.credentials
|
||||
|
||||
import Session
|
||||
import com.jetpackduba.gitnuro.ssh.libssh.ChannelWrapper
|
||||
import uniffi.gitnuro.Session
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
|
@ -1,14 +1,12 @@
|
||||
package com.jetpackduba.gitnuro.credentials
|
||||
|
||||
import Session
|
||||
import org.eclipse.jgit.transport.RemoteSession
|
||||
import org.eclipse.jgit.transport.URIish
|
||||
import uniffi.gitnuro.AuthStatus
|
||||
import uniffi.gitnuro.Session
|
||||
import java.util.concurrent.CancellationException
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
private const val DEFAULT_SSH_PORT = 22
|
||||
private const val NOT_EXPLICIT_PORT = -1
|
||||
|
||||
class SshRemoteSession @Inject constructor(
|
||||
@ -31,18 +29,18 @@ class SshRemoteSession @Inject constructor(
|
||||
}
|
||||
|
||||
fun setup(uri: URIish) {
|
||||
val session = Session()
|
||||
val session = Session.new()
|
||||
|
||||
val port = if (uri.port == NOT_EXPLICIT_PORT) {
|
||||
null
|
||||
} else
|
||||
uri.port
|
||||
|
||||
session.setup(uri.host, uri.user, port?.toUShort())
|
||||
session.setup(uri.host, uri.user ?: "", port)//?.toUShort())
|
||||
|
||||
var result = session.publicKeyAuth("")
|
||||
|
||||
if (result == AuthStatus.DENIED) {
|
||||
if (result == 2) {//AuthStatus.DENIED) {
|
||||
credentialsStateManager.updateState(CredentialsRequested.SshCredentialsRequested)
|
||||
|
||||
var credentials = credentialsStateManager.currentCredentialsState
|
||||
@ -57,13 +55,14 @@ class SshRemoteSession @Inject constructor(
|
||||
|
||||
result = session.publicKeyAuth(password)
|
||||
|
||||
if (result != AuthStatus.SUCCESS) {
|
||||
if (result != 1) {//AuthStatus.SUCCESS) {
|
||||
result = session.passwordAuth(password)
|
||||
}
|
||||
}
|
||||
|
||||
if (result != AuthStatus.SUCCESS)
|
||||
if (result != 1) {//AuthStatus.SUCCESS)
|
||||
throw Exception("Something went wrong with authentication. Code $result")
|
||||
}
|
||||
|
||||
this.session = session
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import com.jetpackduba.gitnuro.updates.UpdatesService
|
||||
import dagger.Provides
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
import javax.inject.Inject
|
||||
|
||||
@dagger.Module
|
||||
class NetworkModule {
|
||||
|
@ -1,17 +1,17 @@
|
||||
package com.jetpackduba.gitnuro.git
|
||||
|
||||
import FileChanged
|
||||
import FileWatcher
|
||||
import WatchDirectoryNotifier
|
||||
import com.jetpackduba.gitnuro.git.workspace.GetIgnoreRulesUseCase
|
||||
import com.jetpackduba.gitnuro.system.systemSeparator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.eclipse.jgit.lib.Constants
|
||||
import org.eclipse.jgit.lib.Repository
|
||||
import uniffi.gitnuro.WatchDirectoryNotifier
|
||||
import uniffi.gitnuro.watchDirectory
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import javax.inject.Inject
|
||||
@ -37,19 +37,48 @@ class FileChangesWatcher @Inject constructor(
|
||||
Constants.SQUASH_MSG,
|
||||
)
|
||||
|
||||
val checker = object : WatchDirectoryNotifier {
|
||||
override fun shouldKeepLooping(): Boolean {
|
||||
return isActive
|
||||
}
|
||||
// val checker = object : WatchDirectoryNotifier {
|
||||
// override fun shouldKeepLooping(): Boolean {
|
||||
// return isActive
|
||||
// }
|
||||
//
|
||||
// override fun detectedChange(paths: List<String>) = runBlocking {
|
||||
// val hasGitIgnoreChanged = paths.any { it == "$workspacePath$systemSeparator.gitignore" }
|
||||
//
|
||||
// if (hasGitIgnoreChanged) {
|
||||
// ignoreRules = getIgnoreRulesUseCase(repository)
|
||||
// }
|
||||
//
|
||||
// val areAllPathsIgnored = paths.all { path ->
|
||||
// val matchesAnyIgnoreRule = ignoreRules.any { rule ->
|
||||
// rule.isMatch(path, Files.isDirectory(Paths.get(path)))
|
||||
// }
|
||||
//
|
||||
// val isGitIgnoredFile = gitDirIgnoredFiles.any { ignoredFile ->
|
||||
// "$workspacePath$systemSeparator.git$systemSeparator$ignoredFile" == path
|
||||
// }
|
||||
//
|
||||
// matchesAnyIgnoreRule || isGitIgnoredFile
|
||||
// }
|
||||
//
|
||||
// val hasGitDirChanged = paths.any { it.startsWith("$workspacePath$systemSeparator.git$systemSeparator") }
|
||||
//
|
||||
// if (!areAllPathsIgnored) {
|
||||
// _changesNotifier.emit(hasGitDirChanged)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
override fun detectedChange(paths: List<String>) = runBlocking {
|
||||
val hasGitIgnoreChanged = paths.any { it == "$workspacePath$systemSeparator.gitignore" }
|
||||
val checker = object : WatchDirectoryNotifier {
|
||||
override fun detectedChange(path: FileChanged) = runBlocking {
|
||||
val path = path.path
|
||||
val hasGitIgnoreChanged = path == "$workspacePath$systemSeparator.gitignore"
|
||||
|
||||
if (hasGitIgnoreChanged) {
|
||||
ignoreRules = getIgnoreRulesUseCase(repository)
|
||||
}
|
||||
|
||||
val areAllPathsIgnored = paths.all { path ->
|
||||
// val areAllPathsIgnored = paths.all { path ->
|
||||
val matchesAnyIgnoreRule = ignoreRules.any { rule ->
|
||||
rule.isMatch(path, Files.isDirectory(Paths.get(path)))
|
||||
}
|
||||
@ -58,10 +87,10 @@ class FileChangesWatcher @Inject constructor(
|
||||
"$workspacePath$systemSeparator.git$systemSeparator$ignoredFile" == path
|
||||
}
|
||||
|
||||
matchesAnyIgnoreRule || isGitIgnoredFile
|
||||
}
|
||||
val areAllPathsIgnored = matchesAnyIgnoreRule || isGitIgnoredFile
|
||||
// }
|
||||
|
||||
val hasGitDirChanged = paths.any { it.startsWith("$workspacePath$systemSeparator.git$systemSeparator") }
|
||||
val hasGitDirChanged = path.startsWith("$workspacePath$systemSeparator.git$systemSeparator")
|
||||
|
||||
if (!areAllPathsIgnored) {
|
||||
_changesNotifier.emit(hasGitDirChanged)
|
||||
@ -69,6 +98,7 @@ class FileChangesWatcher @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
watchDirectory(workspacePath, gitRepoPath, checker)
|
||||
val fileWatcher = FileWatcher.new()
|
||||
fileWatcher.watch(workspacePath, gitRepoPath, checker)
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package com.jetpackduba.gitnuro
|
||||
import com.jetpackduba.gitnuro.repositories.initPreferencesPath
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
System.load("/home/abde/Projects/Compose/Gitnuro/rs/target/release/libgitnuro_rs.so")
|
||||
initPreferencesPath()
|
||||
|
||||
val app = App()
|
||||
|
@ -1,13 +1,13 @@
|
||||
package com.jetpackduba.gitnuro.ssh.libssh
|
||||
|
||||
import Channel
|
||||
import Session
|
||||
import com.jetpackduba.gitnuro.ssh.libssh.streams.SshChannelInputErrStream
|
||||
import com.jetpackduba.gitnuro.ssh.libssh.streams.SshChannelInputStream
|
||||
import com.jetpackduba.gitnuro.ssh.libssh.streams.SshChannelOutputStream
|
||||
import uniffi.gitnuro.Channel
|
||||
import uniffi.gitnuro.Session
|
||||
|
||||
class ChannelWrapper internal constructor(sshSession: Session) {
|
||||
private var channel = Channel(sshSession)
|
||||
private val channel = Channel.new(sshSession)
|
||||
|
||||
val outputStream = SshChannelOutputStream(channel)
|
||||
val inputStream = SshChannelInputStream(channel)
|
||||
@ -27,5 +27,6 @@ class ChannelWrapper internal constructor(sshSession: Session) {
|
||||
|
||||
fun close() {
|
||||
channel.closeChannel()
|
||||
channel.close()
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package com.jetpackduba.gitnuro.ssh.libssh.streams
|
||||
|
||||
import uniffi.gitnuro.Channel
|
||||
import Channel
|
||||
import java.io.InputStream
|
||||
|
||||
class SshChannelInputErrStream(private val sshChannel: Channel) : InputStream() {
|
||||
@ -8,7 +8,7 @@ class SshChannelInputErrStream(private val sshChannel: Channel) : InputStream()
|
||||
|
||||
override fun read(): Int {
|
||||
return if (sshChannel.pollHasBytes(true)) {
|
||||
val read = sshChannel.read(true, 1u)
|
||||
val read = sshChannel.read(true, 1L) // TODO it was a long
|
||||
val byteArray = read.data
|
||||
|
||||
val first = byteArray.first()
|
||||
|
@ -1,11 +1,11 @@
|
||||
package com.jetpackduba.gitnuro.ssh.libssh.streams
|
||||
|
||||
import uniffi.gitnuro.Channel
|
||||
import Channel
|
||||
import java.io.InputStream
|
||||
|
||||
class SshChannelInputStream(private val sshChannel: Channel) : InputStream() {
|
||||
override fun read(b: ByteArray, off: Int, len: Int): Int {
|
||||
val result = sshChannel.read(false, len.toULong())
|
||||
val result = sshChannel.read(false, len.toLong())//.toULong())
|
||||
val byteArray = result.data
|
||||
val read = result.readCount
|
||||
|
||||
@ -18,7 +18,7 @@ class SshChannelInputStream(private val sshChannel: Channel) : InputStream() {
|
||||
|
||||
override fun read(): Int {
|
||||
|
||||
val result = sshChannel.read(false, 1u)
|
||||
val result = sshChannel.read(false, 1L)//1u)
|
||||
|
||||
val first = result.data.first()
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
package com.jetpackduba.gitnuro.ssh.libssh.streams
|
||||
|
||||
import uniffi.gitnuro.Channel
|
||||
import Channel
|
||||
import java.io.OutputStream
|
||||
|
||||
class SshChannelOutputStream(private val sshChannel: Channel) : OutputStream() {
|
||||
|
Loading…
Reference in New Issue
Block a user