Rust: Dateien aus Ordner verarbeiten und gleichzeitig Ordner auf Änderungen überwachen.

jb_alvarado

Lieutenant
Registriert
Sep. 2015
Beiträge
576
Hallo Allerseits,
ich lerne gerade Rust und nebenbei versuche ich ein Programm, welches ich in python geschrieben hatte, in Rust nach zubauen.

Hier ist es so, dass ich gerne den Inhalt eines Ordners (mit mp4 Videos) abspielen möchte und gleichzeitig soll der Ordner überwacht werden ob Dateien sich ändern. Wenn es eine Änderung gibt, soll sich das direkt auf die Liste der abzuspielenden Dateien auswirken.

Für die Dateiliste dachte ich, könnte ich ein struct nehmen:
Rust:
struct Source {
    files: Vec<String>
}

impl Source {
    fn new() -> Self {
        Self {
            files: vec![]
        }
    }

    fn push(&mut self, file: String) {
        self.files.push(file)
    }

    fn rm(&mut self, file: String) {
        self.files.retain(|x| x != &file);
    }

    fn mv(&mut self, old_file: String, new_file: String) {
        let i = self.files
            .iter()
            .position(|x| *x == old_file)
            .unwrap();
        self.files[i] = new_file;
    }
}

Zum überwachen des Ordner nehme ich notify:

Rust:
fn watch_folder(path: &String, source: &mut Source) {
    // let mut source = Source::new();
    let (sender, receiver) = channel();

    let mut watcher = watcher(sender, Duration::from_secs(2)).unwrap();
    watcher.watch(path, RecursiveMode::Recursive).unwrap();

    println!("watch path: '{}'", path);

    loop {
        match receiver.recv() {
            Ok(event) => match event {
                Create(new_path) => {
                    println!("Create new file: {:?}", new_path);
                    source.push(new_path.display().to_string());
                }
                Remove(old_path) => {
                    println!("Remove file: {:?}", old_path);
                    source.rm(old_path.display().to_string());
                }
                Rename(old_path, new_path) => {
                    println!("Rename file: {:?} to {:?}", old_path, new_path);
                    source.mv(old_path.display().to_string(),
                        new_path.display().to_string());
                }
                _ => (),
            },
            Err(e) => println!("watch error: {:?}", e),
        }
    }

Und in einer weiteren Funktion würde ich nun den Ordner einlesen und verarbeiten:

Rust:
pub fn walk(path: &String) {
    let mut source = Source::new();
    let mut index: usize = 0;

    for entry in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) {
        if entry.path().is_file() {
            source.push(entry.path().display().to_string());
        }
    }

    watch_folder(&path, &mut source);

    loop {
       while index < source.files.len() {
            println!("Play file {}: {:?}", index, source.files[index]);
            index += 1;
            thread::sleep(time::Duration::from_secs(10)); // simulate playing
        }
        index = 0
    }
}

Hierbei habe ich jetzt allerdings zwei Probleme:
1. watch_folder() muss im Hintergrund ausgeführt werden. Was wäre hier die beste Methode? async, mit Threads oder mit dieses tokio? Mein erster async versuch (mit block_on()) ist gescheitet, watch_folder() hatte es erst gestartet als der loop fertig war.
2. Bei Rust ist es ja so, dass man keine Varibale weiter verarbeiten darf, wenn sie schon ausgeliehen ist. Also wenn ich den Vector mit den Dateien drinnen an die watch_folder Function übergeben würde, könnte ich ihn nicht danach im loop auslesen. Was gibt es hier für Möglichkeiten?
 
Zuletzt bearbeitet:
jb_alvarado schrieb:
Was wäre hier die beste Methode? async, mit Threads oder mit dieses tokio? Mein erster async versuch (mit block_on()) ist gescheitet, watch_folder() hatte es erst gestartet als der loop fertig war.
Ich würd tokio::spawn nutzen

Vorteile:
  • du brauchst dich nicht um Threads kümmern
  • du könntest damit auch async io nutzen

jb_alvarado schrieb:
Bei Rust ist es ja so, dass man keine Varibale weiter verarbeiten darf, wenn sie schon ausgeliehen ist. Also wenn ich den Vector mit den Dateien drinnen an die watch_folder Function übergeben würde, könnte ich ihn nicht danach im loop auslesen. Was gibt es hier für Möglichkeiten?
Wenn watch_folder den Vektor nur ausleiht, kannst den Vektor danach trotzdem in einer loop auslesen.
Natürlich nur, wenn watch_folder dann schon fertig ist und nicht nebenbei ausgeführt wird oder watch_folder auch nur lesend zugreift (falls es parallelen Zugriff geben soll) ;-)
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: jb_alvarado
Danke @KitKat::new() für deine Antwort. watch_folder() braucht auch Schreibzugriff auf den Vector und soll auch nebenbei laufen, sonst kann die Liste ja nicht aktualisiert werden, wenn es Änderungen im Dateisystem gibt.
 
Dann musst du wohl einen Mutex nutzen.

Alternativ kannst du explizit zwischen dem Durchläufen auch regelmäßig auf Änderungen prüfen.
 
Das Mutex schaut interessant aus, aber ich denke wenn regelmäßig nach Änderungen geschaut wird, würde das absolut reichen. Kannst du mir ein kleines Beispiel zeigen, wie das gehen würde?
 
Afaik sollte folgendes gehen: ruf in der Haupt-Schleife irgendwo watch_folder auf und in watch_folder nutzt du dann try_recv statt recv ohne Schleife, wo du dann sofort eine Rückmeldung bekommst, ob sich etwas geändert hat.
Den channel und den watcher erstellst du außerhalb der Funktion einmal am Anfang von walk, wobei du der Funktion immer den receiver leihst
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: jb_alvarado
Danke, das hat wirklich geklappt! Habe es so hinbekommen:

Rust:
fn watch_folder(source: &mut Source, receiver: &Receiver<notify::DebouncedEvent>) {
    match receiver.try_recv() {
        Ok(event) => match event {
            Create(new_path) => {
                println!("Create new file: {:?}", new_path);
                source.push(new_path.display().to_string());
            }
            Remove(old_path) => {
                println!("Remove file: {:?}", old_path);
                source.rm(old_path.display().to_string());
            }
            Rename(old_path, new_path) => {
                println!("Rename file: {:?} to {:?}", old_path, new_path);
                source.mv(old_path.display().to_string(),
                    new_path.display().to_string());
            }
            _ => (),
        },
        Err(_) => (),
    }
}

pub fn walk(path: &String, extensions: &Vec<String>) {
    if !Path::new(path).exists() {
        println!("Folder path not exists: '{}'", path);
        process::exit(0x0100);
    }
    let mut source = Source::new(path, extensions);
    let mut index: usize = 0;

    let (sender, receiver) = channel();

    let mut watcher = watcher(sender, Duration::from_secs(2)).unwrap();
    watcher.watch(path, RecursiveMode::Recursive).unwrap();

    loop {
        while index < source.files.len() {
            watch_folder(&mut source, &receiver);
            println!("Play file {}: {:?}", index, source.files[index]);
            index += 1;

            thread::sleep(time::Duration::from_secs(1));
        }
        index = 0
    }
}

Unschön finde ich noch, dass nach jedem Aufruf, wo es keine Änderung gab, der Err(_) ausgelöst wird. Habe das einfach gemuted, was ich jetzt nicht so toll finde.

Im nächsten Schritt möchte ich versuchen so eine Art Generator aus der Walk Funktion zu bauen. In Pyhton hätte ich dann das aktuelle File mit yield ausgegeben. Das ist ja in Rust nicht so üblich...
 
jb_alvarado schrieb:
Unschön finde ich noch, dass nach jedem Aufruf, wo es keine Änderung gab, der Err(_) ausgelöst wird. Habe das einfach gemuted, was ich jetzt nicht so toll finde.
Ich würde es trotzdem nicht ganz muten, und den Fehler nur weitergeben, wenn er disconnected ist.

jb_alvarado schrieb:
Im nächsten Schritt möchte ich versuchen so eine Art Generator aus der Walk Funktion zu bauen. In Pyhton hätte ich dann das aktuelle File mit yield ausgegeben. Das ist ja in Rust nicht so üblich...
Vielleicht, weil noch nicht offiziell Teil der Sprache ;-)
https://doc.rust-lang.org/beta/unstable-book/language-features/generators.html

BTW. vielleicht möchtest du von diesem Update gebrauch machen: https://www.heise.de/news/Programmi...rings-und-verschmaelert-den-Pfad-6326974.html

Iteratoren sind auch ganz nützlich: https://doc.rust-lang.org/rust-by-example/flow_control/for.html#for-and-iterators
 
  • Gefällt mir
Reaktionen: jb_alvarado
Danke für deine Tipps, die sind sehr hilfreich! Schön auch, dass es Format-Strings gibt, die habe ich in Python sehr geschätzt. Finde es eh interessant zu sehen, dass vieles aus anderen Sprachen adaptiert wurde, das macht es etwas leichter. Gerade für Leute wie mich, dich vorher nur mit Scriptsprachen zu tun hatten.

Wie ist das bei Rust eigentlich zu verstehen, wenn neue Funktionen als unstable gekennzeichnet sind: Heißt das, dass sie in der Praxis nicht so zuverlässig arbeiten und potenziell Fehler verursachen können? Oder das sie einfach noch nicht so ausgereift sind, so dass sich die Funktion vom Konzept noch ändern kann?
 
jb_alvarado schrieb:
Wie ist das bei Rust eigentlich zu verstehen, wenn neue Funktionen als unstable gekennzeichnet sind: Heißt das, dass sie in der Praxis nicht so zuverlässig arbeiten und potenziell Fehler verursachen können?
When using the nightly channel of Rust or when compiling Rust manually with the appropriate flags, it's possible to use unstable features. These features include language additions, library additions, compiler features, and other capabilities not subject to Rust's usual stability guarantees. Most of these features are temporarily-unstable additions that will become stable after a period of time has passed during which testing, discussion, and further design has completed. Some features, however, are intentionally permanently-unstable features intended for internal compiler use. Other features may be removed completely when a better solution has been found or when it was determined that the downsides of the feature outweighed the advantages. Each feature has an associated tracking issue on the rust-lang/rust Github repository.
https://fuchsia.dev/fuchsia-src/development/languages/rust/unstable#:~:text=When using the nightly channel,to Rust's usual stability guarantees.

https://doc.rust-lang.org/cargo/reference/unstable.html

Fehler und unzuverlässiges Arbeiten ist mir bisher selbst noch nicht passiert
 
  • Gefällt mir
Reaktionen: jb_alvarado
Nightly ist oft auch zum experimentieren gedacht. Ich habe zum Beispiel selbst eine Funktion in den nightly Channel eingebracht, weil sie für mein Tool zwingend notwendig ist.
Ich wollte damit aber erstmal nur experimentieren um vlt. nochmal das Design zu verbessern und hatte keine Lust ewig mit Leuten zu diskutieren, dafür ist nightly als Testbereich perfekt. Wenn sich eine Funktion dort ca. 1 Jahr bewährt hat, kann jemand den Antrag stellen sie als stable zu markieren.
Da muss man dann aber sehr gute Argumente haben, schließlich können stable Funktionen aufgrund der Rückwärtskompabilität nie mehr entfernt werden.
 
ZuseZ3 schrieb:
Da muss man dann aber sehr gute Argumente haben, schließlich können stable Funktionen aufgrund der Rückwärtskompabilität nie mehr entfernt werden.
scheint nicht so, denn breaking changes sind Teil von verschiedenen Rust Editionen
 
Ja und nein, alle Fehler können / werden über Editionen nicht gefixed werden.
Z.B. müssen die Versionen ABI kompatibel bleiben. Außerdem scheinen sie Sachen auch dann nicht zu fixen, wenn es nicht-triviale Änderungen am code erfordert, welche nicht automatisiert werden können. Zumindest wird das dann gut Überlegt. Deswegen gibt es ja auch einige Deprecated Elemente welche nicht in der letzten Edition entfernt wurden sondern deprecated bleiben. 100% ist mir aber auch nicht klar, wann sie etwas fixen und wann nicht. Ich denke das wird hier aber etwas arg ot, sollten wir wenn dann vlt. eher im users.rust-lang.org/ forum besprechen?
 
Hm, interessant. Leider hat es der Generator bis heute nicht geschafft in Stable zu landen, obwohl er schon mehrere Jahre drin ist.
 
Richtig, ist auch ein recht komplizierter Teil. Vmtl. fuehlen sich da auch einige an ranges errinert, die wurden leider sehr ungeschickt implementiert. (0...42) ist ein Iterator, es waere aber viel sinniger wenn er nur intoIter implementieren wuerde statt selbst einer zu sein.
Es sind aber wohl sehr viele daran interessiert und es scheint recht weit oben auf der Agenda zu stehen, soweit ich weis.
 
  • Gefällt mir
Reaktionen: jb_alvarado
Ich merke gerade wieder, dass man sich nicht auf die Konzepte versteifen sollte, die man als erstes im Kopf hat... @KitKat::new() hatte auch schon das Stichwort Iterator in den Raum geworfen und habe jetzt zudem gesehen, dass man sich den auch selbst zusammen bauen kann. Als in etwa so:

Rust:
struct List {
    arr: Vec<u8>,
    i: usize,
}

impl Iterator for List {
    type Item = u8;

    fn next(&mut self) -> Option<Self::Item> {
        if self.i == self.arr.len() - 1 {
            let current = self.arr[self.i];
            self.i = 0;

            return Some(current)
        } else {
            let current = self.arr[self.i];
            self.i += 1;

            return Some(current)
        }
    }
}

Das ist viel simpler und reicht wahrscheinlich völlig für mein Vorhaben.
 
Zurück
Oben