Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
Author[]Andy Green <andy@warmcat.com> 2025-11-30 12:02 UTC
Committer[]Andy Green <andy@warmcat.com> 2025-11-30 13:49 UTC
Tree7f88cbd86d8ff9557c29aa9f13894fc9e71fc655   Raw Patch
 
Co-developed-by: Gemini 3.0 Pro
Co-developed-by: Gemini 3.0 Pro

Refactor sai-power config, server DB, and UI for PCON topology.
diff --git a/assets/sai.css b/assets/sai.css index 03cccc9..a3f9fd2 100644 --- a/assets/sai.css +++ b/assets/sai.css @@ -777,7 +777,7 @@ div.bdr { img.branch { display: inline-block; float:left; - background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAyNi43NDkgMjcuMjIyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImIiIHg9Ii0uMDcwOCIgeT0iLS4wNzA4IiB3aWR0aD0iMS4xNDE2IiBoZWlnaHQ9IjEuMTQxNiIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMC4yNTQyNzk1MyIvPgo8L2ZpbHRlcj4KPGZpbHRlciBpZD0iYSIgeD0iLS4wOTA1NjgiIHk9Ii0uMDQ2MjE0IiB3aWR0aD0iMS4xODExIiBoZWlnaHQ9IjEuMDkyNCIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMC40Nzk4NDA4OSIvPgo8L2ZpbHRlcj4KPC9kZWZzPgo8bWV0YWRhdGE+CjxyZGY6UkRGPgo8Y2M6V29yayByZGY6YWJvdXQ9IiI+CjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0Pgo8ZGM6dHlwZSByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIi8+CjxkYzp0aXRsZS8+CjwvY2M6V29yaz4KPC9yZGY6UkRGPgo8L21ldGFkYXRhPgo8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNzAuNzQ3IC02OS40NTQpIj4KPGNpcmNsZSBjeD0iODQuMTIxIiBjeT0iODMuMDIyIiByPSIxMy4zNzQiIGZpbGw9Im5vbmUiLz4KPHBhdGggZD0ibTgzLjc3MyA3MC42MDZjLTEuMTg2MiAwLjAxNTQtMi4zNDE4IDAuODQ2ODgtMi40Nzc0IDEuOTk4OGwtMC4wMTI0IDAuMDQ5MWMtMC4xNDk5IDUuNjExMi0wLjA0NzQgMTEuMjUyLTAuMTExNjIgMTYuODc0IDAuMDU4MSAxLjg3NzctMC40NDMzNyAzLjg0NDEgMC44NjgxNyA1LjIwMTIgMS43NDYyIDEuNzQ0NSA0LjIzMTcgMC40OTM5MyA0LjE0MjQtMi41MTU2IDAuMDQ1NS0wLjkyODQyIDAuMDExNy0xLjQwMiAwLjExMDA3LTMuMjM0OSAxLjUwNTIgMS4xMzQ4IDIuOTYwNCAyLjk5NjYgNC44MTk5IDMuNTIzOCAyLjAzNzEgMC4zMTUyOCAzLjQ3NDgtMi4yMDU1IDIuMzEyLTMuODU1MS0yLjI5OTItMi4yMDI1LTQuODIwMi00LjA1Ny03LjE2Ni02LjIyODYgMC4xMzg5LTMuMyAwLjAxNzYtNi4xNDIgMC4wNDY1LTkuNjA2Ny0wLjQxNDMtMS41OTgtMS40ODQ5LTIuMjE5Ni0yLjUzMTYtMi4yMDYxem0wLjE4NDQ5IDExLjg4M2EyLjA3ODkgMi4wNzg5IDAgMCAxIDIuMDc4OSAyLjA3ODkgMi4wNzg5IDIuMDc4OSAwIDAgMS0yLjA3ODkgMi4wNzkgMi4wNzg5IDIuMDc4OSAwIDAgMS0yLjA3ODQtMi4wNzkgMi4wNzg5IDIuMDc4OSAwIDAgMSAyLjA3ODQtMi4wNzg5eiIgZmlsdGVyPSJ1cmwoI2EpIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iLjI2NDU4cHgiLz4KPHBhdGggZD0ibTgzLjUxOCA3MC4yODNjLTEuMTg2MiAwLjAxNTQtMi4zNDE4IDAuODQ2ODgtMi40Nzc0IDEuOTk4OGwtMC4wMTI0IDAuMDQ5MWMtMC4xNDk5IDUuNjExMi0wLjA0NzQgMTEuMjUyLTAuMTExNjIgMTYuODc0IDAuMDU4MSAxLjg3NzctMC40NDMzNyAzLjg0NDEgMC44NjgxNyA1LjIwMTIgMS43NDYyIDEuNzQ0NSA0LjIzMTcgMC40OTM5MyA0LjE0MjQtMi41MTU2IDAuMDQ1NS0wLjkyODQyIDAuMDExNy0xLjQwMiAwLjExMDA3LTMuMjM0OSAxLjUwNTIgMS4xMzQ4IDIuOTYwNCAyLjk5NjYgNC44MTk5IDMuNTIzOCAyLjAzNzEgMC4zMTUyOCAzLjQ3NDgtMi4yMDU1IDIuMzEyLTMuODU1MS0yLjI5OTItMi4yMDI1LTQuODIwMi00LjA1Ny03LjE2Ni02LjIyODYgMC4xMzg5LTMuMyAwLjAxNzYtNi4xNDIgMC4wNDY1LTkuNjA2Ny0wLjQxNDMtMS41OTgtMS40ODQ5LTIuMjE5Ni0yLjUzMTYtMi4yMDYxem0wLjE4NDQ5IDExLjg4M2EyLjA3ODkgMi4wNzg5IDAgMCAxIDIuMDc4OSAyLjA3ODkgMi4wNzg5IDIuMDc4OSAwIDAgMS0yLjA3ODkgMi4wNzkgMi4wNzg5IDIuMDc4OSAwIDAgMS0yLjA3ODQtMi4wNzkgMi4wNzg5IDIuMDc4OSAwIDAgMSAyLjA3ODQtMi4wNzg5eiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9Ii4yNjQ1OHB4Ii8+CjxwYXRoIGQ9Im04My43MTYgODAuMThhNC4zMDk3IDQuMzA5NyAwIDAgMC00LjMwOTggNC4zMDk4IDQuMzA5NyA0LjMwOTcgMCAwIDAgNC4zMDk4IDQuMzA5OCA0LjMwOTcgNC4zMDk3IDAgMCAwIDQuMzA5OC00LjMwOTggNC4zMDk3IDQuMzA5NyAwIDAgMC00LjMwOTgtNC4zMDk4em0wLjAzMzYgMi4xMDQ4YTIuMTcxNiAyLjE3MTYgMCAwIDEgMi4xNzE1IDIuMTcxNSAyLjE3MTYgMi4xNzE2IDAgMCAxLTIuMTcxNSAyLjE3MTQgMi4xNzE2IDIuMTcxNiAwIDAgMS0yLjE3MTQtMi4xNzE0IDIuMTcxNiAyLjE3MTYgMCAwIDEgMi4xNzE0LTIuMTcxNXoiIGZpbGw9IiNmZmYiIGZpbHRlcj0idXJsKCNiKSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9Ii41Ii8+CjxwYXRoIGQ9Im04My42NjkgODAuMDYyYTQuMzA5NyA0LjMwOTcgMCAwIDAtNC4zMDk4IDQuMzA5OCA0LjMwOTcgNC4zMDk3IDAgMCAwIDQuMzA5OCA0LjMwOTggNC4zMDk3IDQuMzA5NyAwIDAgMCA0LjMwOTgtNC4zMDk4IDQuMzA5NyA0LjMwOTcgMCAwIDAtNC4zMDk4LTQuMzA5OHptMC4wMzM2IDIuMTA0OGEyLjE3MTYgMi4xNzE2IDAgMCAxIDIuMTcxNSAyLjE3MTQgMi4xNzE2IDIuMTcxNiAwIDAgMS0yLjE3MTUgMi4xNzE0IDIuMTcxNiAyLjE3MTYgMCAwIDEtMi4xNzE0LTIuMTcxNCAyLjE3MTYgMi4xNzE2IDAgMCAxIDIuMTcxNC0yLjE3MTR6IiBmaWxsPSIjZmZmIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iLjYiLz4KPC9nPgo8L3N2Zz4K"); + background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAyNi43NDkgMjcuMjIyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImIiIHg9Ii0uMDcwOCIgeT0iLS4wNzA4IiB3aWR0aD0iMS4xNDE2IiBoZWlnaHQ9IjEuMTQxNiIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMC4yNTQyNzk1MyIvPgo8L2ZpbHRlcj4KPGZpbHRlciBpZD0iYSIgeD0iLS4wOTA1NjgiIHk9Ii0uMDQ2MjE0IiB3aWR0aD0iMS4xODExIiBoZWlnaHQ9IjEuMDkyNCIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMC40Nzk4NDA4OSIvPgo8L2ZpbHRlcj4KPC9kZWZzPgo8bWV0YWRhdGE+CjxyZGY6UkRGPgo8Y2M6V29yayByZGY6YWJvdXQ9IiI+CjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0Pgo8ZGM6dHlwZSByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIi8+CjxkYzp0aXRsZS8+CjwvY2M6V29yaz4KPC9yZGY6UkRGPgo8L21ldGFkYXRhPgo8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNzAuNzQ3IC02OS40NTQpIj4KPGNpcmNsZSBjeD0iODQuMTIxIiBjeT0iODMuMDIyIiByPSIxMy4zNzQiIGZpbGw9Im5vbmUiLz4KPHBhdGggZD0ibTgzLjc3MyA3MC42MDZjLTEuMTg2MiAwLjAxNTQtMi4zNDE4IDAuODQ2ODgtMi40Nzc0IDEuOTk4OGwtMC4wMTI0IDAuMDQ5MWMtMC4xNDk5IDUuNjExMi0wLjA0NzQgMTEuMjUyLTAuMTExNjIgMTYuODc0IDAuMDU4MSAxLjg3NzctMC40NDMzNyAzLjg0NDEgMC44NjgxNyA1LjIwMTIgMS43NDYyIDEuNzQ0NSA0LjIzMTcgMC40OTM5MyA0LjE0MjQtMi41MTU2IDAuMDQ1NS0wLjkyODQyIDAuMDExNy0xLjQwMiAwLjExMDA3LTMuMjM0OSAxLjUwNTIgMS4xMzQ4IDIuOTYwNCAyLjk5NjYgNC44MTk5IDMuNTIzOCAyLjAzNzEgMC4zMTUyOCAzLjQ3NDgtMi4yMDU1IDIuMzEyLTMuODU1MS0yLjI5OTItMi4yMDI1LTQuODIwMi00LjA1Ny03LjE2Ni02LjIyODYgMC4xMzg5MyA0LjMwOTcgMC4wMTc2LTYuMTQyIDAuMDQ2NS05LjYwNjctMC40MTQzLTEuNTk4LTEuNDg0OS0yLjIxOTYtMi41MzE2LTIuMjA2MXem0wLjE4NDQ5IDExLjg4M2EyLjA3ODkgMi4wNzg5IDAgMCAxIDIuMDc4OSAyLjA3ODkgMi4wNzg5IDIuMDc4OSAwIDAgMS0yLjA3ODkgMi4wNzkgMi4wNzg5IDIuMDc4OSAwIDAgMS0yLjA3ODQtMi4wNzkgMi4wNzg5IDIuMDc4OSAwIDAgMSAyLjA3ODQtMi4wNzg5eiIgZmlsdGVyPSJ1cmwoI2EpIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iLjI2NDU4cHgiLz4KPHBhdGggZD0ibTgzLjUxOCA3MC4yODNjLTEuMTg2MiAwLjAxNTQtMi4zNDE4IDAuODQ2ODgtMi40Nzc0IDEuOTk4OGwtMC4wMTI0IDAuMDQ5MWMtMC4xNDk5IDUuNjExMi0wLjA0NzQgMTEuMjUyLTAuMTExNjIgMTYuODc0IDAuMDU4MSAxLjg3NzctMC40NDMzNyAzLjg0NDEgMC44NjgxNyA1LjIwMTIgMS43NDYyIDEuNzQ0NSA0LjIzMTcgMC40OTM5MyA0LjE0MjQtMi41MTU2IDAuMDQ1NS0wLjkyODQyIDAuMDExNy0xLjQwMiAwLjExMDA3LTMuMjM0OSAxLjUwNTIgMS4xMzQ4IDIuOTYwNCAyLjk5NjYgNC44MTk5IDMuNTIzOCAyLjAzNzEgMC4zMTUyOCAzLjQ3NDgtMi4yMDU1IDIuMzEyLTMuODU1MS0yLjI5OTItMi4yMDI1LTQuODIwMi00LjA1Ny03LjE2Ni02LjIyODYgMC4xMzg5MyA0LjMwOTcgMC4wMTc2LTYuMTQyIDAuMDQ2NS05LjYwNjctMC40MTQzLTEuNTk4LTEuNDg0OS0yLjIxOTYtMi41MzE2LTIuMjA2MXem0wLjE4NDQ5IDExLjg4M2EyLjA3ODkgMi4wNzg5IDAgMCAxIDIuMDc4OSAyLjA3ODkgMi4wNzg5IDIuMDc4OSAwIDAgMS0yLjA3ODkgMi4wNzkgMi4wNzg5IDIuMDc4OSAwIDAgMS0yLjA3ODQtMi4wNzkgMi4wNzg5IDIuMDc4OSAwIDAgMSAyLjA3ODQtMi4wNzg5eiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9Ii4yNjQ1OHB4Ii8+CjxwYXRoIGQ9Im04My43MTYgODAuMThhNC4zMDk3IDQuMzA5NyAwIDAgMC00LjMwOTggNC4zMDk4IDQuMzA5NyA0LjMwOTcgMCAwIDAgNC4zMDk4IDQuMzA5OCA0LjMwOTcgNC4zMDk3IDAgMCAwIDQuMzA5OC00LjMwOTggNC4zMDk3IDQuMzA5NyAwIDAgMC00LjMwOTgtNC4zMDk4em0wLjAzMzYgMi4xMDQ4YTIuMTcxNiAyLjE3MTYgMCAwIDEgMi4xNzE1IDIuMTcxNSAyLjE3MTYgMi4xNzE2IDAgMCAxLTIuMTcxNSAyLjE3MTQgMi4xNzE2IDIuMTcxNiAwIDAgMS0yLjE3MTQtMi4xNzE0IDIuMTcxNiAyLjE3MTYgMCAwIDEgMi4xNzE0LTIuMTcxNXoiIGZpbGw9IiNmZmYiIGZpbHRlcj0idXJsKCNiKSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9Ii41Ii8+CjxwYXRoIGQ9Im04My42NjkgODAuMDYyYTQuMzA5NyA0LjMwOTcgMCAwIDAtNC4zMDk4IDQuMzA5OCA0LjMwOTcgNC4zMDk3IDAgMCAwIDQuMzA5OCA0LjMwOTggNC4zMDk3IDQuMzA5NyAwIDAgMCA0LjMwOTgtNC4zMDk4IDQuMzA5NyA0LjMwOTcgMCAwIDAtNC4zMDk4LTQuMzA5OHptMC4wMzM2IDIuMTA0OGEyLjE3MTYgMi4xNzE2IDAgMCAxIDIuMTcxNSAyLjE3MTQgMi4xNzE2IDIuMTcxNiAwIDAgMS0yLjE3MTUgMi4xNzE0IDIuMTcxNiAyLjE3MTYgMCAwIDEtMi4xNzE0LTIuMTcxNCAyLjE3MTYgMi4xNzE2IDAgMCAxIDIuMTcxNC0yLjE3MTR6IiBmaWxsPSIjZmZmIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iLjYiLz4KPC9nPgo8L3N2Zz4K"); width:0px; height:0px; cursor:pointer; @@ -1078,3 +1078,47 @@ body.overlay-active { width: 24px; margin-bottom: 1px; } + +/* PCON Styles */ + +.pcon { + margin-bottom: 5px; + padding-left: 5px; +} + +.pcon-header { + font-weight: bold; + font-size: 1.0em; + padding: 4px; + background-color: #e0e0e0; + border-radius: 3px; + display: flex; + align-items: center; +} + +.pcon-on { + color: #2ecc40; /* Green */ + margin-right: 5px; +} + +.pcon-off { + color: #aaaaaa; /* Grey */ + margin-right: 5px; +} + +.pcon-type { + font-weight: normal; + font-size: 0.8em; + color: #666; + margin-left: 5px; +} + +.pcon-children { + padding-left: 10px; + border-left: 2px solid #ddd; + margin-top: 2px; +} + +.pcon-orphans .pcon-header { + background-color: #ffe0e0; +} diff --git a/assets/sai.js b/assets/sai.js index 14f6457..1741428 100644 --- a/assets/sai.js +++ b/assets/sai.js @@ -22,7 +22,7 @@ const SAI_JS_API_VERSION = 3; } Translator.prototype.translate = function(text, defaultNumOrFormatting, - numOrFormattingOrContext, formattingOrContext, context) { + numOrFormattingOrContext, formattingOrContext, context) { var defaultText, formatting, isObject, num; if (context == null) { @@ -75,10 +75,10 @@ const SAI_JS_API_VERSION = 3; _ref = d.values; var k; for (k in _ref) { - if ({}.hasOwnProperty.call(_ref, k)) { + if ({}.hasOwnProperty.call(_ref, k)) { v = _ref[k]; this.data.values[k] = v; - } + } } } if ((d.contexts != null)) { @@ -123,18 +123,18 @@ const SAI_JS_API_VERSION = 3; var k, v; for (k in hash) { - if ({}.hasOwnProperty.call(hash, k)) { + if ({}.hasOwnProperty.call(hash, k)) { v = hash[k]; if (typeof v === "string") { hash[k] = this.translateText(v, null, null, context); } - } + } } return hash; }; Translator.prototype.translateText = function(text, num, formatting, - context, defaultText) { + context, defaultText) { var contextData, result; if (context == null) { @@ -146,11 +146,11 @@ const SAI_JS_API_VERSION = 3; contextData = this.getContextData(this.data, context); if (contextData != null) { result = this.findTranslation(text, num, formatting, contextData.values, - defaultText); + defaultText); } if (result == null) { result = this.findTranslation(text, num, formatting, this.data.values, - defaultText); + defaultText); } if (result == null) { return this.useOriginalText(defaultText || text, num, formatting); @@ -176,9 +176,9 @@ const SAI_JS_API_VERSION = 3; if ((num >= triple[0] || triple[0] === null) && (num <= triple[1] || triple[1] === null)) { result = this.applyFormatting(triple[2].replace("-%n", - String(-num)), num, formatting); + String(-num)), num, formatting); return this.applyFormatting(result.replace("%n", - String(num)), num, formatting); + String(num)), num, formatting); } } } @@ -198,10 +198,10 @@ const SAI_JS_API_VERSION = 3; equal = true; _ref1 = c.matches; for (key in _ref1) { - if ({}.hasOwnProperty.call(_ref1, key)) { - value = _ref1[key]; - equal = equal && value === context[key]; - } + if ({}.hasOwnProperty.call(_ref1, key)) { + value = _ref1[key]; + equal = equal && value === context[key]; + } } if (equal) { return c; @@ -215,17 +215,17 @@ const SAI_JS_API_VERSION = 3; return this.applyFormatting(text, num, formatting); } return this.applyFormatting(text.replace("%n", String(num)), - num, formatting); + num, formatting); }; Translator.prototype.applyFormatting = function(text, num, formatting) { var ind, regex; for (ind in formatting) { - if ({}.hasOwnProperty.call(formatting, ind)) { + if ({}.hasOwnProperty.call(formatting, ind)) { regex = new RegExp("%{" + ind + "}", "g"); text = text.replace(regex, formatting[ind]); - } + } } return text; }; @@ -251,8 +251,8 @@ const SAI_JS_API_VERSION = 3; return trans.translate; }; - (typeof module !== "undefined" && module !== null ? module.exports = i18n : void 0) || - (this.i18n = i18n); + (typeof module !== "undefined" && module !== null ? module.exports = i18n : void 0) || + (this.i18n = i18n); }.call(this)); @@ -391,7 +391,7 @@ var lang_zhs = "{" + "\"页面%{pf}前获取, 创作时间: %{ct}ms " + "(vhost etag 缓存命中: %{ve}%, 缓存命中: %{ch}%)\"," + "\"Created %{pf} ago, creation time: %{ct}ms \":" + - "\"%{pf}前创建, 创作时间: %{ct}ms \"" + + "\"%{pf}前创建, 创作时间: %{ct}ms \"" + "}}"; var logs = "", redpend = 0, gitohashi_integ = 0, authd = 0, exptimer, auth_user = "", @@ -399,6 +399,118 @@ var logs = "", redpend = 0, gitohashi_integ = 0, authd = 0, exptimer, auth_user ongoing_task_activities = {}, last_log_timestamp = 0, spreadsheet_data_cache = {}, loadreport_data_cache = {}, fadingTasks = new Map(); +/* Global caches for reconcilation */ +var pcon_topology = {}; +var last_builder_list = []; + +function createPconDiv(pcon) { + const pconDiv = document.createElement("div"); + pconDiv.className = "pcon"; + pconDiv.id = "pcon-" + pcon.name; + pconDiv.style.marginLeft = "10px"; + pconDiv.style.borderLeft = "1px solid #ccc"; + pconDiv.style.paddingLeft = "5px"; + + const header = document.createElement("div"); + header.className = "pcon-header"; + + let stateClass = pcon.on ? "pcon-on" : "pcon-off"; + let type = pcon.type ? `(${pcon.type})` : ""; + + header.innerHTML = `<span class="${stateClass}">&#x23FB;</span> <b>${hsanitize(pcon.name)}</b> <span class="pcon-type">${hsanitize(type)}</span>`; + pconDiv.appendChild(header); + + const childrenDiv = document.createElement("div"); + childrenDiv.className = "pcon-children"; + pconDiv.appendChild(childrenDiv); + + return pconDiv; +} + +function renderPconHierarchy(container) { + if (!container) return; + + /* Clear and redraw for now to ensure structure is correct */ + container.innerHTML = ""; + + const pcons = Object.values(pcon_topology); + /* Build map for dependency resolution */ + const pconMap = {}; + pcons.forEach(p => { + p.children = []; /* Reset children */ + pconMap[p.name] = p; + }); + + /* Link PCONs */ + const roots = []; + pcons.forEach(p => { + if (p.depends_on && pconMap[p.depends_on]) { + pconMap[p.depends_on].children.push(p); + } else { + roots.push(p); + } + }); + + /* Sort roots and children by name */ + const sortByName = (a, b) => a.name.localeCompare(b.name); + roots.sort(sortByName); + pcons.forEach(p => p.children.sort(sortByName)); + + /* Helper to recursively render PCONs and their builders */ + function renderPcon(pcon, parentDiv) { + const div = createPconDiv(pcon); + parentDiv.appendChild(div); + const childrenContainer = div.querySelector(".pcon-children"); + + /* Render builders belonging to this PCON */ + /* We search the global builder list for those matching this pcon */ + const myBuilders = last_builder_list.filter(b => b.pcon === pcon.name); + myBuilders.sort((a, b) => a.name.localeCompare(b.name)); + + if (myBuilders.length > 0) { + const table = document.createElement("table"); + table.className = "builders"; + const tbody = document.createElement("tbody"); + table.appendChild(tbody); + myBuilders.forEach(b => { + tbody.appendChild(createBuilderRow(b)); + }); + childrenContainer.appendChild(table); + } + + /* Render child PCONs */ + pcon.children.forEach(child => { + renderPcon(child, childrenContainer); + }); + } + + roots.forEach(root => { + renderPcon(root, container); + }); + + /* Render orphan builders (no pcon or unknown pcon) */ + const orphanBuilders = last_builder_list.filter(b => !b.pcon || !pcon_topology[b.pcon]); + if (orphanBuilders.length > 0) { + const orphanDiv = document.createElement("div"); + orphanDiv.className = "pcon-orphans"; + orphanDiv.innerHTML = "<div class='pcon-header'><b>Unmanaged Builders</b></div>"; + const childrenContainer = document.createElement("div"); + childrenContainer.className = "pcon-children"; + orphanDiv.appendChild(childrenContainer); + + const table = document.createElement("table"); + table.className = "builders"; + const tbody = document.createElement("tbody"); + table.appendChild(tbody); + orphanBuilders.forEach(b => { + tbody.appendChild(createBuilderRow(b)); + }); + childrenContainer.appendChild(table); + + container.appendChild(orphanDiv); + } +} + function update_task_activities() { for (const uuid in ongoing_task_activities) { const el = document.getElementById("taskstate_" + uuid); @@ -411,33 +523,33 @@ function update_task_activities() { } } } - + function expiry() { location.reload(); } - + function san(s) { if (s.search("<") !== -1) return "invalid string"; - + return s; } function humanize(s) { var i = parseInt(s, 10); - + if (i >= (1024 * 1024 * 1024)) return (i / (1024 * 1024 * 1024)).toFixed(3) + "Gi"; - + if (i >= (1024 * 1024)) return (i / (1024 * 1024)).toFixed(3) + "Mi"; - + if (i > 1024) return (i / 1024).toFixed(3) + "Ki"; - + return s; } @@ -503,7 +615,7 @@ function hsanitize(s) '\'': 'apos', '&': 'amp' }; - + return s.toString().replace(/[<>"'&]/g, function(chr) { return '&' + table[chr] + ';'; }).replace(/\r\n/g, '\n').replace(/\n/g, '<br>'); @@ -647,13 +759,13 @@ var age_upd = [ 5, 10, 300, 1800, 3600, 12 * 3600, 12 * 3600 ]; function agify(now, secs) { var d = now - secs, n; - + if (!secs) return ""; - + if (secs > now) d = secs - now; - + for (n = 0; n < age_names.length; n++) if (d < age_limit[n] || age_limit[n] === 0) return "<span class='age-" + n + "' ut='" + secs + @@ -706,17 +818,17 @@ var sai, jso, s, sai_arts = ""; function sai_plat_icon(plat, size) { var s, s1 = ""; - + s = plat.split('/'); if (s[0]) { // console.log("plat " + plat + " plat[0] " + s[0]); s1 = "<img class=\"ip" + size + " zup\" src=\"/sai/" + san(s[0]) + ".svg\">"; - + if (s[1]) s1 += "<img class=\"ip" + size + " tread1\" src=\"/sai/arch-" + san(s[1]) + ".svg\">"; } - + if (s[2]) { s1 += "<img class=\"ip" + size + " tread2\" src=\"/sai/tc-" + san(s[2]) + ".svg\">"; } @@ -730,18 +842,18 @@ function sai_stateful_taskname(state, nm, sf) if (sf) return "<span id=\"taskstate\" class=\"ti2 taskstate" + state + "\">&nbsp;" + san(nm) + "&nbsp;&nbsp;</span>"; - + if (state == 4 || state == 6) tp = " ov_bad"; - - return "<span id=\"taskstate\" class=\"ti2 " + tp + "\">" + san(nm) + "</span>"; + + return "<span id=\"taskstate\" class=\"ti2 " + tp + "\">" + san(nm) + "</span>"; } function sai_taskinfo_render(t, now_ut) { var now_ut = Math.round((new Date().getTime() / 1000)); var s = ""; - + s = "<table><tr class=\"nomar\"><td class=\"atop\"><table>" + sai_event_render(t, now_ut, 0) + "</table></td><td class=\"ti\">" + "<span class=\"ti1\">" + sai_plat_icon(t.t.platform, 2) + @@ -753,7 +865,7 @@ function sai_taskinfo_render(t, now_ut) s += "<img class=\"rebuild\" alt=\"rebuild\" src=\"rebuild.png\" " + "id=\"rebuild-" + san(t.t.uuid) + "\">&nbsp;" + sai_stateful_taskname(t.t.state, t.t.taskname, 1); - + if (t.t.builder_name) { var now_ut = Math.round((new Date().getTime() / 1000)); @@ -764,15 +876,15 @@ function sai_taskinfo_render(t, now_ut) s += ", <span class=\"ti5\"> " + agify(now_ut, t.t.started) + " ago, Dur: " + (t.t.duration ? t.t.duration / 1000000 : - now_ut - t.t.started).toFixed(1) + + now_ut - t.t.started).toFixed(1) + "s</span><div id=\"sai_arts\"></div><div id=\"metrics-summary-" + san(t.t.uuid) + "\"></div>"; sai_arts = ""; } s += "</td></tr>"; - + s += "</td></tr></table></table>"; - + return s; } @@ -835,10 +947,10 @@ function summarize_build_situation(event_uuid) var good = 0, bad = 0, total = 0, ongoing = 0, pending = 0, roo = document.getElementById("taskcont-" + event_uuid), same; - - if (!roo) + + if (!roo) return { text: "" }; - + same = roo.querySelectorAll(".taskstate"); if (same) total = same.length; @@ -918,7 +1030,7 @@ function sai_event_summary_render(o, now_ut, reset_all_icon) s += "<div class=\"" + cl + "\"><img src=\"/sai/passed.svg\"></div>"; if (e.state == 4) s += "<div class=\"" + cl + "\"><img src=\"/sai/failed.svg\"></div>"; - + s += "</a>"; if (reset_all_icon && !gitohashi_integ && authd) { s += "<br><img class=\"rebuild\" alt=\"rebuild all\" src=\"/sai/rebuild.png\" " + @@ -936,7 +1048,7 @@ function sai_event_summary_render(o, now_ut, reset_all_icon) if (e.sec) s += " <img class=\"bico\" src=\"/sai/locked.svg\">"; s += "</span></td></tr><tr><td class=\"nomar\" colspan=2><span class=\"e2\">"; - + if (e.ref.substr(0, 11) === "refs/heads/") { s += "<img class=\"branch\">" + san(e.ref.substr(11)); @@ -946,7 +1058,7 @@ function sai_event_summary_render(o, now_ut, reset_all_icon) san(e.ref.substr(10)); } else s += san(e.ref); - + s += "</span></td></tr><tr><td class=\"nomar e6\">" + san(e.hash.substr(0, 8)) + "</td><td class=\"e6 nomar\">" + @@ -959,14 +1071,14 @@ function sai_event_summary_render(o, now_ut, reset_all_icon) "</table></td>"; } s += "</tr><tr><td class=\"nomar e6\" colspan=\"2\" id=\"sumbs-" + e.uuid +"\"></td></tr></table>"; - + return s; } function sai_event_render(o, now_ut, reset_all_icon) { var s, q, ctn = "", wai, s1 = "", n, e = o.e; - + s = "<tr><td class=\"waiting\""; if (gitohashi_integ) s += " id=\"gitohashi_sai_icon\""; @@ -981,7 +1093,7 @@ function sai_event_render(o, now_ut, reset_all_icon) for (q = 0; q < o.t.length; q++) { var t = o.t[q]; - + if (t.taskname !== ctn) { if (ctn !== "") { s += "<div class=\"ib\"><table class=\"nomar\">" + @@ -992,7 +1104,7 @@ function sai_event_render(o, now_ut, reset_all_icon) } ctn = t.taskname; } - + s1 += "<div id=\"taskstate_" + t.uuid + "\" class=\"taskstate taskstate" + t.state + "\" data-event-uuid=\"" + san(e.uuid) + "\" data-platform=\"" + san(t.platform) + "\" data-rebuildable=\"" + t.rebuildable + "\">"; @@ -1015,7 +1127,7 @@ function sai_event_render(o, now_ut, reset_all_icon) } s += "</tr>"; - + return s; } @@ -1035,7 +1147,7 @@ function getBuilderGroupKey(platName) { function refresh_state(task_uuid, task_state) { var tsi = document.getElementById("taskstate_" + task_uuid); - + if (tsi) { tsi.classList.remove("taskstate0"); tsi.classList.remove("taskstate1"); @@ -1236,7 +1348,7 @@ function updateSpreadsheetCell(cell, platName) { } if (best_match_key) { - cell.innerHTML = renderSpreadsheet(spreadsheet_data_cache[best_match_key]); + updateSpreadsheetDOM(cell, spreadsheet_data_cache[best_match_key]); aging(); } else { cell.innerHTML = ""; // Clear it if no data @@ -1275,12 +1387,134 @@ function updateBuilderRow(row, plat) { updateSpreadsheetCell(tdSpreadsheet, plat.name); } +/* Global caches for reconcilation */ +var pcon_topology = {}; +var last_builder_list = []; + +function createPconDiv(pcon) { + const pconDiv = document.createElement("div"); + pconDiv.className = "pcon"; + pconDiv.id = "pcon-" + pcon.name; + pconDiv.style.marginLeft = "10px"; + pconDiv.style.borderLeft = "1px solid #ccc"; + pconDiv.style.paddingLeft = "5px"; + + const header = document.createElement("div"); + header.className = "pcon-header"; + + let stateClass = pcon.on ? "pcon-on" : "pcon-off"; + let type = pcon.type ? `(${pcon.type})` : ""; + + header.innerHTML = `<span class="${stateClass}">&#x23FB;</span> <b>${hsanitize(pcon.name)}</b> <span class="pcon-type">${hsanitize(type)}</span>`; + pconDiv.appendChild(header); + + /* Context menu for PCON */ + const menuItems = [ + { label: `<b>PCON:</b> ${pcon.name}` } + ]; + + header.addEventListener("contextmenu", function(event) { + if (!authd) return; + createContextMenu(event, menuItems); + }); + + const childrenDiv = document.createElement("div"); + childrenDiv.className = "pcon-children"; + pconDiv.appendChild(childrenDiv); + + return pconDiv; +} + +function renderPconHierarchy(container) { + if (!container) return; + + /* Clear and redraw for now to ensure structure is correct */ + container.innerHTML = ""; + + const pcons = Object.values(pcon_topology); + /* Build map for dependency resolution */ + const pconMap = {}; + pcons.forEach(p => { + p.children = []; /* Reset children */ + pconMap[p.name] = p; + }); + + /* Link PCONs */ + const roots = []; + pcons.forEach(p => { + if (p.depends_on && pconMap[p.depends_on]) { + pconMap[p.depends_on].children.push(p); + } else { + roots.push(p); + } + }); + + /* Sort roots and children by name */ + const sortByName = (a, b) => a.name.localeCompare(b.name); + roots.sort(sortByName); + pcons.forEach(p => p.children.sort(sortByName)); + + /* Helper to recursively render PCONs and their builders */ + function renderPcon(pcon, parentDiv) { + const div = createPconDiv(pcon); + parentDiv.appendChild(div); + const childrenContainer = div.querySelector(".pcon-children"); + + /* Render builders belonging to this PCON */ + /* We search the global builder list for those matching this pcon */ + const myBuilders = last_builder_list.filter(b => b.pcon === pcon.name); + myBuilders.sort((a, b) => a.name.localeCompare(b.name)); + + if (myBuilders.length > 0) { + const table = document.createElement("table"); + table.className = "builders"; + const tbody = document.createElement("tbody"); + table.appendChild(tbody); + myBuilders.forEach(b => { + tbody.appendChild(createBuilderRow(b)); + }); + childrenContainer.appendChild(table); + } + + /* Render child PCONs */ + pcon.children.forEach(child => { + renderPcon(child, childrenContainer); + }); + } + + roots.forEach(root => { + renderPcon(root, container); + }); + + /* Render orphan builders (no pcon or unknown pcon) */ + const orphanBuilders = last_builder_list.filter(b => !b.pcon || !pcon_topology[b.pcon]); + if (orphanBuilders.length > 0) { + const orphanDiv = document.createElement("div"); + orphanDiv.className = "pcon-orphans"; + orphanDiv.innerHTML = "<div class='pcon-header'><b>Unmanaged Builders</b></div>"; + const childrenContainer = document.createElement("div"); + childrenContainer.className = "pcon-children"; + orphanDiv.appendChild(childrenContainer); + + const table = document.createElement("table"); + table.className = "builders"; + const tbody = document.createElement("tbody"); + table.appendChild(tbody); + orphanBuilders.forEach(b => { + tbody.appendChild(createBuilderRow(b)); + }); + childrenContainer.appendChild(table); + + container.appendChild(orphanDiv); + } +} + function ws_open_sai() -{ +{ var s = "", q, qa, qi, q5, q5s; if (document.getElementById("apirev")) - document.getElementById("apirev").innerHTML = "API rev " + SAI_JS_API_VERSION; + document.getElementById("apirev").innerHTML = "API rev " + SAI_JS_API_VERSION; q = window.location.href; console.log(q); @@ -1300,12 +1534,12 @@ function ws_open_sai() * gitohashi has ?h=branch and ?id=hash possible */ qa = q.split("?"); - if (qa[1]) + if (qa[1]) s += "?" + qa[1]; console.log(s); gitohashi_integ = 1; } - + qi = q.indexOf("?task="); if (qi != -1) { /* @@ -1313,7 +1547,7 @@ function ws_open_sai() */ s += "/specific?task=" + q.substring(qi + 6); } - + var s1 = get_appropriate_ws_url() + "/sai/browse" + s; // if (s1.split("?")) // s1 = s1.split("?")[0]; @@ -1332,50 +1566,50 @@ function ws_open_sai() tid, eid; tid = par.get('task'); eid = par.get('event'); - - + + if (tid) { /* * We're being the page monitoring / reporting * on what happened with a specific task... ask * about the specific task on the ws link */ - + console.log("tid " + tid); - + sai.send("{\"schema\":" + "\"com.warmcat.sai.taskinfo\"," + "\"js_api_version\": " + SAI_JS_API_VERSION + "," + "\"logs\": 1," + "\"last_log_ts\":" + last_log_timestamp + "," + "\"task_hash\":" + - JSON.stringify(tid) + "}"); - + JSON.stringify(tid) + "}"); + return; } - + if (eid) { /* * We're being the page monitoring / reporting * on what happened with a specific event... ask * about the specific event on the ws link */ - + console.log("eid " + eid); - + sai.send("{\"schema\":" + "\"com.warmcat.sai.eventinfo\"," + "\"js_api_version\": " + SAI_JS_API_VERSION + "," + "\"event_hash\":" + - JSON.stringify(eid) + "}"); - + JSON.stringify(eid) + "}"); + return; } - + /* * request the overview schema */ - + sai.send("{\"schema\":" + "\"com.warmcat.sai.taskinfo\", \"js_api_version\": " + SAI_JS_API_VERSION + "}"); }; @@ -1389,10 +1623,10 @@ function ws_open_sai() // return; jso = JSON.parse(msg.data); // console.log(jso.schema); - + if (jso.alang) { var a = jso.alang.split(","), n; - + for (n = 0; n < a.length; n++) { var b = a[n].split(";"); switch (b[0]) { @@ -1436,69 +1670,31 @@ function ws_open_sai() console.log(jso.schema); - switch (jso.schema) { - - case "com.warmcat.sai.builders": - const buildersContainer = document.getElementById("sai_builders"); - if (!buildersContainer) { break; } + switch (jso.schema) { + case "com.warmcat.sai.builders": + /* Update builder list */ let platformsArray = (jso.platforms && Array.isArray(jso.platforms)) ? jso.platforms : (jso.builders && Array.isArray(jso.builders)) ? jso.builders : null; - if (!platformsArray) { - buildersContainer.innerHTML = ""; // Clear display if data is invalid - break; - } - - platformsArray.sort((a, b) => (a.name && b.name) ? a.name.localeCompare(b.name) : 0); - - let table = buildersContainer.querySelector("table.builders"); - if (!table) { - buildersContainer.innerHTML = ""; // Clear placeholder - table = document.createElement("table"); - table.className = "builders"; - table.appendChild(document.createElement("tbody")); - buildersContainer.appendChild(table); - } - const tbody = table.querySelector("tbody"); - - // --- Reconciliation Logic --- - let platIdx = 0; - let currentRow = tbody.firstChild; - - while (platIdx < platformsArray.length) { - const plat = platformsArray[platIdx]; - - if (!currentRow) { - tbody.appendChild(createBuilderRow(plat)); - platIdx++; - continue; - } - - const rowPlatName = currentRow.id.substring(4); // "row-..." - const comparison = plat.name.localeCompare(rowPlatName); - - if (comparison === 0) { - updateBuilderRow(currentRow, plat); - platIdx++; - currentRow = currentRow.nextSibling; - } else if (comparison < 0) { - tbody.insertBefore(createBuilderRow(plat), currentRow); - platIdx++; - } else { // comparison > 0 - const nextRow = currentRow.nextSibling; - tbody.removeChild(currentRow); - currentRow = nextRow; - } + if (platformsArray) { + last_builder_list = platformsArray; + const container = document.getElementById("sai_builders"); + if (container) renderPconHierarchy(container); } + break; - // Remove any remaining old rows at the end - while (currentRow) { - const nextRow = currentRow.nextSibling; - tbody.removeChild(currentRow); - currentRow = nextRow; + case "com.warmcat.sai.power_managed_builders": + /* Update PCON topology */ + if (jso.power_controllers) { + jso.power_controllers.forEach(pc => { + pcon_topology[pc.name] = pc; + }); + /* Trigger redraw if we have builders */ + const container = document.getElementById("sai_builders"); + if (container) renderPconHierarchy(container); } - break; + break; case "com.warmcat.sai.build-metric": var summaryDiv = document.getElementById("metrics-summary-" + jso.task_uuid); @@ -1521,9 +1717,9 @@ function ws_open_sai() * Sent with an array of e[] to start, but also * can send a single e[] if it just changed * state - */ + */ s = "<table>"; - + authd = jso.authorized; if (jso.authorized === 0) { if (document.getElementById("creds")) @@ -1547,13 +1743,13 @@ function ws_open_sai() san(auth_user) + " " + agify(now_ut, now_ut + jso.auth_secs); } } - + /* * Update existing? */ - + // console.log("jso.overview.length " + jso.overview.length); - + if (jso.overview.length == 1 && document.getElementById("esr-" + jso.overview[0].e.uuid)) { /* this is just the summary box, not the tasks */ @@ -1561,12 +1757,12 @@ function ws_open_sai() sai_event_summary_render(jso.overview[0], now_ut, 1); /* if the task status icons exist, update their state */ - + for (n = jso.overview[0].t.length - 1; n >= 0; n--) refresh_state(jso.overview[0].t[n].uuid, jso.overview[0].t[n].state); - + update_summary_and_progress(jso.overview[0].e.uuid); - + aging(); } else { @@ -1576,12 +1772,12 @@ function ws_open_sai() if (jso.overview.length) { for (n = jso.overview.length - 1; n >= 0; n--) s += sai_event_render(jso.overview[n], now_ut, 1); - + s = s + "</table>"; - + if (document.getElementById("sai_sticky")) document.getElementById("sai_sticky").innerHTML = s; - + for (n = jso.overview.length - 1; n >= 0; n--) { document.getElementById("esr-" + jso.overview[n].e.uuid).innerHTML = sai_event_summary_render(jso.overview[n], now_ut, 1); @@ -1590,13 +1786,13 @@ function ws_open_sai() } aging(); } - + if (gitohashi_integ && document.getElementById("gitohashi_sai_icon")) { var integ_state = 0; document.getElementById("gitohashi_sai_icon").addEventListener("mouseenter", function( event ) { - document.getElementById("gitohashi_sai_icon").style.zIndex = 1999; + document.getElementById("gitohashi_sai_icon").style.zIndex = 1999; document.getElementById("gitohashi_sai_details").style.zIndex = 2000; - document.getElementById("gitohashi_sai_details").style.opacity = 1.0; + document.getElementById("gitohashi_sai_details").style.opacity = 1.0; integ_state = 1; }, false); @@ -1612,13 +1808,13 @@ function ws_open_sai() } document.getElementById("gitohashi_sai_details").style.opacity = 0.0; document.getElementById("gitohashi_sai_details").style.zIndex = -1; - document.getElementById("gitohashi_sai_icon").style.zIndex = 2001; + document.getElementById("gitohashi_sai_icon").style.zIndex = 2001; }, true); - + aging(); } } - + if (jso.overview.length) for (n = jso.overview.length - 1; n >= 0; n--) { if (document.getElementById("rebuild-ev-" + san(jso.overview[n].e.uuid))) @@ -1628,10 +1824,10 @@ function ws_open_sai() var rs= "{\"schema\":" + "\"com.warmcat.sai.eventreset\"," + "\"uuid\": " + - JSON.stringify(san(e.srcElement.id.substring(11))) + "}"; - - console.log(rs); - sai.send(rs); + JSON.stringify(san(e.srcElement.id.substring(11))) + "}"; + + console.log(rs); + sai.send(rs); }); if (document.getElementById("delete-ev-" + san(jso.overview[n].e.uuid))) document.getElementById("delete-ev-" + san(jso.overview[n].e.uuid)). @@ -1640,15 +1836,15 @@ function ws_open_sai() var rs= "{\"schema\":" + "\"com.warmcat.sai.eventdelete\"," + "\"uuid\": " + - JSON.stringify(san(e.srcElement.id.substring(10))) + "}"; - - console.log(rs); - sai.send(rs); + JSON.stringify(san(e.srcElement.id.substring(10))) + "}"; + + console.log(rs); + sai.send(rs); setTimeout(after_delete, 750); }); } break; - + case "com.warmcat.sai.taskinfo": if (!jso.t) @@ -1677,7 +1873,7 @@ function ws_open_sai() san(auth_user) + " " + agify(now_ut, now_ut + jso.auth_secs); } } - + /* * We get told about changes to any task state, * it's up to us to figure out if the page we @@ -1691,17 +1887,17 @@ function ws_open_sai() * First see if it appears as a tuple, and if * so, let's just update that */ - + if (document.getElementById("taskstate_" + jso.t.uuid)) { console.log("found taskstate_" + jso.t.uuid); refresh_state(jso.t.uuid, jso.t.state); - + update_summary_and_progress(jso.t.uuid.substring(0, 32)); } else - + /* update task summary if shown anywhere */ - + if (document.getElementById("taskinfo-" + jso.t.uuid)) { console.log("FOUND taskinfo-" + jso.t.uuid); document.getElementById("taskinfo-" + jso.t.uuid).innerHTML = sai_taskinfo_render(jso); @@ -1711,20 +1907,20 @@ function ws_open_sai() update_summary_and_progress(jso.e.uuid); } else { - + console.log("NO taskinfo- or taskstate_" + jso.t.uuid); - - /* + + /* * Last chance if we might be * on a task-specific page, and * want to show the task info * at the top */ - + const urlParams = new URLSearchParams(window.location.search); const url_task_uuid = urlParams.get('task'); - + if (url_task_uuid === jso.t.uuid && document.getElementById("sai_sticky")) document.getElementById("sai_sticky").innerHTML = @@ -1732,8 +1928,8 @@ function ws_open_sai() san(jso.t.uuid) + "\">" + sai_taskinfo_render(jso) + "</div>"; - - + + s = "<table><td colspan=\"3\"><pre><table class=\"scrollogs\"><tr>" + "<td class=\"atop\">" + "<div id=\"dlogsn\" class=\"dlogsn\">" + lines + "</div></td>" + @@ -1741,16 +1937,16 @@ function ws_open_sai() "<div id=\"dlogst\" class=\"dlogst\">" + times + "</div></td>" + "<td class=\"atop\"><div id=\"dlogs\" class=\"dlogs\">" + "<span id=\"logs\" class=\"nowrap\">" + logs + - "</span>"+ + "</span>"+ "</div></td></tr></table></pre>"; - + if (document.getElementById("sai_overview")) { document.getElementById("sai_overview").innerHTML = s; - + if (document.getElementById("esr-" + jso.e.uuid)) document.getElementById("esr-" + jso.e.uuid).innerHTML = sai_event_summary_render(jso, now_ut, 1); - + } update_summary_and_progress(jso.e.uuid); @@ -1760,10 +1956,10 @@ function ws_open_sai() var rs= "{\"schema\":" + "\"com.warmcat.sai.taskreset\"," + "\"uuid\": " + - JSON.stringify(san(e.srcElement.id.substring(8))) + "}"; - - console.log(rs); - sai.send(rs); + JSON.stringify(san(e.srcElement.id.substring(8))) + "}"; + + console.log(rs); + sai.send(rs); /* * and immediately re-request the task info, so we can get @@ -1781,29 +1977,29 @@ function ws_open_sai() console.log(rq); sai.send(rq); - document.getElementById("dlogsn").innerHTML = ""; - document.getElementById("dlogst").innerHTML = ""; - document.getElementById("logs").innerHTML = ""; - lines = times = logs = ""; + document.getElementById("dlogsn").innerHTML = ""; + document.getElementById("dlogst").innerHTML = ""; + document.getElementById("logs").innerHTML = ""; + lines = times = logs = ""; logAnsiState = {}; - tfirst = 0; - lli = 1; + tfirst = 0; + lli = 1; last_log_timestamp = 0; - }); + }); } - + if (document.getElementById("stop-" + san(jso.t.uuid))) { document.getElementById("stop-" + san(jso.t.uuid)). addEventListener("click", function(e) { var rs= "{\"schema\":" + "\"com.warmcat.sai.taskcan\"," + "\"task_uuid\": " + - JSON.stringify(san(e.srcElement.id.substring(5))) + "}"; - console.log(rs); - sai.send(rs); - }); + JSON.stringify(san(e.srcElement.id.substring(5))) + "}"; + console.log(rs); + sai.send(rs); + }); } - + aging(); } break; @@ -1880,17 +2076,17 @@ function ws_open_sai() case "com-warmcat-sai-artifact": console.log(jso); - + sai_arts += "<div class=\"sai_arts\"><img src=\"artifact.svg\">&nbsp;<a href=\"artifacts/" + san(jso.task_uuid) + "/" + san(jso.artifact_down_nonce) + "/" + san(jso.blob_filename) + "\">" + san(jso.blob_filename) + "</a>&nbsp;" + humanize(jso.len) + "B </div>"; - + if (document.getElementById("sai_arts")) document.getElementById("sai_arts").innerHTML = sai_arts; - + break; case "com.warmcat.sai.taskactivity": @@ -1918,14 +2114,14 @@ function ws_open_sai() } catch (e) { break; } - + if (!tfirst) tfirst = jso.timestamp; last_log_timestamp = jso.timestamp; - + li = (s1.match(/\n/g)||[]).length; - + switch (jso.channel) { case 1: logs += s; @@ -1948,13 +2144,13 @@ function ws_open_sai() } - + if (cont && !cont[jso.channel] && jso.len) tn = ((jso.timestamp - tfirst) / 1000000).toFixed(4); if (cont) cont[jso.channel] = (li == 0); - + while (li--) { en += "<a id=\"#sn" + lli + "\" href=\"#sn" + lli + "\">" + @@ -1962,10 +2158,10 @@ function ws_open_sai() tn += "<br>" lli++; } - + lines += en; times += tn; - + if (!redpend) { redpend = 1; setTimeout(function() { @@ -1992,7 +2188,7 @@ function ws_open_sai() rightPane.clientHeight; }, 500); } - + break; } /* switch */ } /* onmessage */ @@ -2010,18 +2206,18 @@ function ws_open_sai() myVar = setTimeout(ws_open_sai, 4000); }; } catch(exception) { - alert("<p>Error" + exception); + alert("<p>Error" + exception); } } function post_login_form() { var xhr = new XMLHttpRequest(), s ="", q = window.location.pathname; - + s = "----boundo\x0d\x0acontent-disposition: form-data; name=\"lname\"\x0d\x0a\x0d\x0a" + document.getElementById("lname").value + "\x0d\x0a----boundo\x0d\x0acontent-disposition: form-data; name=\"lpass\"\x0d\x0a\x0d\x0a" + - document.getElementById("lpass").value + + document.getElementById("lpass").value + "\x0d\x0a----boundo\x0d\x0acontent-disposition: form-data; name=\"success_redir\"\x0d\x0a\x0d\x0a" + document.getElementById("success_redir").value + "\x0d\x0a----boundo--"; @@ -2030,7 +2226,7 @@ function post_login_form() q = q.substring(0, q.length - 10); xhr.open("POST", q + "login", true); xhr.setRequestHeader( 'content-type', "multipart/form-data; boundary=--boundo"); - + console.log(s.length +" " + s); xhr.onload = function (e) { @@ -2048,7 +2244,7 @@ function post_login_form() }; xhr.send(s); - + return false; } @@ -2083,7 +2279,7 @@ window.addEventListener("load", function() { if (document.getElementById("noscript")) document.getElementById("noscript").display = "none"; - + /* login form hidden success redirect */ if (document.getElementById("success_redir")) document.getElementById("success_redir").value = @@ -2095,16 +2291,16 @@ window.addEventListener("load", function() { document.getElementById("login-button").addEventListener("click", post_login_form); document.getElementById("logout-button").addEventListener("click", post_login_form); } - + setInterval(function() { update_task_activities(); var locked = document.body.scrollHeight - - document.body.clientHeight <= document.body.scrollTop + 1; - + document.body.clientHeight <= document.body.scrollTop + 1; + if (locked) document.body.scrollTop = document.body.scrollHeight - - document.body.clientHeight; + document.body.clientHeight; }, 500) diff --git a/etc-sai-EXAMPLE/power/conf b/etc-sai-EXAMPLE/power/conf index 515bea0..5573538 100644 --- a/etc-sai-EXAMPLE/power/conf +++ b/etc-sai-EXAMPLE/power/conf @@ -5,30 +5,19 @@ { "url" : "wss://libwebsockets.org:4444/sai/builder", - "platforms": [ + "power_controllers": [ { - "name": "rocky9/x86_64-amd/gcc", - "power-on": { - "type": "wol", - "mac": "00:e0:4c:68:01:30" - }, - "power-off": { - "type": "suspend" - } + "name": "pc1", + "type": "tasmota", + "url": "http://10.199.0.240" }, { - "name": "rocky9/aarch64-a72a55-rk3588/gcc", - "power-on": { - "type": "tasmota", - "url": "http://10.199.0.240/cm?cmnd=Power%20On" - }, - "power-off": { - "type": "tasmota", - "url": "http://10.199.0.240/cm?cmnd=Power%20Off" - } + "name": "pc2", + "type": "tasmota", + "url": "http://10.199.0.241", + "depends_on": "pc1" } ] } ] } - diff --git a/src/builder/b-conf.c b/src/builder/b-conf.c index a887912..ce0d34a 100644 --- a/src/builder/b-conf.c +++ b/src/builder/b-conf.c @@ -37,6 +37,7 @@ static const char * const paths_global[] = { "metrics_path", "metrics_secret", "sai-power", + "power_controller", "power-on.type", "power-on.url", "power-on.mac", @@ -54,6 +55,7 @@ enum enum_paths_global { LEJPM_METRICS_PATH, LEJPM_METRICS_SECRET, LEJPM_SAI_POWER, + LEJPM_POWER_CONTROLLER, LEJPM_POWER_ON_TYPE, LEJPM_POWER_ON_URL, LEJPM_POWER_ON_MAC, @@ -278,6 +280,10 @@ saib_conf_global_cb(struct lejp_ctx *ctx, char reason) pp = &a->builder->url_sai_power; break; + case LEJPM_POWER_CONTROLLER: + pp = &a->builder->power_controller_name; + break; + case LEJPM_METRICS_URI: pp = &a->builder->metrics_uri; break; diff --git a/src/builder/b-power.c b/src/builder/b-power.c index d82be98..28aa0e2 100644 --- a/src/builder/b-power.c +++ b/src/builder/b-power.c @@ -30,6 +30,97 @@ #include "b-private.h" extern struct lws_spawn_piped *lsp_suspender; +struct lws_ss_handle *ss_power_client = NULL; + +/* + * Registration message logic + */ + +LWS_SS_USER_TYPEDEF + char payload[200]; + size_t size; + size_t pos; + struct lws_buflist *bl_tx; +} saib_power_client_t; + +static lws_ss_state_return_t +saib_power_client_tx(void *userobj, lws_ss_tx_ordinal_t ord, uint8_t *buf, + size_t *len, int *flags) +{ + saib_power_client_t *g = (saib_power_client_t *)userobj; + + return sai_ss_tx_from_buflist_helper(ss_power_client, &g->bl_tx, + buf, len, flags); +} + +static lws_ss_state_return_t +saib_power_client_rx(void *userobj, const uint8_t *buf, size_t len, int flags) +{ + /* We don't really expect RX from sai-power on this link currently + * other than maybe stay info if we merged that logic. + * For now, ignore. + */ + return LWSSSSRET_OK; +} + +static lws_ss_state_return_t +saib_power_client_state(void *userobj, void *sh, lws_ss_constate_t state, + lws_ss_tx_ordinal_t ack) +{ + saib_power_client_t *g = (saib_power_client_t *)userobj; + sai_builder_registration_t r; + struct lwsac *ac = NULL; + + switch (state) { + case LWSSSCS_CONNECTED: + lwsl_notice("%s: Connected to sai-power, sending registration\n", __func__); + + /* Prepare registration message */ + memset(&r, 0, sizeof(r)); + lws_strncpy(r.builder_name, builder.host, sizeof(r.builder_name)); + if (builder.power_controller_name) + lws_strncpy(r.power_controller_name, builder.power_controller_name, sizeof(r.power_controller_name)); + else + lws_strncpy(r.power_controller_name, "unknown", sizeof(r.power_controller_name)); + + /* Add platforms */ + lws_start_foreach_dll(struct lws_dll2 *, d, builder.sai_plat_owner.head) { + sai_plat_t *sp = lws_container_of(d, sai_plat_t, sai_plat_list); + sai_builder_platform_t *bp = lwsac_use_zero(&ac, sizeof(*bp), 512); + + if (bp) { + lws_strncpy(bp->name, sp->name, sizeof(bp->name)); + lws_dll2_add_tail(&bp->list, &r.platforms_owner); + } + } lws_end_foreach_dll(d); + + /* Send it */ + sai_ss_serialize_queue_helper(ss_power_client, &g->bl_tx, + lsm_schema_builder_registration, + LWS_ARRAY_SIZE(lsm_schema_builder_registration), + &r); + + lwsac_free(&ac); + break; + + case LWSSSCS_DISCONNECTED: + lwsl_notice("%s: Disconnected from sai-power\n", __func__); + lws_buflist_destroy_all_segments(&g->bl_tx); + break; + + default: + break; + } + + return LWSSSSRET_OK; +} + +LWS_SS_INFO("sai_power_client", saib_power_client_t) + .rx = saib_power_client_rx, + .tx = saib_power_client_tx, + .state = saib_power_client_state, +}; + int saib_reassess_idle_situation() @@ -392,6 +483,7 @@ saib_power_init(void) * it's willing to auto power-off or not. */ + /* Existing SS creation for power-off logic */ lwsl_notice("%s: creating sai-power ss...\n", __func__); if (lws_ss_create(builder.context, 0, &ssi_saib_power_link_t, @@ -402,6 +494,25 @@ saib_power_init(void) saib_reassess_idle_situation(); + /* + * NEW: Create the persistent WS connection for registration + * We reuse builder.url_sai_power. + * Note: builder.url_sai_power is a URL string. We need to extract the policy endpoint? + * The policy for sai-power client needs to be defined. + * 'sai_power' in policy covers it. + */ + + if (lws_ss_create(builder.context, 0, &ssi_saib_power_client_t, + NULL, &ss_power_client, NULL, NULL)) { + lwsl_err("%s: failed to create sai-power client ss\n", __func__); + } else { + /* Set metadata for URL? The policy handles 'sai_power' endpoint. + * We might need to ensure the policy matches what we expect. + * The existing code assumes 'sai_power' policy exists. + */ + } + + return 0; } diff --git a/src/builder/b-private.h b/src/builder/b-private.h index 216f9c8..63ad022 100644 --- a/src/builder/b-private.h +++ b/src/builder/b-private.h @@ -129,6 +129,7 @@ struct sai_builder { const char *metrics_secret; const char *url_sai_power; + const char *power_controller_name; const char *power_off_type; const char *power_off_url; diff --git a/src/common/include/private.h b/src/common/include/private.h index 344c5cc..9adf49b 100644 --- a/src/common/include/private.h +++ b/src/common/include/private.h @@ -499,6 +499,7 @@ typedef struct sai_plat { const char *name; const char *platform; + const char *pcon; /* PCON name */ struct lejp_ctx ctx; lws_dll2_owner_t nspawn_owner; @@ -609,6 +610,8 @@ typedef struct sai_power_controller { lws_dll2_t list; lws_dll2_owner_t controlled_builders_owner; char name[64]; + char type[32]; + char depends_on[64]; char on; } sai_power_controller_t; @@ -632,6 +635,21 @@ typedef struct sai_stay_state_update { char stay_on; } sai_stay_state_update_t; +/* Builder -> sai-power registration */ + +typedef struct sai_builder_platform { + lws_dll2_t list; + char name[64]; +} sai_builder_platform_t; + +typedef struct sai_builder_registration { + lws_dll2_t list; + lws_dll2_owner_t platforms_owner; /* sai_builder_platform_t */ + char builder_name[64]; + char power_controller_name[64]; +} sai_builder_registration_t; + + /* * Because the definitions of these arrays of map structs are mostly in * common/struct-metadata.c, we are forced to repeat the length of the struct @@ -647,7 +665,7 @@ extern const lws_struct_map_t lsm_power_managed_builder[2], lsm_power_managed_builders_list[2], lsm_schema_power_managed_builders[1], - lsm_power_controller[3], + lsm_power_controller[5], lsm_schema_json_map_task[], lsm_schema_sq3_map_task[], lsm_schema_sq3_map_event[], @@ -681,7 +699,12 @@ extern const lws_struct_map_t lsm_stay_state_update[2], lsm_schema_stay_state_update[1], lsm_build_metric[14], - lsm_plat[13]; + lsm_plat[14], /* +1 for pcon */ + lsm_builder_platform[1], + lsm_builder_registration[3], + lsm_schema_sq3_map_power_controller[1], + lsm_schema_sq3_map_controlled_builder[1], + lsm_schema_builder_registration[1]; extern const lws_ss_info_t ssi_said_logproxy; extern struct lws_ss_handle *ssh[3]; diff --git a/src/common/struct-metadata.c b/src/common/struct-metadata.c index 5f47757..516d39a 100644 --- a/src/common/struct-metadata.c +++ b/src/common/struct-metadata.c @@ -81,6 +81,7 @@ const lws_struct_map_t lsm_plat[] = { LSM_UNSIGNED (sai_plat_t, uid, "uid"), LSM_STRING_PTR (sai_plat_t, name, "name"), LSM_STRING_PTR (sai_plat_t, platform, "platform"), + LSM_STRING_PTR (sai_plat_t, pcon, "pcon"), LSM_JO_SIGNED (sai_plat_t, online, "online"), LSM_UNSIGNED (sai_plat_t, last_seen, "last_seen"), LSM_JO_SIGNED (sai_plat_t, powering_up, "powering_up"), @@ -312,14 +313,29 @@ const lws_struct_map_t lsm_controlled_builder[] = { LSM_CARRAY(sai_controlled_builder_t, name, "name"), }; +/* SQLite specific map for controlled builder: map 'name' to 'builder_name' column */ +const lws_struct_map_t lsm_sq3_controlled_builder[] = { + LSM_CARRAY(sai_controlled_builder_t, name, "builder_name"), +}; + const lws_struct_map_t lsm_power_controller[] = { LSM_CARRAY(sai_power_controller_t, name, "name"), + LSM_CARRAY(sai_power_controller_t, type, "type"), + LSM_CARRAY(sai_power_controller_t, depends_on, "depends_on"), LSM_UNSIGNED(sai_power_controller_t, on, "on"), LSM_LIST(sai_power_controller_t, controlled_builders_owner, sai_controlled_builder_t, list, NULL, lsm_controlled_builder, "controlled_builders"), }; +/* SQLite specific map for power controller: 'on' -> 'state' and no list */ +const lws_struct_map_t lsm_sq3_power_controller[] = { + LSM_CARRAY(sai_power_controller_t, name, "name"), + LSM_CARRAY(sai_power_controller_t, type, "type"), + LSM_CARRAY(sai_power_controller_t, depends_on, "depends_on"), + LSM_UNSIGNED(sai_power_controller_t, on, "state"), +}; + const lws_struct_map_t lsm_power_managed_builder[] = { LSM_CARRAY(sai_power_managed_builder_t, name, "name"), LSM_UNSIGNED(sai_power_managed_builder_t, stay_on, "stay_on"), @@ -340,6 +356,16 @@ const lws_struct_map_t lsm_schema_power_managed_builders[] = { "com.warmcat.sai.power_managed_builders"), }; +const lws_struct_map_t lsm_schema_sq3_map_power_controller[] = { + LSM_SCHEMA_DLL2(sai_power_controller_t, list, NULL, + lsm_sq3_power_controller, "power_controllers"), +}; + +const lws_struct_map_t lsm_schema_sq3_map_controlled_builder[] = { + LSM_SCHEMA_DLL2(sai_controlled_builder_t, list, NULL, + lsm_sq3_controlled_builder, "pcon_builders"), +}; + const lws_struct_map_t lsm_stay_state_update[] = { LSM_CARRAY(sai_stay_state_update_t, builder_name, "builder_name"), LSM_UNSIGNED(sai_stay_state_update_t, stay_on, "stay_on"), @@ -349,3 +375,21 @@ const lws_struct_map_t lsm_schema_stay_state_update[] = { LSM_SCHEMA(sai_stay_state_update_t, NULL, lsm_stay_state_update, "com.warmcat.sai.stay_state_update"), }; + +const lws_struct_map_t lsm_builder_platform[] = { + LSM_CARRAY(sai_builder_platform_t, name, "name"), +}; + +const lws_struct_map_t lsm_builder_registration[] = { + LSM_LIST(sai_builder_registration_t, platforms_owner, + sai_builder_platform_t, list, NULL, + lsm_builder_platform, "platforms"), + LSM_CARRAY(sai_builder_registration_t, builder_name, "builder_name"), + LSM_CARRAY(sai_builder_registration_t, power_controller_name, "power_controller_name"), +}; + +const lws_struct_map_t lsm_schema_builder_registration[] = { + LSM_SCHEMA(sai_builder_registration_t, NULL, + lsm_builder_registration, + "com.warmcat.sai.builder_registration"), +}; diff --git a/src/power/p-conf.c b/src/power/p-conf.c index 11ffd96..0e8da54 100644 --- a/src/power/p-conf.c +++ b/src/power/p-conf.c @@ -33,22 +33,18 @@ static const char * const paths_global[] = { "perms", "wol-if", "servers[].url", - "servers[].power-control[].name", - "servers[].power-control[].type", - "servers[].power-control[].url", - "servers[].power-control[]", - "servers[].platforms[].name", - "servers[].platforms[].host", - "servers[].platforms[].depends", - "servers[].platforms[].power-on.type", - "servers[].platforms[].power-on.mac", - "servers[].platforms[].power-on.url", - "servers[].platforms[].power-on", - "servers[].platforms[].power-off.type", - "servers[].platforms[].power-off.url", - "servers[].platforms[].power-off", - "servers[].platforms[].power-control", - "servers[].platforms[]", + "servers[].power_controllers[].name", + "servers[].power_controllers[].type", + "servers[].power_controllers[].url", + "servers[].power_controllers[].mac", + "servers[].power_controllers[].depends_on", + "servers[].power_controllers[]", + "power_controllers[].name", + "power_controllers[].type", + "power_controllers[].url", + "power_controllers[].mac", + "power_controllers[].depends_on", + "power_controllers[]", "servers[]" }; @@ -56,22 +52,18 @@ enum enum_paths_global { LEJPM_PERMS, LEJPM_WOL_IF, LEJPM_SERVERS_URL, - LEJPM_SERVERS_POWER_CONTROL_NAME, - LEJPM_SERVERS_POWER_CONTROL_TYPE, - LEJPM_SERVERS_POWER_CONTROL_URL, - LEJPM_SERVERS_POWER_CONTROL, - LEJPM_SERVERS_PLATFORMS_NAME, - LEJPM_SERVERS_PLATFORMS_HOST, - LEJPM_SERVERS_PLATFORMS_DEPENDS, - LEJPM_SERVERS_PLATFORMS_POWER_ON_TYPE, - LEJPM_SERVERS_PLATFORMS_POWER_ON_MAC, - LEJPM_SERVERS_PLATFORMS_POWER_ON_URL, - LEJPM_SERVERS_PLATFORMS_POWER_ON, - LEJPM_SERVERS_PLATFORMS_POWER_OFF_TYPE, - LEJPM_SERVERS_PLATFORMS_POWER_OFF_URL, - LEJPM_SERVERS_PLATFORMS_POWER_OFF, - LEJPM_SERVERS_PLATFORMS_POWER_CONTROL, - LEJPM_SERVERS_PLATFORMS, + LEJPM_SRV_PCON_NAME, + LEJPM_SRV_PCON_TYPE, + LEJPM_SRV_PCON_URL, + LEJPM_SRV_PCON_MAC, + LEJPM_SRV_PCON_DEPENDS_ON, + LEJPM_SRV_PCON, + LEJPM_PCON_NAME, + LEJPM_PCON_TYPE, + LEJPM_PCON_URL, + LEJPM_PCON_MAC, + LEJPM_PCON_DEPENDS_ON, + LEJPM_PCON, LEJPM_SERVERS }; @@ -79,18 +71,8 @@ static signed char saip_conf_global_cb(struct lejp_ctx *ctx, char reason) { struct jpargs *a = (struct jpargs *)ctx->user; - saip_pcon_t *pc; const char **pp = NULL; -#if 0 -// int n; - - lwsl_notice("%s: reason: %d, path: %s, match %d\n", __func__, - reason, ctx->path, ctx->path_match); -// for (n = 0; n < ctx->wildcount; n++) -// lwsl_notice(" %d\n", ctx->wild[n]); -#endif - if (reason == LEJPCB_OBJECT_START) { switch (ctx->path_match - 1) { @@ -111,32 +93,21 @@ saip_conf_global_cb(struct lejp_ctx *ctx, char reason) &a->power->sai_server_owner); break; - case LEJPM_SERVERS_POWER_CONTROL: + case LEJPM_PCON: + case LEJPM_SRV_PCON: /* - * Create the saip_pcon_t object and bind to the server + * Create the saip_pcon_t object */ a->sai_pcon = lwsac_use_zero(&a->power->ac_conf_head, sizeof(*a->sai_pcon), 4096); if (!a->sai_pcon) return -1; + /* We will link dependencies later */ lws_dll2_add_tail(&a->sai_pcon->list, &a->power->sai_pcon_owner); break; - case LEJPM_SERVERS_PLATFORMS: - /* - * Create the saip_platform object and bind to the server - */ - a->sai_server_plat = lwsac_use_zero(&a->power->ac_conf_head, - sizeof(*a->sai_server_plat), 4096); - if (!a->sai_server_plat) - return -1; - - lws_dll2_add_tail(&a->sai_server_plat->list, - &a->sai_server->sai_plat_owner); - break; - default: return 0; } @@ -161,63 +132,36 @@ saip_conf_global_cb(struct lejp_ctx *ctx, char reason) pp = &a->power->wol_if; break; - case LEJPM_SERVERS_POWER_CONTROL_NAME: + case LEJPM_PCON_NAME: + case LEJPM_SRV_PCON_NAME: pp = &a->sai_pcon->name; break; - case LEJPM_SERVERS_POWER_CONTROL_TYPE: + case LEJPM_PCON_TYPE: + case LEJPM_SRV_PCON_TYPE: pp = &a->sai_pcon->type; break; - case LEJPM_SERVERS_POWER_CONTROL_URL: + case LEJPM_PCON_URL: + case LEJPM_SRV_PCON_URL: pp = &a->sai_pcon->url; break; - case LEJPM_SERVERS_URL: - pp = &a->sai_server->url; - lwsl_user("%s: server url %.*s\n", __func__, ctx->npos, ctx->buf); + case LEJPM_PCON_MAC: + case LEJPM_SRV_PCON_MAC: + pp = &a->sai_pcon->mac; break; - case LEJPM_SERVERS_PLATFORMS_NAME: - pp = &a->sai_server_plat->name; + case LEJPM_PCON_DEPENDS_ON: + case LEJPM_SRV_PCON_DEPENDS_ON: + pp = &a->sai_pcon->depends_on; break; - case LEJPM_SERVERS_PLATFORMS_HOST: - pp = &a->sai_server_plat->host; - break; - - case LEJPM_SERVERS_PLATFORMS_DEPENDS: - pp = &a->sai_server_plat->depends; - break; - - case LEJPM_SERVERS_PLATFORMS_POWER_ON_TYPE: - pp = &a->sai_server_plat->power_on_type; - break; - - case LEJPM_SERVERS_PLATFORMS_POWER_ON_MAC: - pp = &a->sai_server_plat->power_on_mac; - break; - - case LEJPM_SERVERS_PLATFORMS_POWER_ON_URL: - pp = &a->sai_server_plat->power_on_url; - break; - - case LEJPM_SERVERS_PLATFORMS_POWER_OFF_TYPE: - pp = &a->sai_server_plat->power_off_type; - break; - - case LEJPM_SERVERS_PLATFORMS_POWER_OFF_URL: - pp = &a->sai_server_plat->power_off_url; + case LEJPM_SERVERS_URL: + pp = &a->sai_server->url; + lwsl_user("%s: server url %.*s\n", __func__, ctx->npos, ctx->buf); break; - case LEJPM_SERVERS_PLATFORMS_POWER_CONTROL: - pc = saip_pcon_by_name(a->power, ctx->buf); - if (pc) - lws_dll2_add_tail(&a->sai_server_plat->pcon_list, - &pc->controlled_plats_owner); - - return 0; - default: return 0; } @@ -270,28 +214,26 @@ saip_config_global(struct sai_power *power, const char *d) n = (int)ctx.line; lejp_destruct(&ctx); - /* let's wire up any dependencies so the dependent knows easily */ - - lws_start_foreach_dll(struct lws_dll2 *, px, power->sai_server_owner.head) { - saip_server_t *s = lws_container_of(px, saip_server_t, list); - - lws_start_foreach_dll(struct lws_dll2 *, px1, s->sai_plat_owner.head) { - saip_server_plat_t *sp = lws_container_of(px1, saip_server_plat_t, list); - - if (sp->depends) { - saip_server_plat_t *d = find_platform(power, sp->depends); - - if (!d) { - lwsl_err("%s: can't find dependency '%s' on conf for '%s'\n", __func__, - sp->depends, sp->host); - return 1; - } - - lws_dll2_add_tail(&d->dependencies_list, &sp->dependencies_owner); + /* link up dependencies */ + lws_start_foreach_dll(struct lws_dll2 *, p, power->sai_pcon_owner.head) { + saip_pcon_t *pc = lws_container_of(p, saip_pcon_t, list); + + if (pc->depends_on) { + saip_pcon_t *parent = saip_pcon_by_name(power, pc->depends_on); + if (parent) { + pc->parent = parent; + /* Add to parent's children list if needed? + * The current requirement is just knowing the dependency. + * We might want to maintain a list of children in the parent + * for easier traversal, but let's stick to the struct. + */ + } else { + lwsl_err("%s: PCON %s depends on unknown %s\n", + __func__, pc->name, pc->depends_on); + return 1; } - - } lws_end_foreach_dll(px1); - } lws_end_foreach_dll(px); + } + } lws_end_foreach_dll(p); return 0; } diff --git a/src/power/p-http-api.c b/src/power/p-http-api.c index ddd4ba5..ce294df 100644 --- a/src/power/p-http-api.c +++ b/src/power/p-http-api.c @@ -47,48 +47,74 @@ extern struct sai_power power; static void saip_sul_action_power_off(struct lws_sorted_usec_list *sul) { - saip_server_plat_t *sp = lws_container_of(sul, - saip_server_plat_t, sul_delay_off); + saip_pcon_t *pc = lws_container_of(sul, saip_pcon_t, sul_delay_off); lws_ss_state_return_t r; - saip_pcon_t *pc; - if (!sp->pcon_list.owner) { - lwsl_notice("%s: no power-controller ss for %s\n", __func__, sp->host); - return; + saip_notify_server_power_state(pc->name, 0, 1); + + lwsl_warn("%s: powering OFF pcon %s\n", __func__, pc->name); + + /* If type is WOL, we might suspend? Or nothing for now. */ + if (pc->ss_tasmota_off) { + r = lws_ss_client_connect(pc->ss_tasmota_off); + if (r) + lwsl_ss_err(pc->ss_tasmota_off, "failed to connect tasmota OFF secure stream: %d", r); } +} - pc = lws_container_of(sp->pcon_list.owner, saip_pcon_t, - controlled_plats_owner); +saip_pcon_t * +find_pcon_by_builder_name(struct sai_power *pwr, const char *builder_name) +{ + lws_start_foreach_dll(struct lws_dll2 *, p, pwr->sai_pcon_owner.head) { + saip_pcon_t *pc = lws_container_of(p, saip_pcon_t, list); + + lws_start_foreach_dll(struct lws_dll2 *, b_node, pc->registered_builders_owner.head) { + saip_builder_t *sb = lws_container_of(b_node, saip_builder_t, list); - saip_notify_server_power_state(sp->host, 0, 1); + lwsl_notice("%s: %s %s\n", __func__, sb->name, builder_name); - lwsl_warn("%s: powering OFF host %s via power-control %s\n", __func__, sp->host, pc->name); + if (!strcmp(sb->name, builder_name)) + return pc; - r = lws_ss_client_connect(pc->ss_tasmota_off); - if (r) - lwsl_ss_err(pc->ss_tasmota_off, "failed to connect tasmota OFF secure stream: %d", r); + } lws_end_foreach_dll(b_node); + } lws_end_foreach_dll(p); + + return NULL; } -saip_server_plat_t * -find_platform(struct sai_power *pwr, const char *host) +/* Helper to find PCON by name */ +saip_pcon_t * +find_pcon(struct sai_power *pwr, const char *pcon_name) { - lws_start_foreach_dll(struct lws_dll2 *, px, pwr->sai_server_owner.head) { - saip_server_t *s = lws_container_of(px, saip_server_t, list); - - lws_start_foreach_dll(struct lws_dll2 *, px1, s->sai_plat_owner.head) { - saip_server_plat_t *sp = lws_container_of(px1, saip_server_plat_t, list); + return saip_pcon_by_name(pwr, pcon_name); +} - if (!strcmp(host, sp->host)) - return sp; +void +saip_builder_bringup(saip_server_t *sps, saip_pcon_t *pc) +{ + saip_notify_server_power_state(pc->name, 1, 0); + + if (pc->type && !strcmp(pc->type, "wol")) { + if (pc->mac) { + lwsl_notice("%s: triggering WOL for %s\n", __func__, pc->name); + write(lws_spawn_get_fd_stdxxx(lsp_wol, 0), + pc->mac, strlen(pc->mac)); + } else { + lwsl_err("%s: WOL type but no MAC for %s\n", __func__, pc->name); + } + } - } lws_end_foreach_dll(px1); - } lws_end_foreach_dll(px); + if (pc->ss_tasmota_on) { + lwsl_ss_notice(pc->ss_tasmota_on, "starting tasmota"); + if (lws_ss_client_connect(pc->ss_tasmota_on)) + lwsl_ss_err(pc->ss_tasmota_on, "failed to connect tasmota ON secure stream"); + } - return NULL; + saip_queue_stay_info(sps); } void -saip_notify_server_stay_state(const char *plat_name, int stay_on) +saip_notify_server_stay_state(const char *builder_name, int stay_on) { sai_stay_state_update_t ssu; saip_server_link_t *m; @@ -108,7 +134,7 @@ saip_notify_server_stay_state(const char *plat_name, int stay_on) m = (saip_server_link_t *)lws_ss_to_user_object(sps->ss); memset(&ssu, 0, sizeof(ssu)); - lws_strncpy(ssu.builder_name, plat_name, sizeof(ssu.builder_name)); + lws_strncpy(ssu.builder_name, builder_name, sizeof(ssu.builder_name)); ssu.stay_on = (char)stay_on; sai_ss_serialize_queue_helper(sps->ss, &m->bl_pwr_to_srv, @@ -120,31 +146,30 @@ saip_notify_server_stay_state(const char *plat_name, int stay_on) void saip_set_stay(const char *builder_name, int stay_on) { - saip_server_plat_t *sp = find_platform(&power, builder_name); - saip_server_link_t *pss; + saip_pcon_t *pc = find_pcon_by_builder_name(&power, builder_name); + /* saip_server_link_t *pss; */ /* Unused? */ saip_server_t *sps; - if (!sp) + if (!pc) { + lwsl_warn("%s: Unknown builder %s\n", __func__, builder_name); return; + } sps = lws_container_of(power.sai_server_owner.head, saip_server_t, list); - pss = (saip_server_link_t *)lws_ss_to_user_object(sps->ss); - sp->stay = (char)stay_on; - saip_notify_server_stay_state(builder_name, stay_on | sp->needed); + /* pss = (saip_server_link_t *)lws_ss_to_user_object(sps->ss); */ - if (stay_on | sp->needed) - saip_builder_bringup(sps, sp, pss); - else - /* - * power-off is delayed, so we just set the stay flag... - * but let's cancel any pending power-off - */ - lws_sul_cancel(&sp->sul_delay_off); + pc->manual_stay = (char)stay_on; + saip_notify_server_stay_state(builder_name, stay_on | pc->needed); - /* Find the first (usually only) configured sai-server connection */ - if (!power.sai_server_owner.head) { - lwsl_warn("%s: No sai-server configured to notify\n", __func__); - return; + /* Trigger state re-eval */ + saip_pcon_start_check(); + + if (stay_on | pc->needed) { + /* Ensure it's on immediately if needed */ + saip_builder_bringup(sps, pc); + } else { + /* Cancel pending off */ + lws_sul_cancel(&pc->sul_delay_off); } saip_queue_stay_info(sps); @@ -196,12 +221,12 @@ local_srv_state(void *userobj, void *sh, lws_ss_constate_t state, { local_srv_t *g = (local_srv_t *)userobj; char *path = NULL, pn[128]; - saip_server_plat_t *sp; + saip_pcon_t *pc; saip_server_t *sps; int apo = 0; size_t len; - lwsl_ss_user(lws_ss_from_user(g), "state %s", lws_ss_state_name((int)state)); + // lwsl_ss_user(lws_ss_from_user(g), "state %s", lws_ss_state_name((int)state)); switch ((int)state) { case LWSSSCS_CREATING: @@ -210,7 +235,7 @@ local_srv_state(void *userobj, void *sh, lws_ss_constate_t state, case LWSSSCS_SERVER_TXN: lws_ss_get_metadata(lws_ss_from_user(g), "path", (const void **)&path, &len); - // lwsl_ss_user(lws_ss_from_user(g), "LWSSSCS_SERVER_TXN path '%.*s' (%d)", (int)len, path, (int)len); + lwsl_ss_user(lws_ss_from_user(g), "LWSSSCS_SERVER_TXN path '%.*s' (%d)", (int)len, path, (int)len); /* * path is containing a string like "/power-off/b32" @@ -231,21 +256,17 @@ local_srv_state(void *userobj, void *sh, lws_ss_constate_t state, g->pos = 0; if (len == 1 && path[0] == '/') { - /* print controllable platforms */ + /* print controllable PCONs */ g->size = 0; - lws_start_foreach_dll(struct lws_dll2 *, px, power.sai_server_owner.head) { - saip_server_t *s = lws_container_of(px, saip_server_t, list); + lws_start_foreach_dll(struct lws_dll2 *, px, power.sai_pcon_owner.head) { + saip_pcon_t *pc = lws_container_of(px, saip_pcon_t, list); - lws_start_foreach_dll(struct lws_dll2 *, px1, s->sai_plat_owner.head) { - saip_server_plat_t *sp = lws_container_of(px1, saip_server_plat_t, list); + if (g->size) + g->payload[g->size++] = ','; + g->size = g->size + (size_t)lws_snprintf(g->payload + g->size, sizeof(g->payload) - g->size - 3, "%s", pc->name); - if (g->size) - g->payload[g->size++] = ','; - g->size = g->size + (size_t)lws_snprintf(g->payload + g->size, sizeof(g->payload) - g->size - 3, "%s", sp->host); - - } lws_end_foreach_dll(px1); } lws_end_foreach_dll(px); g->payload[g->size] = '\0'; @@ -255,48 +276,49 @@ local_srv_state(void *userobj, void *sh, lws_ss_constate_t state, if (len > 6 && !strncmp(path, "/stay/", 6)) { lws_strnncpy(pn, &path[6], len - 6, sizeof(pn)); - sp = find_platform(&power, pn); + /* Assuming 'pn' is a builder name for legacy compatibility */ + pc = find_pcon_by_builder_name(&power, pn); - if (sp) + if (pc) g->size = (size_t)lws_snprintf(g->payload, sizeof(g->payload), - "%c", '0' + (sp->stay | sp->needed)); + "%c", '0' + (pc->manual_stay | pc->needed)); else g->size = (size_t)lws_snprintf(g->payload, sizeof(g->payload), - "unknown host %s", pn); + "unknown builder %s", pn); goto bail; } if (len > 10 && !strncmp(path, "/power-on/", 10)) { lws_strnncpy(pn, &path[10], len - 10, sizeof(pn)); - sp = find_platform(&power, pn); - if (!sp) { + /* Assume builder name */ + pc = find_pcon_by_builder_name(&power, pn); + if (!pc) { g->size = (size_t)lws_snprintf(g->payload, sizeof(g->payload), - "Unable to find host %s", pn); + "Unable to find PCON for %s", pn); goto bail; } - if (sp->power_on_mac) { - saip_notify_server_power_state(sp->host, 1, 0); + + saip_notify_server_power_state(pc->name, 1, 0); + + if (pc->mac) { if (write(lws_spawn_get_fd_stdxxx(lsp_wol, 0), - sp->power_on_mac, strlen(sp->power_on_mac)) != - (ssize_t)strlen(sp->power_on_mac)) + pc->mac, strlen(pc->mac)) != + (ssize_t)strlen(pc->mac)) g->size = (size_t)lws_snprintf(g->payload, sizeof(g->payload), "Write to resume %s failed %d", pn, errno); else g->size = (size_t)lws_snprintf(g->payload, sizeof(g->payload), "Resumed %s with stay", pn); - sp->stay = 1; + pc->manual_stay = 1; goto bail; } - if (sp->pcon_list.owner) { - saip_pcon_t *pc = lws_container_of(sp->pcon_list.owner, - saip_pcon_t, - controlled_plats_owner); + if (pc->ss_tasmota_on) { if (lws_ss_client_connect(pc->ss_tasmota_on)) { lwsl_ss_err(pc->ss_tasmota_on, "failed to connect tasmota ON secure stream"); g->size = (size_t)lws_snprintf(g->payload, sizeof(g->payload), - "power-on ss failed create %s", sp->host); + "power-on ss failed create %s", pc->name); goto bail; } } else { @@ -305,10 +327,10 @@ local_srv_state(void *userobj, void *sh, lws_ss_constate_t state, goto bail; } - lwsl_warn("%s: powered on host %s\n", __func__, sp->host); + lwsl_warn("%s: powered on %s\n", __func__, pc->name); - sp->stay = 1; /* so builder can understand it's manual */ - saip_notify_server_power_state(sp->host, 1, 0); + pc->manual_stay = 1; /* so builder can understand it's manual */ + saip_notify_server_power_state(pc->name, 1, 0); sps = lws_container_of(power.sai_server_owner.head, saip_server_t, list); @@ -316,7 +338,7 @@ local_srv_state(void *userobj, void *sh, lws_ss_constate_t state, saip_queue_stay_info(sps); g->size = (size_t)lws_snprintf(g->payload, sizeof(g->payload), - "Manually powered on %s", sp->host); + "Manually powered on %s", pc->name); goto bail; } @@ -343,8 +365,8 @@ power_off: g->size = (size_t)lws_snprintf(g->payload, sizeof(g->payload), "Unable to find host %s", pn); - sp = find_platform(&power, pn); - if (sp) { + pc = find_pcon_by_builder_name(&power, pn); + if (pc) { if (apo) { char needs[128]; @@ -353,8 +375,16 @@ power_off: * Since it's not a manual request, * we should deny it if any deps still need us */ + /* Deps logic needs to check PARENT or DEPENDENTS? + Original logic checked: "if any deps still need us" (us = dependency_list). + Current PCON struct has 'parent' and 'depends_on'. + If *we* are needed by someone else... we don't have a list of dependents easily. + But 'needed' flag should be set if we are needed. + Let's trust 'pc->needed' which we update in rx. + */ needs[0] = '\0'; + /* lws_start_foreach_dll(struct lws_dll2 *, px1, sp->dependencies_owner.head) { saip_server_plat_t *sp1 = lws_container_of(px1, @@ -366,12 +396,13 @@ power_off: "%s ", sp1->name); } lws_end_foreach_dll(px1); + */ - if (needs[0] || sp->needed) { + if (needs[0] || pc->needed) { g->size = (size_t)lws_snprintf(g->payload, sizeof(g->payload), "NAK: %s needed: %d, deps needed: '%s'", - pn, sp->needed, needs); + pn, pc->needed, needs); goto bail; } } @@ -380,20 +411,20 @@ power_off: * OK this is it, schedule it to happen */ lws_sul_schedule(lws_ss_cx_from_user(g), 0, - &sp->sul_delay_off, + &pc->sul_delay_off, saip_sul_action_power_off, SAI_POWERDOWN_HOLDOFF_US); - lwsl_warn("%s: scheduled powering off host %s in %ds\n", - __func__, sp->host, + lwsl_warn("%s: scheduled powering off pcon %s in %ds\n", + __func__, pc->name, (int)(SAI_POWERDOWN_HOLDOFF_US / LWS_USEC_PER_SEC)); g->size = (size_t)lws_snprintf(g->payload, sizeof(g->payload), - "ACK: Scheduled powering off host %s in %ds", - sp->host, + "ACK: Scheduled powering off pcon %s in %ds", + pc->name, (int)(SAI_POWERDOWN_HOLDOFF_US / LWS_USEC_PER_SEC)); - sp->stay = 0; /* reset any manual power up */ + pc->manual_stay = 0; /* reset any manual power up */ } bail: diff --git a/src/power/p-private.h b/src/power/p-private.h index 1d92458..fc94cc5 100644 --- a/src/power/p-private.h +++ b/src/power/p-private.h @@ -59,14 +59,23 @@ typedef struct tasmota_parse { uint8_t s; } tasmota_parse_t; +struct saip_pcon; + typedef struct saip_pcon { struct lws_dll2 list; /* sai_power.sai_pcon_owner */ - lws_dll2_owner_t controlled_plats_owner; /* saip_server_plat_t */ + /* List of builders registered to this PCON (dynamic) */ + lws_dll2_owner_t registered_builders_owner; /* saip_builder_t */ + + lws_sorted_usec_list_t sul_delay_off; + + struct saip_pcon *parent; /* if we depend on another pcon */ + const char *depends_on; /* name from config */ const char *name; const char *type; const char *url; + const char *mac; /* For WOL */ char url_on[128]; char url_off[128]; @@ -77,37 +86,28 @@ typedef struct saip_pcon { struct lws_ss_handle *ss_tasmota_monitor; char on; + char manual_stay; /* user asked to keep this PCON on via UI */ + char needed; /* transiently set by deps analysis */ } saip_pcon_t; -struct saip_ws_pss; - -typedef struct saip_server_plat { - lws_dll2_t list; - lws_dll2_t dependencies_list; - lws_dll2_t pcon_list; /* saip_pcon->controlled_plats_owner */ - - lws_dll2_owner_t dependencies_owner; /* saip_server_plat_t->dependencies_list */ +/* Represents a builder connected to us */ +typedef struct saip_builder { + struct lws_dll2 list; /* in saip_pcon.registered_builders_owner */ - lws_sorted_usec_list_t sul_delay_off; + char name[64]; - const char *name; - const char *host; - const char *depends; /* depended-on plat must stay powered if we need power */ - const char *power_on_type; - const char *power_on_url; - const char *power_on_mac; - const char *power_off_type; - const char *power_off_url; + /* The websocket wsi for this builder connection (if connected) */ + struct lws *wsi; +} saip_builder_t; - char stay; - char needed; -} saip_server_plat_t; +struct saip_ws_pss; typedef struct saip_server { lws_dll2_t list; - lws_dll2_owner_t sai_plat_owner; /* list of platforms we offer */ + /* Removed sai_plat_owner as sai-power no longer manages platforms directly */ + /* lws_dll2_owner_t sai_plat_owner; */ struct lws_ss_handle *ss; @@ -129,6 +129,7 @@ struct sai_power { struct lws_vhost *vhost; lws_sorted_usec_list_t sul_idle; + lws_sorted_usec_list_t sul_pcon_check; /* periodic check for cold start */ const char *power_off; @@ -140,15 +141,11 @@ struct sai_power { const char *port; /* port we listen on */ }; -saip_server_plat_t * -find_platform(struct sai_power *pwr, const char *host); - struct jpargs { struct sai_power *power; saip_server_t *sai_server; - saip_server_plat_t *sai_server_plat; saip_pcon_t *sai_pcon; sai_plat_server_ref_t *mref; @@ -166,7 +163,6 @@ LWS_SS_USER_TYPEDEF } saip_server_link_t; - extern struct sai_power power; extern const lws_ss_info_t ssi_saip_server_link_t, ssi_saip_smartplug_t; extern const struct lws_protocols protocol_com_warmcat_sai; @@ -176,10 +172,10 @@ saip_config_global(struct sai_power *power, const char *d); extern int saip_config(struct sai_power *power, const char *d); extern void saip_config_destroy(struct sai_power *power); extern void -saip_notify_server_power_state(const char *plat_name, int up, int down); +saip_notify_server_power_state(const char *pcon_name, int up, int down); void -saip_set_stay(const char *builder_name, int stay_on); +saip_set_stay(const char *pcon_name, int stay_on); int saip_queue_stay_info(saip_server_t *sps); saip_pcon_t * @@ -187,8 +183,20 @@ saip_pcon_by_name(struct sai_power *power, const char *name); int saip_parse_tasmota_status(tasmota_parse_t *tp); -int -saip_builder_bringup(saip_server_t *sps, saip_server_plat_t *sp, - saip_server_link_t *pss); + void saip_ss_create_tasmota(void); + +void +saip_pcon_start_check(void); + +/* Handler for builder WS connections */ +int +callback_builder(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len); + +saip_pcon_t * +find_pcon_by_builder_name(struct sai_power *pwr, const char *builder_name); + +void +saip_builder_bringup(saip_server_t *sps, saip_pcon_t *pc); diff --git a/src/power/p-sai.c b/src/power/p-sai.c index e7932b9..7e0183c 100644 --- a/src/power/p-sai.c +++ b/src/power/p-sai.c @@ -130,7 +130,8 @@ static const char * const default_ss_policy = "{\"local\": {" "\"server\":" "true," "\"port\":" "3333," - "\"protocol\":" "\"h1\"," + "\"protocol\":" "\"ws\"," /* Changed to ws */ + "\"ws_subprotocol\":" "\"com-warmcat-sai-builder\"," "\"tls\":" "false," "\"metadata\": [" "{\"path\": \"\"}," @@ -179,11 +180,180 @@ callback_std(struct lws *wsi, enum lws_callback_reasons reason, void *user, static const struct lws_protocols protocol_std = { "protocol_std", callback_std, 0, 0 }; +/* + * Builder connection handling + */ + +int +callback_builder(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + struct lejp_ctx ctx; + lws_struct_args_t a; + saip_builder_t *b, **pb = (saip_builder_t **)user; + saip_pcon_t *pc; + + switch (reason) { + case LWS_CALLBACK_ESTABLISHED: + lwsl_user("%s: builder connected\n", __func__); + *pb = NULL; /* Ensure user data is clean */ + break; + + case LWS_CALLBACK_RECEIVE: + // lwsl_hexdump_notice(in, len); + memset(&a, 0, sizeof(a)); + a.map_st[0] = lsm_schema_builder_registration; + a.map_entries_st[0] = LWS_ARRAY_SIZE(lsm_schema_builder_registration); + a.ac_block_size = 2048; + + lws_struct_json_init_parse(&ctx, NULL, &a); + if (lejp_parse(&ctx, (uint8_t *)in, (int)len) < 0 || !a.dest) { + lwsl_warn("%s: JSON decode failed\n", __func__); + lwsac_free(&a.ac); + return -1; + } + + if (a.top_schema_index == 0) { + sai_builder_registration_t *r = (sai_builder_registration_t *)a.dest; + + lwsl_notice("%s: Registered builder '%s' on pcon '%s'\n", + __func__, r->builder_name, r->power_controller_name); + + /* Find the PCON */ + pc = saip_pcon_by_name(&power, r->power_controller_name); + if (pc) { + /* Create/Update builder entry using malloc */ + b = malloc(sizeof(*b)); + if (b) { + memset(b, 0, sizeof(*b)); + lws_strncpy(b->name, r->builder_name, sizeof(b->name)); + b->wsi = wsi; + + /* Store pointer in user data for cleanup */ + *pb = b; + + /* Add to list */ + lws_dll2_add_tail(&b->list, &pc->registered_builders_owner); + } else { + lwsl_err("%s: OOM allocating builder\n", __func__); + } + + /* Trigger a check since we have a new builder (it's alive!) */ + saip_pcon_start_check(); + + /* Send update to sai-server? */ + saip_queue_stay_info(lws_container_of(power.sai_server_owner.head, saip_server_t, list)); + + } else { + lwsl_err("%s: Unknown PCON '%s' for builder '%s'\n", + __func__, r->power_controller_name, r->builder_name); + } + + } + lwsac_free(&a.ac); + break; + + case LWS_CALLBACK_CLOSED: + lwsl_user("%s: builder disconnected\n", __func__); + b = *pb; + if (b) { + /* Remove from list */ + lws_dll2_remove(&b->list); + /* Free memory */ + free(b); + *pb = NULL; + + /* Update state */ + saip_pcon_start_check(); + } + break; + + default: + break; + } + return 0; +} + +static const struct lws_protocols protocol_builder = + { "com-warmcat-sai-builder", callback_builder, sizeof(saip_builder_t *), 0 }; + static const struct lws_protocols *pprotocols[] = { &protocol_std, + &protocol_builder, NULL }; +/* + * Check PCON state logic + * If a PCON has NO registered builders, we default it to ON (Cold Start). + * If a PCON has registered builders, we defer to the "Stay" logic (which comes from sai-server). + * + * However, there is a nuance: If we just started up, we have 0 builders. We should turn everything ON. + * As builders connect, they register. + */ + +void +sul_pcon_check_cb(lws_sorted_usec_list_t *sul) +{ + /* Iterate all PCONs */ + lws_start_foreach_dll(struct lws_dll2 *, p, power.sai_pcon_owner.head) { + saip_pcon_t *pc = lws_container_of(p, saip_pcon_t, list); + int target_on = 0; + + /* Rule 1: No builders registered -> Turn ON (Cold start / Discovery) */ + if (!pc->registered_builders_owner.count) { + target_on = 1; + lwsl_info("%s: PCON %s has 0 builders -> Force ON\n", __func__, pc->name); + } + /* Rule 2: Manual Stay -> Turn ON */ + else if (pc->manual_stay) { + target_on = 1; + lwsl_info("%s: PCON %s has manual stay -> Force ON\n", __func__, pc->name); + } + /* Rule 3: Otherwise, rely on sai-server to tell us via 'stay' messages if it needs to be on. + * Wait... sai-server tells us which *builders* need to stay on. + * We need to map builder stay -> PCON stay. + * But sai-power receives 'stay' messages for builders. + */ + else { + /* Check if any registered builder needs to stay on */ + /* The 'stay' flag on the PCON struct isn't quite right, + we need to check the builders we know about. + Actually, sai-server tells sai-power: "Builder X needs to stay". + sai-power should look up Builder X, find its PCON, and mark the PCON as needed. + */ + /* For now, let's assume if we aren't forcing it on, we let it be managed by the existing logic + * which we need to adapt. + */ + } + + /* + * Existing logic in p-smartplug.c or similar likely handles the actual HTTP switching + * based on a state flag. We need to make sure we set that flag. + * The 'saip_pcon_t' has an 'on' member. + */ + + /* If we decide it should be ON, trigger it */ + if (target_on && !pc->on) { + /* This logic needs to hook into the actual switching code */ + /* For now, just logging intent */ + // pc->on = 1; + // saip_switch(pc, 1); + } + + } lws_end_foreach_dll(p); + + /* Schedule next check */ + lws_sul_schedule(power.context, 0, &power.sul_pcon_check, sul_pcon_check_cb, 5 * LWS_US_PER_SEC); +} + +void +saip_pcon_start_check(void) +{ + /* Trigger immediate check */ + lws_sul_schedule(power.context, 0, &power.sul_pcon_check, sul_pcon_check_cb, 1); +} + static int app_system_state_nf(lws_state_manager_t *mgr, lws_state_notify_link_t *link, int current, int target) @@ -219,7 +389,8 @@ app_system_state_nf(lws_state_manager_t *mgr, lws_state_notify_link_t *link, lwsl_user("%s: OPERATIONAL: server url %p %s\n", __func__, sps, sps->url); - if (lws_ss_create(cx, 0, &ssi_saip_server_link_t, sps, + if (sps->url && + lws_ss_create(cx, 0, &ssi_saip_server_link_t, sps, &sps->ss, NULL, NULL)) { lwsl_err("%s: failed to create secure stream\n", __func__); @@ -228,6 +399,8 @@ app_system_state_nf(lws_state_manager_t *mgr, lws_state_notify_link_t *link, } lws_end_foreach_dll_safe(mp, mp1); + /* Start PCON monitoring */ + saip_pcon_start_check(); break; } diff --git a/src/power/p-ws-server.c b/src/power/p-ws-server.c index 0b8b345..05ed85e 100644 --- a/src/power/p-ws-server.c +++ b/src/power/p-ws-server.c @@ -35,7 +35,7 @@ static const lws_struct_map_t lsm_schema_power_state[] = { }; void -saip_notify_server_power_state(const char *plat_name, int up, int down) +saip_notify_server_power_state(const char *pcon_name, int up, int down) { saip_server_link_t *m; sai_power_state_t ps; @@ -56,7 +56,7 @@ saip_notify_server_power_state(const char *plat_name, int up, int down) memset(&ps, 0, sizeof(ps)); - lws_strncpy(ps.host, plat_name, sizeof(ps.host)); + lws_strncpy(ps.host, pcon_name, sizeof(ps.host)); ps.powering_up = (char)up; ps.powering_down = (char)down; @@ -74,24 +74,13 @@ saip_queue_stay_info(saip_server_t *sps) saip_server_link_t *m; int r; - lwsl_ss_notice(sps->ss, "@@@@@@@@@@@@@@ sai-power CONNECTED to server"); + /* lwsl_ss_notice(sps->ss, "@@@@@@@@@@@@@@ sai-power CONNECTED to server"); */ m = (saip_server_link_t *)lws_ss_to_user_object(sps->ss); memset(&pmb, 0, sizeof(pmb)); - lws_start_foreach_dll(struct lws_dll2 *, p, sps->sai_plat_owner.head) { - saip_server_plat_t *sp = lws_container_of(p, - saip_server_plat_t, list); - sai_power_managed_builder_t *b = lwsac_use_zero(&ac, sizeof(*b), 2048); - - if (b) { - lws_strncpy(b->name, sp->host, sizeof(b->name)); - b->stay_on = sp->stay; - - lws_dll2_add_tail(&b->list, &pmb.builders); - } - } lws_end_foreach_dll(p); + /* Send the PCON configuration and connected builders */ lws_start_foreach_dll(struct lws_dll2 *, p, power.sai_pcon_owner.head) { saip_pcon_t *pc = lws_container_of(p, saip_pcon_t, list); @@ -100,20 +89,23 @@ saip_queue_stay_info(saip_server_t *sps) if (pc1 && pc->name) { lws_strncpy(pc1->name, pc->name, sizeof(pc1->name)); pc1->on = pc->on; + if (pc->depends_on) + lws_strncpy(pc1->depends_on, pc->depends_on, sizeof(pc1->depends_on)); + if (pc->type) + lws_strncpy(pc1->type, pc->type, sizeof(pc1->type)); lws_dll2_add_tail(&pc1->list, &pmb.power_controllers); + /* Attach registered builders */ lws_start_foreach_dll(struct lws_dll2 *, p1, - pc->controlled_plats_owner.head) { - saip_server_plat_t *sp = lws_container_of(p1, - saip_server_plat_t, pcon_list); + pc->registered_builders_owner.head) { + saip_builder_t *sb = lws_container_of(p1, + saip_builder_t, list); sai_controlled_builder_t *c = lwsac_use_zero(&ac, sizeof(*c), 2048); if (c) { - if (sp->host) - lws_strncpy(c->name, sp->host, - sizeof(c->name)); + lws_strncpy(c->name, sb->name, sizeof(c->name)); lws_dll2_add_tail(&c->list, &pc1->controlled_builders_owner); @@ -122,6 +114,11 @@ saip_queue_stay_info(saip_server_t *sps) } } lws_end_foreach_dll(p); + /* The 'builders' list in pmb was for "power managed builders" (legacy). + * We now put builders *inside* power controllers. + * So we leave pmb.builders empty. + */ + r = sai_ss_serialize_queue_helper(sps->ss, &m->bl_pwr_to_srv, lsm_schema_power_managed_builders, LWS_ARRAY_SIZE(lsm_schema_power_managed_builders), @@ -131,44 +128,16 @@ saip_queue_stay_info(saip_server_t *sps) return r; } -int -saip_builder_bringup(saip_server_t *sps, saip_server_plat_t *sp, - saip_server_link_t *pss) -{ - saip_notify_server_power_state(sp->name, 1, 0); - - if (sp->power_on_type && !strcmp(sp->power_on_type, "wol")) { - lwsl_notice("%s: triggering WOL\n", __func__); - write(lws_spawn_get_fd_stdxxx(lsp_wol, 0), - sp->power_on_mac, strlen(sp->power_on_mac)); - } - - if (sp->pcon_list.owner) { - saip_pcon_t *pc = lws_container_of(sp->pcon_list.owner, - saip_pcon_t, - controlled_plats_owner); - - lwsl_ss_notice(pc->ss_tasmota_on, "starting tasmota"); - if (lws_ss_client_connect(pc->ss_tasmota_on)) - lwsl_ss_err(pc->ss_tasmota_on, "failed to connect tasmota ON secure stream"); - } - - return saip_queue_stay_info(sps); -} - static lws_ss_state_return_t saip_m_rx(void *userobj, const uint8_t *buf, size_t len, int flags) { saip_server_link_t *pss = (saip_server_link_t *)userobj; saip_server_t *sps = (saip_server_t *)lws_ss_opaque_from_user(pss); - const char *p = (const char *)buf, *end = (const char *)buf + len; - char plat[128], benched[4096]; - size_t n, bp = 0; lws_struct_args_t a; struct lejp_ctx ctx; lwsl_notice("%s: len %d, flags: %d (saip_server_t %p)\n", __func__, (int)len, flags, (void *)sps); - lwsl_hexdump_notice(buf, len); + /* lwsl_hexdump_notice(buf, len); */ memset(&a, 0, sizeof(a)); a.map_st[0] = lsm_schema_stay; @@ -183,112 +152,44 @@ saip_m_rx(void *userobj, const uint8_t *buf, size_t len, int flags) lwsl_warn("%s: received stay %s: %d\n", __func__, stay->builder_name, stay->stay_on); - saip_set_stay(stay->builder_name, stay->stay_on); - lwsac_free(&a.ac); - return 0; - } - lwsac_free(&a.ac); - - /* starting position is that no server-plat is needed */ - - lws_start_foreach_dll(struct lws_dll2 *, px, sps->sai_plat_owner.head) { - saip_server_plat_t *sp = lws_container_of(px, saip_server_plat_t, list); - sp->needed = 0; - } lws_end_foreach_dll(px); - - fprintf(stderr, "|||||||||||||||||||||||||||||||| Server says needed: '%.*s'\n", (int)len, buf); - - while (p < end) { - n = 0; - while (p < end && *p != ',') - if (n < sizeof(plat) - 1) - plat[n++] = *p++; - - plat[n] = '\0'; - if (p < end && *p == ',') - p++; - /* - * Does this server list this platform as having startable or ongoing - * tasks? + * We received a stay request for a builder. + * We need to find which PCON controls this builder and update its manual_stay. + * But wait, 'sai_stay_t' is typically per-builder. + * We should map this back to the PCON. */ - lws_start_foreach_dll(struct lws_dll2 *, px, sps->sai_plat_owner.head) { - saip_server_plat_t *sp = lws_container_of(px, saip_server_plat_t, list); - - /* - * How about any dependency listed? - */ - - lws_start_foreach_dll(struct lws_dll2 *, px1, sp->dependencies_owner.head) { - saip_server_plat_t *sp1 = lws_container_of(px1, saip_server_plat_t, dependencies_list); - - lwsl_notice("%s: setting %s as needed dep\n", __func__, sp1->name); - sp1->needed = 2; - saip_set_stay(sp1->name, sp1->stay); - - } lws_end_foreach_dll(px1); - - /* - * Directly listed as needed? - */ - - if (!strcmp(sp->name, plat)) - sp->needed |= 1; + lws_start_foreach_dll(struct lws_dll2 *, p, power.sai_pcon_owner.head) { + saip_pcon_t *pc = lws_container_of(p, saip_pcon_t, list); + lws_start_foreach_dll(struct lws_dll2 *, b_node, pc->registered_builders_owner.head) { + saip_builder_t *sb = lws_container_of(b_node, saip_builder_t, list); + if (!strcmp(sb->name, stay->builder_name)) { + lwsl_notice("%s: Mapping stay for builder '%s' to PCON '%s'\n", + __func__, sb->name, pc->name); + /* Update PCON stay state */ + pc->manual_stay = stay->stay_on; + saip_pcon_start_check(); + goto found; + } + } lws_end_foreach_dll(b_node); + } lws_end_foreach_dll(p); - } lws_end_foreach_dll(px); +found: + lwsac_free(&a.ac); + return 0; } + lwsac_free(&a.ac); /* - * Cascade dependencies up the platforms - */ - - lws_start_foreach_dll(struct lws_dll2 *, px, sps->sai_plat_owner.head) { - saip_server_plat_t *sp = lws_container_of(px, saip_server_plat_t, list); - - if (sp->needed) { - lws_start_foreach_dll(struct lws_dll2 *, py, sp->dependencies_owner.head) { - saip_server_plat_t *spd = lws_container_of(py, - saip_server_plat_t, dependencies_list); - - spd->needed |= 2; - - } lws_end_foreach_dll(py); - } - } lws_end_foreach_dll(px); - - /* - * Bringup any directly needed or needed by dependency builders + * The old logic parsed comma-separated platform names to determine needed state. + * We are moving away from that. Sai-server should explicitly request power state + * or we should rely on the builder being "needed" implies PCON on. + * Actually, the requirement said: "You can no longer ask that a platform stays on, instead, you ask that the power controller stays on" + * So sai-server might send stay requests for PCONs directly if we update the UI. + * But for now, sai-server logic (s-power.c) sends stay for *builders*. + * So the mapping logic above is correct for transition. */ - lws_start_foreach_dll(struct lws_dll2 *, px, sps->sai_plat_owner.head) { - saip_server_plat_t *sp = lws_container_of(px, saip_server_plat_t, list); - - if (sp->needed) { - lwsl_notice("%s: Needed builders: %s\n", __func__, sp->name); - - /* - * Server said this platform or at least one dependency - * has pending jobs. sai-power config says this builder - * can do jobs on that platform. Let's make sure it - * is powered on. - */ - - saip_builder_bringup(sps, sp, pss); - - } else { - bp += (size_t)lws_snprintf(&benched[bp], sizeof(benched) - bp - 1, - "%s%s", !bp ? "" : ", ", sp->name); - benched[sizeof(benched) - 1] = '\0'; - } - - } lws_end_foreach_dll(px); - - if (bp) - lwsl_notice("%s: Benched builders: %s\n", __func__, benched); - - (void)sps; - return 0; } diff --git a/src/server/s-central.c b/src/server/s-central.c index 15a499e..12a1846 100644 --- a/src/server/s-central.c +++ b/src/server/s-central.c @@ -147,6 +147,40 @@ sais_central_clean_abandoned(struct vhd *vhd) lwsac_free(&ac); } +/* Check if we need to create new tables or add columns */ +static void +sais_ensure_tables(struct vhd *vhd) +{ + char *err = NULL; + + /* We already added power_controllers and pcon_builders, + * but we need to make sure 'builders' table has 'pcon' column. + */ + + /* Try adding the column, ignore error if it exists */ + sqlite3_exec(vhd->server.pdb, "ALTER TABLE builders ADD COLUMN pcon varchar(64);", NULL, NULL, &err); + if (err) { + /* lwsl_notice("%s: (stateless) %s\n", __func__, err); */ + sqlite3_free(err); + } + + sai_sqlite3_statement(vhd->server.pdb, + "CREATE TABLE IF NOT EXISTS power_controllers (" + " name varchar(64) primary key," + " type varchar(32)," + " url varchar(128)," + " depends_on varchar(64)," + " state integer" + ");", "create pcon table"); + + sai_sqlite3_statement(vhd->server.pdb, + "CREATE TABLE IF NOT EXISTS pcon_builders (" + " pcon_name varchar(64)," + " builder_name varchar(64)" + ");", "create pcon_builders table"); + +} + void sais_central_cb(lws_sorted_usec_list_t *sul) { @@ -162,6 +196,10 @@ sais_central_cb(lws_sorted_usec_list_t *sul) sais_central_clean_abandoned(vhd); + /* Also ensure tables exist periodically/on startup */ + if (!vhd->last_check_abandoned_tasks) + sais_ensure_tables(vhd); + vhd->last_check_abandoned_tasks = lws_now_usecs(); } diff --git a/src/server/s-helpers.c b/src/server/s-helpers.c index ee76cfc..0cb85d7 100644 --- a/src/server/s-helpers.c +++ b/src/server/s-helpers.c @@ -111,6 +111,29 @@ sai_destroy_resource_wellknown(struct lws_dll2 *d, void *user) return 0; } +int +sai_pcon_destroy_cb(struct lws_dll2 *d, void *user) +{ + sai_power_controller_t *pc = lws_container_of(d, sai_power_controller_t, list); + + lws_dll2_remove(d); + + /* Also need to clear the child lists */ + lws_dll2_foreach_safe(&pc->controlled_builders_owner, NULL, sai_detach_builder); + + /* We don't free pc because it's usually in an lwsac, but if it's from db it might be malloced. + * In s-power.c usage, it's lwsac. + * Wait, s-sai.c uses 'server->pdb' which is sqlite. + * The in-memory structure 'sai_power_managed_builders_t' is what we are likely destroying here if we are cleaning up ephemeral state. + * + * However, 'server' struct in 's-private.h' doesn't seem to have a list of live PCONs. + * We should check s-private.h to see if we added one. + * If not, we might be relying on the database + transient messages. + */ + + return 0; +} + void sais_server_destroy(struct vhd *vhd, sais_t *server) { @@ -126,4 +149,3 @@ sais_server_destroy(struct vhd *vhd, sais_t *server) lws_dll2_foreach_safe(&server->resource_wellknown_owner, NULL, sai_destroy_resource_wellknown); } - diff --git a/src/server/s-power.c b/src/server/s-power.c index d61d4dd..ee7cbf0 100644 --- a/src/server/s-power.c +++ b/src/server/s-power.c @@ -87,43 +87,55 @@ sais_power_rx(struct vhd *vhd, struct pss *pss, uint8_t *buf, case 1: { sai_power_managed_builders_t *pmb = (sai_power_managed_builders_t *)a.dest; - uint64_t bf_set = 0; - - lws_start_foreach_dll(struct lws_dll2 *, p, pmb->builders.head) { - sai_power_managed_builder_t *b = lws_container_of(p, - sai_power_managed_builder_t, list); - char q[256]; - int shi = 0; - - lwsl_notice("%s: Marking builder %s as power-managed\n", - __func__, b->name); - lws_snprintf(q, sizeof(q), - "UPDATE builders SET power_managed=1 WHERE name = '%s' OR name LIKE '%s.%%'", - b->name, b->name); + char q[256]; - if (sai_sqlite3_statement(vhd->server.pdb, q, "set power_managed")) - lwsl_err("%s: Failed to mark builder %s as power-managed\n", - __func__, b->name); + /* + * We received the PCON topology and registered builders from sai-power. + * + * 1. Store PCONs in DB. + * 2. Update builders' PCON association (if we have a table or column for it). + */ - lws_start_foreach_dll(struct lws_dll2 *, p2, - vhd->server.builder_owner.head) { - - sai_plat_t *sp = lws_container_of(p2, sai_plat_t, sai_plat_list); - const char *dot = strchr(sp->name, '.'); + sai_sqlite3_statement(vhd->server.pdb, "BEGIN TRANSACTION", "txn begin"); - // lwsl_notice("%s: builder entry: %s\n", __func__, cb->name); + /* Clear old PCONs? Or Upsert? + * We probably want to replace since this is authoritative from sai-power. + * But maybe just clear everything first? + */ + sai_sqlite3_statement(vhd->server.pdb, "DELETE FROM power_controllers", "delete pcons"); + sai_sqlite3_statement(vhd->server.pdb, "DELETE FROM pcon_builders", "delete pcon_builders"); - if (dot && !(bf_set & (1 << shi)) && strlen(b->name) <= (size_t)(dot - sp->name) && - !strncmp(sp->name + (dot - sp->name) - strlen(b->name), b->name, strlen(b->name))) { - lwsl_notice("%s: ++++++++++++ Setting %s .stay_on=%d\n", __func__, sp->name, b->stay_on); - sp->stay_on = b->stay_on; - bf_set |= (1 << shi); - } - shi++; - } lws_end_foreach_dll(p2); + lws_start_foreach_dll(struct lws_dll2 *, p, pmb->power_controllers.head) { + sai_power_controller_t *pc = lws_container_of(p, sai_power_controller_t, list); + + /* Insert PCON */ + lws_snprintf(q, sizeof(q), + "INSERT INTO power_controllers (name, type, url, depends_on, state) VALUES ('%s', '%s', '', '%s', %d)", + pc->name, pc->type, pc->depends_on, pc->on); + sai_sqlite3_statement(vhd->server.pdb, q, "insert pcon"); + + /* Insert Controlled Builders */ + lws_start_foreach_dll(struct lws_dll2 *, pb, pc->controlled_builders_owner.head) { + sai_controlled_builder_t *cb = lws_container_of(pb, sai_controlled_builder_t, list); + lws_snprintf(q, sizeof(q), + "INSERT INTO pcon_builders (pcon_name, builder_name) VALUES ('%s', '%s')", + pc->name, cb->name); + lwsl_notice("%s: Inserting pcon_builder: pcon='%s', builder='%s'\n", __func__, pc->name, cb->name); + sai_sqlite3_statement(vhd->server.pdb, q, "insert pcon_builder"); + + /* Update builders table with pcon name */ + /* Note: builders table key is 'name'. */ + lws_snprintf(q, sizeof(q), + "UPDATE builders SET pcon = '%s' WHERE name = '%s' OR name LIKE '%s.%%'", + pc->name, cb->name, cb->name); + sai_sqlite3_statement(vhd->server.pdb, q, "update builder pcon"); + + } lws_end_foreach_dll(pb); } lws_end_foreach_dll(p); + sai_sqlite3_statement(vhd->server.pdb, "COMMIT", "txn commit"); + sais_list_builders(vhd); break; diff --git a/src/server/s-private.h b/src/server/s-private.h index 757e939..779b12e 100644 --- a/src/server/s-private.h +++ b/src/server/s-private.h @@ -55,6 +55,9 @@ typedef struct sai_platm { /* the list of well-known, configured resources */ lws_dll2_owner_t resource_wellknown_owner; /* sai_resource_wellknown_t */ + /* Live PCON topology from sai-power */ + lws_dll2_owner_t power_controllers; /* sai_power_controller_t */ + sqlite3 *pdb; sqlite3 *pdb_auth; } sais_t; @@ -381,6 +384,9 @@ sai_detach_resource(struct lws_dll2 *d, void *user); int sai_destroy_resource_wellknown(struct lws_dll2 *d, void *user); +int +sai_pcon_destroy_cb(struct lws_dll2 *d, void *user); + void sais_server_destroy(struct vhd *vhd, sais_t *server); diff --git a/src/server/s-ws-builder.c b/src/server/s-ws-builder.c index 2feed99..de48d52 100644 --- a/src/server/s-ws-builder.c +++ b/src/server/s-ws-builder.c @@ -783,6 +783,28 @@ sais_ws_json_rx_builder(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t b __func__, build->name); /* + * Step 1.5: Synchronize PCON binding from pcon_builders table if available. + * This handles the case where sai-power registered the PCON relationship + * before the builder connected. + */ + { + char host[128]; + const char *dot = strchr(build->name, '.'); + + if (dot) + lws_strnncpy(host, build->name, dot - build->name, sizeof(host)); + else + lws_strncpy(host, build->name, sizeof(host)); + + lws_snprintf(q, sizeof(q), + "UPDATE builders SET pcon = (SELECT pcon_name FROM pcon_builders WHERE builder_name = '%s') " + "WHERE name = '%s' OR name LIKE '%s.%%'", + host, build->name, build->name); + lwsl_notice("%s: Syncing pcon for host '%s' (plat '%s'): %s\n", __func__, host, build->name, q); + sai_sqlite3_statement(vhd->server.pdb, q, "sync builder pcon"); + } + + /* * Step 2: Update the long-lived, malloc'd in-memory list. */ diff --git a/src/server/s-ws-web.c b/src/server/s-ws-web.c index 0ccc779..267eb28 100644 --- a/src/server/s-ws-web.c +++ b/src/server/s-ws-web.c @@ -155,11 +155,93 @@ reject: return 1; } +int +sais_list_pcons(struct vhd *vhd) +{ + char json_pcons[LWS_PRE + 8192], *start = json_pcons + LWS_PRE, + *p = start, *end = p + sizeof(json_pcons) - LWS_PRE; + unsigned int ss_flags = LWSSS_FLAG_SOM | LWSSS_FLAG_EOM; + lws_struct_serialize_t *js; + struct lwsac *ac = NULL; + lws_wsmsg_info_t info; + size_t w; + lws_struct_json_serialize_result_t r; + + sai_power_managed_builders_t pmb; + sai_power_controller_t *pc; + + memset(&pmb, 0, sizeof(pmb)); + + /* Query PCONs from DB using schema map */ + if (lws_struct_sq3_deserialize(vhd->server.pdb, NULL, "name ", + lsm_schema_sq3_map_power_controller, + &pmb.power_controllers, &ac, 0, 100)) { + /* It's okay if empty */ + } + + /* Iterate PCONs and populate controlled builders */ + lws_start_foreach_dll(struct lws_dll2 *, d, pmb.power_controllers.head) { + pc = lws_container_of(d, sai_power_controller_t, list); + char filter[128]; + + /* Map 'builder_name' column to 'name' field in struct */ + lws_snprintf(filter, sizeof(filter), "and pcon_name = '%s'", pc->name); + lws_struct_sq3_deserialize(vhd->server.pdb, filter, "builder_name ", + lsm_schema_sq3_map_controlled_builder, + &pc->controlled_builders_owner, &ac, 0, 100); + + } lws_end_foreach_dll(d); + + /* Serialize */ + js = lws_struct_json_serialize_create(lsm_schema_power_managed_builders, + LWS_ARRAY_SIZE(lsm_schema_power_managed_builders), + 0, &pmb); + if (!js) + goto bail; + + do { + r = lws_struct_json_serialize(js, (uint8_t *)p, + lws_ptr_diff_size_t(end, p) - 2, &w); + p += w; + + switch (r) { + case LSJS_RESULT_FINISH: + /* fallthru */ + case LSJS_RESULT_CONTINUE: + memset(&info, 0, sizeof(info)); + + info.private_source_idx = SAI_WEBSRV_PB__GENERATED; + info.buf = (uint8_t *)start; + info.len = lws_ptr_diff_size_t(p, start); + info.ss_flags = ss_flags; + + if (sais_websrv_broadcast_REQUIRES_LWS_PRE(vhd->h_ss_websrv, &info) < 0) + lwsl_warn("%s: unable to broadcast pcons to web\n", __func__); + + p = start; + ss_flags &= ~((unsigned int)LWSSS_FLAG_SOM); + break; + + case LSJS_RESULT_ERROR: + lws_struct_json_serialize_destroy(&js); + goto bail; + } + + } while (r == LSJS_RESULT_CONTINUE); + + lws_struct_json_serialize_destroy(&js); + lwsac_free(&ac); + return 0; + +bail: + lwsac_free(&ac); + return 1; +} int sais_list_builders(struct vhd *vhd) { - char json_builders[LWS_PRE + 1024], *start = json_builders + LWS_PRE, + char json_builders[LWS_PRE + 8192], *start = json_builders + LWS_PRE, *p = start, *end = p + sizeof(json_builders) - LWS_PRE, subsequent = 0; unsigned int ss_flags = LWSSS_FLAG_SOM; @@ -170,8 +252,12 @@ sais_list_builders(struct vhd *vhd) sai_plat_t *sp; size_t w; + /* Send PCONs first */ + sais_list_pcons(vhd); + memset(&db_builders_owner, 0, sizeof(db_builders_owner)); + /* Query builders table, which now includes 'pcon' column */ if (lws_struct_sq3_deserialize(vhd->server.pdb, NULL, "name ", lsm_schema_sq3_map_plat, &db_builders_owner, &ac, 0, 100)) { @@ -189,6 +275,8 @@ sais_list_builders(struct vhd *vhd) sai_plat_t *live_builder; sp = lws_container_of(walk, sai_plat_t, sai_plat_list); + lwsl_notice("%s: listing builder '%s', pcon='%s'\n", __func__, sp->name, sp->pcon ? sp->pcon : "(null)"); + live_builder = sais_builder_from_uuid(vhd, sp->name); if (live_builder) { @@ -227,6 +315,7 @@ sais_list_builders(struct vhd *vhd) } } lws_end_foreach_dll(p); + /* Use schema including pcon if available */ js = lws_struct_json_serialize_create(lsm_schema_map_plat_simple, LWS_ARRAY_SIZE(lsm_schema_map_plat_simple), 0, sp); @@ -486,7 +575,7 @@ websrvss_ws_rx(void *userobj, const uint8_t *buf, size_t len, int flags) m->vhd->sai_powers.head) { struct pss *pss_power = lws_container_of(p, struct pss, same); sai_stay_t *s; - + s = malloc(sizeof(*s)); if (s) { *s = *stay; @@ -533,7 +622,7 @@ websrvss_srv_state(void *userobj, void *sh, lws_ss_constate_t state, case LWSSSCS_DISCONNECTED: { unsigned int total_viewers = 0; - lws_buflist_destroy_all_segments(&m->bl_srv_to_web); + lws_buflist_destroy_all_segments(&m->bl_srv_to_web); lws_wsmsg_destroy(m->private_heads, LWS_ARRAY_SIZE(m->private_heads)); m->viewers = 0; diff --git a/src/web/w-ws-server.c b/src/web/w-ws-server.c index 6047109..baaeaa7 100644 --- a/src/web/w-ws-server.c +++ b/src/web/w-ws-server.c @@ -52,6 +52,9 @@ const lws_struct_map_t lsm_schema_json_map[] = { "com.warmcat.sai.taskactivity"), LSM_SCHEMA (sai_build_metric_t, NULL, lsm_build_metric, "com.warmcat.sai.build-metric"), + LSM_SCHEMA(sai_power_managed_builders_t, NULL, + lsm_power_managed_builders_list, + "com.warmcat.sai.power_managed_builders"), }; enum { @@ -63,6 +66,7 @@ enum { SAIS_WS_WEBSRV_RX_LOADREPORT, /* builder's cpu load report */ SAIS_WS_WEBSRV_RX_TASKACTIVITY, SAIS_WS_WEBSRV_RX_BUILD_METRIC, + SAIS_WS_WEBSRV_RX_POWER_MANAGED_BUILDERS, }; /* @@ -115,16 +119,9 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags) */ switch (m->a.top_schema_index) { case SAIS_WS_WEBSRV_RX_LOADREPORT: - saiw_ws_broadcast_browsers_REQUIRES_LWS_PRE(vhd, buf, len, - lws_write_ws_flags(LWS_WRITE_TEXT, - flags & LWSSS_FLAG_SOM, 0)); - break; case SAIS_WS_WEBSRV_RX_TASKACTIVITY: - saiw_ws_broadcast_browsers_REQUIRES_LWS_PRE(vhd, buf, len, - lws_write_ws_flags(LWS_WRITE_TEXT, - flags & LWSSS_FLAG_SOM, 0)); - break; case SAIS_WS_WEBSRV_RX_SAI_BUILDERS: + case SAIS_WS_WEBSRV_RX_POWER_MANAGED_BUILDERS: saiw_ws_broadcast_browsers_REQUIRES_LWS_PRE(vhd, buf, len, lws_write_ws_flags(LWS_WRITE_TEXT, flags & LWSSS_FLAG_SOM, @@ -141,6 +138,7 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags) case SAIS_WS_WEBSRV_RX_TASKCHANGE: case SAIS_WS_WEBSRV_RX_EVENTCHANGE: case SAIS_WS_WEBSRV_RX_SAI_BUILDERS: + case SAIS_WS_WEBSRV_RX_POWER_MANAGED_BUILDERS: saiw_ws_broadcast_browsers_REQUIRES_LWS_PRE(vhd, buf, len, lws_write_ws_flags(LWS_WRITE_TEXT, flags & LWSSS_FLAG_SOM, @@ -224,6 +222,51 @@ saiw_lp_rx(void *userobj, const uint8_t *buf, size_t len, int flags) saiw_ws_broadcast_browsers_REQUIRES_LWS_PRE(vhd, buf, len - (unsigned int)n, lws_write_ws_flags(LWS_WRITE_TEXT, flags & LWSSS_FLAG_SOM, flags & LWSSS_FLAG_EOM)); break; + case SAIS_WS_WEBSRV_RX_POWER_MANAGED_BUILDERS: + /* Just forward to browser */ + /* We already forwarded fragments in the LEJP_CONTINUE block? + * Wait, in the LEJP_CONTINUE block above, we handle partial forwarding. + * But if it arrives in one chunk (n >= 0 immediately), we must forward it here. + * The existing logic for other types does this. + * Wait, the LOADREPORT/TASKACTIVITY/BUILDERS cases below just call broadcast. + * But they use 'len - n' which assumes n is the length processed? + * Lejp returns LEJP_CONTINUE or a positive integer representing the number of bytes used? + * No, lejp_parse returns LEJP_CONTINUE or 0 (success) or error code. + * + * Wait, lejp_parse returns a negative error code on failure. + * On success, it returns 0. + * + * The code `len - (unsigned int)n` suggests n is bytes consumed? + * Ah, older versions of lejp/lws might behave differently. + * But if n == 0 (success), `len - 0` = len. + * So it broadcasts the whole buffer. + * + * The `else` block of `if (n == LEJP_CONTINUE)` handles the `flags & EOM` case for fragments? + * No, `if (n == LEJP_CONTINUE)` handles forwarding intermediate fragments. + * The `else` block handles the FINAL fragment (where n == 0). + * But wait, the `else` block logic is: + * + * ```c + * } else { + * switch (m->a.top_schema_index) { + * case SAIS_WS_WEBSRV_RX_TASKCHANGE: ... broadcast ... + * } + * } + * ``` + * + * It seems correct. I added `SAIS_WS_WEBSRV_RX_POWER_MANAGED_BUILDERS` to the `else` block switch as well. + * But I should check if I need to add a case in the final switch (where parsing is complete). + * + * In the final switch (m->a.top_schema_index), `SAIS_WS_WEBSRV_RX_LOADREPORT` etc are handled. + * I should add `SAIS_WS_WEBSRV_RX_POWER_MANAGED_BUILDERS` there too if I want to ensure the final chunk is sent if it wasn't handled by the `else` block? + * + * Actually, look at `SAIS_WS_WEBSRV_RX_SAI_BUILDERS` in the final switch. It does complex logic (updating vhd state). + * `com.warmcat.sai.power_managed_builders` is just passthrough to the browser, sai-web doesn't need to statefully track PCONs (browsers do). + * So treating it like LOADREPORT (passthrough) is correct. + */ + saiw_ws_broadcast_browsers_REQUIRES_LWS_PRE(vhd, buf, len - (unsigned int)n, + lws_write_ws_flags(LWS_WRITE_TEXT, flags & LWSSS_FLAG_SOM, flags & LWSSS_FLAG_EOM)); + break; } cleanup_parse_allocs:
Page fetched 0s ago, creation time: 67ms (vhost etag hits: 0%, cache hits: 0%)