Skip to content

Commit

Permalink
Use fsinfo for cockpit.file.watch()
Browse files Browse the repository at this point in the history
By using the new fsinfo channel fswatch1 can now read the tag without
reading the file when adding a new watch. As side-effect this no longer
reads the full file when `{ read: false }` is passed to `watch()`.

Closes: #19983

Co-Authored-By: Allison Karlitskaya <allison.karlitskaya@redhat.com>
  • Loading branch information
jelly and allisonkarlitskaya committed Feb 20, 2024
1 parent 20f6c70 commit 5c789bd
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 20 deletions.
16 changes: 16 additions & 0 deletions pkg/base1/test-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,22 @@ QUnit.test("watching without reading", assert => {
}, { read: false });
});

QUnit.test("watching without reading pre-created", async assert => {
const done = assert.async();
assert.expect(3);

// Pre-create foobar
const file = cockpit.file(dir + "/fsinfo");
await cockpit.spawn(["bash", "-c", `echo 1234 > ${dir}/fsinfo`]);
const watch = file.watch((content, tag) => {
assert.equal(content, null, "non-existant because read is false");
assert.notEqual(tag, null, "non empty tag");
assert.notEqual(tag, "-", "non empty tag");
watch.remove();
done();
}, { read: false });
});

QUnit.test("watching directory", assert => {
const done = assert.async();
assert.expect(20);
Expand Down
136 changes: 136 additions & 0 deletions pkg/lib/cockpit-fsinfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"use strict";

function is_json_dict(value: cockpit.JsonValue): value is cockpit.JsonObject {
return value?.constructor === Object;
}

/* RFC 7396 — JSON Merge Patch — functional */
function json_merge(current: cockpit.JsonValue, patch: cockpit.JsonValue): cockpit.JsonValue {
if (is_json_dict(patch)) {
const updated = is_json_dict(current) ? { ...current } : { };

for (const [key, value] of Object.entries(patch)) {
if (value === null) {
delete updated[key];
} else {
updated[key] = json_merge(updated[key], value);
}
}

return updated;
} else {
return patch;
}
}

interface FileInfo {
type?: string;
tag?: string;
mode?: number;
size?: number;
uid?: number;
user?: string | number;
gid?: number;
group?: string | number;
mtime?: number;
content?: string;
target?: string;
entries?: {
[filename: string]: FileInfo;
};
targets?: {
[filename: string]: FileInfo;
};
}

interface FsInfoError {
problem?: string;
message?: string;
errno?: string;
}

interface FileInfoState {
info: FileInfo | null;
error: FsInfoError | null;
}

interface FsInfoHandle {
close(): void;
effect(callback: ((state: FileInfoState) => void)): void;
entry(name: string): FileInfo | null;
state: FileInfoState;
target(name: string): FileInfo | null;
}

export function fsinfo(path: string, attrs: string[], options?: cockpit.JsonObject) {
const self: FsInfoHandle = {
close,
effect,
entry,
state: {
info: null,
error: null,
},
target,
};

const callbacks: ((state: FileInfoState) => void)[] = [];

function close() {
channel.close();
}

function effect(callback: (state: FileInfoState) => void) {
callback(self.state);
callbacks.push(callback);
return () => callbacks.splice(callbacks.indexOf(callback), 1);
}

function entry(name: string): FileInfo | null {
return self.state.info?.entries?.[name] ?? null;
}

function target(name: string): FileInfo | null {
const entries = self.state.info?.entries ?? {};
const targets = self.state.info?.targets ?? {};

let entry = entries[name] ?? null;
for (let i = 0; i < 40; i++) {
const target = entry?.target;
if (!target)
return entry;
entry = entries[target] ?? targets[target] ?? null;
}
return null; // ELOOP
}

const channel = cockpit.channel({
superuser: "try",
payload: "fsinfo",
path,
attrs,
"close-on-error": false,
watch: true,
...options
});

let state: cockpit.JsonValue = null;
channel.addEventListener("message", (_event: CustomEvent, payload: string) => {
state = json_merge(state, JSON.parse(payload));

if (is_json_dict(state) && !state.partial) {
self.state = {
info: is_json_dict(state.info) ? state.info : null,
error: is_json_dict(state.error) ? state.error : null,
};

for (const callback of callbacks) {
callback(self.state);
}
}
});

return self;
}

import cockpit from "./cockpit.js";
40 changes: 20 additions & 20 deletions pkg/lib/cockpit.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

/* eslint-disable indent,no-empty */

import { fsinfo } from "./cockpit-fsinfo";

let url_root;

const meta_url_root = document.head.querySelector("meta[name='url-root']");
Expand Down Expand Up @@ -3633,24 +3635,23 @@ function factory() {
if (watch_channel)
return;

const opts = {
payload: "fswatch1",
path,
superuser: base_channel_options.superuser,
};
watch_channel = cockpit.channel(opts);
watch_channel.addEventListener("message", function (event, message_string) {
let message;
try {
message = JSON.parse(message_string);
} catch (e) {
message = null;
}
if (message && message.path == path && message.tag && message.tag != watch_tag) {
if (options && options.read !== undefined && !options.read)
fire_watch_callbacks(null, message.tag);
else
read();
watch_channel = fsinfo(path, ["tag"], { superuser: base_channel_options.superuser });
watch_channel.effect(state => {
if (state.error || state.info) {
// Behave like fsread1, not-found is not a fatal error
if (state.error && state?.error?.problem !== "not-found") {
const error = new BasicError(state.error.problem, state.error.message);
fire_watch_callbacks(null, "-", error);
} else {
const tag = state?.info?.tag || "-";
if (tag !== watch_tag) {
if (tag === "-" || options?.read === false) {
fire_watch_callbacks(null, tag);
} else {
read();
}
}
}
}
});
} else {
Expand All @@ -3673,7 +3674,6 @@ function factory() {
ensure_watch_channel(options);

watch_tag = null;
read();

return {
remove: function () {
Expand All @@ -3694,7 +3694,7 @@ function factory() {
if (replace_channel)
replace_channel.close("cancelled");
if (watch_channel)
watch_channel.close("cancelled");
watch_channel.close();
}

return self;
Expand Down

0 comments on commit 5c789bd

Please sign in to comment.