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
  • Loading branch information
jelly committed Feb 14, 2024
1 parent 8f2bed6 commit 86d9139
Show file tree
Hide file tree
Showing 3 changed files with 170 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
134 changes: 134 additions & 0 deletions pkg/lib/cockpit-fsinfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"use strict";

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

/* RFC 7396 — JSON Merge Patch — functional */
function json_merge(current: window.cockpit.JsonValue, patch: window.cockpit.JsonValue): window.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?: window.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 = window.cockpit.channel({
superuser: "try",
payload: "fsinfo",
path,
attrs,
"close-on-error": false,
watch: true,
...options
});

let state: window.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;
}
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 => {
console.log("state", state);
if (state.error || state.info) {
if (state.error) {
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 86d9139

Please sign in to comment.