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 21, 2024
1 parent 20f6c70 commit 7203112
Show file tree
Hide file tree
Showing 3 changed files with 174 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 fsinfo test file
const file = cockpit.file(dir + "/fsinfo");
await file.replace("1234");
const watch = file.watch((content, tag) => {
assert.equal(content, null, "non-existant because read is false");
assert.notEqual(tag, null, "non empty tag");
assert.equal(tag.startsWith("1:"), true, "tag always stars with 1:");
watch.remove();
done();
}, { read: false });
});

QUnit.test("watching directory", assert => {
const done = assert.async();
assert.expect(20);
Expand Down
138 changes: 138 additions & 0 deletions pkg/lib/cockpit-fsinfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"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;
}

// FIXME: import at the end of the file to prevent circular import build issues
// this is a temporary measure until we move cockpit.js to cockpit.ts in a follow up.
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 7203112

Please sign in to comment.