LCOV - code coverage report
Current view: top level - src - rest.js Coverage Total Hit
Test: cockpit-podman Lines: 94.0 % 117 110
Test Date: 2025-05-21 17:35:03

            Line data    Source code
       1              : import cockpit from "cockpit";
       2              : 
       3              : import { debug } from "./util.js";
       4              : 
       5           22 : function manage_error(reject, error, content) {
       6           22 :     let content_o = {};
       7           22 :     if (content) {
       8           22 :         try {
       9           22 :             content_o = JSON.parse(content);
      10            3 :         } catch {
      11            3 :             content_o.message = content;
      12            3 :         }
      13           22 :     }
      14           22 :     const c = { ...error, ...content_o };
      15           22 :     reject(c);
      16           22 : }
      17              : 
      18              : // calls are async, so keep track of a call counter to associate a result with a call
      19           43 : let call_id = 0;
      20              : 
      21           43 : const NL = '\n'.charCodeAt(0); // always 10, but avoid magic constant
      22           43 : const CR = '\r'.charCodeAt(0); // always 13, but avoid magic constant
      23              : 
      24           43 : const PODMAN_SYSTEM_ADDRESS = "/run/podman/podman.sock";
      25              : 
      26              : /* uid: null for logged in session user, otherwise standard Unix user ID
      27              :  * Return { path, superuser } */
      28           43 : function getAddress(uid) {
      29           41 :     if (uid === null) {
      30              :         // FIXME: make this async and call cockpit.user()
      31           41 :         const xrd = sessionStorage.getItem('XDG_RUNTIME_DIR');
      32           41 :         if (xrd)
      33           41 :             return { path: xrd + "/podman/podman.sock", superuser: null };
      34            0 :         console.warn("$XDG_RUNTIME_DIR is not present. Cannot use user service.");
      35            0 :         return { path: "", superuser: null };
      36            0 :     }
      37              : 
      38           29 :     if (uid === 0)
      39           29 :         return { path: PODMAN_SYSTEM_ADDRESS, superuser: "require" };
      40              : 
      41            1 :     if (Number.isInteger(uid))
      42            1 :         return { path: `/run/user/${uid}/podman/podman.sock`, superuser: "require" };
      43              : 
      44            0 :     throw new Error(`getAddress: uid ${uid} not supported`);
      45           43 : }
      46              : 
      47              : // split an Uint8Array at \r\n\r\n (separate headers from body)
      48           43 : function splitAtNLNL(array) {
      49           43 :     for (let i = 0; i <= array.length - 4; i++) {
      50           43 :         if (array[i] === CR && array[i + 1] === NL && array[i + 2] === CR && array[i + 3] === NL) {
      51           43 :             return [array.subarray(0, i), array.subarray(i + 4)];
      52           43 :         }
      53           43 :     }
      54            0 :     console.error("did not find NLNL in array", array); // not-covered: if this happens, it's a podman bug
      55            0 :     return [array, null]; // not-covered: dito
      56           43 : }
      57              : 
      58              : /* uid: null for logged in session user; 0 for root; in the future we'll support other users
      59              :  * Returns a connection object with methods monitor(), call(), and close(), and an `uid` property.
      60              :  */
      61           43 : function connect(uid) {
      62           43 :     const addr = getAddress(uid);
      63              :     /* This doesn't create a channel until a request */
      64              :     /* HACK: use binary channel to work around https://github.com/cockpit-project/cockpit/issues/19235 */
      65           43 :     const http = cockpit.http(addr.path, { superuser: addr.superuser, binary: true });
      66           43 :     const raw_channels = [];
      67           43 :     const connection = { uid };
      68           43 :     const decoder = new TextDecoder();
      69            1 :     const user_str = (uid === null) ? "user" : (uid === 0) ? "root" : `uid ${uid}`;
      70              : 
      71           43 :     connection.call = function (options) {
      72           43 :         const id = call_id++;
      73           43 :         debug(user_str, `call ${id}:`, JSON.stringify(options));
      74           43 :         return new Promise((resolve, reject) => {
      75            0 :             options = options || {};
      76           43 :             http.request(options)
      77           43 :                     .then(result => {
      78           43 :                         const text = decoder.decode(result);
      79           43 :                         debug(user_str, `call ${id} result:`, text);
      80           43 :                         resolve(text);
      81           43 :                     })
      82           21 :                     .catch((error, content) => {
      83           21 :                         const text = decoder.decode(content);
      84           21 :                         debug(user_str, `call ${id} error:`, JSON.stringify(error), "content", text);
      85           21 :                         manage_error(reject, error, text);
      86           21 :                     });
      87           43 :         });
      88           43 :     };
      89              : 
      90           43 :     connection.monitor = function(path, callback, return_raw = false) {
      91           43 :         return new Promise((resolve, reject) => {
      92           43 :             const ch = cockpit.channel({ unix: addr.path, superuser: addr.superuser, payload: "stream", binary: true });
      93           43 :             raw_channels.push(ch);
      94           43 :             let buffer = new Uint8Array();
      95              : 
      96            7 :             ch.addEventListener("close", () => {
      97            7 :                 debug(user_str, "monitor", path, "closed");
      98            7 :                 resolve();
      99            7 :             });
     100              : 
     101           43 :             const onHTTPMessage = message => {
     102           43 :                 const [headers_bin, body] = splitAtNLNL(message.detail);
     103           43 :                 const headers = decoder.decode(headers_bin);
     104           43 :                 debug(user_str, "monitor", path, "HTTP response:", headers);
     105           43 :                 if (headers.match(/^HTTP\/1.*\s+200\s/)) {
     106              :                     // any further message is actual streaming data
     107           43 :                     ch.removeEventListener("message", onHTTPMessage);
     108           43 :                     ch.addEventListener("message", onDataMessage);
     109              : 
     110              :                     // process the initial response data
     111           43 :                     if (body)
     112           43 :                         onDataMessage({ detail: body });
     113            1 :                 } else {
     114            1 :                     manage_error(reject, { reason: headers.split('\r\n')[0] }, body);
     115            1 :                 }
     116           43 :             };
     117              : 
     118           43 :             const onDataMessage = message => {
     119            5 :                 if (return_raw) {
     120              :                     // debug(user_str, "monitor", path, "raw data:", message.detail);
     121            5 :                     callback(message.detail);
     122            5 :                 } else {
     123           43 :                     buffer = new Uint8Array([...buffer, ...message.detail]);
     124              : 
     125              :                     // split the buffer into lines on NL (this is safe with UTF-8)
     126           43 :                     for (;;) {
     127           43 :                         const idx = buffer.indexOf(NL);
     128           43 :                         if (idx < 0)
     129           43 :                             break;
     130              : 
     131           43 :                         const line = buffer.slice(0, idx);
     132           43 :                         buffer = buffer.slice(idx + 1);
     133              : 
     134           43 :                         const line_str = decoder.decode(line);
     135           43 :                         debug(user_str, "monitor", path, "data:", line_str);
     136           43 :                         callback(JSON.parse(line_str));
     137           43 :                     }
     138           43 :                 }
     139           43 :             };
     140              : 
     141              :             // the initial message is the HTTP status response
     142           43 :             ch.addEventListener("message", onHTTPMessage);
     143              : 
     144           43 :             ch.send("GET " + path + " HTTP/1.0\r\nContent-Length: 0\r\n\r\n");
     145           43 :         });
     146           43 :     };
     147              : 
     148            8 :     connection.close = function () {
     149            8 :         http.close();
     150            2 :         raw_channels.forEach(ch => ch.close());
     151            8 :     };
     152              : 
     153           43 :     return connection;
     154           43 : }
     155              : 
     156           43 : export default {
     157           43 :     connect,
     158           43 :     getAddress,
     159           43 : };
        

Generated by: LCOV version 2.0-1