diff --git a/doc/external-editor-json-rpc.md b/doc/external-editor-json-rpc.md index a3404ce1ebe..6fd5d20eead 100644 --- a/doc/external-editor-json-rpc.md +++ b/doc/external-editor-json-rpc.md @@ -1,5 +1,9 @@ # Viewer to External Editor JSON-RPC
Message Interfaces Documentation +> **This file is a mirror.** The canonical source is `doc/Message_Interfaces.md` in the +> object_publish extension repository. Do not edit this copy — edit the canonical file and +> re-copy it here. + This document describes all the message interfaces defined for WebSocket communication between the Second Life viewer and an external editor such as a VSCode extension. ## Table of Contents @@ -7,6 +11,7 @@ This document describes all the message interfaces defined for WebSocket communi - [Usage Flow](#usage-flow) - [VS Code Launch URI](#vs-code-launch-uri) - [JSON-RPC Method Summary](#json-rpc-method-summary) +- [Error Handling](#error-handling) - [Session Management Interfaces](#session-management-interfaces) - [SessionHandshake](#sessionhandshake) - [SessionHandshakeResponse](#sessionhandshakeresponse) @@ -25,7 +30,7 @@ This document describes all the message interfaces defined for WebSocket communi - [ScriptUnsubscribe](#scriptunsubscribe) - [ScriptList](#scriptlist) - [Compilation Interfaces](#compilation-interfaces) - - [CompilationError](#compilationerror) + - [Diagnostic](#diagnostic) - [CompilationResult](#compilationresult) - [Runtime Event Interfaces](#runtime-event-interfaces) - [RuntimeDebug](#runtimedebug) @@ -33,8 +38,9 @@ This document describes all the message interfaces defined for WebSocket communi - [Handler and Configuration Interfaces](#handler-and-configuration-interfaces) - [WebSocketHandlers](#websockethandlers) - [ClientInfo](#clientinfo) -- [Object Content Interfaces](#object-content-interfaces) +- [Object Explorer Interfaces](#object-explorer-interfaces) - [Core Data Types](#core-data-types) + - [Common preconditions](#common-preconditions) - [ObjectPublish](#objectpublish) - [ObjectUnpublish](#objectunpublish) - [ObjectUpdate](#objectupdate) @@ -43,9 +49,14 @@ This document describes all the message interfaces defined for WebSocket communi - [ObjectItemCreate](#objectitemcreate) - [ObjectItemDelete](#objectitemdelete) - [ObjectScriptSetRunning](#objectscriptsetrunning) + - [ObjectScriptReset](#objectscriptreset) - [ObjectRequest](#objectrequest) + - [ObjectList](#objectlist) - [ObjectModify](#objectmodify) - [ObjectItemModify](#objectitemmodify) +- [Command Interfaces](#command-interfaces) + - [CommandExecute](#commandexecute) + - [CommandList](#commandlist) ## Usage Flow @@ -68,10 +79,10 @@ This document describes all the message interfaces defined for WebSocket communi - When subscription needs to be terminated, viewer sends `script.unsubscribe` notification with `ScriptUnsubscribe` data - Extension handles unsubscription by cleaning up local script tracking -4. **Object Content Publishing:** +4. **Object Explorer:** - - Viewer sends `object.publish` notification when an in-world object's contents are made available for editing - - Viewer sends `object.unpublish` notification when an object is removed or the owner stops publishing + - Viewer sends `object.publish` notification when an in-world object's contents are made available for editing (user clicks "Explore in IDE") + - Viewer sends `object.unpublish` notification when an object is removed or the user stops exploring - Viewer sends `object.update` notification when object inventory changes (full replacement or delta) - Extension calls `object.content.get` to fetch an item's content on demand - Extension calls `object.content.save` to write modified content back to the viewer @@ -103,8 +114,8 @@ vscode://lindenlab.sl-vscode-plugin/connect[?port=][&object=][&scrip | Parameter | Required | Description | | --------- | -------- | ----------- | -| `port` | No | Port number the viewer's WebSocket server is listening on. Overrides the user's configured port for this session. Defaults to the configured `slVscodeEdit.network.websocketPort` (default `9020`) if absent. Must be in range 1024-65535. | -| `object` | No | UUID of a root prim. After the handshake completes the extension calls `object.request` to ask the viewer to publish this object. The viewer then sends an `object.publish` notification and the object appears as a workspace folder in the Explorer. | +| `port` | No | Port number the viewer's WebSocket server is listening on. Overrides the user's configured port for this session. Defaults to the configured `slVscodeEdit.network.websocketPort` (default `9020`) if absent. Must be in range 1024–65535. | +| `object` | No | UUID of a root prim. After the handshake completes the extension calls `object.request` to ask the viewer to start exploring this object. The viewer then sends an `object.publish` notification and the object appears as a workspace folder in the Explorer. | | `script` | No | UUID of a script. After the handshake completes the extension locates the corresponding temp file via `script.list` and opens it, triggering the normal `script.subscribe` + live-sync flow. | `object` and `script` are mutually exclusive in typical use but both may be supplied; the extension will process both. @@ -118,7 +129,7 @@ vscode://lindenlab.sl-vscode-plugin/connect # Connect on a custom port vscode://lindenlab.sl-vscode-plugin/connect?port=9021 -# Connect and immediately publish a specific object +# Connect and immediately explore a specific object vscode://lindenlab.sl-vscode-plugin/connect?port=9020&object=550e8400-e29b-41d4-a716-446655440000 # Connect and open a specific script for editing @@ -131,18 +142,18 @@ When the URI contains an `object` or `script` parameter the extension acts only ``` URI received by extension - | - v -WebSocket connects -> session.handshake -> session.ok - | - |- object= -> object.request({ object_id }) call - | | - | v (async, when viewer is ready) - | object.publish notification - | - \- script= -> script.list call -> open temp file - | - v + │ + ▼ +WebSocket connects → session.handshake → session.ok + │ + ├─ object= → object.request({ object_id }) call + │ │ + │ ▼ (async, when viewer is ready) + │ object.publish notification + │ + └─ script= → script.list call → open temp file + │ + ▼ script.subscribe + live-sync ``` @@ -150,52 +161,110 @@ WebSocket connects -> session.handshake -> session.ok | Method | Direction | Type | Interface/Parameters | | ------------------------------- | ------------------ | ------------ | -------------------------- | -| `session.handshake` | Viewer -> Extension | Call | `SessionHandshake` | -| `session.handshake` (response) | Extension -> Viewer | Response | `SessionHandshakeResponse` | -| `session.ok` | Viewer -> Extension | Notification | _(no interface)_ | +| `session.handshake` | Viewer → Extension | Call | `SessionHandshake` | +| `session.handshake` (response) | Extension → Viewer | Response | `SessionHandshakeResponse` | +| `session.ok` | Viewer → Extension | Notification | _(no interface)_ | | `session.disconnect` | Bidirectional | Notification | `SessionDisconnect` | | `session.ping` | Bidirectional | Call | `SessionPing` | | `session.ping` (response) | Bidirectional | Response | `SessionPingResponse` | -| `script.subscribe` | Extension -> Viewer | Call | `ScriptSubscribe` | -| `script.subscribe` (response) | Viewer -> Extension | Response | `ScriptSubscribeResponse` | -| `script.unsubscribe` | Viewer -> Extension | Notification | `ScriptUnsubscribe` | -| `script.list` | Extension -> Viewer | Call | _(no parameters)_ | -| `script.list` (response) | Viewer -> Extension | Response | `ScriptList` | -| `language.syntax.id` | Extension -> Viewer | Call | _(no parameters)_ | -| `language.syntax.id` (response) | Viewer -> Extension | Response | `{ id: string }` | -| `language.syntax` | Extension -> Viewer | Call | `{ kind: string }` | -| `language.syntax` (response) | Viewer -> Extension | Response | `LanguageInfo` | -| `language.syntax.cache` | Extension -> Viewer | Call | _(no parameters)_ | -| `language.syntax.cache` (response) | Viewer -> Extension | Response | `SyntaxCacheList` | -| `language.syntax.get` | Extension -> Viewer | Call | `{ filename: string, as_json?: boolean }` | -| `language.syntax.get` (response) | Viewer -> Extension | Response | `SyntaxCacheFile` | -| `language.syntax.change` | Viewer -> Extension | Notification | `SyntaxChange` | -| `script.compiled` | Viewer -> Extension | Notification | `CompilationResult` | -| `runtime.debug` | Viewer -> Extension | Notification | `RuntimeDebug` | -| `runtime.error` | Viewer -> Extension | Notification | `RuntimeError` | -| `object.publish` | Viewer -> Extension | Notification | `ObjectPublishMessage` | -| `object.unpublish` | Viewer -> Extension | Notification | `ObjectUnpublishMessage` || `object.unpublish` | Extension → Viewer | Call | `ObjectUnpublishParams` | -| `object.unpublish` (response) | Viewer → Extension | Response | `ObjectUnpublishResponse` || `object.update` | Viewer -> Extension | Notification | `ObjectUpdateMessage` | -| `object.content.get` | Extension -> Viewer | Call | `ObjectContentGetParams` | -| `object.content.get` (response) | Viewer -> Extension | Response | `ObjectContentGetResponse` | -| `object.content.save` | Extension -> Viewer | Call | `ObjectContentSaveParams` | -| `object.content.save` (response)| Viewer -> Extension | Response | `ObjectContentSaveResponse`| -| `object.item.create` | Extension -> Viewer | Call | `ObjectItemCreateParams` | -| `object.item.create` (response) | Viewer -> Extension | Response | `ObjectItemCreateResponse` | -| `object.item.delete` | Extension -> Viewer | Call | `ObjectItemDeleteParams` | -| `object.item.delete` (response) | Viewer -> Extension | Response | `ObjectItemDeleteResponse` | -| `object.script.set_running` | Extension -> Viewer | Call | `ObjectScriptSetRunningParams` | -| `object.script.set_running` (response) | Viewer -> Extension | Response | `ObjectScriptSetRunningResponse` | -| `object.script.reset` | Extension -> Viewer | Call | `ObjectScriptResetParams` | -| `object.script.reset` (response)| Viewer -> Extension | Response | `ObjectScriptResetResponse` | -| `object.request` | Extension -> Viewer | Call | `ObjectRequestParams` | -| `object.request` (response) | Viewer -> Extension | Response | `ObjectRequestResponse` | -| `object.list` | Extension -> Viewer | Call | `{}` (no params) | -| `object.list` (response) | Viewer -> Extension | Response | `ObjectListResponse` | -| `object.modify` | Extension -> Viewer | Call | `ObjectModifyParams` | -| `object.modify` (response) | Viewer -> Extension | Response | `ObjectModifyResponse` | -| `object.item.modify` | Extension -> Viewer | Call | `ObjectItemModifyParams` | -| `object.item.modify` (response) | Viewer -> Extension | Response | `ObjectItemModifyResponse` | +| `script.subscribe` | Extension → Viewer | Call | `ScriptSubscribe` | +| `script.subscribe` (response) | Viewer → Extension | Response | `ScriptSubscribeResponse` | +| `script.unsubscribe` | Viewer → Extension | Notification | `ScriptUnsubscribe` | +| `script.unsubscribe` | Extension → Viewer | Call | `ScriptUnsubscribeParams` | +| `script.unsubscribe` (response) | Viewer → Extension | Response | `null` | +| `script.list` | Extension → Viewer | Call | _(no parameters)_ | +| `script.list` (response) | Viewer → Extension | Response | `ScriptList` | +| `language.syntax.id` | Extension → Viewer | Call | _(no parameters)_ | +| `language.syntax.id` (response) | Viewer → Extension | Response | `{ id: string }` | +| `language.syntax` | Extension → Viewer | Call | `{ kind: string }` | +| `language.syntax` (response) | Viewer → Extension | Response | `LanguageInfo` | +| `language.syntax.cache` | Extension → Viewer | Call | _(no parameters)_ | +| `language.syntax.cache` (response) | Viewer → Extension | Response | `SyntaxCacheList` | +| `language.syntax.get` | Extension → Viewer | Call | `{ filename: string, as_json?: boolean }` | +| `language.syntax.get` (response) | Viewer → Extension | Response | `SyntaxCacheFile` | +| `language.syntax.change` | Viewer → Extension | Notification | `SyntaxChange` | +| `script.compiled` | Viewer → Extension | Notification | `CompilationResult` | +| `runtime.debug` | Viewer → Extension | Notification | `RuntimeDebug` | +| `runtime.error` | Viewer → Extension | Notification | `RuntimeError` | +| `object.publish` | Viewer → Extension | Notification | `ObjectPublishMessage` | +| `object.unpublish` | Viewer → Extension | Notification | `ObjectUnpublishMessage` | +| `object.unpublish` | Extension → Viewer | Call | `ObjectUnpublishParams` | +| `object.unpublish` (response) | Viewer → Extension | Response | `ObjectUnpublishResponse` | +| `object.update` | Viewer → Extension | Notification | `ObjectUpdateMessage` | +| `object.content.get` | Extension → Viewer | Call | `ObjectContentGetParams` | +| `object.content.get` (response) | Viewer → Extension | Response | `ObjectContentGetResponse` | +| `object.content.save` | Extension → Viewer | Call | `ObjectContentSaveParams` | +| `object.content.save` (response)| Viewer → Extension | Response | `ObjectContentSaveResponse`| +| `object.item.create` | Extension → Viewer | Call | `ObjectItemCreateParams` | +| `object.item.create` (response) | Viewer → Extension | Response | `ObjectItemCreateResponse` | +| `object.item.delete` | Extension → Viewer | Call | `ObjectItemDeleteParams` | +| `object.item.delete` (response) | Viewer → Extension | Response | `ObjectItemDeleteResponse` | +| `object.script.set_running` | Extension → Viewer | Call | `ObjectScriptSetRunningParams` | +| `object.script.set_running` (response) | Viewer → Extension | Response | `ObjectScriptSetRunningResponse` | +| `object.script.reset` | Extension → Viewer | Call | `ObjectScriptResetParams` | +| `object.script.reset` (response)| Viewer → Extension | Response | `ObjectScriptResetResponse` | +| `object.request` | Extension → Viewer | Call | `ObjectRequestParams` | +| `object.request` (response) | Viewer → Extension | Response | `ObjectRequestResponse` | +| `object.list` | Extension → Viewer | Call | `{}` (no params) | +| `object.list` (response) | Viewer → Extension | Response | `ObjectListResponse` | +| `object.modify` | Extension → Viewer | Call | `ObjectModifyParams` | +| `object.modify` (response) | Viewer → Extension | Response | `ObjectModifyResponse` | +| `object.item.modify` | Extension → Viewer | Call | `ObjectItemModifyParams` | +| `object.item.modify` (response) | Viewer → Extension | Response | `ObjectItemModifyResponse` | +| `command.execute` | Bidirectional | Call | `CommandExecuteParams` | +| `command.execute` (response) | Bidirectional | Response | `CommandExecuteResponse` | +| `command.list` | Bidirectional | Call | _(no params)_ | +| `command.list` (response) | Bidirectional | Response | `CommandListResponse` | + +## Error Handling + +A call that fails returns a JSON-RPC 2.0 `error` object rather than a result. There is no partial +success: a response carries either `result` or `error`, never both. + +```json +{ + "jsonrpc": "2.0", + "id": 12, + "error": { + "code": -32602, + "message": "Invalid params: No syntax category specified" + } +} +``` + +**Standard JSON-RPC codes:** + +| Code | Meaning | +| ---- | ------- | +| `-32700` | Parse error — invalid JSON was received | +| `-32600` | Invalid Request — the JSON sent is not a valid Request object | +| `-32601` | Method not found | +| `-32602` | Invalid params | +| `-32603` | Internal error | + +**Server-specific codes.** The range `-32000` to `-32099` is reserved for server errors. The +transport defines the following; `-32001` and `-32003` are the ones handlers commonly raise. + +| Code | Meaning | +| ---- | ------- | +| `-32000` | Connection closed unexpectedly | +| `-32001` | Request timed out | +| `-32002` | Authentication required | +| `-32003` | Access denied | +| `-32004` | Too many requests | +| `-32005` | Service temporarily unavailable | +| `-32006` | Message exceeds maximum size | +| `-32007` | Session expired or invalid | + +**Message format.** Standard errors prefix the handler's detail text with a fixed label, so +`error.message` reads `"Invalid params: "`, `"Internal error: "`, +`"Method not found: "`, and so on. Server-specific errors carry the detail text alone +(e.g. `"Access denied"`). Clients should branch on `error.code`, not on `error.message`. + +**`success` is not an error channel.** Several results carry a `success` field. Where a method +reports failure through a JSON-RPC error, that field is `true` on every response it ever +appears in, and a failed call produces no result to inspect. Do not treat a missing or false +`success` as the failure signal unless the method's own section documents it as one. ## Session Management Interfaces @@ -235,6 +304,8 @@ interface SessionHandshake { - `live_sync`: Viewer supports live script synchronisation with the external editor - `compilation`: Viewer will forward compilation results via `script.compiled` - `syntax_cache`: Viewer supports `language.syntax.cache` and `language.syntax.get` for retrieving syntax definition files + - `commands`: Both sides support `command.execute` and `command.list` + - `unified_diagnostics`: Viewer supports the unified diagnostic reporting format ### SessionHandshakeResponse @@ -262,7 +333,14 @@ interface SessionHandshakeResponse { - `protocol_version`: Protocol version the client supports - `challenge_response` (optional): The UUID read from the temporary file identified by the `challenge` field in the handshake. Must be provided if `challenge` was present, otherwise the connection will be closed. - `languages`: Array of languages supported by the client -- `features`: Dictionary of features supported by the client +- `features`: Dictionary of features supported by the client. Known flags: + - `live_sync`: Client supports live script synchronisation + - `error_reporting`: Client accepts `runtime.error` notifications + - `unified_diagnostics`: Client supports the unified diagnostic reporting format + - `object_publish`: Client supports the object explorer methods and notifications + - `commands`: Both sides support `command.execute` and `command.list` + - `debugging`: Advertised as `false`. Reserved; no debugging support. + - `breakpoints`: Advertised as `false`. Reserved; no breakpoint support. - `script_name` (optional): Name of the script currently open in the editor - `script_language` (optional): Language of the script currently open in the editor (e.g. `"lsl"`, `"luau"`) @@ -301,6 +379,10 @@ interface SessionDisconnect { Heartbeat call used to verify the connection is alive and measure latency. Either side can initiate a ping; the recipient responds with the original timestamp plus its own server time. +In practice the extension initiates and the viewer only answers — the viewer never sends +`session.ping` itself. The extension pings every 30 seconds and tears the connection down after +two consecutive failures. + ```typescript interface SessionPing { timestamp: number; @@ -322,7 +404,7 @@ interface SessionPingResponse { **Response Fields:** -- `timestamp`: The original timestamp from the request (echoed back) +- `timestamp`: The original timestamp from the request. Echoed back only when the request supplied one. - `server_time`: Unix timestamp in milliseconds when the response was generated **Example Request:** @@ -403,23 +485,24 @@ Requests the in-memory keyword definitions for a specific language. These defini ```typescript interface LanguageInfo { id: string; - defs?: object; // Present only on success + defs: object; success: boolean; - error?: string; // Present only on failure } ``` **Response Fields:** - `id`: The current syntax version identifier -- `defs` (optional): The keyword definitions object. Only present when `success` is `true`. Structure varies by language. -- `success`: Whether the definitions were found and returned successfully -- `error` (optional): Human-readable error description. Only present when `success` is `false` +- `defs`: The keyword definitions object. Structure varies by language. +- `success`: Always `true`. Failures are returned as JSON-RPC errors — see below. -**Error cases:** +**Errors:** -- No `kind` parameter supplied: `success: false`, `error: "No syntax category specified"` -- Unknown `kind` value: `success: false`, `error: "Unknown syntax category requested"` +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| No `kind` parameter supplied | `-32602` | `Invalid params: No syntax category specified` | +| Unknown `kind` value | `-32602` | `Invalid params: Unknown syntax category requested` | +| Definitions unavailable for a valid `kind` | `-32603` | `Internal error: Syntax definitions are unavailable` | ### Language Syntax Cache List @@ -456,7 +539,7 @@ interface SyntaxCacheList { | `lua_keywords_pretty.xml` | Luau keyword definitions in formatted LLSD XML format | | `secondlife_selene.yml` | Luau Selene linter configuration in YAML format | -Not all files may be present in every cache - the actual list returned by `language.syntax.cache` reflects only what is available on the viewer's local filesystem at the time of the request. +Not all files may be present in every cache — the actual list returned by `language.syntax.cache` reflects only what is available on the viewer's local filesystem at the time of the request. ### Language Syntax Cache Get @@ -482,23 +565,24 @@ Requests the content of a specific file from the syntax definition cache. The fi ```typescript interface SyntaxCacheFile { - content?: string | object; // Present only on success. String if as_json is false/omitted, object if as_json is true + content: string | object; // String if as_json is false/omitted, object if as_json is true success: boolean; - error?: string; // Present only on failure } ``` **Response Fields:** -- `content`: The file content. Only present when `success` is `true`. Is a raw text string when `as_json` is omitted or `false`; is a parsed object when `as_json` is `true`. -- `success`: Whether the file was found and read successfully -- `error` (optional): Human-readable error description. Only present when `success` is `false` +- `content`: The file content. Is a raw text string when `as_json` is omitted or `false`; is a parsed object when `as_json` is `true`. +- `success`: Always `true`. Failures are returned as JSON-RPC errors — see below. -**Error cases:** +**Errors:** -- No `filename` parameter supplied: `success: false`, `error: "No filename specified"` -- Name not found in cache: `success: false`, `error: "Requested syntax cache file not found"` -- File could not be loaded: `success: false`, `error: "Failed to load syntax cache file"` (or `"Failed to load and format syntax cache file."` when `as_json` is `true`) +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| No `filename` parameter supplied | `-32602` | `Invalid params: No filename specified` | +| Name not found in cache | `-32602` | `Invalid params: Requested syntax cache file not found` | +| File could not be loaded (`as_json` omitted or `false`) | `-32603` | `Internal error: Failed to load syntax cache file` | +| File could not be loaded or parsed (`as_json` is `true`) | `-32603` | `Internal error: Failed to load and format syntax cache file.` | ## Script Subscription Interfaces @@ -533,9 +617,10 @@ interface ScriptSubscribeResponse { script_id: string; success: boolean; status: number; - object_id?: string; - item_id?: string; - message?: string; + message: string; + object_id?: string; // Success only + root_id?: string; // Success only + item_id?: string; // Success only } ``` @@ -545,19 +630,30 @@ interface ScriptSubscribeResponse { - `success`: Whether the subscription was successful - `status`: Numeric status code indicating the result: - `0`: Success - - `1`: Invalid editor - the script editor panel is no longer open - - `2`: Invalid subscription - no subscription found for the given `script_id` - - `3`: Already subscribed - another connection is already subscribed to this script + - `1`: Invalid editor — the script editor panel is no longer open + - `2`: Invalid subscription — no subscription found for the given `script_id` + - `3`: Already subscribed — another connection is already subscribed to this script - `4`: Internal server error -- `object_id` (optional): The in-world UUID of the object containing the script -- `item_id` (optional): The inventory item UUID of the script within the object -- `message` (optional): Additional information about the subscription result +- `message`: Always present. Fixed text matching `status`: + +| `status` | `message` | +| -------- | --------- | +| `0` | `OK` | +| `1` | `Invalid editor handle` | +| `2` | `No subscription found for script` | +| `3` | `Script already subscribed` | +| `4` | `Internal server error` | + +- `object_id`: UUID of the **prim** that owns the script. For a script in a child prim this is the child's UUID, not the linkset root's. Present only when `success` is `true`. +- `root_id`: UUID of the root prim of the linkset containing the script. Present only when `success` is `true`. If the prim cannot be resolved, `root_id` is set equal to `object_id`, so equality does not by itself mean the script lives in the root prim. +- `item_id`: The inventory item UUID of the script within the prim. Present only when `success` is `true`. ### ScriptUnsubscribe **JSON-RPC Method:** `script.unsubscribe` (notification from viewer) -Notification sent by the viewer when a script subscription should be terminated. +Notification sent by the viewer when a script subscription should be terminated. It is delivered +only to the connection holding the subscription, not to all connections. ```typescript interface ScriptUnsubscribe { @@ -569,6 +665,23 @@ interface ScriptUnsubscribe { - `script_id`: Unique identifier for the script to unsubscribe from +**JSON-RPC Method:** `script.unsubscribe` (call from extension to viewer) + +The extension may also call `script.unsubscribe` to end a subscription it holds. + +```typescript +interface ScriptUnsubscribeParams { + script_id: string; // Script whose subscription should be dropped +} +``` + +The result is `null`. + +The viewer drops the subscription only when the calling connection owns it. The call is +idempotent: an unknown `script_id`, or one held by a different connection, is ignored and still +returns a successful `null` result. A client therefore cannot use the response to detect that it +targeted the wrong script. + ### ScriptList **JSON-RPC Method:** `script.list` (call from extension to viewer) @@ -593,12 +706,12 @@ interface ScriptList { ## Compilation Interfaces -### CompilationError +### Diagnostic Individual compilation error record. ```typescript -interface CompilationError { +interface Diagnostic { row: number; column: number; level: string; @@ -626,7 +739,7 @@ interface CompilationResult { script_id: string; success: boolean; running: boolean; - errors?: CompilationError[]; + diagnostics?: Diagnostic[]; } ``` @@ -635,7 +748,26 @@ interface CompilationResult { - `script_id`: Unique identifier for the script that was compiled - `success`: Whether the compilation was successful - `running`: Whether the compiled script is currently running -- `errors` (optional): Array of compilation errors if any occurred +- `diagnostics` (optional): Array of `Diagnostic` records if any occurred + +**Delivery:** routed only to the connection subscribed to that script. This differs from +`runtime.debug` and `runtime.error`, which are broadcast to every connection. + +**Two compile-feedback paths.** Compilation results reach a client by one of two routes, +depending on how the save was made: + +| Save route | Feedback | +| ---------- | -------- | +| `object.content.save` (object explorer) | `compiled` and `diagnostics` returned inline in the response | +| Live-sync editing of a subscribed script | `script.compiled` notification | + +A client using only the object explorer never receives `script.compiled`; a client waiting for +`script.compiled` after an `object.content.save` will wait indefinitely. Consolidating these two +paths is tracked separately. + +**Known limitation.** `script.compiled` is produced only while the viewer's script editor for that +script is open. If that editor has closed, compilation results are dropped without notice — no +result, no error, and not necessarily a preceding `script.unsubscribe`. Tracked separately. ## Runtime Event Interfaces @@ -647,19 +779,27 @@ Debug message notification sent by the viewer during script execution. ```typescript interface RuntimeDebug { - script_id: string; + script_id: string; // Not currently sent — see note below object_id: string; + prim_id: string; + item_id: string; object_name: string; message: string; + channel: "debug" | "owner_say"; + item: ItemRef; } ``` **Fields:** -- `script_id`: Unique identifier for the script generating the debug message -- `object_id`: Unique identifier for the object containing the script +- `script_id`: Identifier for the script generating the debug message +- `object_id`: UUID of the root prim of the object containing the script +- `prim_id`: UUID of the prim that owns the script +- `item_id`: Inventory item UUID of the script - `object_name`: Human-readable name of the object - `message`: The debug message content +- `channel`: Source of the text. `"debug"` for script debug output, `"owner_say"` for owner-directed chat. +- `item`: Reference identifying the originating script. See `ItemRef` under `RuntimeError`. ### RuntimeError @@ -669,25 +809,54 @@ Runtime error notification sent by the viewer when a script encounters an error ```typescript interface RuntimeError { - script_id: string; + script_id: string; // Not currently sent — see note below object_id: string; + prim_id: string; + item_id: string; object_name: string; message: string; error: string; line: number; - stack?: string[]; + column: number; + stack: string[]; + channel: "debug" | "owner_say"; + item: ItemRef; +} + +interface ItemRef { + root_id: string; + prim_id: string; + item_id: string; + name: string; + language: "lsl" | "luau"; } ``` **Fields:** -- `script_id`: Unique identifier for the script that encountered the error -- `object_id`: Unique identifier for the object containing the script +- `script_id`: Identifier for the script that encountered the error +- `object_id`: UUID of the root prim of the object containing the script +- `prim_id`: UUID of the prim that owns the script +- `item_id`: Inventory item UUID of the script - `object_name`: Human-readable name of the object - `message`: The full raw chat text of the runtime error message as received from the simulator -- `error`: Extracted error description. Currently always an empty string - runtime error extraction from the simulator's multi-message format is not yet fully implemented. -- `line`: Line number where the error occurred. Currently always `0` for the same reason. -- `stack` (optional): Stack trace lines if they could be extracted from the error message +- `error`: Extracted runtime error description. This remains a top-level compatibility field while the protocol stays on version `1.0`. +- `line`: Line number where the error occurred when the runtime format can be parsed; otherwise `0`. +- `column`: Column position where the error occurred when the runtime format can be parsed; otherwise `0`. +- `stack`: Stack trace lines extracted from the error message. Always present; an empty array when no trace could be extracted, so test its length rather than its presence. +- `channel`: Source of the text. `"debug"` for script debug output, `"owner_say"` for owner-directed chat. +- `item`: Reference identifying the originating script. + - `root_id`: UUID of the root prim of the linkset. + - `prim_id`: UUID of the prim that owns the script. + - `item_id`: Inventory item UUID of the script. + - `name`: Script name as it appears in the prim's inventory. + - `language`: The script's source language. Independent of the compile target; the VM is not carried in runtime messages. + +**Note on `script_id`:** this field is part of the contract but is **not currently sent** by the +viewer for either `runtime.debug` or `runtime.error`. Implementation is tracked separately. + +**Delivery:** `runtime.debug` and `runtime.error` are broadcast to all connections. An event is +emitted when the originating object is published or its script is subscribed. ## Handler and Configuration Interfaces @@ -743,9 +912,14 @@ interface ClientInfo { --- -## Object Content Interfaces +## Object Explorer Interfaces -These interfaces support publishing in-world object inventories (scripts and notecards) to the external editor as a browseable virtual filesystem. The extension exposes published objects under the `sl://objects/` URI scheme. +These interfaces support exploring in-world object inventories (scripts and notecards) from the external editor as a browseable virtual filesystem. The extension exposes explored objects under the `sl://objects/` URI scheme. + +Explored objects are shared across all connections rather than owned by the connection that +requested them. `object.publish`, `object.update` and `object.unpublish` are broadcast to every +connected client, so a client will receive notifications for objects it never requested, and must +drop an object from its own state when an `object.unpublish` for it arrives. ### Core Data Types @@ -780,7 +954,7 @@ interface ObjectInventoryItem { /** A linked (child) prim within a linkset */ interface LinkedObject { link_id: string; // UUID of the linked prim - link_number: number; // Link number (root=1, children>=2) + link_number: number; // Link number (root=1, children≥2) link_name: string; link_description?: string; inventory: ObjectInventoryItem[]; @@ -799,18 +973,59 @@ interface PublishedObject { region?: string; owner_id?: string; permissions?: ObjectPermissions; + can_save_back?: boolean; // Whether Save Back to Contents is currently available for this object inventory: ObjectInventoryItem[]; // Root prim's scripts and notecards linked_objects?: LinkedObject[]; // Child prims } ``` -**Script display extensions** (synthetic, derived from `subtype`): +**Display names.** The extension synthesises a file extension for scripts when presenting items in +the virtual filesystem: + +| Item | Extension | +| ---- | --------- | +| Script, `subtype` `0` (LSL) | `.lsl` | +| Script, `subtype` `1` (Luau) | `.luau` | +| Notecard | _(none)_ | + +Notecards receive no synthetic extension; the inventory name is used verbatim, so an extension the +user gave the notecard is preserved and none is added. + +These extensions exist only for display and are never part of the item's inventory name. Names sent +to and received from the viewer — including in `object.item.create` and `object.item.modify` — are +always the pure inventory name, without an extension. + +### Common preconditions + +Every method that addresses an item by `prim_id` + `item_id` — `object.content.get`, +`object.content.save`, `object.item.delete` and `object.item.modify` — runs the same validation +before doing any work, in this order: -| `subtype` | Extension | -| --------- | --------- | -| `0` (LSL) | `.lsl` | -| `1` (Luau)| `.luau` | -| notecard | `.txt` | +1. Both `prim_id` and `item_id` are present. +2. The prim exists. +3. The object containing the prim is currently published. +4. The item exists in that prim's inventory. +5. The item is a script or a notecard. +6. The caller holds the permissions that method requires. + +An object must be published before any of its items can be addressed. Learning an object's id from +`object.list` is not sufficient on its own — the object must be published, which `object.list` +reports and `object.request` initiates. + +**Errors:** + +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| `prim_id` or `item_id` missing | `-32602` | `Invalid params: prim_id and item_id are required` | +| Prim not found | `-32602` | `Invalid params: Prim not found` | +| Object is not published | `-32003` | `Object is not published` | +| Item not in the prim's inventory | `-32602` | `Invalid params: Item not found in prim inventory` | +| Item is not a script or notecard | `-32602` | `Invalid params: Item is not a script or notecard` | +| Required item permission denied | `-32003` | `Insufficient permissions` | +| Modify denied on the containing prim | `-32003` | `No modify permission on object` | + +Writes require modify permission on both the item **and** the prim that contains it. A no-modify +object can be published and read, but its contents cannot be changed. --- @@ -828,7 +1043,8 @@ interface ObjectPublishMessage { **Fields:** -- `object`: The full published object tree, including root prim inventory and all linked prim inventories. +- `object`: The full object tree being explored, including root prim inventory and all linked prim inventories. + - `can_save_back` (optional): Capability hint for UI actions. When `true`, the object currently supports the `viewer.object.save_back_to_contents` command. --- @@ -836,7 +1052,7 @@ interface ObjectPublishMessage { **JSON-RPC Method:** `object.unpublish` (notification from viewer) -Sent when the viewer removes a previously published object - for example when the owner deselects it, moves away, or the object is deleted. +Sent when the viewer stops exploring a previously explored object — for example when the user stops exploring it from the viewer UI, the extension calls `object.unpublish`, or the object is deleted or linked into another object. ```typescript interface ObjectUnpublishMessage { @@ -847,12 +1063,23 @@ interface ObjectUnpublishMessage { **Fields:** -- `object_id`: UUID of the root prim that is being unpublished -- `reason` (optional): Human-readable explanation (e.g. `"object deleted"`, `"out of range"`) +- `object_id`: UUID of the root prim that is no longer being explored +- `reason` (optional): Machine-readable token identifying why exploring stopped. One of: + +| Value | Meaning | +| ----- | ------- | +| `manual` | The extension called `object.unpublish` for this object. | +| `republish` | The object is being re-published; a fresh `object.publish` follows. | +| `user` | The user stopped exploring the object from the viewer UI. | +| `deleted` | The object was deleted. | +| `linked` | The object became a child prim of a linkset and is no longer a root. | + +The field is omitted only when no reason was supplied; in practice every unpublish carries one +of the values above. **JSON-RPC Method:** `object.unpublish` (call from extension to viewer) -The extension may also call `object.unpublish` to manually stop tracking an object. The viewer will stop publishing it and send a corresponding `object.unpublish` notification back to the caller. +The extension may also call `object.unpublish` to manually stop exploring an object. The viewer will stop and broadcast a corresponding `object.unpublish` notification to all connections. ```typescript interface ObjectUnpublishParams { @@ -867,10 +1094,10 @@ interface ObjectUnpublishResponse { **Fields:** -- `object_id`: UUID of the root prim to unpublish. -- `success`: `true` if the object was published and has been removed. +- `object_id`: UUID of the root prim to stop exploring. +- `success`: `true` if the object was being explored and has been removed. -**Note:** The viewer also sends an `object.unpublish` notification to the caller immediately after responding. Extensions should handle that notification idempotently. +**Note:** The viewer also broadcasts an `object.unpublish` notification immediately after responding. The caller receives it too, so extensions should handle that notification idempotently. --- @@ -878,43 +1105,70 @@ interface ObjectUnpublishResponse { **JSON-RPC Method:** `object.update` (notification from viewer) -Sent when the inventory of a published object changes. Supports two modes: -- **Full replacement**: `inventory` and/or `linked_objects` fields replace the entire prior state. -- **Delta update**: `changes` field describes only what changed. Takes precedence over full replacement fields when present. +Sent when an explored object's inventory, properties, or linkset membership change. -```typescript -interface InventoryChanges { - added?: ObjectInventoryItem[]; - removed?: string[]; // item_ids removed - modified?: ObjectInventoryItem[]; // metadata-only changes - content_changed?: string[]; // item_ids whose content changed (invalidates cache) - running_changed?: { item_id: string; running: boolean }[]; // running state toggled -} +**Shapes currently emitted.** The viewer sends exactly the following five payloads: -interface LinkedObjectChanges { - added?: LinkedObject[]; - removed?: string[]; // link_ids removed - modified?: { - link_id: string; - link_name?: string; - inventory?: InventoryChanges; - }[]; -} +| Trigger | Payload | +| ------- | ------- | +| Root prim inventory changed | `object_id`, `inventory` (complete replacement array) | +| Child prim inventory changed | `object_id`, `changes.linked_objects.modified[]` with `link_id` and `inventory` (complete replacement array for that child) | +| Root prim name/description changed | `object_id`, `object_name` and/or `object_description` | +| Child prim name/description changed | `object_id`, `changes.linked_objects.modified[]` with `link_id` and `link_name` and/or `link_description` | +| Linkset membership changed | `object_id`, `linked_objects` (complete replacement array covering every child prim) | + +`inventory` and `linked_objects` are always complete replacements of the prior state, never +increments. Linkset membership changes are coalesced behind a short flush delay, so several +rapid link or unlink operations may arrive as a single update. +```typescript interface ObjectUpdateMessage { object_id: string; object_name?: string; - // Full replacement (used when changes is absent) + object_description?: string; + // Full replacement inventory?: ObjectInventoryItem[]; linked_objects?: LinkedObject[]; - // Delta (takes precedence when present) changes?: { - inventory?: InventoryChanges; + inventory?: InventoryChanges; // Not implemented — see below linked_objects?: LinkedObjectChanges; }; } + +interface LinkedObjectChanges { + added?: LinkedObject[]; // Not implemented — see below + removed?: string[]; // Not implemented — see below + modified?: { + link_id: string; + link_name?: string; + link_description?: string; + inventory?: ObjectInventoryItem[]; // Complete replacement array, not a delta + }[]; +} ``` +**Note:** `changes.linked_objects.modified` is emitted, as shown in the table above. +`changes.linked_objects.added` and `changes.linked_objects.removed` are not. + +#### Not implemented + +The delta sub-protocol below is specified but is **not currently emitted by the viewer**. +Implementation is tracked separately. Clients must rely on the full-replacement shapes listed +above; code written to consume these types will never run against the current viewer. + +```typescript +interface InventoryChanges { + added?: ObjectInventoryItem[]; + removed?: string[]; // item_ids removed + modified?: ObjectInventoryItem[]; // metadata-only changes + content_changed?: string[]; // item_ids whose content changed (invalidates cache) + running_changed?: { item_id: string; running: boolean }[]; // running state toggled +} +``` + +Also not implemented: `LinkedObjectChanges.added` and `LinkedObjectChanges.removed`. Linkset +membership changes are sent as a complete `linked_objects` replacement array instead. + --- ### ObjectContentGet @@ -925,7 +1179,7 @@ Requests the text content of a script or notecard. The extension calls this lazi ```typescript interface ObjectContentGetParams { - prim_id: string; // UUID of any prim (root or child) - no object_id + link_id needed + prim_id: string; // UUID of any prim (root or child) — no object_id + link_id needed item_id: string; } @@ -942,7 +1196,15 @@ interface ObjectContentGetResponse { - `prim_id`: UUID of the prim that owns the item. Child prims are addressable directly by UUID without knowing the root object_id. - `item_id`: Inventory item UUID. - `success`: `true` on success. -- `content`: The raw text content of the item. For notecards, the `Linden text version 2` envelope is stripped - only the body text is returned. +- `content`: The raw text content of the item. For notecards, the `Linden text version 2` envelope is stripped — only the body text is returned. + +**Permissions.** Scripts require both `PERM_COPY` and `PERM_MODIFY` on the item: the source of a +no-copy or no-modify script is never exposed. Notecards require no permission at all, so that +no-modify notecards remain readable in the external editor. See +[Common preconditions](#common-preconditions) for the shared checks and errors. + +**Timeout.** 30 seconds to fetch the asset, after which the call fails with `-32001` +(`Asset fetch timed out`). --- @@ -958,6 +1220,7 @@ interface ObjectContentSaveParams { item_id: string; content: string; vm?: "mono" | "lsl2" | "luau"; + running?: boolean; // Scripts only: run state applied after compilation. Defaults to false. } interface ObjectContentSaveResponse { @@ -965,8 +1228,7 @@ interface ObjectContentSaveResponse { prim_id?: string; item_id?: string; compiled?: boolean; - errors?: string[]; - message?: string; + diagnostics?: Diagnostic[]; } ``` @@ -976,10 +1238,19 @@ interface ObjectContentSaveResponse { - `item_id`: UUID of the saved inventory item. - `content`: Raw script/notecard source text to store. - `vm` (optional): Scripts only compile target. Accepted values are `"mono"`, `"lsl2"`, `"luau"`. When `"luau"` is specified for an LSL script (as opposed to a native Luau script), the viewer automatically selects the correct LSL-on-Luau compile path. If omitted, inferred from item metadata or content analysis. +- `running` (optional): Scripts only. The run state the viewer applies to the script once the upload and compilation complete. Defaults to `false` when omitted. To preserve a script's current run state across a save, echo the `running` value from the corresponding `ObjectInventoryItem` in the most recent `object.publish` or `object.update`. - `success`: Whether the upload/save operation succeeded. - `compiled` (optional): Scripts only. `true` when compilation succeeded, `false` when source saved but compile failed. -- `errors` (optional): Scripts only. Compiler diagnostics when `compiled` is `false`. -- `message` (optional): Error description on failure. +- `diagnostics` (optional): Scripts only. Compiler diagnostics when `compiled` is `false`. + +> **Warning:** Omitting `running` does not leave the script's run state unchanged — it stops the script. A client that saves a running script without sending `running: true` will silently stop it. + +**Permissions.** Requires `PERM_MODIFY` on the item and modify permission on the containing prim. +See [Common preconditions](#common-preconditions) for the shared checks and errors. + +**Timeouts.** Scripts allow 60 seconds for upload and compilation; notecards allow 30 seconds. A +client's own timeout must exceed the longer of the two. Exceeding either fails the call with +`-32001`. --- @@ -987,18 +1258,17 @@ interface ObjectContentSaveResponse { **JSON-RPC Method:** `object.item.create` (call from extension to viewer) -Creates a new script in a prim's inventory. The call is asynchronous - the viewer sends -`RezScript` to the simulator and waits for the inventory-changed callback before returning -the created item's details. The simulator may rename the item if a duplicate name exists. - -Notecard creation is not yet supported and will return an error. +Creates a new script or notecard in a prim's inventory. The call is asynchronous and returns the +created item's details once the simulator confirms the item exists. The simulator may rename the +item if a duplicate name exists. ```typescript interface ObjectItemCreateParams { prim_id: string; // UUID of the prim to create the item in - name: string; // Pure SL inventory name - no file extension - type: InventoryItemType; // "script" ("notecard" reserved for future) - vm: ScriptVM; // Required for scripts: "luau" | "mono" | "lsl2" + name: string; // Pure SL inventory name — no file extension + type: InventoryItemType; // "script" | "notecard" + vm?: ScriptVM; // Scripts only (required): "luau" | "mono" | "lsl2" + text?: string; // Notecards only (optional): initial body text } // On success, returns an ObjectInventoryItem with prim_id: @@ -1007,12 +1277,32 @@ interface ObjectItemCreateResponse extends ObjectInventoryItem { } ``` +**Fields:** + +- `prim_id`: UUID of the prim to create the item in. Child prims are addressable directly by UUID. +- `name`: Pure SL inventory name, without a file extension. +- `type`: `"script"` or `"notecard"`. +- `vm`: Scripts only. Required when `type` is `"script"`; accepted values are `"luau"`, `"mono"` and `"lsl2"`. Ignored for notecards. +- `text` (optional): Notecards only. Initial body text for the new notecard. Ignored for scripts. + **Notes:** - The response matches the `ObjectInventoryItem` structure (same fields as items in `object.publish` and `object.update` notifications). - The `name` in the response may differ from the request if the simulator renamed it. - An `object.update` notification will also fire for the prim (since inventory changed). -- Timeout: 30 seconds. Returns a JSON-RPC internal error if the simulator does not respond. + +**Errors:** + +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| `type` is not `"script"` or `"notecard"` | `-32602` | `Invalid params: Unsupported item type: ` | +| `prim_id` missing | `-32602` | `Invalid params: prim_id is required` | +| Prim not found | `-32602` | `Invalid params: Prim not found` | +| `name` missing | `-32602` | `Invalid params: name is required` | +| `vm` missing or invalid for a script | `-32602` | `Invalid params: vm must be 'luau', 'mono', or 'lsl2'` | +| Object is not published | `-32003` | `Object is not published` | +| Another `object.item.create` is already in flight for this prim | `-32600` | `Invalid Request: An item.create is already in flight for this prim` | +| Simulator did not respond within 30 seconds | `-32001` | `Timed out waiting for item creation` | --- @@ -1035,6 +1325,9 @@ interface ObjectItemDeleteResponse { } ``` +**Permissions.** Requires `PERM_MODIFY` on the item and modify permission on the containing prim. +See [Common preconditions](#common-preconditions) for the shared checks and errors. + --- ### ObjectScriptSetRunning @@ -1052,10 +1345,29 @@ interface ObjectScriptSetRunningParams { interface ObjectScriptSetRunningResponse { success: boolean; - message?: string; } ``` +**`success` means dispatched, not applied.** The viewer sends a message to the simulator and +returns immediately. `success: true` confirms the message was sent — it does not confirm the +simulator started or stopped the script. Confirmation, if it arrives, comes later as an +`object.update` notification. Treat the response as an acknowledgement and wait for the update +before reporting the new run state to the user. + +**Permissions.** Requires `PERM_MODIFY` on the script. This method validates independently of the +shared item validator and accepts scripts only. + +**Errors:** + +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| `prim_id` or `item_id` missing | `-32602` | `Invalid params: prim_id and item_id are required` | +| Prim not found | `-32602` | `Invalid params: Prim not found` | +| Object is not published | `-32003` | `Object is not published` | +| Script not in the prim's inventory | `-32602` | `Invalid params: Script not found in prim inventory` | +| Item is not a script | `-32602` | `Invalid params: Item is not a script` | +| No modify permission on the script | `-32003` | `No modify permission on script` | + --- ### ObjectScriptReset @@ -1072,10 +1384,18 @@ interface ObjectScriptResetParams { interface ObjectScriptResetResponse { success: boolean; - message?: string; } ``` +**`success` means dispatched, not applied.** As with `object.script.set_running`, the viewer sends +a message to the simulator and returns immediately. `success: true` does not confirm the script +was reset. + +**Permissions.** Requires `PERM_MODIFY` on the script. This method validates independently of the +shared item validator and accepts scripts only. + +**Errors:** identical to `object.script.set_running` above. + --- ### ObjectRequest @@ -1088,20 +1408,29 @@ This is typically called immediately after the handshake completes when the exte ```typescript interface ObjectRequestParams { - object_id: string; // UUID of the root prim to request publishing for + object_id: string; // UUID of the root prim to request exploring } interface ObjectRequestResponse { success: boolean; - message?: string; // reason on failure (e.g. "object not found", "permission denied") } ``` **Fields:** -- `object_id`: UUID of the root prim of the linkset to publish. -- `success`: Whether the viewer accepted the request. A `true` response does not mean `object.publish` has been sent yet - it means the viewer will send it. -- `message` (optional): Human-readable failure reason. Only present when `success` is `false`. +- `object_id`: UUID of the root prim of the linkset to explore. +- `success`: Whether the viewer accepted the request. A `true` response does not mean `object.publish` has been sent yet — it means the viewer will send it. Failures are returned as JSON-RPC errors — see below. + +**Permissions.** Requires modify permission on the object. + +**Errors:** + +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| `object_id` missing | `-32602` | `Invalid params: No object_id specified` | +| Object not found | `-32602` | `Invalid params: Object not found` | +| No modify permission on the object | `-32003` | `Permission denied` | +| Publish could not be started | `-32603` | `Internal error: Failed to initiate publish` | **Sequence:** 1. Extension calls `object.request` @@ -1114,21 +1443,21 @@ interface ObjectRequestResponse { **JSON-RPC Method:** `object.list` (call from extension to viewer) -Requests the complete list of currently published objects. Called by the extension immediately after the handshake completes (`session.ok`) to restore state for any objects the viewer already has published. +Requests the complete list of currently explored objects. Called by the extension immediately after the handshake completes (`session.ok`) to restore state for any objects the viewer already has open for exploration. -The viewer responds synchronously with all published objects in the same format as `object.publish` notifications. No follow-up notifications are sent. +The viewer responds synchronously with all explored objects in the same format as `object.publish` notifications. No follow-up notifications are sent. ```typescript // No request parameters interface ObjectListResponse { - objects: PublishedObject[]; // All currently published objects; empty array if none + objects: PublishedObject[]; // All currently explored objects; empty array if none } ``` **Fields:** -- `objects`: Array of `PublishedObject` records (same shape as the `object` field in `object.publish`). Empty array when no objects are currently published. +- `objects`: Array of `PublishedObject` records (same shape as the `object` field in `object.publish`). Empty array when no objects are currently being explored. **Sequence:** 1. Viewer sends `session.ok` @@ -1156,7 +1485,6 @@ interface ObjectModifyParams { interface ObjectModifyResponse { success: boolean; prim_id: string; // Echoed back from request - message?: string; // Error description on failure } ``` @@ -1167,8 +1495,24 @@ interface ObjectModifyResponse { - `description` (optional): New description for the prim. If omitted, description remains unchanged. - `permissions` (optional): Permission changes. - `next_owner`: Permission mask applied when the object is transferred. Uses same bit flags as `ItemPermissions` (e.g., `PERM_MODIFY=0x4000`, `PERM_COPY=0x8000`, `PERM_TRANSFER=0x2000`). -- `success`: Whether the update operation succeeded. -- `message` (optional): Error description. Only present when `success` is `false`. +- `success`: Whether the property messages were dispatched. + +**`success` means dispatched, not applied.** Each supplied property is sent to the simulator as a +separate message and the viewer returns immediately. `success: true` confirms the messages were +sent, not that any of them took effect — and because they travel independently, one may be applied +while another is not. Confirmation arrives later as an `object.update` notification. + +**Permissions.** Requires modify permission on the prim. + +**Errors:** + +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| `prim_id` missing | `-32602` | `Invalid params: prim_id is required` | +| No property supplied | `-32602` | `Invalid params: At least one property (name, description, or permissions) must be specified` | +| Prim not found | `-32602` | `Invalid params: Prim not found` | +| Object is not published | `-32003` | `Object is not published` | +| No modify permission on the prim | `-32003` | `No modify permission on object` | **Notes:** - At least one property field (`name`, `description`, or `permissions`) must be specified. @@ -1198,7 +1542,6 @@ interface ObjectItemModifyResponse { success: boolean; prim_id: string; // Echoed back from request item_id: string; // Echoed back from request - message?: string; // Error description on failure } ``` @@ -1210,11 +1553,180 @@ interface ObjectItemModifyResponse { - `description` (optional): New description for the item. If omitted, description remains unchanged. - `permissions` (optional): Permission changes. - `next_owner`: Permission mask applied when the item is transferred. Uses same bit flags as `ItemPermissions` (e.g., `PERM_MODIFY=0x4000`, `PERM_COPY=0x8000`, `PERM_TRANSFER=0x2000`). -- `success`: Whether the update operation succeeded. -- `message` (optional): Error description. Only present when `success` is `false`. +- `success`: Whether the property messages were dispatched. + +**`success` means dispatched, not applied.** The viewer sends the change to the simulator and +returns immediately; confirmation arrives later as an `object.update` notification. + +**Permissions.** Requires `PERM_MODIFY` on the item and modify permission on the containing prim. +See [Common preconditions](#common-preconditions) for the shared checks and errors. + +**Errors:** as listed under [Common preconditions](#common-preconditions), plus `-32602` when no +property field is supplied. **Notes:** - At least one property field (`name`, `description`, or `permissions`) must be specified. - An `object.update` notification will fire after successful modification. - Owner permissions cannot be modified directly — only `next_owner` can be changed. - If the item is renamed, the virtual filesystem path will change and the extension must handle the rename appropriately. + +--- + +## Command Interfaces + +These interfaces provide a general-purpose, bidirectional command channel. Either side may invoke a named command on the other side and receive a structured result. The feature is optional and must be negotiated via the `commands` flag in the session handshake. + +### CommandExecute + +**JSON-RPC Method:** `command.execute` (call, bidirectional) + +Invokes a named command on the receiving side. Commands are identified by a namespaced string and carry an optional freeform parameter map. + +```typescript +interface CommandExecuteParams { + command: string; // namespaced command id, e.g. "viewer.teleport" + params?: Record; // command-specific arguments +} + +interface CommandExecuteResponse { + success: boolean; + result?: unknown; // optional command-specific return value +} +``` + +**Fields:** + +- `command`: Namespaced command identifier. The prefix before the first `.` identifies the side that owns and executes the command: + - `viewer.*` — commands executed by the viewer (e.g. `viewer.teleport`, `viewer.camera.focus`) + - `editor.*` — commands executed by the extension (e.g. `editor.open_file`, `editor.show_message`) +- `params` (optional): Command-specific argument map. Structure varies by command. +- `success`: `true` when the command executed. Failures are returned as JSON-RPC errors — see below. +- `result` (optional): Command-specific return value. Only present when the command produces output. + +**Errors:** + +| Condition | Code | `error.message` | +| --------- | ---- | --------------- | +| `command` missing | `-32602` | `Invalid params: command is required` | +| Command not registered | `-32602` | `Invalid params: Unknown command: ` | + +The invoked command's own handler may raise further errors — `-32602` for bad arguments, +`-32003` when the action is not permitted, `-32603` on internal failure. Clients must handle any +error code, not only the two above. + +**Capability gate:** A side MUST NOT send `command.execute` unless the peer advertised +`commands: true` in the handshake. A receiver that receives the call without having negotiated the +feature should respond with a JSON-RPC error. **Not currently enforced on receive by the viewer** +— the gate is applied only when sending. Implementation is tracked separately. + +**Example — extension asks viewer to teleport:** + +```json +{ + "jsonrpc": "2.0", + "method": "command.execute", + "id": 7, + "params": { + "command": "viewer.teleport", + "params": { "object_id": "550e8400-e29b-41d4-a716-446655440000" } + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "id": 7, + "result": { "success": true } +} +``` + +**Example — extension asks viewer to save object back to contents:** + +```json +{ + "jsonrpc": "2.0", + "method": "command.execute", + "id": 9, + "params": { + "command": "viewer.object.save_back_to_contents", + "params": { "object_id": "550e8400-e29b-41d4-a716-446655440000" } + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "id": 9, + "result": { "success": true, "result": { "object_id": "550e8400-e29b-41d4-a716-446655440000" } } +} +``` + +**Example — viewer asks extension to show a message:** + +```json +{ + "jsonrpc": "2.0", + "method": "command.execute", + "id": 8, + "params": { + "command": "editor.show_message", + "params": { "message": "Script reset complete", "level": "info" } + } +} +``` + +--- + +### CommandList + +**JSON-RPC Method:** `command.list` (call, bidirectional) + +Requests the list of commands the receiving side supports. Intended for tooling and autocomplete; implementations may omit this method and return an error response if discovery is not needed. + +This method takes no parameters. + +**Response:** + +```typescript +interface CommandListResponse { + commands: CommandInfo[]; +} + +interface CommandInfo { + command: string; + description?: string; + params?: Record; +} + +interface CommandParamInfo { + type: "string" | "number" | "boolean" | "object" | "array"; + required?: boolean; + description?: string; +} +``` + +**Response Fields:** + +- `commands`: Array of commands the responder supports. Each entry describes one command. + - `command`: The namespaced command identifier. + - `description` (optional): Human-readable description of what the command does. + - `params` (optional): Map of parameter names to their type descriptors. **Not currently + populated** — the viewer returns only `command` and `description`, so parameter discovery + does not work. Implementation is tracked separately. + +**Known viewer commands:** + +| Command | Required params | Description | +|---------|----------------|-------------| +| `viewer.teleport` | `object_id: string` | Teleport agent to an in-world object. | +| `viewer.camera.focus` | `object_id: string` | Zoom camera to an in-world object (same behavior as context menu Zoom In). | +| `viewer.object.save_back_to_contents` | `object_id: string` | Save an in-world object back to source object contents. | + +**Known extension commands:** + +| Command | Required params | Description | +|---------|----------------|-------------| +| `editor.open_file` | `path: string` | Open a file in the editor. Optional `line: number`. | +| `editor.show_message` | `message: string` | Show a notification. Optional `level: "info" \| "warn" \| "error"`. | diff --git a/indra/cmake/Boost.cmake b/indra/cmake/Boost.cmake index bc2f16553ba..1f5cc147d50 100644 --- a/indra/cmake/Boost.cmake +++ b/indra/cmake/Boost.cmake @@ -1,8 +1,8 @@ include_guard() add_library(ll::boost INTERFACE IMPORTED) -find_package(Boost CONFIG REQUIRED COMPONENTS context dll fiber filesystem program_options url) -target_link_libraries(ll::boost INTERFACE Boost::disable_autolinking Boost::headers Boost::dll Boost::fiber Boost::context Boost::filesystem Boost::program_options Boost::url) +find_package(Boost CONFIG REQUIRED COMPONENTS context dll fiber filesystem process program_options url) +target_link_libraries(ll::boost INTERFACE Boost::disable_autolinking Boost::headers Boost::dll Boost::fiber Boost::context Boost::filesystem Boost::process Boost::program_options Boost::url) if(WINDOWS) find_package(Boost CONFIG REQUIRED COMPONENTS stacktrace_windbg) @@ -11,3 +11,8 @@ else() find_package(Boost CONFIG REQUIRED COMPONENTS stacktrace_basic) target_link_libraries(ll::boost INTERFACE Boost::stacktrace_basic) endif() + +if(WINDOWS) + # Boost.Process v2 needs ntdll (NtSuspendProcess/NtResumeProcess) + target_link_libraries(ll::boost INTERFACE ntdll) +endif(WINDOWS) diff --git a/indra/cmake/Linking.cmake b/indra/cmake/Linking.cmake index 09392aa6032..ea2911cfe87 100644 --- a/indra/cmake/Linking.cmake +++ b/indra/cmake/Linking.cmake @@ -60,6 +60,7 @@ else() find_library(COREAUDIO_LIBRARY CoreAudio) find_library(COREGRAPHICS_LIBRARY CoreGraphics) find_library(AUDIOTOOLBOX_LIBRARY AudioToolbox) + find_library(UNIFORMTYPEIDENTIFIERS_LIBRARY UniformTypeIdentifiers) target_link_libraries( ll::oslibraries INTERFACE ${COCOA_LIBRARY} @@ -70,6 +71,7 @@ else() ${COREAUDIO_LIBRARY} ${AUDIOTOOLBOX_LIBRARY} ${COREGRAPHICS_LIBRARY} + ${UNIFORMTYPEIDENTIFIERS_LIBRARY} Threads::Threads ) endif() diff --git a/indra/llappearance/llpolymesh.cpp b/indra/llappearance/llpolymesh.cpp index 45f51950ec0..b93a2324198 100644 --- a/indra/llappearance/llpolymesh.cpp +++ b/indra/llappearance/llpolymesh.cpp @@ -862,6 +862,7 @@ LLPolyMesh *LLPolyMesh::getMesh(const std::string &name, LLPolyMesh* reference_m //----------------------------------------------------------------------------- void LLPolyMesh::freeAllMeshes() { + LL_PROFILE_ZONE_SCOPED; // delete each item in the global lists for_each(sGlobalSharedMeshList.begin(), sGlobalSharedMeshList.end(), DeletePairedPointer()); sGlobalSharedMeshList.clear(); diff --git a/indra/llappearance/lltexlayer.cpp b/indra/llappearance/lltexlayer.cpp index e1228c49660..76f92d5c2f2 100644 --- a/indra/llappearance/lltexlayer.cpp +++ b/indra/llappearance/lltexlayer.cpp @@ -1427,6 +1427,15 @@ void LLTexLayer::renderMorphMasks(S32 x, S32 y, S32 width, S32 height, const LLC // We should only be doing this when we believe something has changed with respect to the user's appearance. { LL_DEBUGS("Morph") << "gl alpha cache of morph mask not found, doing readback: " << getName() << LL_ENDL; + + // Replace the cached mask without leaking its old allocation. + alpha_cache_t::iterator cached = mAlphaCache.find(cache_index); + if (cached != mAlphaCache.end()) + { + ll_aligned_free_32(cached->second); + mAlphaCache.erase(cached); + } + // clear out a slot if we have filled our cache S32 max_cache_entries = getTexLayerSet()->getAvatarAppearance()->isSelf() ? 4 : 1; while ((S32)mAlphaCache.size() >= max_cache_entries) @@ -1463,6 +1472,7 @@ void LLTexLayer::renderMorphMasks(S32 x, S32 y, S32 width, S32 height, const LLC U8* temp = (U8*)ll_aligned_malloc_32(mem_size << 2); // allocate same size, but RGBA if (!temp) { + ll_aligned_free_32(alpha_data); LLError::LLUserWarningMsg::showOutOfMemory(); LL_ERRS() << "Failed to allocate temporary memory for morph texture readback: " << (S32)(mem_size << 2) << LL_ENDL; return; @@ -1505,6 +1515,7 @@ void LLTexLayer::renderMorphMasks(S32 x, S32 y, S32 width, S32 height, const LLC U8* temp_data = (U8*)ll_aligned_malloc_32(mem_size * TEMP_BYTES_PER_PIXEL); if (!temp_data) { + ll_aligned_free_32(alpha_data); LLError::LLUserWarningMsg::showOutOfMemory(); LL_ERRS() << "Failed to allocate temporary memory for morph texture: " << (S32)(mem_size * TEMP_BYTES_PER_PIXEL) << LL_ENDL; return; diff --git a/indra/llappearance/llwearable.cpp b/indra/llappearance/llwearable.cpp index 4acb0ef3d4a..5758f797914 100644 --- a/indra/llappearance/llwearable.cpp +++ b/indra/llappearance/llwearable.cpp @@ -727,17 +727,22 @@ void LLWearable::writeToAvatar(LLAvatarAppearance* avatarp) if (!avatarp) return; // Pull params - for( const LLVisualParam* param = avatarp->getFirstVisualParam(); param; param = avatarp->getNextVisualParam() ) + // Iterate over wearable's params first since they should be fewer than avatar params. + // Wearables are cloned from avatar params by mType, so they should be always be a + // subset of the avatar's params and a situation where mType param doesn't exist in + // a wearable yet exist in the avatar should not happen. + for (const visual_param_index_map_t::value_type& vp_pair : mVisualParamIndexMap) { + LLVisualParam* wearable_param = vp_pair.second; + // cross-wearable parameters are not authoritative, as they are driven by a different wearable. So don't copy the values to the // avatar object if cross wearable. Cross wearable params get their values from the avatar, they shouldn't write the other way. - if( (((LLViewerVisualParam*)param)->getWearableType() == mType) && (!((LLViewerVisualParam*)param)->getCrossWearable()) ) - { - S32 param_id = param->getID(); - F32 weight = getVisualParamWeight(param_id); + if (((LLViewerVisualParam*)wearable_param)->getCrossWearable()) + continue; - avatarp->setVisualParamWeight( param_id, weight); - } + F32 weight = wearable_param->getWeight(); + // Silently fails if param was not found + avatarp->setVisualParamWeight(vp_pair.first, mType, weight); } } diff --git a/indra/llaudio/llaudioengine_openal.cpp b/indra/llaudio/llaudioengine_openal.cpp index 11ab655f73a..835c8e57157 100644 --- a/indra/llaudio/llaudioengine_openal.cpp +++ b/indra/llaudio/llaudioengine_openal.cpp @@ -164,9 +164,122 @@ bool LLAudioEngine_OpenAL::init(void* userdata, const std::string &app_title) mDisconnectPollTimer.reset(); } + initSystemDefaultFollowing(); + return true; } +// ALC_SOFT_system_events tells us when the OS default output device +// changes; ALC_SOFT_reopen_device lets us move onto it without tearing +// down the context. Both are needed, and both only matter while the user +// is running on the system default — a pinned device stays pinned. +void LLAudioEngine_OpenAL::initSystemDefaultFollowing() +{ + mFollowSystemDefault = false; + mEventControlSOFT = nullptr; + mEventCallbackSOFT = nullptr; + + if (alcIsExtensionPresent(mALCDevice, "ALC_SOFT_reopen_device") != ALC_TRUE || + alcIsExtensionPresent(mALCDevice, "ALC_SOFT_system_events") != ALC_TRUE) + { + LL_INFOS() << "LLAudioEngine_OpenAL::init() ALC_SOFT_system_events / " + "ALC_SOFT_reopen_device unavailable; a restart is needed " + "to pick up OS default device changes." << LL_ENDL; + return; + } + + mEventControlSOFT = reinterpret_cast( + alcGetProcAddress(mALCDevice, "alcEventControlSOFT")); + mEventCallbackSOFT = reinterpret_cast( + alcGetProcAddress(mALCDevice, "alcEventCallbackSOFT")); + if (!mEventControlSOFT || !mEventCallbackSOFT) + { + mEventControlSOFT = nullptr; + mEventCallbackSOFT = nullptr; + LL_WARNS() << "LLAudioEngine_OpenAL::init() ALC_SOFT_system_events " + "advertised but its entry points did not resolve." + << LL_ENDL; + return; + } + + // Subscribe regardless of the current preference: the user can move + // back to "default" at runtime, and re-arming then would mean + // resolving entry points against a device we may no longer hold. + mDefaultDeviceChanged = false; + mEventCallbackSOFT(&LLAudioEngine_OpenAL::onDeviceEventSOFT, this); + ALCenum events[] = { ALC_EVENT_TYPE_DEFAULT_DEVICE_CHANGED_SOFT }; + mEventControlSOFT(1, events, ALC_TRUE); + + mFollowSystemDefault = mPreferredDevice.empty(); + if (mFollowSystemDefault) + { + LL_INFOS() << "LLAudioEngine_OpenAL::init() following the OS default " + "output device." << LL_ENDL; + } + else + { + LL_INFOS() << "LLAudioEngine_OpenAL::init() output device pinned to '" + << mPreferredDevice << "'; OS default changes ignored." + << LL_ENDL; + } +} + +// static +void ALC_APIENTRY LLAudioEngine_OpenAL::onDeviceEventSOFT(ALCenum event_type, ALCenum device_type, + ALCdevice* /*device*/, ALCsizei /*length*/, + const ALCchar* /*message*/, void* user_param) noexcept +{ + // Runs on OpenAL's own event thread. Do nothing here but raise the + // flag — idle() does the reopen on the main thread, where the rest + // of the engine's AL state is owned. + if (event_type != ALC_EVENT_TYPE_DEFAULT_DEVICE_CHANGED_SOFT) return; + + // The same event reports the default CAPTURE device changing. Reopening + // playback for that would interrupt audio because the user picked a + // different microphone. + if (device_type != ALC_PLAYBACK_DEVICE_SOFT) return; + + if (auto* self = static_cast(user_param)) + { + self->mDefaultDeviceChanged = true; + } +} + +void LLAudioEngine_OpenAL::reopenOnDefaultDevice() +{ + if (!mFollowSystemDefault || !mALCDevice) return; + + using ReopenFn = ALCboolean (ALC_APIENTRY *)(ALCdevice*, const ALCchar*, + const ALCint*); + auto reopen = reinterpret_cast( + alcGetProcAddress(mALCDevice, "alcReopenDeviceSOFT")); + if (!reopen) return; + + LL_INFOS() << "LLAudioEngine_OpenAL: OS default output device changed; " + "reopening (was '" << mActiveDevice << "')" << LL_ENDL; + + // Clear any pending device error so the failure branch reports ours. + (void)alcGetError(mALCDevice); + + if (reopen(mALCDevice, nullptr, nullptr) != ALC_TRUE) + { + LL_WARNS() << "LLAudioEngine_OpenAL::reopenOnDefaultDevice() " + "alcReopenDeviceSOFT failed: 0x" << std::hex + << alcGetError(mALCDevice) << std::dec << LL_ENDL; + return; + } + + if (const char* opened = alcGetString(mALCDevice, ALC_ALL_DEVICES_SPECIFIER)) + { + mActiveDevice = opened; + } + LL_INFOS() << "LLAudioEngine_OpenAL::reopenOnDefaultDevice() now on '" + << mActiveDevice << "'" << LL_ENDL; + + // The Sound prefs combo shows the active device, so let it refresh. + mDevicesChangedSignal(); +} + // virtual std::string LLAudioEngine_OpenAL::getDriverName(bool verbose) { @@ -218,6 +331,18 @@ void LLAudioEngine_OpenAL::shutdown() // ordering. shutdownEfx(); + // Unhook the OS default-device callback before the device is closed, + // so OpenAL's event thread can't call back into a half-torn engine. + if (mEventControlSOFT && mEventCallbackSOFT) + { + ALCenum events[] = { ALC_EVENT_TYPE_DEFAULT_DEVICE_CHANGED_SOFT }; + mEventControlSOFT(1, events, ALC_FALSE); + mEventCallbackSOFT(nullptr, nullptr); + } + mFollowSystemDefault = false; + mEventControlSOFT = nullptr; + mEventCallbackSOFT = nullptr; + LL_INFOS() << "About to LLAudioEngine::shutdown()" << LL_ENDL; LLAudioEngine::shutdown(); @@ -300,6 +425,12 @@ void LLAudioEngine_OpenAL::setOutputDevice(const std::string& id) return; // already running on this device } mPreferredDevice = id; + // Following the OS default is only correct while the user is ON the + // default; picking a device pins it, picking "default" resumes. Set it + // here so the early exits below cannot leave it disagreeing with + // mPreferredDevice. + mFollowSystemDefault = mPreferredDevice.empty() && mEventControlSOFT != nullptr; + mDefaultDeviceChanged = false; if (!mALCDevice) { @@ -361,6 +492,13 @@ void LLAudioEngine_OpenAL::idle() // independent of any of it and just rides on top once a second. LLAudioEngine::idle(); + // Raised on OpenAL's event thread by onDeviceEventSOFT. + if (mDefaultDeviceChanged) + { + mDefaultDeviceChanged = false; + reopenOnDefaultDevice(); + } + if (!mDisconnectExtAvailable || !mALCDevice) return; // Polling ALC_CONNECTED on every frame would generate an ALC call diff --git a/indra/llaudio/llaudioengine_openal.h b/indra/llaudio/llaudioengine_openal.h index 6e7d663392b..965024ea251 100644 --- a/indra/llaudio/llaudioengine_openal.h +++ b/indra/llaudio/llaudioengine_openal.h @@ -36,6 +36,7 @@ #include "llaudioengine.h" #include "lllistener_openal.h" #include "llwindgen.h" +#include "llatomic.h" #include #include @@ -151,6 +152,23 @@ class LLAudioEngine_OpenAL : public LLAudioEngine int mConsecutiveDisconnects = 0; LLFrameTimer mDisconnectPollTimer; + // ALC_SOFT_system_events + ALC_SOFT_reopen_device let us follow + // the OS default output device while the viewer is running. Only + // armed when the user hasn't pinned a specific device — if + // mPreferredDevice names one, following the OS default would drag + // them off their choice. mDefaultDeviceChanged is written from + // OpenAL's own event thread and consumed in idle(). + bool mFollowSystemDefault = false; + LPALCEVENTCONTROLSOFT mEventControlSOFT = nullptr; + LPALCEVENTCALLBACKSOFT mEventCallbackSOFT = nullptr; + LLAtomicBool mDefaultDeviceChanged{false}; + + void initSystemDefaultFollowing(); + static void ALC_APIENTRY onDeviceEventSOFT(ALCenum event_type, ALCenum device_type, + ALCdevice* device, ALCsizei length, + const ALCchar* message, void* user_param) noexcept; + void reopenOnDefaultDevice(); + // Pre-allocated buffer pool. Replaces the historical per-frame // alGenBuffers / alDeleteBuffers churn that ran every update // tick. At initWind we gen MAX_NUM_WIND_BUFFERS buffer ids diff --git a/indra/llcharacter/llcharacter.cpp b/indra/llcharacter/llcharacter.cpp index 4e8f4eca04c..963449b62a8 100644 --- a/indra/llcharacter/llcharacter.cpp +++ b/indra/llcharacter/llcharacter.cpp @@ -319,6 +319,31 @@ bool LLCharacter::setVisualParamWeight(S32 index, F32 weight) return false; } +//----------------------------------------------------------------------------- +// setVisualParamWeight() +//----------------------------------------------------------------------------- +bool LLCharacter::setVisualParamWeight(S32 index, S32 type, F32 weight) +{ + visual_param_index_map_t::iterator index_iter = mVisualParamIndexMap.find(index); + if (index_iter != mVisualParamIndexMap.end()) + { + LLVisualParam* param = index_iter->second; + if (param->getWearableType() == type) + { + param->setWeight(weight); + return true; + } + else + { + // setVisualParamWeight at the moment is only used in writeToAvatar. + // The type is supposed to match since wearable is a subset of avatar by type. + llassert(false); + LL_WARNS() << "Visual param index " << index << " is not of type " << type << LL_ENDL; + } + } + return false; +} + //----------------------------------------------------------------------------- // getVisualParamWeight() //----------------------------------------------------------------------------- diff --git a/indra/llcharacter/llcharacter.h b/indra/llcharacter/llcharacter.h index 7019802a32a..148e5f38142 100644 --- a/indra/llcharacter/llcharacter.h +++ b/indra/llcharacter/llcharacter.h @@ -202,6 +202,7 @@ class LLCharacter virtual bool setVisualParamWeight(const LLVisualParam *which_param, F32 weight); virtual bool setVisualParamWeight(const char* param_name, F32 weight); virtual bool setVisualParamWeight(S32 index, F32 weight); + virtual bool setVisualParamWeight(S32 index, S32 type, F32 weight); // get visual param weight by param or name F32 getVisualParamWeight(LLVisualParam *distortion); diff --git a/indra/llcharacter/llvisualparam.h b/indra/llcharacter/llvisualparam.h index 4ceb1720053..edaecaf96e9 100644 --- a/indra/llcharacter/llvisualparam.h +++ b/indra/llcharacter/llvisualparam.h @@ -119,6 +119,7 @@ class alignas(16) LLVisualParam // Pure virtuals //virtual bool parseData( LLXmlTreeNode *node ) = 0; virtual void apply( ESex avatar_sex ) = 0; + virtual S32 getWearableType() const = 0; // Default functions virtual void setWeight(F32 weight); virtual void setAnimationTarget( F32 target_value); diff --git a/indra/llcommon/llapr.cpp b/indra/llcommon/llapr.cpp index 8a7571ea5a6..9eaeb89fc64 100644 --- a/indra/llcommon/llapr.cpp +++ b/indra/llcommon/llapr.cpp @@ -135,11 +135,35 @@ apr_pool_t* LLAPRPool::getAPRPool() bool _ll_apr_warn_status(apr_status_t status, const char* file, int line) { if(APR_SUCCESS == status) return false; - - char buf[MAX_STRING]; /* Flawfinder: ignore */ + char buf[MAX_STRING]; apr_strerror(status, buf, sizeof(buf)); - LL_WARNS("APR") << "APR: " << file << ":" << line << " " << buf << LL_ENDL; +#ifdef LL_WINDOWS + // On Windows, APR error strings may be in the system's ANSI code page (e.g., Cyrillic) + // Convert to UTF-8 for proper logging + std::string error_msg = buf; + int wlen = MultiByteToWideChar(CP_ACP, 0, buf, -1, nullptr, 0); + if (wlen > 0) + { + std::wstring wbuf(wlen, L'\0'); + MultiByteToWideChar(CP_ACP, 0, buf, -1, &wbuf[0], wlen); + + int utf8len = WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), -1, nullptr, 0, nullptr, nullptr); + if (utf8len > 0) + { + std::string utf8buf(utf8len, '\0'); + WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), -1, &utf8buf[0], utf8len, nullptr, nullptr); + error_msg = utf8buf.c_str(); // Remove null terminator + } + LL_WARNS("APR") << "APR: " << file << ":" << line << " " << error_msg << " (0x" << std::hex << status << std::dec << ")" << LL_ENDL; + } + else + { + LL_WARNS("APR") << "APR: " << file << ":" << line << " " << buf << " (0x" << std::hex << status << std::dec << ")" << LL_ENDL; + } +#else + LL_WARNS("APR") << "APR: " << file << ":" << line << " " << buf << " (0x" << std::hex << status << std::dec << ")" << LL_ENDL; +#endif return true; } diff --git a/indra/llcommon/llcoros.cpp b/indra/llcommon/llcoros.cpp index b68bf00885a..380ad440d2b 100644 --- a/indra/llcommon/llcoros.cpp +++ b/indra/llcommon/llcoros.cpp @@ -253,6 +253,7 @@ void LLCoros::setStackSize(S32 stacksize) void LLCoros::printActiveCoroutines(const std::string& when) { + LL_PROFILE_ZONE_SCOPED; LL_INFOS("LLCoros") << "Number of active coroutines " << when << ": " << CoroData::instanceCount() << LL_ENDL; if (CoroData::instanceCount() > 0) diff --git a/indra/llcommon/llerror.h b/indra/llcommon/llerror.h index 99937b5aead..ae8b28ebc75 100644 --- a/indra/llcommon/llerror.h +++ b/indra/llcommon/llerror.h @@ -309,6 +309,7 @@ namespace LLError ERROR_OTHER = 0, ERROR_BAD_ALLOC = 1, ERROR_MISSING_FILES = 2, + ERROR_INIT_FAILED = 3, } eLastExecEvent; // tittle, message and error code to include in error marker file diff --git a/indra/llcommon/llprocess.cpp b/indra/llcommon/llprocess.cpp index 0bf1ac706ae..aac4af6f8f1 100644 --- a/indra/llcommon/llprocess.cpp +++ b/indra/llcommon/llprocess.cpp @@ -25,37 +25,133 @@ */ #include "linden_common.h" -#include "llwin32headers.h" - #include "llprocess.h" #include "llsdutil.h" #include "llsdserialize.h" #include "llsingleton.h" #include "llstring.h" -#include "stringize.h" -#include "llapr.h" -#include "apr_signal.h" #include "llevents.h" #include "llexception.h" +#include "stringize.h" -#include +#include +#include +#include +#include +#include #include #include +#include #include #include #include #include +#include +#include +#include +#include #include #include +#include #include +#if !LL_WINDOWS + // not necessarily available on random SDL platforms + // for waitpid() +#include +#include +#include +#endif + +#if LL_WINDOWS +#include "llwin32headers.h" +#include + +namespace { + // Global job object that will kill all child processes when parent terminates + HANDLE g_jobObject = NULL; + bool g_jobObjectInitialized = false; + std::mutex g_jobObjectMutex; + + void InitializeJobObject() + { + if (g_jobObjectInitialized) + return; + std::lock_guard lock(g_jobObjectMutex); + if (g_jobObjectInitialized) + return; + + g_jobObjectInitialized = true; + + // Create a job object + g_jobObject = ::CreateJobObjectW(NULL, NULL); + if (g_jobObject == NULL) + { + LL_WARNS("LLProcess") << "Failed to create job object: " << ::GetLastError() << LL_ENDL; + return; + } + + // Configure the job to kill all processes when the last handle closes + // (i.e., when the parent process exits) + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = { 0 }; + jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + // Windows 8+ supports nested jobs (for VS studio) + jeli.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_BREAKAWAY_OK; + + if (!::SetInformationJobObject(g_jobObject, JobObjectExtendedLimitInformation, + &jeli, sizeof(jeli))) + { + LL_WARNS("LLProcess") << "Failed to set job object limits: " << ::GetLastError() << LL_ENDL; + ::CloseHandle(g_jobObject); + g_jobObject = NULL; + return; + } + + LL_INFOS("LLProcess") << "Job object created - child processes will terminate with parent" << LL_ENDL; + } + + void AssignProcessToJob(HANDLE hProcess, const std::string& desc) + { + if (!g_jobObjectInitialized) + InitializeJobObject(); + + if (g_jobObject != NULL) + { + if (!::AssignProcessToJobObject(g_jobObject, hProcess)) + { + DWORD error = ::GetLastError(); + // ERROR_ACCESS_DENIED (5) means the process is already in a job + // This can happen if the parent viewer is itself in a job + if (error == ERROR_ACCESS_DENIED) + { + LL_WARNS("LLProcess") << "Autokill requested but process " << desc + << " is already in a job object (ERROR_ACCESS_DENIED)" + << LL_ENDL; + } + else + { + LL_WARNS("LLProcess") << "Failed to assign process " << desc + << " to job object: error " << error << LL_ENDL; + } + } + else + { + LL_DEBUGS("LLProcess") << "Process " << desc << " assigned to job object" << LL_ENDL; + } + } + } +} +#endif + +namespace bp = boost::process::v2; +namespace asio = boost::asio; + /***************************************************************************** * Helpers *****************************************************************************/ + static const char* whichfile_[] = { "stdin", "stdout", "stderr" }; -static std::string empty; -static LLProcess::Status interpret_status(int status); -static std::string getDesc(const LLProcess::Params& params); static std::string whichfile(LLProcess::FILESLOT index) { @@ -64,799 +160,865 @@ static std::string whichfile(LLProcess::FILESLOT index) return STRINGIZE("file slot " << index); } -/** - * Ref-counted "mainloop" listener. As long as there are still outstanding - * LLProcess objects, keep listening on "mainloop" so we can keep polling APR - * for process status. - */ -class LLProcessListener +std::ostream& operator<<(std::ostream& out, const LLProcess::Params& params) { - LOG_CLASS(LLProcessListener); -public: - LLProcessListener(): - mCount(0) - {} - - void addPoll(const LLProcess&) - { - // Unconditionally increment mCount. If it was zero before - // incrementing, listen on "mainloop". - if (mCount++ == 0) - { - LL_DEBUGS("LLProcess") << "listening on \"mainloop\"" << LL_ENDL; - mConnection = LLEventPumps::instance().obtain("mainloop") - .listen("LLProcessListener", boost::bind(&LLProcessListener::tick, this, _1)); - } - } - - void dropPoll(const LLProcess&) + if (params.cwd.isProvided()) { - // Unconditionally decrement mCount. If it's zero after decrementing, - // stop listening on "mainloop". - if (--mCount == 0) - { - LL_DEBUGS("LLProcess") << "disconnecting from \"mainloop\"" << LL_ENDL; - mConnection.disconnect(); - } + out << "cd " << LLStringUtil::quote(params.cwd) << ": "; } - -private: - /// called once per frame by the "mainloop" LLEventPump - bool tick(const LLSD&) + out << LLStringUtil::quote(params.executable); + for (const std::string& arg : params.args) { - // Tell APR to sense whether each registered LLProcess is still - // running and call handle_status() appropriately. We should be able - // to get the same info from an apr_proc_wait(APR_NOWAIT) call; but at - // least in APR 1.4.2, testing suggests that even with APR_NOWAIT, - // apr_proc_wait() blocks the caller. We can't have that in the - // viewer. Hence the callback rigmarole. (Once we update APR, it's - // probably worth testing again.) Also -- although there's an - // apr_proc_other_child_refresh() call, i.e. get that information for - // one specific child, it accepts an 'apr_other_child_rec_t*' that's - // mentioned NOWHERE else in the documentation or header files! I - // would use the specific call in LLProcess::getStatus() if I knew - // how. As it is, each call to apr_proc_other_child_refresh_all() will - // call callbacks for ALL still-running child processes. That's why we - // centralize such calls, using "mainloop" to ensure it happens once - // per frame, and refcounting running LLProcess objects to remain - // registered only while needed. - LL_DEBUGS("LLProcess") << "calling apr_proc_other_child_refresh_all()" << LL_ENDL; - apr_proc_other_child_refresh_all(APR_OC_REASON_RUNNING); - return false; + out << ' ' << LLStringUtil::quote(arg); } - - /// If this object is destroyed before mCount goes to zero, stop - /// listening on "mainloop" anyway. - LLTempBoundListener mConnection; - unsigned mCount; -}; -static LLProcessListener sProcessListener; - + return out; +} /***************************************************************************** -* WritePipe and ReadPipe +* Helper classes for pipe I/O *****************************************************************************/ -LLProcess::BasePipe::~BasePipe() {} -const LLProcess::BasePipe::size_type - // use funky syntax to call max() to avoid blighted max() macros - LLProcess::BasePipe::npos((std::numeric_limits::max)()); -class WritePipeImpl: public LLProcess::WritePipe +class WritePipeImpl : public LLProcess::WritePipe { LOG_CLASS(WritePipeImpl); public: - WritePipeImpl(const std::string& desc, apr_file_t* pipe): + WritePipeImpl(const std::string& desc, + std::shared_ptr pipe) : mDesc(desc), mPipe(pipe), - // Essential to initialize our std::ostream with our special streambuf! - mStream(&mStreambuf) + mStream(&mStreambuf), + mWritePending(false) + {} + + virtual ~WritePipeImpl() = default; + + virtual std::ostream& get_ostream() override { return mStream; } + + virtual size_type size() const override { - mConnection = LLEventPumps::instance().obtain("mainloop") - .listen(LLEventPump::inventName("WritePipe"), - boost::bind(&WritePipeImpl::tick, this, _1)); - -#if ! LL_WINDOWS - // We can't count on every child process reading everything we try to - // write to it. And if the child terminates with WritePipe data still - // pending, unless we explicitly suppress it, Posix will hit us with - // SIGPIPE. That would terminate the viewer, boom. "Ignoring" it means - // APR gets the correct errno, passes it back to us, we log it, etc. - signal(SIGPIPE, SIG_IGN); -#endif + return mStreambuf.size(); } - virtual std::ostream& get_ostream() { return mStream; } - virtual size_type size() const { return mStreambuf.size(); } + // Called from LLProcess::tick() to initiate writing buffered data. + void tick() override + { + startAsyncWrite(); + } - bool tick(const LLSD&) +private: + void startAsyncWrite() { - typedef boost::asio::streambuf::const_buffers_type const_buffer_sequence; - // If there's anything to send, try to send it. - std::size_t total(mStreambuf.size()), consumed(0); - if (total) + if (mWritePending || !mPipe || !mPipe->is_open() || mStreambuf.size() == 0) + return; + + mWritePending = true; + + // Move the pending bytes into a buffer this operation owns. Handing + // async_write a view into mStreambuf would not survive a concurrent + // append: writing through get_ostream() calls overflow() -> reserve(), + // which memmoves the pending data to offset 0 and may reallocate the + // underlying vector, leaving the in-flight buffer dangling. Fixing the + // length is not enough, because it is the address that moves. A + // read completion dispatched from the same poll_one() loop as this + // write can do precisely that (LLLeap answers stdout on stdin). + const std::size_t writeSize = mStreambuf.size(); + mWriteBuf.resize(writeSize); + asio::buffer_copy(asio::buffer(mWriteBuf), mStreambuf.data(), writeSize); + mStreambuf.consume(writeSize); + + // Do NOT self-chain in the completion handler: sending the next buffer + // immediately can cause the child to respond within the same tick(), + // which posts a read event before the test listener has a chance to + // disconnect -- that was the root cause of test 18 "more than 3 + // events" and test 9 "many small messages" failures. tick() calls + // mWritePipe->tick() on every mainloop frame, so anything queued + // meanwhile is sent then. + asio::async_write(*mPipe, asio::buffer(mWriteBuf), + [this](const boost::system::error_code& ec, std::size_t bytes_transferred) { - const_buffer_sequence bufs = mStreambuf.data(); - // In general, our streambuf might contain a number of different - // physical buffers; iterate over those. - bool keepwriting = true; - for (auto bufi(boost::asio::buffer_sequence_begin(bufs)), bufend(boost::asio::buffer_sequence_end(bufs)); - bufi != bufend && keepwriting; ++bufi) - { - // http://www.boost.org/doc/libs/1_49_0_beta1/doc/html/boost_asio/reference/buffer.html#boost_asio.reference.buffer.accessing_buffer_contents - // Although apr_file_write() accepts const void*, we - // manipulate const char* so we can increment the pointer. - const char* remainptr = static_cast(bufi->data()); - std::size_t remainlen = bufi->size(); - while (remainlen) - { - // Tackle the current buffer in discrete chunks. On - // Windows, we've observed strange failures when trying to - // write big lengths (~1 MB) in a single operation. Even a - // 32K chunk seems too large. At some point along the way - // apr_file_write() returns 11 (Resource temporarily - // unavailable, i.e. EAGAIN) and says it wrote 0 bytes -- - // even though it did write the chunk! Our next write - // attempt retries with the same chunk, resulting in the - // chunk being duplicated at the child end. Using smaller - // chunks is empirically more reliable. - std::size_t towrite((std::min)(remainlen, std::size_t(4*1024))); - apr_size_t written(towrite); - apr_status_t err = apr_file_write(mPipe, remainptr, &written); - // EAGAIN is exactly what we want from a nonblocking pipe. - // Rather than waiting for data, it should return immediately. - if (! (err == APR_SUCCESS || APR_STATUS_IS_EAGAIN(err))) - { - LL_WARNS("LLProcess") << "apr_file_write(" << towrite << ") on " << mDesc - << " got " << err << ":" << LL_ENDL; - ll_apr_warn_status(err); - } - - // 'written' is modified to reflect the number of bytes actually - // written. Make sure we consume those later. (Don't consume them - // now, that would invalidate the buffer iterator sequence!) - consumed += written; - // don't forget to advance to next chunk of current buffer - remainptr += written; - remainlen -= written; - - LL_DEBUGS("LLProcess") << "wrote " << written << " of " << towrite - << " bytes to " << mDesc - << " (original " << total << ")," - << " code " << err << ": "; - char msgbuf[512]; - LL_CONT << apr_strerror(err, msgbuf, sizeof(msgbuf)) - << LL_ENDL; - - // The parent end of this pipe is nonblocking. If we weren't able - // to write everything we wanted, don't keep banging on it -- that - // won't change until the child reads some. Wait for next tick(). - if (written < towrite) - { - keepwriting = false; // break outer loop over buffers too - break; - } - } // next chunk of current buffer - } // next buffer - // In all, we managed to write 'consumed' bytes. Remove them from the - // streambuf so we don't keep trying to send them. This could be - // anywhere from 0 up to mStreambuf.size(); anything we haven't yet - // sent, we'll try again later. - mStreambuf.consume(consumed); - } + mWritePending = false; - return false; + if (!ec) + { + LL_DEBUGS("LLProcess") << "Wrote " << bytes_transferred + << " bytes to " << mDesc << LL_ENDL; + } + else if (ec != asio::error::operation_aborted) + { + // async_write only reports short counts alongside an error, and + // the pipe is unusable by then, so the remainder is dropped. + LL_WARNS("LLProcess") << "Write error on " << mDesc << " after " + << bytes_transferred << " of " << mWriteBuf.size() + << " bytes: " << ec.message() << LL_ENDL; + } + }); } -private: std::string mDesc; - apr_file_t* mPipe; - LLTempBoundListener mConnection; - boost::asio::streambuf mStreambuf; + std::shared_ptr mPipe; + asio::streambuf mStreambuf; + // Owned by the in-flight async_write; see startAsyncWrite(). + std::vector mWriteBuf; std::ostream mStream; + bool mWritePending; }; -class ReadPipeImpl: public LLProcess::ReadPipe +class ReadPipeImpl : public LLProcess::ReadPipe { LOG_CLASS(ReadPipeImpl); public: - ReadPipeImpl(const std::string& desc, apr_file_t* pipe, LLProcess::FILESLOT index): + ReadPipeImpl(const std::string& desc, + std::shared_ptr pipe, + LLProcess::FILESLOT slot) : mDesc(desc), mPipe(pipe), - mIndex(index), - // Essential to initialize our std::istream with our special streambuf! + mSlot(slot), mStream(&mStreambuf), - mPump("ReadPipe", true), // tweak name as needed to avoid collisions + mPump("ReadPipe", true), mLimit(0), mEOF(false) { - mConnection = LLEventPumps::instance().obtain("mainloop") - .listen(LLEventPump::inventName("ReadPipe"), - boost::bind(&ReadPipeImpl::tick, this, _1)); + // Start async read + startAsyncRead(); } - ~ReadPipeImpl() + virtual ~ReadPipeImpl() { - if (mConnection.connected()) + if (mPipe && mPipe->is_open()) { - mConnection.disconnect(); + boost::system::error_code ec; + mPipe->close(ec); } } - // Much of the implementation is simply connecting the abstract virtual - // methods with implementation data concealed from the base class. - virtual std::istream& get_istream() { return mStream; } - virtual std::string getline() { return LLProcess::getline(mStream); } - virtual LLEventPump& getPump() { return mPump; } - virtual void setLimit(size_type limit) { mLimit = limit; } - virtual size_type getLimit() const { return mLimit; } - virtual size_type size() const { return mStreambuf.size(); } + virtual std::istream& get_istream() override { return mStream; } + + virtual std::string getline() override + { + return LLProcess::getline(mStream); + } + + virtual LLEventPump& getPump() override { return mPump; } + + virtual void setLimit(size_type limit) override { mLimit = limit; } + + virtual size_type getLimit() const override { return mLimit; } - virtual std::string read(size_type len) + virtual bool atEOF() const override { return mEOF; } + + virtual size_type size() const override { return mStreambuf.size(); } + + virtual std::string read(size_type len) override { - // Read specified number of bytes into a buffer. - size_type readlen((std::min)(size(), len)); - // Formally, &buffer[0] is invalid for a vector of size() 0. Exit - // early in that situation. - if (! readlen) + size_type readlen = (std::min)(size(), len); + if (!readlen) return ""; - // Make a buffer big enough. + std::vector buffer(readlen); mStream.read(&buffer[0], readlen); - // Since we've already clamped 'readlen', we can think of no reason - // why mStream.read() should read fewer than 'readlen' bytes. - // Nonetheless, use the actual retrieved length. return std::string(&buffer[0], mStream.gcount()); } - virtual std::string peek(size_type offset=0, size_type len=npos) const + virtual std::string peek(size_type offset = 0, size_type len = npos) const override { - // Constrain caller's offset and len to overlap actual buffer content. std::size_t real_offset = (std::min)(mStreambuf.size(), std::size_t(offset)); - size_type want_end = (len == npos)? npos : (real_offset + len); - std::size_t real_end = (std::min)(mStreambuf.size(), std::size_t(want_end)); - boost::asio::streambuf::const_buffers_type cbufs = mStreambuf.data(); - return std::string(boost::asio::buffers_begin(cbufs) + real_offset, - boost::asio::buffers_begin(cbufs) + real_end); + size_type want_end = (len == npos) ? npos : (real_offset + len); + std::size_t real_end = (std::min)(mStreambuf.size(), std::size_t(want_end)); + + auto cbufs = mStreambuf.data(); + return std::string(asio::buffers_begin(cbufs) + real_offset, + asio::buffers_begin(cbufs) + real_end); } - virtual size_type find(const std::string& seek, size_type offset=0) const + virtual size_type find(const std::string& seek, size_type offset = 0) const override { - // If we're passing a string of length 1, use find(char), which can - // use an O(n) std::find() rather than the O(n^2) std::search(). if (seek.length() == 1) - { return find(seek[0], offset); - } - // If offset is beyond the whole buffer, can't even construct a valid - // iterator range; can't possibly find the string we seek. if (offset > mStreambuf.size()) - { return npos; - } - boost::asio::streambuf::const_buffers_type cbufs = mStreambuf.data(); - boost::asio::buffers_iterator - begin(boost::asio::buffers_begin(cbufs)), - end (boost::asio::buffers_end(cbufs)), - found(std::search(begin + offset, end, seek.begin(), seek.end())); - return (found == end)? npos : (found - begin); + auto cbufs = mStreambuf.data(); + auto begin = asio::buffers_begin(cbufs); + auto end = asio::buffers_end(cbufs); + auto found = std::search(begin + offset, end, seek.begin(), seek.end()); + return (found == end) ? npos : (found - begin); } - virtual size_type find(char seek, size_type offset=0) const + virtual size_type find(char seek, size_type offset = 0) const override { - // If offset is beyond the whole buffer, can't even construct a valid - // iterator range; can't possibly find the char we seek. if (offset > mStreambuf.size()) - { return npos; - } - boost::asio::streambuf::const_buffers_type cbufs = mStreambuf.data(); - boost::asio::buffers_iterator - begin(boost::asio::buffers_begin(cbufs)), - end (boost::asio::buffers_end(cbufs)), - found(std::find(begin + offset, end, seek)); - return (found == end)? npos : (found - begin); + auto cbufs = mStreambuf.data(); + auto begin = asio::buffers_begin(cbufs); + auto end = asio::buffers_end(cbufs); + auto found = std::find(begin + offset, end, seek); + return (found == end) ? npos : (found - begin); } - bool tick(const LLSD&) +private: + void startAsyncRead() { - // Once we've hit EOF, skip all the rest of this. - if (mEOF) - return false; - - typedef boost::asio::streambuf::mutable_buffers_type mutable_buffer_sequence; - // Try, every time, to read into our streambuf. In fact, we have no - // idea how much data the child might be trying to send: keep trying - // until we're convinced we've temporarily exhausted the pipe. - enum PipeState { RETRY, EXHAUSTED, CLOSED }; - PipeState state = RETRY; - std::size_t committed(0); - do + if (!mPipe || !mPipe->is_open() || mEOF) + return; + + // Always read data regardless of mLimit to prevent INTEGRATION_TEST_llleap + // deadlock: stopping reads fills the OS pipe buffer and blocks the child, + // which prevents large (~1 MB) messages from being fully received. + // mLimit only controls how many bytes appear in event notifications. + auto bufs = mStreambuf.prepare(4096); + + mPipe->async_read_some(bufs, + [this](const boost::system::error_code& ec, std::size_t bytes_transferred) { - // attempt to read an arbitrary size - mutable_buffer_sequence bufs = mStreambuf.prepare(4096); - // In general, the mutable_buffer_sequence returned by prepare() might - // contain a number of different physical buffers; iterate over those. - std::size_t tocommit(0); - for (auto bufi(boost::asio::buffer_sequence_begin(bufs)), bufend(boost::asio::buffer_sequence_end(bufs)); - bufi != bufend; ++bufi) + if (!ec) + { + mStreambuf.commit(bytes_transferred); + LL_DEBUGS("LLProcess") << "Read " << bytes_transferred + << " bytes from " << mDesc << LL_ENDL; + + // Restore original 7-field event contract: + // data, len, slot, name, desc, eof, exhst + // A successful async read corresponds to the original EXHAUSTED + // state: we got data, pipe is not yet closed. + size_type data_len = (std::min)(mStreambuf.size(), mLimit); + LLSD event; + event["data"] = peek(0, data_len); + event["len"] = LLSD::Integer(mStreambuf.size()); + event["slot"] = LLSD::Integer(mSlot); + event["name"] = whichfile(mSlot); + event["desc"] = mDesc; + event["eof"] = false; + event["exhst"] = true; + + // Arm the next read before posting: LLEventStream::post() is synchronous + // and listeners may destroy this object. + startAsyncRead(); + mPump.post(event); + } + else if (ec == asio::error::eof +#if LL_WINDOWS + // On Windows anonymous pipes, the write end closing + // delivers ERROR_BROKEN_PIPE (109), not asio::error::eof. + || ec.value() == ERROR_BROKEN_PIPE +#endif + ) { - // http://www.boost.org/doc/libs/1_49_0_beta1/doc/html/boost_asio/reference/buffer.html#boost_asio.reference.buffer.accessing_buffer_contents - std::size_t toread(bufi->size()); - apr_size_t gotten(toread); - apr_status_t err = apr_file_read(mPipe, - bufi->data(), - &gotten); - // EAGAIN is exactly what we want from a nonblocking pipe. - // Rather than waiting for data, it should return immediately. - if (! (err == APR_SUCCESS || APR_STATUS_IS_EAGAIN(err))) + if (bytes_transferred > 0) { - // Handle EOF specially: it's part of normal-case processing. - if (err == APR_EOF) - { - LL_DEBUGS("LLProcess") << "EOF on " << mDesc << LL_ENDL; - } - else - { - LL_WARNS("LLProcess") << "apr_file_read(" << toread << ") on " << mDesc - << " got " << err << ":" << LL_ENDL; - ll_apr_warn_status(err); - } - // Either way, though, we won't need any more tick() calls. - mConnection.disconnect(); - // Ignore any subsequent calls we might get anyway. - mEOF = true; - state = CLOSED; // also break outer retry loop - break; + mStreambuf.commit(bytes_transferred); } - // 'gotten' was modified to reflect the number of bytes actually - // received. Make sure we commit those later. (Don't commit them - // now, that would invalidate the buffer iterator sequence!) - tocommit += gotten; - LL_DEBUGS("LLProcess") << "filled " << gotten << " of " << toread - << " bytes from " << mDesc << LL_ENDL; - - // The parent end of this pipe is nonblocking. If we weren't even - // able to fill this buffer, don't loop to try to fill the next -- - // that won't change until the child writes more. Wait for next - // tick(). - if (gotten < toread) - { - // break outer retry loop too - state = EXHAUSTED; - break; - } + mEOF = true; + LL_DEBUGS("LLProcess") << "EOF on " << mDesc << LL_ENDL; + + // Match the original behavior: pack all 7 fields into the + // single eof event so consumers can "use it or lose it" -- + // this is the last chance to see any remaining buffered data. + // EOF corresponds to the original CLOSED state. + size_type data_len = (std::min)(mStreambuf.size(), mLimit); + LLSD eof_event; + eof_event["data"] = peek(0, data_len); + eof_event["len"] = LLSD::Integer(mStreambuf.size()); + eof_event["slot"] = LLSD::Integer(mSlot); + eof_event["name"] = whichfile(mSlot); + eof_event["desc"] = mDesc; + eof_event["eof"] = true; + eof_event["exhst"] = false; + + mPump.post(eof_event); } - - // Don't forget to "commit" the data! - mStreambuf.commit(tocommit); - committed += tocommit; - - // state is changed from RETRY when we can't fill any one buffer - // of the mutable_buffer_sequence established by the current - // prepare() call -- whether due to error or not enough bytes. - // That is, if state is still RETRY, we've filled every physical - // buffer in the mutable_buffer_sequence. In that case, for all we - // know, the child might have still more data pending -- go for it! - } while (state == RETRY); - - // Once we recognize that the pipe is closed, make one more call to - // listener. The listener might be waiting for a particular substring - // to arrive, or a particular length of data or something. The event - // with "eof" == true announces that nothing further will arrive, so - // use it or lose it. - if (committed || state == CLOSED) - { - // If we actually received new data, publish it on our LLEventPump - // as advertised. Constrain it by mLimit. But show listener the - // actual accumulated buffer size, regardless of mLimit. - size_type datasize((std::min)(mLimit, size_type(mStreambuf.size()))); - mPump.post(LLSDMap - ("data", peek(0, datasize)) - ("len", LLSD::Integer(mStreambuf.size())) - ("slot", LLSD::Integer(mIndex)) - ("name", whichfile(mIndex)) - ("desc", mDesc) - ("eof", state == CLOSED) - ("exhst", state == EXHAUSTED)); - } - - return false; + else if (ec != asio::error::operation_aborted) + { + LL_WARNS("LLProcess") << "Read error on " << mDesc + << ": " << ec.message() << LL_ENDL; + } + }); } -private: std::string mDesc; - apr_file_t* mPipe; - LLProcess::FILESLOT mIndex; - LLTempBoundListener mConnection; - boost::asio::streambuf mStreambuf; + std::shared_ptr mPipe; + LLProcess::FILESLOT mSlot; + mutable asio::streambuf mStreambuf; std::istream mStream; - LLEventStream mPump; + LLEventStream mPump; // pump specific to this pipe size_type mLimit; bool mEOF; }; /***************************************************************************** -* LLProcess itself +* LLProcess implementation *****************************************************************************/ -/// Need an exception to avoid constructing an invalid LLProcess object, but -/// internal use only -struct LLProcessError: public LLException + +const LLProcess::BasePipe::size_type LLProcess::BasePipe::npos = +static_cast(-1); + +LLProcess::LLProcess(const Params& params) : + mStatus(), + mDesc(params.desc.isProvided() ? params.desc() : basename(params.executable())), + mPostend(params.postend.isProvided() ? params.postend() : ""), + mAutokill(params.autokill), + mAttached(params.attached.isProvided() ? params.attached() : bool(params.autokill)), + mKillCalled(false) { - LLProcessError(const std::string& msg): LLException(msg) {} -}; + launch(params); +} +LLProcess::~LLProcess() +{ + if (mChild && mStatus.mState == RUNNING) + { + if (mAttached && mAutokill && !mKillCalled) + { + LL_INFOS("LLProcess") << "Terminating child process " << mDesc << LL_ENDL; + boost::system::error_code ec; + mChild->terminate(ec); + +#if !LL_WINDOWS + // On POSIX, terminate() sends SIGTERM which allows graceful shutdown. + // Poll with waitpid(WNOHANG) rather than mChild->running() to avoid + // competing with tick()'s own waitpid call. + pid_t pid = mChild->id(); + for (int i = 0; i < 30; ++i) + { + int child_status = 0; + pid_t w = ::waitpid(pid, &child_status, WNOHANG); + if (w == pid || (w == -1 && errno == ECHILD)) + break; // child exited or was already reaped + if (w == -1 && errno != EINTR) + { + LL_WARNS("LLProcess") << "waitpid(" << pid << ") failed while terminating " + << mDesc << ": " << strerror(errno) << LL_ENDL; + break; + } + // Sleep before next poll: applies to both EINTR and + // still-running (w == 0) cases. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // Force kill if still running + { + int child_status; + if (::waitpid(pid, &child_status, WNOHANG) == 0) + { + LL_WARNS("LLProcess") << "Force killing " << mDesc << LL_ENDL; + (void)::kill(pid, SIGKILL); + } + } +#else + // On Windows, terminate() already does an immediate hard kill via TerminateProcess() + // Only wait if terminate succeeded (process was running) + + if (!ec) + { + DWORD exit_code = 0; + bool still_running = (::GetExitCodeProcess(mChild->native_handle(), &exit_code) + && exit_code == STILL_ACTIVE); + if (still_running) + { + // We are likely on the main thread. Don't wait long! + // Maybe shouldn't wait at all. + WaitForSingleObject(mChild->native_handle(), 10); + } + } + else + { + LL_WARNS("LLProcess") << "Process " << mDesc + << " terminate failed with error " << ec.value() + << " (" << ec.message() << "), skipping wait" << LL_ENDL; + } +#endif + } + else if (!mKillCalled) + { + LL_INFOS("LLProcess") << "Not terminating " << mDesc + << " (attached=" << mAttached + << ", autokill=" << mAutokill << ")" << LL_ENDL; + // Detach the bp::process so its destructor does not send SIGKILL + // to the still-running process (boost::process v2 terminates the + // child in bp::process::~process() if the handle is still valid). + mChild->detach(); + } + } + + if (mMainloopConnection.connected()) + { + mMainloopConnection.disconnect(); + } +} + +//static LLProcessPtr LLProcess::create(const LLSDOrParams& params) { try { - return LLProcessPtr(new LLProcess(params)); + // Construct and then register the mainloop connection separately. + // connectMainloop() calls shared_from_this(), which requires the + // shared_ptr control block to be fully initialized — this is only true + // after make_shared returns, not during the constructor. + LLProcessPtr ptr = std::make_shared(params); + ptr->connectMainloop(); + return ptr; } - catch (const LLProcessError& e) + catch (const std::exception& e) { - LL_WARNS("LLProcess") << e.what() << LL_ENDL; + LL_WARNS("LLProcess") << "Failed to create process: " << e.what() << LL_ENDL; - // If caller is requesting an event on process termination, send one - // indicating bad launch. This may prevent someone waiting forever for - // a termination post that can't arrive because the child never - // started. - if (params.postend.isProvided()) + // Even on failure, fire the postend event if requested, so callers + // that listen for it can detect the launch failure. + if (params.postend.isProvided() && !params.postend().empty()) { - LLEventPumps::instance().obtain(params.postend) - .post(LLSDMap - // no "id" - ("desc", getDesc(params)) - ("state", LLProcess::UNSTARTED) - // no "data" - ("string", e.what()) - ); + std::string desc = params.desc.isProvided() ? params.desc() : + (params.executable.isProvided() ? LLProcess::basename(params.executable()) : ""); + LLSD event; + event["desc"] = desc; + event["state"] = LLProcess::UNSTARTED; + event["string"] = e.what(); + LLEventPumps::instance().obtain(params.postend()).post(event); } return LLProcessPtr(); } } -/// Call an apr function returning apr_status_t. On failure, log warning and -/// throw LLProcessError mentioning the function call that produced that -/// result. -#define chkapr(func) \ - if (ll_apr_warn_status(func)) \ - throw LLProcessError(#func " failed") - -LLProcess::LLProcess(const LLSDOrParams& params): - mAutokill(params.autokill), - // Because 'autokill' originally meant both 'autokill' and 'attached', to - // preserve existing semantics, we promise that mAttached defaults to the - // same setting as mAutokill. - mAttached(params.attached.isProvided()? params.attached : params.autokill), - mPool(NULL) +void LLProcess::launch(const LLSDOrParams& params) { - mPipes.resize(NSLOTS); - - if (! params.validateBlock(true)) + if (!params.validateBlock(true)) { - LLTHROW(LLProcessError(STRINGIZE("not launched: failed parameter validation\n" - << LLSDNotationStreamer(params)))); + LL_WARNS("LLProcess") << "Failed parameter validation " << LLSDNotationStreamer(params) << LL_ENDL; + throw std::runtime_error("not launched: failed parameter validation\n"); } - mPostend = params.postend; + // Validate FileParam types before attempting to launch + int file_idx = 0; + for (const auto& fparam : params.files) + { + if (fparam.type.isProvided()) + { + const std::string& type = fparam.type(); + // Only "" (inherit) and "pipe" are supported + if (!type.empty() && type != "pipe") + { + std::string slotname; + switch (file_idx) + { + case STDIN: slotname = "stdin"; break; + case STDOUT: slotname = "stdout"; break; + case STDERR: slotname = "stderr"; break; + default: slotname = STRINGIZE("file slot " << file_idx); break; + } + + LL_WARNS("LLProcess") << "For " << params.executable() + << ": unsupported FileParam for " << slotname + << ": type='" << type << "'"; + + if (fparam.name.isProvided()) + { + LL_CONT << ", name='" << fparam.name() << "'"; + } - apr_pool_create(&mPool, gAPRPoolp); - if (!mPool) + LL_CONT << LL_ENDL; + + throw std::runtime_error( + STRINGIZE("unsupported FileParam type '" << type + << "' for " << slotname)); + } + + // Warn about internal pipe names (not yet supported) + if (type == "pipe" && fparam.name.isProvided() && !fparam.name().empty()) + { + LL_WARNS("LLProcess") << "Internal pipe name '" << fparam.name() + << "' not yet supported; ignoring" << LL_ENDL; + } + } + file_idx++; + } + + // Build arguments vector + std::vector args; + for (const auto& arg : params.args) { - LLTHROW(LLProcessError(STRINGIZE("failed to create apr pool"))); + args.push_back(arg); } - apr_procattr_t *procattr = NULL; - chkapr(apr_procattr_create(&procattr, mPool)); - - // IQA-490, CHOP-900: On Windows, ask APR to jump through hoops to - // constrain the set of handles passed to the child process. Before we - // changed to APR, the Windows implementation of LLProcessLauncher called - // CreateProcess(bInheritHandles=false), meaning to pass NO open handles - // to the child process. Now that we support pipes, though, we must allow - // apr_proc_create() to pass bInheritHandles=true. But without taking - // special pains, that causes trouble in a number of ways, due to the fact - // that the viewer is constantly opening and closing files -- most of - // which CreateProcess() passes to every child process! -#if ! defined(APR_HAS_PROCATTR_CONSTRAIN_HANDLE_SET) - // Our special preprocessor symbol isn't even defined -- wrong APR - LL_WARNS("LLProcess") << "This version of APR lacks Linden " - << "apr_procattr_constrain_handle_set() extension" << LL_ENDL; -#else - chkapr(apr_procattr_constrain_handle_set(procattr, 1)); -#endif + // Determine pipe configuration + bool use_stdin_pipe = false; + bool use_stdout_pipe = false; + bool use_stderr_pipe = false; - // For which of stdin, stdout, stderr should we create a pipe to the - // child? In the viewer, there are only a couple viable - // apr_procattr_io_set() alternatives: inherit the viewer's own stdxxx - // handle (APR_NO_PIPE, e.g. for stdout, stderr), or create a pipe that's - // blocking on the child end but nonblocking at the viewer end - // (APR_CHILD_BLOCK). - // Other major options could include explicitly creating a single APR pipe - // and passing it as both stdout and stderr (apr_procattr_child_out_set(), - // apr_procattr_child_err_set()), or accepting a filename, opening it and - // passing that apr_file_t (simple <, >, 2> redirect emulation). - std::vector select; - for (const FileParam& fparam : params.files) + file_idx = 0; + for (const auto& fparam : params.files) { - // Every iteration, we're going to append an item to 'select'. At the - // top of the loop, its size() is, in effect, an index. Use that to - // pick a string description for messages. - std::string which(whichfile(FILESLOT(select.size()))); - if (fparam.type().empty()) // inherit our file descriptor - { - select.push_back(APR_NO_PIPE); - } - else if (fparam.type() == "pipe") // anonymous pipe + if (fparam.type.isProvided() && fparam.type() == "pipe") { - if (! fparam.name().empty()) + switch (file_idx) { - LL_WARNS("LLProcess") << "For " << params.executable() - << ": internal names for reusing pipes ('" - << fparam.name() << "' for " << which - << ") are not yet supported -- creating distinct pipe" - << LL_ENDL; + case STDIN: use_stdin_pipe = true; break; + case STDOUT: use_stdout_pipe = true; break; + case STDERR: use_stderr_pipe = true; break; } - // The viewer can't block for anything: the parent end MUST be - // nonblocking. As the APR documentation itself points out, it - // makes very little sense to set nonblocking I/O for the child - // end of a pipe: only a specially-written child could deal with - // that. - select.push_back(APR_CHILD_BLOCK); - } - else - { - LLTHROW(LLProcessError(STRINGIZE("For " << params.executable() - << ": unsupported FileParam for " << which - << ": type='" << fparam.type() - << "', name='" << fparam.name() << "'"))); } + file_idx++; } - // By default, pass APR_NO_PIPE for unspecified slots. - while (select.size() < NSLOTS) + + // Create pipes if needed - v2 uses asio pipes directly + // NOTE: From parent's perspective: write to stdin, read from stdout/stderr + if (use_stdin_pipe) { - select.push_back(APR_NO_PIPE); + mStdinPipe = std::make_shared(mIOContext); + LL_DEBUGS("LLProcess") << "Created stdin pipe for " << mDesc << LL_ENDL; } - chkapr(apr_procattr_io_set(procattr, select[STDIN], select[STDOUT], select[STDERR])); - - // Thumbs down on implicitly invoking the shell to invoke the child. From - // our point of view, the other major alternative to APR_PROGRAM_PATH - // would be APR_PROGRAM_ENV: still copy environment, but require full - // executable pathname. I don't see a downside to searching the PATH, - // though: if our caller wants (e.g.) a specific Python interpreter, s/he - // can still pass the full pathname. - chkapr(apr_procattr_cmdtype_set(procattr, APR_PROGRAM_PATH)); - // YES, do extra work if necessary to report child exec() failures back to - // parent process. - chkapr(apr_procattr_error_check_set(procattr, 1)); - // Do not start a non-autokill child in detached state. On Posix - // platforms, this setting attempts to daemonize the new child, closing - // std handles and the like, and that's a bit more detachment than we - // want. autokill=false just means not to implicitly kill the child when - // the parent terminates! -// chkapr(apr_procattr_detach_set(procattr, mAutokill? 0 : 1)); - - if (mAutokill) + if (use_stdout_pipe) { -#if ! defined(APR_HAS_PROCATTR_AUTOKILL_SET) - // Our special preprocessor symbol isn't even defined -- wrong APR - LL_WARNS("LLProcess") << "This version of APR lacks Linden apr_procattr_autokill_set() extension" << LL_ENDL; -#elif ! APR_HAS_PROCATTR_AUTOKILL_SET - // Symbol is defined, but to 0: expect apr_procattr_autokill_set() to - // return APR_ENOTIMPL. -#else // APR_HAS_PROCATTR_AUTOKILL_SET nonzero - ll_apr_warn_status(apr_procattr_autokill_set(procattr, 1)); -#endif + mStdoutPipe = std::make_shared(mIOContext); + LL_DEBUGS("LLProcess") << "Created stdout pipe for " << mDesc << LL_ENDL; } - - // In preparation for calling apr_proc_create(), we collect a number of - // const char* pointers obtained from std::string::c_str(). Turns out - // LLInitParam::Block's helpers Optional, Mandatory, Multiple et al. - // guarantee that converting to the wrapped type (std::string in our - // case), e.g. by calling operator(), returns a reference to *the same - // instance* of the wrapped type that's stored in our Block subclass. - // That's important! We know 'params' persists throughout this method - // call; but without that guarantee, when you see params.cwd().c_str(), - // grit your teeth and smile and carry on. - - if (params.cwd.isProvided()) + if (use_stderr_pipe) { - chkapr(apr_procattr_dir_set(procattr, params.cwd().c_str())); + mStderrPipe = std::make_shared(mIOContext); + LL_DEBUGS("LLProcess") << "Created stderr pipe for " << mDesc << LL_ENDL; } - // create an argv vector for the child process - std::vector argv; + // Build the process + try + { +#if !LL_WINDOWS + // Ignore SIGPIPE so that writing to a child's closed stdin doesn't + // terminate the viewer process. The write will fail with EPIPE instead. + signal(SIGPIPE, SIG_IGN); +#endif - // Add the executable path. See above remarks about c_str(). - argv.push_back(params.executable().c_str()); + // Create child process with appropriate redirections. + // v2 uses process_stdio for I/O redirection + bp::process_stdio stdio; - // Add arguments. See above remarks about c_str(). - for (const std::string& arg : params.args) - { - argv.push_back(arg.c_str()); - } + if (use_stdin_pipe) + stdio.in = *mStdinPipe; + else + stdio.in = nullptr; // inherit - // terminate with a null pointer - argv.push_back(NULL); + if (use_stdout_pipe) + stdio.out = *mStdoutPipe; + else + stdio.out = nullptr; // inherit - // Launch! The NULL would be the environment block, if we were passing - // one. Hand-expand chkapr() macro so we can fill in the actual command - // string instead of the variable names. - if (ll_apr_warn_status(apr_proc_create(&mProcess, argv[0], &argv[0], NULL, procattr, - mPool))) - { - LLTHROW(LLProcessError(STRINGIZE(params << " failed"))); - } + if (use_stderr_pipe) + stdio.err = *mStderrPipe; + else + stdio.err = nullptr; // inherit - // arrange to call status_callback() - apr_proc_other_child_register(&mProcess, &LLProcess::status_callback, this, mProcess.in, - mPool); - // and make sure we poll it once per "mainloop" tick - sProcessListener.addPoll(*this); - mStatus.mState = RUNNING; - - mDesc = STRINGIZE(getDesc(params) << " (" << mProcess.pid << ')'); - LL_INFOS("LLProcess") << mDesc << ": launched " << params << LL_ENDL; - - // Unless caller explicitly turned off autokill (child should persist), - // take steps to terminate the child. This is all suspenders-and-belt: in - // theory our destructor should kill an autokill child, but in practice - // that doesn't always work (e.g. VWR-21538). - if (mAutokill) - { -/*==========================================================================*| - // NO: There may be an APR bug, not sure -- but at least on Mac, when - // gAPRPoolp is destroyed, OUR process receives SIGTERM! Apparently - // either our own PID is getting into the list of processes to kill() - // (unlikely), or somehow one of those PIDs is getting zeroed first, - // so that kill() sends SIGTERM to the whole process group -- this - // process included. I'd have to build and link with a debug version - // of APR to know for sure. It's too bad: this mechanism would be just - // right for dealing with static autokill LLProcessPtr variables, - // which aren't destroyed until after APR is no longer available. - - // Tie the lifespan of this child process to the lifespan of our APR - // pool: on destruction of the pool, forcibly kill the process. Tell - // APR to try SIGTERM and suspend 3 seconds. If that didn't work, use - // SIGKILL. - apr_pool_note_subprocess(gAPRPoolp, &mProcess, APR_KILL_AFTER_TIMEOUT); -|*==========================================================================*/ - - // On Windows, associate the new child process with our Job Object. - autokill(); - } + // Build executable path. + // Resolve bare executable names (i.e. "outleap-agent") through the + // environment's PATH + boost::filesystem::path executable_path(params.executable()); + if (!executable_path.has_parent_path()) + { + boost::filesystem::path resolved = + bp::environment::find_executable(executable_path); + if (!resolved.empty()) + { + executable_path = resolved; + } + else + { + LL_WARNS("LLProcess") << "Could not locate '" << params.executable() + << "' on PATH -- launch will likely fail" << LL_ENDL; + // Let bp::process try to produce an error. + } + } - // Instantiate the proper pipe I/O machinery - // want to be able to point to apr_proc_t::in, out, err by index - typedef apr_file_t* apr_proc_t::*apr_proc_file_ptr; - static apr_proc_file_ptr members[] = - { &apr_proc_t::in, &apr_proc_t::out, &apr_proc_t::err }; - for (size_t i = 0; i < NSLOTS; ++i) - { - if (select[i] != APR_CHILD_BLOCK) - continue; - std::string desc(STRINGIZE(mDesc << ' ' << whichfile(FILESLOT(i)))); - apr_file_t* pipe(mProcess.*(members[i])); - if (i == STDIN) + // In Boost.Process v2, error_code cannot be passed as an initializer. + // Use the throwing overload and catch the exception instead. + try { - mPipes[i] = std::make_unique(desc, pipe); + if (params.cwd.isProvided()) + { + mChild = std::make_unique( + mIOContext, + executable_path, + args, + bp::process_start_dir(params.cwd()), + stdio + ); + } + else + { + mChild = std::make_unique( + mIOContext, + executable_path, + args, + stdio + ); + } } - else + catch (const boost::system::system_error& ex) { - mPipes[i] = std::make_unique(desc, pipe, FILESLOT(i)); + throw std::runtime_error(STRINGIZE("failed to launch " << params.executable() + << ": " << ex.what())); } - // Removed temporaily for Xcode 7 build tests: error was: - // "error: expression with side effects will be evaluated despite - // being used as an operand to 'typeid' [-Werror,-Wpotentially-evaluated-expression]"" - //LL_DEBUGS("LLProcess") << "Instantiating " << typeid(mPipes[i]).name() - // << "('" << desc << "')" << LL_ENDL; - } -} -// Helper to obtain a description string, given a Params block -static std::string getDesc(const LLProcess::Params& params) -{ - // If caller specified a description string, by all means use it. - if (params.desc.isProvided()) - return params.desc; +#if LL_WINDOWS + // Add the process to the job object so it terminates when parent dies. + // This is done for all processes with autokill=true (the default). + // Job objects are the Windows-recommended way to ensure child processes + // don't become orphaned if the parent crashes or is killed. + if (mAutokill && mChild) + { + AssignProcessToJob(mChild->native_handle(), mDesc); + } +#else + // boost::process v2 may install a SIGCHLD handler via boost::asio + // without SA_RESTART when mIOContext is passed to bp::process. + // Without SA_RESTART, blocking waitpid() calls elsewhere in the + // process return EINTR when a child exits. Ensure SA_RESTART is set. + { + struct sigaction sa_chld; + if (sigaction(SIGCHLD, nullptr, &sa_chld) != 0) + { + LL_WARNS("LLProcess") << "Failed to read SIGCHLD disposition: " + << strerror(errno) << LL_ENDL; + } + else if (!(sa_chld.sa_flags & SA_RESTART)) + { + sa_chld.sa_flags |= SA_RESTART; + if (sigaction(SIGCHLD, &sa_chld, nullptr) != 0) + { + LL_WARNS("LLProcess") << "Failed to set SA_RESTART on SIGCHLD: " + << strerror(errno) << LL_ENDL; + } + } + } +#endif + + mStatus.mState = RUNNING; + + // Create pipe wrappers + if (mStdinPipe) + { + mWritePipe = std::make_unique( + STRINGIZE(mDesc << " stdin"), + mStdinPipe + ); + } + if (mStdoutPipe) + { + mStdoutReadPipe = std::make_unique( + STRINGIZE(mDesc << " stdout"), + mStdoutPipe, + STDOUT + ); + } + if (mStderrPipe) + { + mStderrReadPipe = std::make_unique( + STRINGIZE(mDesc << " stderr"), + mStderrPipe, + STDERR + ); + } - // Caller didn't say. Use the executable name -- but use just the filename - // part. On Mac, for instance, full pathnames get cumbersome. - return LLProcess::basename(params.executable); + LL_INFOS("LLProcess") << "Launched " << mDesc + << " (PID: " << mChild->id() << ")" << LL_ENDL; + } + catch (const std::exception& e) + { + throw std::runtime_error(STRINGIZE("failed to create process: " << e.what())); + } } -//static -std::string LLProcess::basename(const std::string& path) +void LLProcess::connectMainloop() { - // If there are Linden utility functions to manipulate pathnames, I - // haven't found them -- and for this usage, Boost.Filesystem seems kind - // of heavyweight. - std::string::size_type delim = path.find_last_of("\\/"); - // If path contains no pathname delimiters, return the whole thing. - if (delim == std::string::npos) - return path; - - // Return just the part beyond the last delimiter. - return path.substr(delim + 1); + // Capture a weak_ptr to prevent use-after-free: a listener responding to + // a synchronous event post inside tick() may drop the last LLProcessPtr, + // destroying this object while tick() is on the call stack. Locking the + // weak_ptr before calling tick() keeps *this alive for the duration of + // the callback. + // NOTE: shared_from_this() is only valid after the shared_ptr owning this + // object has been fully constructed, so this must be called from create() + // rather than from the constructor. + std::weak_ptr weak = shared_from_this(); + mMainloopConnection = LLEventPumps::instance().obtain("mainloop") + .listen(LLEventPump::inventName("LLProcess"), + [weak](const LLSD&) + { + // Lock weak_ptr to keep *this alive during tick(), preventing + // destruction mid-callback if a listener drops the last LLProcessPtr. + auto self = weak.lock(); + if (self) self->tick(); + return false; + }); } -LLProcess::~LLProcess() +void LLProcess::tick() { - // In the Linden viewer, there's at least one static LLProcessPtr. Its - // destructor will be called *after* ll_cleanup_apr(). In such a case, - // unregistering is pointless (and fatal!) -- and kill(), which also - // relies on APR, is impossible. - if (! gAPRPoolp) - return; - - // Only in state RUNNING are we registered for callback. In UNSTARTED we - // haven't yet registered. And since receiving the callback is the only - // way we detect child termination, we only change from state RUNNING at - // the same time we unregister. - if (mStatus.mState == RUNNING) + // Initiate pending stdin writes before draining the I/O context so that + // self-chained async writes can keep advancing within the same tick + // instead of waiting for the next mainloop frame. + if (mWritePipe) + mWritePipe->tick(); + + mIOContext.restart(); + while (mIOContext.poll_one() > 0) { - // We're still registered for a callback: unregister. Do it before - // we even issue the kill(): even if kill() somehow prompted an - // instantaneous callback (unlikely), this object is going away! Any - // information updated in this object by such a callback is no longer - // available to any consumer anyway. - apr_proc_other_child_unregister(this); - // One less LLProcess to poll for - sProcessListener.dropPoll(*this); + // Keep polling until no more handlers are ready } - if (mAttached) +#if LL_WINDOWS + // Check process status + if (mChild && mStatus.mState == RUNNING && !mChild->running()) { - kill("destructor"); + // Process has exited. + // We are on the main thread, so get exit code without blocking + DWORD exit_code = STILL_ACTIVE; + if (::GetExitCodeProcess(mChild->native_handle(), &exit_code)) + { + if (exit_code != STILL_ACTIVE) + { + // Exit code is available. + Status exitStatus; + // The STATUS_ACCESS_VIOLATION family (see WinNT.h) means the + // child died on an exception rather than returning a code, and + // callers and the log both want that distinction. This is the + // test APR's why_from_exit_code() used before the rewrite. + exitStatus.mState = + ((exit_code & 0xFFFF0000) == 0xC0000000) ? KILLED : EXITED; + exitStatus.mData = static_cast(exit_code); + handleExit(exitStatus); + } + else + { + // Exit code not ready yet, will retry next tick + // This shouldn't happen if mChild->running() returned false, + // but handle it gracefully + LL_DEBUGS("LLProcess") << "Process " << mDesc + << " exited but exit code not ready yet" << LL_ENDL; + } + } + else + { + LL_WARNS("LLProcess") << "GetExitCodeProcess failed for " << mDesc + << ": error " << ::GetLastError() << LL_ENDL; + // Synthesize exit status + Status exitStatus; + exitStatus.mState = EXITED; + exitStatus.mData = -1; + handleExit(exitStatus); + } } +#else + // Check process status using WNOHANG to avoid blocking or generating + // signals that interfere with other waitpid() callers. + if (mChild && mStatus.mState == RUNNING) + { + int status = 0; + pid_t result; + // Retry on EINTR: SA_RESTART only applies to blocking calls, not + // WNOHANG waitpid(), so manual retry is still needed. + do { // manual EINTR retry (SA_RESTART does not apply to WNOHANG) + result = ::waitpid(mChild->id(), &status, WNOHANG); + } while (result == -1 && errno == EINTR); + + if (result == mChild->id()) + { + // Child has exited; decode exit status now (before handleExit, + // since the child has already been reaped by this waitpid call). + Status exitStatus; + if (WIFEXITED(status)) + { + exitStatus.mState = EXITED; + exitStatus.mData = WEXITSTATUS(status); + } + else if (WIFSIGNALED(status)) + { + exitStatus.mState = KILLED; + exitStatus.mData = WTERMSIG(status); + } + else + { + exitStatus.mState = EXITED; + exitStatus.mData = 0; + } + handleExit(exitStatus); + } + else if (result == -1 && errno == ECHILD) + { + // The zombie was already reaped by someone else (e.g. a SIGCHLD + // handler from APR or another library). We can't determine the + // real exit code; synthesize EXITED/0 so the process is no longer + // considered "running" and waitfor() doesn't spin for 60 seconds. + Status exitStatus; + exitStatus.mState = EXITED; + exitStatus.mData = 0; + handleExit(exitStatus); + } + // result == 0 means still running + } +#endif - if (mPool) + // Keep pumping after process exit until all ReadPipes report EOF, then + // disconnect from mainloop to avoid losing trailing EOF notifications. + if (mStatus.mState != RUNNING && mMainloopConnection.connected()) { - apr_pool_destroy(mPool); - mPool = NULL; + bool stdout_eof = (!mStdoutReadPipe || mStdoutReadPipe->atEOF()); + bool stderr_eof = (!mStderrReadPipe || mStderrReadPipe->atEOF()); + if (stdout_eof && stderr_eof) + { + mMainloopConnection.disconnect(); + } } } -bool LLProcess::kill(const std::string& who) +void LLProcess::handleExit(Status exitStatus) { - if (isRunning()) - { - LL_INFOS("LLProcess") << who << " killing " << mDesc << LL_ENDL; + if (mStatus.mState != RUNNING) + return; // Already handled -#if LL_WINDOWS - int sig = -1; -#else // Posix - int sig = SIGTERM; -#endif + mStatus = exitStatus; - ll_apr_warn_status(apr_proc_kill(&mProcess, sig)); + // Drain any remaining pipe handlers before notifying callers. LLLeap + // consumes child stdout/stderr from these callbacks, and some tests write + // enough data to require far more than a handful of async-read completions + // after the child has already exited. + if (mIOContext.stopped()) + { + // If the io_context has reached the stopped state, + // the following loop will never run and trailing stdout/stderr + // handlers (and EOF notifications) may be missed. + LL_INFOS("LLProcess") << "mIOContext was stopped on exit, restarting" << LL_ENDL; + mIOContext.restart(); + } + while (mIOContext.poll_one() > 0) + { + // Keep polling until no more handlers are immediately ready. } - return ! isRunning(); -} + LL_INFOS("LLProcess") << getStatusString(mStatus) << LL_ENDL; -//static -bool LLProcess::kill(const LLProcessPtr& p, const std::string& who) -{ - if (! p) - return true; // process dead! (was never running) - return p->kill(who); + // Post to event pump if configured + if (!mPostend.empty()) + { + LLSD event; + event["id"] = static_cast(getProcessID()); + event["desc"] = mDesc; + event["state"] = mStatus.mState; + event["data"] = mStatus.mData; + event["string"] = getStatusString(mStatus); + + LLEventPumps::instance().obtain(mPostend).post(event); + } + + // Leave mainloop connected until tick() observes EOF on all ReadPipes. } bool LLProcess::isRunning() const { - return getStatus().mState == RUNNING; + return mStatus.mState == RUNNING; } //static -bool LLProcess::isRunning(const LLProcessPtr& p) +bool LLProcess::isRunning(const LLProcessPtr& ptr) { - if (! p) - return false; - return p->isRunning(); + return ptr && ptr->isRunning(); } LLProcess::Status LLProcess::getStatus() const @@ -865,19 +1027,26 @@ LLProcess::Status LLProcess::getStatus() const } //static -LLProcess::Status LLProcess::getStatus(const LLProcessPtr& p) +LLProcess::Status LLProcess::getStatus(const LLProcessPtr& ptr) { - if (! p) + if (!ptr) { - // default-constructed Status has mState == UNSTARTED - return Status(); + Status status; + status.mState = UNSTARTED; + return status; } - return p->getStatus(); + return ptr->getStatus(); } std::string LLProcess::getStatusString() const { - return getStatusString(getStatus()); + return getStatusString(mDesc, mStatus); +} + +//static +std::string LLProcess::getStatusString(const std::string& desc, const LLProcessPtr& ptr) +{ + return getStatusString(desc, getStatus(ptr)); } std::string LLProcess::getStatusString(const Status& status) const @@ -886,464 +1055,320 @@ std::string LLProcess::getStatusString(const Status& status) const } //static -std::string LLProcess::getStatusString(const std::string& desc, const LLProcessPtr& p) +std::string LLProcess::getStatusString(const std::string& desc, const Status& status) { - if (! p) + std::string result = desc + ": "; + switch (status.mState) { - // default-constructed Status has mState == UNSTARTED - return getStatusString(desc, Status()); + case UNSTARTED: return result + "not started"; + case RUNNING: return result + "running"; + case EXITED: return result + STRINGIZE("exited with code " << status.mData); + case KILLED: return result + STRINGIZE("killed by signal " << status.mData); + default: return result + "unknown state"; } - return desc + " " + p->getStatusString(); } -//static -std::string LLProcess::getStatusString(const std::string& desc, const Status& status) +bool LLProcess::kill(const std::string& who) { - if (status.mState == UNSTARTED) - return desc + " was never launched"; - - if (status.mState == RUNNING) - return desc + " running"; + if (!mChild || mStatus.mState != RUNNING) + return true; - if (status.mState == EXITED) - return STRINGIZE(desc << " exited with code " << status.mData); + LL_INFOS("LLProcess") << who << " killing " << mDesc << LL_ENDL; - if (status.mState == KILLED) #if LL_WINDOWS - return STRINGIZE(desc << " killed with exception " << std::hex << status.mData); + // Call TerminateProcess directly with exit code (UINT)-1 so that + // tick()'s GetExitCodeProcess reads back -1 as a signed int, matching + // the original APR-based behavior expected by tests and callers. + // Do NOT use mChild->terminate(): boost::process v2 may use a different + // exit code (e.g. EXIT_FAILURE=1) or invalidate the handle internally, + // which would break the exit-code check in tick(). + if (!::TerminateProcess(mChild->native_handle(), (UINT)-1)) + { + LL_WARNS("LLProcess") << "Failed to terminate " << mDesc + << ": error " << ::GetLastError() << LL_ENDL; + return false; + } #else - return STRINGIZE(desc << " killed by signal " << status.mData - << " (" << apr_signal_description_get(status.mData) << ")"); + // Send SIGTERM so the child can clean up gracefully, and so tick()'s + // waitpid() reports WIFSIGNALED/WTERMSIG == SIGTERM rather than SIGKILL. + // Do NOT call mChild->terminate(): in boost::process v2, that function + // sends SIGKILL and may set the internal pid to -1, which would cause + // tick()'s waitpid(mChild->id(), ...) to wait for any child (-1) and + // never match the result against the stored pid. + pid_t pid = mChild->id(); + if (::kill(pid, SIGTERM) != 0 && errno != ESRCH) + { + LL_WARNS("LLProcess") << "Failed to send SIGTERM to " << mDesc + << " (pid " << pid << "): " << strerror(errno) << LL_ENDL; + return false; + } #endif - return STRINGIZE(desc << " in unknown state " << status.mState << " (" << status.mData << ")"); + // Mark as killed so the destructor doesn't repeat the termination attempt, + // but a better idea might be to modify mState. + mKillCalled = true; + // Don't set status here - let handleExit() do it when the process actually terminates + // so that it will be able to post EOF event. + // At this point in time mStatus.mState is still RUNNING, so this is basically + // retuning false. + return !isRunning(); } -// Classic-C-style APR callback -void LLProcess::status_callback(int reason, void* data, int status) +//static +bool LLProcess::kill(const LLProcessPtr& ptr, const std::string& who) { - // Our only role is to bounce this static method call back into object - // space. - static_cast(data)->handle_status(reason, status); + return !ptr || ptr->kill(who); } -#define tabent(symbol) { symbol, #symbol } -static struct ReasonCode -{ - int code; - const char* name; -} reasons[] = -{ - tabent(APR_OC_REASON_DEATH), - tabent(APR_OC_REASON_UNWRITABLE), - tabent(APR_OC_REASON_RESTART), - tabent(APR_OC_REASON_UNREGISTER), - tabent(APR_OC_REASON_LOST), - tabent(APR_OC_REASON_RUNNING) -}; -#undef tabent - -// Object-oriented callback -void LLProcess::handle_status(int reason, int status) +void LLProcess::pump() { - { - // This odd appearance of LL_DEBUGS is just to bracket a lookup that will - // only be performed if in fact we're going to produce the log message. - LL_DEBUGS("LLProcess") << empty; - std::string reason_str; - for (const ReasonCode& rcp : reasons) - { - if (reason == rcp.code) - { - reason_str = rcp.name; - break; - } - } - if (reason_str.empty()) - { - reason_str = STRINGIZE("unknown reason " << reason); - } - LL_CONT << mDesc << ": handle_status(" << reason_str << ", " << status << ")" << LL_ENDL; - } - - if (! (reason == APR_OC_REASON_DEATH || reason == APR_OC_REASON_LOST)) - { - // We're only interested in the call when the child terminates. - return; - } - - // Somewhat oddly, APR requires that you explicitly unregister even when - // it already knows the child has terminated. We must pass the same 'data' - // pointer as for the register() call, which was our 'this'. - apr_proc_other_child_unregister(this); - // don't keep polling for a terminated process - sProcessListener.dropPoll(*this); - // We overload mStatus.mState to indicate whether the child is registered - // for APR callback: only RUNNING means registered. Track that we've - // unregistered. We know the child has terminated; might be EXITED or - // KILLED; refine below. - mStatus.mState = EXITED; - - // Make last-gasp calls for each of the ReadPipes we have on hand. Since - // they're listening on "mainloop", we can be sure they'll eventually - // collect all pending data from the child. But we want to be able to - // guarantee to our consumer that by the time we post on the "postend" - // LLEventPump, our ReadPipes are already buffering all the data there - // will ever be from the child. That lets the "postend" listener decide - // what to do with that final data. - for (size_t i = 0; i < mPipes.size(); ++i) - { - std::string error; - ReadPipeImpl* ppipe = getPipePtr(error, FILESLOT(i)); - if (ppipe) - { - static LLSD trivial; - ppipe->tick(trivial); - } - } - -// wi->rv = apr_proc_wait(wi->child, &wi->rc, &wi->why, APR_NOWAIT); - // It's just wrong to call apr_proc_wait() here. The only way APR knows to - // call us with APR_OC_REASON_DEATH is that it's already reaped this child - // process, so calling wait() will only produce "huh?" from the OS. We - // must rely on the status param passed in, which unfortunately comes - // straight from the OS wait() call, which means we have to decode it by - // hand. - mStatus = interpret_status(status); - LL_INFOS("LLProcess") << getStatusString() << LL_ENDL; - - // If caller requested notification on child termination, send it. - if (! mPostend.empty()) - { - LLEventPumps::instance().obtain(mPostend) - .post(LLSDMap - ("id", getProcessID()) - ("desc", mDesc) - ("state", mStatus.mState) - ("data", mStatus.mData) - ("string", getStatusString()) - ); - } + tick(); } LLProcess::id LLProcess::getProcessID() const { - return mProcess.pid; -} + if (!mChild) + return 0; -LLProcess::handle LLProcess::getProcessHandle() const -{ #if LL_WINDOWS - return mProcess.hproc; + return static_cast(mChild->id()); #else - return mProcess.pid; + return mChild->id(); #endif } -std::string LLProcess::getPipeName(FILESLOT) const +LLProcess::handle LLProcess::getProcessHandle() const { - // LLProcess::FileParam::type "npipe" is not yet implemented - return ""; -} + if (!mChild) + return 0; -template -PIPETYPE* LLProcess::getPipePtr(std::string& error, FILESLOT slot) -{ - if (slot >= NSLOTS) - { - error = STRINGIZE(mDesc << " has no slot " << slot); - return NULL; - } - if (!mPipes[slot]) - { - error = STRINGIZE(mDesc << ' ' << whichfile(slot) << " not a monitored pipe"); - return NULL; - } - // Make sure we dynamic_cast in pointer domain so we can test, rather than - // accepting runtime's exception. - PIPETYPE* ppipe = dynamic_cast(mPipes[slot].get()); - if (! ppipe) +#if LL_WINDOWS + // Duplicate the process handle so the caller owns an independent copy. + // boost::process v2's process destructor always closes the handle + // (even after process::detach()), so the raw native_handle() becomes invalid + // once ~process() runs. DuplicateHandle gives the caller a handle whose + // lifetime is not tied to boost's internal process cleanup. + HANDLE source = mChild->native_handle(); + if (!source || source == INVALID_HANDLE_VALUE) + return 0; + HANDLE dup = nullptr; + if (!::DuplicateHandle( + ::GetCurrentProcess(), source, + ::GetCurrentProcess(), &dup, + PROCESS_QUERY_INFORMATION | SYNCHRONIZE, + FALSE, 0)) { - error = STRINGIZE(mDesc << ' ' << whichfile(slot) << " not a " << typeid(PIPETYPE).name()); - return NULL; + LL_WARNS("LLProcess") << "DuplicateHandle failed for " << mDesc + << ": error " << ::GetLastError() << LL_ENDL; + return 0; } - - error.clear(); - return ppipe; + return dup; +#else + return mChild->id(); +#endif } -template -PIPETYPE& LLProcess::getPipe(FILESLOT slot) +//static +LLProcess::handle LLProcess::isRunning(handle h, const std::string& desc) { - std::string error; - PIPETYPE* wp = getPipePtr(error, slot); - if (! wp) - { - LLTHROW(NoPipe(error)); - } - return *wp; -} +#if LL_WINDOWS + if (h == 0 || h == INVALID_HANDLE_VALUE) + return 0; -template -boost::optional LLProcess::getOptPipe(FILESLOT slot) -{ - std::string error; - PIPETYPE* wp = getPipePtr(error, slot); - if (! wp) + DWORD exit_code; + if (GetExitCodeProcess(h, &exit_code)) { - LL_DEBUGS("LLProcess") << error << LL_ENDL; - return boost::optional(); + if (exit_code == STILL_ACTIVE) + return h; // process still running + // Process has exited: close the duplicated handle (see getProcessHandle()). } - return *wp; -} - -LLProcess::WritePipe& LLProcess::getWritePipe(FILESLOT slot) -{ - return getPipe(slot); -} - -boost::optional LLProcess::getOptWritePipe(FILESLOT slot) -{ - return getOptPipe(slot); -} - -LLProcess::ReadPipe& LLProcess::getReadPipe(FILESLOT slot) -{ - return getPipe(slot); -} + // Either process exited or GetExitCodeProcess failed; either way we're done. + CloseHandle(h); + return 0; +#else + if (h == 0) + return 0; -boost::optional LLProcess::getOptReadPipe(FILESLOT slot) -{ - return getOptPipe(slot); -} + // Use waitpid with WNOHANG to check if process is still running + // This is more reliable than kill(pid, 0) and properly reaps zombies + int status; + pid_t result; -//static -std::string LLProcess::getline(std::istream& in) -{ - std::string line; - std::getline(in, line); - // Blur the distinction between "\r\n" and plain "\n". std::getline() will - // have eaten the "\n", but we could still end up with a trailing "\r". - std::string::size_type lastpos = line.find_last_not_of("\r"); - if (lastpos != std::string::npos) + // Retry on EINTR (interrupted system call) + do { - // Found at least one character that's not a trailing '\r'. SKIP OVER - // IT and erase the rest of the line. - line.erase(lastpos+1); - } - return line; -} + result = waitpid(h, &status, WNOHANG); + } while (result == -1 && errno == EINTR); -std::ostream& operator<<(std::ostream& out, const LLProcess::Params& params) -{ - if (params.cwd.isProvided()) + if (result == 0) { - out << "cd " << LLStringUtil::quote(params.cwd) << ": "; + // Process still running + return h; } - out << LLStringUtil::quote(params.executable); - for (const std::string& arg : params.args) + else if (result == h) { - out << ' ' << LLStringUtil::quote(arg); - } - return out; -} - -/***************************************************************************** -* Windows specific -*****************************************************************************/ -#if LL_WINDOWS - -static std::string WindowsErrorString(const std::string& operation); - -void LLProcess::autokill() -{ - // hopefully now handled by apr_procattr_autokill_set() -} - -LLProcess::handle LLProcess::isRunning(handle h, const std::string& desc) -{ - // This direct Windows implementation is because we have no access to the - // apr_proc_t struct: we expect it's been destroyed. - if (! h) + // Process has terminated (and we've reaped it) return 0; - - DWORD waitresult = WaitForSingleObject(h, 0); - if(waitresult == WAIT_OBJECT_0) + } + else if (result == -1) { - // the process has completed. - if (! desc.empty()) + // Error occurred + if (errno == ECHILD) { - DWORD status = 0; - if (! GetExitCodeProcess(h, &status)) - { - LL_WARNS("LLProcess") << desc << " terminated, but " - << WindowsErrorString("GetExitCodeProcess()") << LL_ENDL; - } - { - LL_INFOS("LLProcess") << getStatusString(desc, interpret_status(status)) - << LL_ENDL; - } + // Process doesn't exist or was already reaped + return 0; } - CloseHandle(h); + // For other errors, assume process is gone + LL_WARNS("LLProcess") << "waitpid(" << h << ") failed: " + << strerror(errno) << LL_ENDL; return 0; } - return h; + // Shouldn't get here, but if we do, assume process is gone + return 0; +#endif } -static LLProcess::Status interpret_status(int status) +std::string LLProcess::getPipeName(FILESLOT slot) const { - LLProcess::Status result; - - // This bit of code is cribbed from apr/threadproc/win32/proc.c, a - // function (unfortunately static) called why_from_exit_code(): - /* See WinNT.h STATUS_ACCESS_VIOLATION and family for how - * this class of failures was determined - */ - if ((status & 0xFFFF0000) == 0xC0000000) - { - result.mState = LLProcess::KILLED; - } - else - { - result.mState = LLProcess::EXITED; - } - result.mData = status; - - return result; + // Named pipes not yet implemented in this PoC + return ""; } -/// GetLastError()/FormatMessage() boilerplate -static std::string WindowsErrorString(const std::string& operation) +LLProcess::WritePipe& LLProcess::getWritePipe(FILESLOT slot) { - auto result = GetLastError(); - return STRINGIZE(operation << " failed (" << result << "): " - << windows_message(result)); -} - -/***************************************************************************** -* Posix specific -*****************************************************************************/ -#else // Mac and linux + if (slot >= NSLOTS) + throw NoPipe(STRINGIZE(mDesc << ": no slot " << slot)); -#include -#include -#include -#include + if (slot == STDIN) + { + if (!mWritePipe) + throw NoPipe(STRINGIZE(mDesc << ": stdin is not a monitored pipe")); + return *mWritePipe; + } -void LLProcess::autokill() -{ - // What we ought to do here is to: - // 1. create a unique process group and run all autokill children in that - // group (see https://jira.secondlife.com/browse/SWAT-563); - // 2. figure out a way to intercept control when the viewer exits -- - // gracefully or not; - // 3. when the viewer exits, kill off the aforementioned process group. - - // It's point 2 that's troublesome. Although I've seen some signal- - // handling logic in the Posix viewer code, I haven't yet found any bit of - // code that's run no matter how the viewer exits (a try/finally for the - // whole process, as it were). + // STDOUT or STDERR slots: neither has a WritePipe. + // Distinguish "not piped at all" from "piped but wrong direction". + const char* slotname = (slot == STDOUT) ? "stdout" : "stderr"; + bool is_piped = (slot == STDOUT) ? bool(mStdoutReadPipe) : bool(mStderrReadPipe); + if (!is_piped) + throw NoPipe(STRINGIZE(mDesc << ": " << slotname << " is not a monitored pipe")); + else + throw NoPipe(STRINGIZE(mDesc << ": " << slotname << " is a ReadPipe, not a WritePipe")); } -// Attempt to reap a process ID -- returns true if the process has exited and been reaped, false otherwise. -static bool reap_pid(pid_t pid, LLProcess::Status* pstatus=NULL) +LLProcess::ReadPipe& LLProcess::getReadPipe(FILESLOT slot) { - LLProcess::Status dummy; - if (! pstatus) - { - // If caller doesn't want to see Status, give us a target anyway so we - // don't have to have a bunch of conditionals. - pstatus = &dummy; - } + if (slot >= NSLOTS) + throw NoPipe(STRINGIZE(mDesc << ": no slot " << slot)); + + if (slot == STDIN) + throw NoPipe(STRINGIZE(mDesc << ": ReadPipe is invalid for stdin")); - int status = 0; - pid_t wait_result = ::waitpid(pid, &status, WNOHANG); - if (wait_result == pid) + // At this point, slot must be STDOUT or STDERR + // Check if a pipe was configured for this slot + if (slot == STDOUT) { - *pstatus = interpret_status(status); - return true; + if (!mStdoutReadPipe) + throw NoPipe(STRINGIZE(mDesc << ": stdout is not a monitored pipe")); + return *mStdoutReadPipe; } - if (wait_result == 0) + else if (slot == STDERR) { - pstatus->mState = LLProcess::RUNNING; - pstatus->mData = 0; - return false; + if (!mStderrReadPipe) + throw NoPipe(STRINGIZE(mDesc << ": stderr is not a monitored pipe")); + return *mStderrReadPipe; } - - // Clear caller's Status block; caller must interpret UNSTARTED to mean - // "if this PID was ever valid, it no longer is." - *pstatus = LLProcess::Status(); - - // We've dealt with the success cases: we were able to reap the child - // (wait_result == pid) or it's still running (wait_result == 0). It may - // be that the child terminated but didn't hang around long enough for us - // to reap. In that case we still have no Status to report, but we can at - // least state that it's not running. - if (wait_result == -1 && errno == ECHILD) + else { - // No such process -- this may mean we're ignoring SIGCHILD. - return true; + // This should never happen given the checks above, but handle it anyway + throw NoPipe(STRINGIZE(mDesc << ": no slot " << slot)); } - - // Uh, should never happen?! - LL_WARNS("LLProcess") << "LLProcess::reap_pid(): waitpid(" << pid << ") returned " - << wait_result << "; not meaningful?" << LL_ENDL; - // If caller is looping until this pid terminates, and if we can't find - // out, better to break the loop than to claim it's still running. - return true; } -LLProcess::id LLProcess::isRunning(id pid, const std::string& desc) +LLProcess::WritePipe* LLProcess::getOptWritePipe(FILESLOT slot) { - // This direct Posix implementation is because we have no access to the - // apr_proc_t struct: we expect it's been destroyed. - if (! pid) - return 0; + if (slot >= NSLOTS) + { + LL_WARNS("LLProcess") << mDesc << ": no slot " << slot << LL_ENDL; + return nullptr; + } - // Check whether the process has exited, and reap it if it has. - LLProcess::Status status; - if(reap_pid(pid, &status)) + if (slot == STDIN) { - // the process has exited. - if (! desc.empty()) + if (!mWritePipe) { - std::string statstr(desc + " apparently terminated: no status available"); - // We don't just pass UNSTARTED to getStatusString() because, in - // the context of reap_pid(), that state has special meaning. - if (status.mState != UNSTARTED) - { - statstr = getStatusString(desc, status); - } - LL_INFOS("LLProcess") << statstr << LL_ENDL; + LL_WARNS("LLProcess") << mDesc << ": stdin is not a monitored pipe" << LL_ENDL; + return nullptr; } - return 0; + return mWritePipe.get(); } - return pid; + // STDOUT or STDERR slots: neither has a WritePipe. + const char* slotname = (slot == STDOUT) ? "stdout" : "stderr"; + bool is_piped = (slot == STDOUT) ? bool(mStdoutReadPipe) : bool(mStderrReadPipe); + if (!is_piped) + LL_WARNS("LLProcess") << mDesc << ": " << slotname << " is not a monitored pipe" << LL_ENDL; + else + LL_WARNS("LLProcess") << mDesc << ": " << slotname << " is a ReadPipe, not a WritePipe" << LL_ENDL; + return nullptr; } -static LLProcess::Status interpret_status(int status) +LLProcess::ReadPipe* LLProcess::getOptReadPipe(FILESLOT slot) { - LLProcess::Status result; + if (slot >= NSLOTS) + { + LL_WARNS("LLProcess") << mDesc << ": no slot " << slot << LL_ENDL; + return nullptr; + } - if (WIFEXITED(status)) + if (slot == STDIN) { - result.mState = LLProcess::EXITED; - result.mData = WEXITSTATUS(status); + LL_WARNS("LLProcess") << mDesc << ": ReadPipe is invalid for stdin" << LL_ENDL; + return nullptr; } - else if (WIFSIGNALED(status)) + + if (slot == STDOUT) { - result.mState = LLProcess::KILLED; - result.mData = WTERMSIG(status); + if (!mStdoutReadPipe) + { + LL_WARNS("LLProcess") << mDesc << ": stdout is not a monitored pipe" << LL_ENDL; + return nullptr; + } + return mStdoutReadPipe.get(); } - else // uh, shouldn't happen? + else if (slot == STDERR) + { + if (!mStderrReadPipe) + { + LL_WARNS("LLProcess") << mDesc << ": stderr is not a monitored pipe" << LL_ENDL; + return nullptr; + } + return mStderrReadPipe.get(); + } + else { - result.mState = LLProcess::EXITED; - result.mData = status; // someone else will have to decode + LL_WARNS("LLProcess") << mDesc << ": no slot " << slot << LL_ENDL; + return nullptr; } +} - return result; +//static +std::string LLProcess::basename(const std::string& path) +{ + std::string::size_type delim = path.find_last_of("\\/"); + if (delim == std::string::npos) + return path; + return path.substr(delim + 1); } -#endif // Posix +//static +std::string LLProcess::getline(std::istream& in) +{ + std::string line; + std::getline(in, line); + // Trim trailing \r for cross-platform compatibility + if (!line.empty() && line.back() == '\r') + line.pop_back(); + return line; +} diff --git a/indra/llcommon/llprocess.h b/indra/llcommon/llprocess.h index ecdedbbaa30..a016b6a2580 100644 --- a/indra/llcommon/llprocess.h +++ b/indra/llcommon/llprocess.h @@ -30,9 +30,16 @@ #include "llinitparam.h" #include "llsdparam.h" #include "llexception.h" -#include "llwin32headers.h" -#include "apr_thread_proc.h" -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include // std::ostream #if LL_WINDOWS @@ -66,93 +73,31 @@ typedef std::shared_ptr LLProcessPtr; * indra/llcommon/tests/llprocess_test.cpp for an example of waiting for * child-process termination in a standalone test context. */ -class LL_COMMON_API LLProcess + +class LL_COMMON_API LLProcess : public std::enable_shared_from_this { LOG_CLASS(LLProcess); public: /** * Specify what to pass for each of child stdin, stdout, stderr. - * @see LLProcess::Params::files. */ - struct FileParam: public LLInitParam::Block + struct FileParam : public LLInitParam::Block { /** * type of file handle to pass to child process - * - * - "" (default): let the child inherit the same file handle used by - * this process. For instance, if passed as stdout, child stdout - * will be interleaved with stdout from this process. In this case, - * @a name is moot and should be left "". - * - * - "file": open an OS filesystem file with the specified @a name. - * Not yet implemented. - * - * - "pipe" or "tpipe" or "npipe": depends on @a name - * - * - @a name.empty(): construct an OS pipe used only for this slot - * of the forthcoming child process. - * - * - ! @a name.empty(): in a global registry, find or create (using - * the specified @a name) an OS pipe. The point of the (purely - * internal) @a name is that passing the same @a name in more than - * one slot for a given LLProcess -- or for slots in different - * LLProcess instances -- means the same pipe. For example, you - * might pass the same @a name value as both stdout and stderr to - * make the child process produce both on the same actual pipe. Or - * you might pass the same @a name as the stdout for one LLProcess - * and the stdin for another to connect the two child processes. - * Use LLProcess::getPipeName() to generate a unique name - * guaranteed not to already exist in the registry. Not yet - * implemented. - * - * The difference between "pipe", "tpipe" and "npipe" is as follows. - * - * - "pipe": direct LLProcess to monitor the parent end of the pipe, - * pumping nonblocking I/O every frame. The expectation (at least - * for stdout or stderr) is that the caller will listen for - * incoming data and consume it as it arrives. It's important not - * to neglect such a pipe, because it's buffered in memory. If you - * suspect the child may produce a great volume of output between - * frames, consider directing the child to write to a filesystem - * file instead, then read the file later. - * - * - "tpipe": do not engage LLProcess machinery to monitor the - * parent end of the pipe. A "tpipe" is used only to connect - * different child processes. As such, it makes little sense to - * pass an empty @a name. Not yet implemented. - * - * - "npipe": like "tpipe", but use an OS named pipe with a - * generated name. Note that @a name is the @em internal name of - * the pipe in our global registry -- it doesn't necessarily have - * anything to do with the pipe's name in the OS filesystem. Use - * LLProcess::getPipeName() to obtain the named pipe's OS - * filesystem name, e.g. to pass it as the @a name to another - * LLProcess instance using @a type "file". This supports usage - * like bash's <(subcommand...) or >(subcommand...) - * constructs. Not yet implemented. - * - * In all cases the open mode (read, write) is determined by the child - * slot you're filling. Child stdin means select the "read" end of a - * pipe, or open a filesystem file for reading; child stdout or stderr - * means select the "write" end of a pipe, or open a filesystem file - * for writing. - * - * Confusion such as passing the same pipe as the stdin of two - * processes (rather than stdout for one and stdin for the other) is - * explicitly permitted: it's up to the caller to construct meaningful - * LLProcess pipe graphs. + * - "" (default): inherit from parent + * - "pipe": create a pipe for I/O + * - "file": open a filesystem file (future enhancement) */ Optional type; Optional name; - FileParam(const std::string& tp="", const std::string& nm=""): + FileParam(const std::string& tp = "", const std::string& nm = "") : type("type"), name("name") { - // If caller wants to specify values, use explicit assignment to - // set them rather than initialization. - if (! tp.empty()) type = tp; - if (! nm.empty()) name = nm; + if (!tp.empty()) type = tp; + if (!nm.empty()) name = nm; } }; @@ -168,7 +113,8 @@ class LL_COMMON_API LLProcess files("files"), postend("postend"), desc("desc") - {} + { + } /// pathname of executable Mandatory executable; @@ -250,18 +196,11 @@ class LL_COMMON_API LLProcess }; typedef LLSDParamAdapter LLSDOrParams; - /** - * Factory accepting either plain LLSD::Map or Params block. - * MAY RETURN DEFAULT-CONSTRUCTED LLProcessPtr if params invalid! - */ static LLProcessPtr create(const LLSDOrParams& params); virtual ~LLProcess(); /// Is child process still running? bool isRunning() const; - // static isRunning(LLProcessPtr), getStatus(LLProcessPtr), - // getStatusString(LLProcessPtr), kill(LLProcessPtr) handle the case in - // which the passed LLProcessPtr might be NULL (default-constructed). static bool isRunning(const LLProcessPtr&); /** @@ -280,296 +219,136 @@ class LL_COMMON_API LLProcess */ struct Status { - Status(): - mState(UNSTARTED), - mData(0) - {} - - state mState; ///< @see state - /** - * - for mState == EXITED: mData is exit() code - * - for mState == KILLED: mData is signal number (Posix) - * - otherwise: mData is undefined - */ - int mData; + Status() : mState(UNSTARTED), mData(0) {} + state mState; + int mData; // exit code or signal number }; - /// Status query Status getStatus() const; static Status getStatus(const LLProcessPtr&); - /// English Status string query, for logging etc. std::string getStatusString() const; static std::string getStatusString(const std::string& desc, const LLProcessPtr&); - /// English Status string query for previously-captured Status std::string getStatusString(const Status& status) const; - /// static English Status string query static std::string getStatusString(const std::string& desc, const Status& status); - // Attempt to kill the process -- returns true if the process is no longer running when it returns. - // Note that even if this returns false, the process may exit some time after it's called. - bool kill(const std::string& who=""); - static bool kill(const LLProcessPtr& p, const std::string& who=""); + bool kill(const std::string& who = ""); + static bool kill(const LLProcessPtr& p, const std::string& who = ""); + + /// Manually drive pending I/O and check process state. + /// Use this when the mainloop is not yet running or was terminated. + void pump(); #if LL_WINDOWS - typedef int id; ///< as returned by getProcessID() - typedef HANDLE handle; ///< as returned by getProcessHandle() + typedef int id; + typedef HANDLE handle; #else typedef pid_t id; typedef pid_t handle; #endif - /** - * Get an int-like id value. This is primarily intended for a human reader - * to differentiate processes. - */ + id getProcessID() const; - /** - * Get a "handle" of a kind that you might pass to platform-specific API - * functions to engage features not directly supported by LLProcess. - */ handle getProcessHandle() const; + static handle isRunning(handle, const std::string& desc = ""); - /** - * Test if a process (@c handle obtained from getProcessHandle()) is still - * running. Return same nonzero @c handle value if still running, else - * zero, so you can test it like a bool. But if you want to update a - * stored variable as a side effect, you can write code like this: - * @code - * hchild = LLProcess::isRunning(hchild); - * @endcode - * @note This method is intended as a unit-test hook, not as the first of - * a whole set of operations supported on freestanding @c handle values. - * New functionality should be added as nonstatic members operating on - * the same data as getProcessHandle(). - * - * In particular, if child termination is detected by this static isRunning() - * rather than by nonstatic isRunning(), the LLProcess object won't be - * aware of the child's changed status and may encounter OS errors trying - * to obtain it. This static isRunning() is only intended for after the - * launching LLProcess object has been destroyed. - */ - static handle isRunning(handle, const std::string& desc=""); + enum FILESLOT { STDIN = 0, STDOUT = 1, STDERR = 2, NSLOTS = 3 }; - /// Provide symbolic access to child's file slots - enum FILESLOT { STDIN=0, STDOUT=1, STDERR=2, NSLOTS=3 }; + /// Exception thrown by getWritePipe(), getReadPipe() if you didn't ask to + /// create a pipe at the corresponding FILESLOT. + struct NoPipe : public LLException + { + NoPipe(const std::string& what) : LLException(what) {} + }; - /** - * For a pipe constructed with @a type "npipe", obtain the generated OS - * filesystem name for the specified pipe. Otherwise returns the empty - * string. @see LLProcess::FileParam::type - */ std::string getPipeName(FILESLOT) const; - /// base of ReadPipe, WritePipe + /// Base class for pipes class LL_COMMON_API BasePipe { public: - virtual ~BasePipe() = 0; - + virtual ~BasePipe() = default; typedef std::size_t size_type; static const size_type npos; - - /** - * Get accumulated buffer length. - * - * For WritePipe, is there still pending data to send to child? - * - * For ReadPipe, we often need to refrain from actually reading the - * std::istream returned by get_istream() until we've accumulated - * enough data to make it worthwhile. For instance, if we're expecting - * a number from the child, but the child happens to flush "12" before - * emitting "3\n", get_istream() >> myint could return 12 rather than - * 123! - */ virtual size_type size() const = 0; }; - /// As returned by getWritePipe() or getOptWritePipe() - class WritePipe: public BasePipe + /// Write pipe for stdin + class WritePipe : public BasePipe { public: - /** - * Get ostream& on which to write to child's stdin. - * - * @usage - * @code - * myProcess->getWritePipe().get_ostream() << "Hello, child!" << std::endl; - * @endcode - */ virtual std::ostream& get_ostream() = 0; + // Called each mainloop tick to initiate any pending async writes. + virtual void tick() {} }; - /// As returned by getReadPipe() or getOptReadPipe() - class ReadPipe: public BasePipe + /// Read pipe for stdout/stderr + class ReadPipe : public BasePipe { public: - /** - * Get istream& on which to read from child's stdout or stderr. - * - * @usage - * @code - * std::string stuff; - * myProcess->getReadPipe().get_istream() >> stuff; - * @endcode - * - * You should be sure in advance that the ReadPipe in question can - * fill the request. @see getPump() - */ virtual std::istream& get_istream() = 0; - - /** - * Like std::getline(get_istream(), line), but trims off trailing '\r' - * to make calling code less platform-sensitive. - */ virtual std::string getline() = 0; - - /** - * Like get_istream().read(buffer, n), but returns std::string rather - * than requiring caller to construct a buffer, etc. - */ virtual std::string read(size_type len) = 0; + virtual std::string peek(size_type offset = 0, size_type len = npos) const = 0; - /** - * Peek at accumulated buffer data without consuming it. Optional - * parameters give you substr() functionality. - * - * @note You can discard buffer data using get_istream().ignore(n). - */ - virtual std::string peek(size_type offset=0, size_type len=npos) const = 0; - - /** - * Detect presence of a substring (or char) in accumulated buffer data - * without retrieving it. Optional offset allows you to search from - * specified position. - */ template - bool contains(SEEK seek, size_type offset=0) const - { return find(seek, offset) != npos; } - - /** - * Search for a substring in accumulated buffer data without - * retrieving it. Returns size_type position at which found, or npos - * meaning not found. Optional offset allows you to search from - * specified position. - */ - virtual size_type find(const std::string& seek, size_type offset=0) const = 0; + bool contains(SEEK seek, size_type offset = 0) const + { + return find(seek, offset) != npos; + } - /** - * Search for a char in accumulated buffer data without retrieving it. - * Returns size_type position at which found, or npos meaning not - * found. Optional offset allows you to search from specified - * position. - */ - virtual size_type find(char seek, size_type offset=0) const = 0; + virtual size_type find(const std::string& seek, size_type offset = 0) const = 0; + virtual size_type find(char seek, size_type offset = 0) const = 0; - /** - * Get LLEventPump& on which to listen for incoming data. The posted - * LLSD::Map event will contain: - * - * - "data" part of pending data; see setLimit() - * - "len" entire length of pending data, regardless of setLimit() - * - "slot" this ReadPipe's FILESLOT, e.g. LLProcess::STDOUT - * - "name" e.g. "stdout" - * - "desc" e.g. "SLPlugin (pid) stdout" - * - "eof" @c true means there no more data will arrive on this pipe, - * therefore no more events on this pump - * - * If the child sends "abc", and this ReadPipe posts "data"="abc", but - * you don't consume it by reading the std::istream returned by - * get_istream(), and the child next sends "def", ReadPipe will post - * "data"="abcdef". - */ virtual LLEventPump& getPump() = 0; - - /** - * Set maximum length of buffer data that will be posted in the LLSD - * announcing arrival of new data from the child. If you call - * setLimit(5), and the child sends "abcdef", the LLSD event will - * contain "data"="abcde". However, you may still read the entire - * "abcdef" from get_istream(): this limit affects only the size of - * the data posted with the LLSD event. If you don't call this method, - * @em no data will be posted: the default is 0 bytes. - */ virtual void setLimit(size_type limit) = 0; - - /** - * Query the current setLimit() limit. - */ virtual size_type getLimit() const = 0; + // True once the pipe has observed child-process EOF. + virtual bool atEOF() const = 0; }; - /// Exception thrown by getWritePipe(), getReadPipe() if you didn't ask to - /// create a pipe at the corresponding FILESLOT. - struct NoPipe: public LLException - { - NoPipe(const std::string& what): LLException(what) {} - }; - - /** - * Get a reference to the (only) WritePipe for this LLProcess. @a slot, if - * specified, must be STDIN. Throws NoPipe if you did not request a "pipe" - * for child stdin. Use this method when you know how you created the - * LLProcess in hand. - */ - WritePipe& getWritePipe(FILESLOT slot=STDIN); - - /** - * Get a boost::optional to the (only) WritePipe for this - * LLProcess. @a slot, if specified, must be STDIN. The return value is - * empty if you did not request a "pipe" for child stdin. Use this method - * for inspecting an LLProcess you did not create. - */ - boost::optional getOptWritePipe(FILESLOT slot=STDIN); - - /** - * Get a reference to one of the ReadPipes for this LLProcess. @a slot, if - * specified, must be STDOUT or STDERR. Throws NoPipe if you did not - * request a "pipe" for child stdout or stderr. Use this method when you - * know how you created the LLProcess in hand. - */ - ReadPipe& getReadPipe(FILESLOT slot); - - /** - * Get a boost::optional to one of the ReadPipes for this - * LLProcess. @a slot, if specified, must be STDOUT or STDERR. The return - * value is empty if you did not request a "pipe" for child stdout or - * stderr. Use this method for inspecting an LLProcess you did not create. - */ - boost::optional getOptReadPipe(FILESLOT slot); + WritePipe& getWritePipe(FILESLOT slot = STDIN); + ReadPipe& getReadPipe(FILESLOT index); + WritePipe* getOptWritePipe(FILESLOT slot = STDIN); + ReadPipe* getOptReadPipe(FILESLOT index); - /// little utilities that really should already be somewhere else in the - /// code base static std::string basename(const std::string& path); - static std::string getline(std::istream&); + static std::string getline(std::istream& in); - // Non-copyable - LLProcess(const LLProcess&) = delete; - LLProcess& operator=(const LLProcess&) = delete; + // Constructor is public for the sake of make_shared + // but create() should be used instead for proper initialization. + LLProcess(const Params& params); private: - /// constructor is private: use create() instead - LLProcess(const LLSDOrParams& params); - void autokill(); - // Classic-C-style APR callback - static void status_callback(int reason, void* data, int status); - // Object-oriented callback - void handle_status(int reason, int status); - // implementation for get[Opt][Read|Write]Pipe() - template - PIPETYPE& getPipe(FILESLOT slot); - template - boost::optional getOptPipe(FILESLOT slot); - template - PIPETYPE* getPipePtr(std::string& error, FILESLOT slot); + void launch(const LLSDOrParams& params); + void connectMainloop(); + void tick(); + void handleExit(Status exitStatus); + + // Boost.Process v2 components + boost::asio::io_context mIOContext; + std::unique_ptr mChild; + + // Pipes - using Boost.Asio pipes directly (v2 no longer has async_pipe) + // From parent's perspective: write to stdin (writable_pipe), read from stdout/stderr (readable_pipe) + // std::shared_ptr so WritePipeImpl/ReadPipeImpl keep the pipe alive as + // long as async operations are in flight + std::shared_ptr mStdinPipe; + std::shared_ptr mStdoutPipe; + std::shared_ptr mStderrPipe; + + // Our pipe wrapper implementations + std::unique_ptr mWritePipe; + std::unique_ptr mStdoutReadPipe; + std::unique_ptr mStderrReadPipe; + Status mStatus; std::string mDesc; std::string mPostend; - apr_proc_t mProcess; - bool mAutokill, mAttached; - Status mStatus; - // explicitly want this ptr_vector to be able to store NULLs - typedef std::vector> PipeVector; - PipeVector mPipes; - apr_pool_t* mPool; + bool mAutokill; + bool mAttached; + bool mKillCalled; + + // For integrating with LLEventPump mainloop + boost::signals2::scoped_connection mMainloopConnection; }; /// for logging diff --git a/indra/llcommon/llprocessor.cpp b/indra/llcommon/llprocessor.cpp index 9d0ef45bee8..456f2b02d40 100644 --- a/indra/llcommon/llprocessor.cpp +++ b/indra/llcommon/llprocessor.cpp @@ -123,6 +123,9 @@ namespace eSSE4_1_Features = 38, eSSE4_2_Features = 39, eSSE4a_Features = 40, + eAVX_Features = 41, + eAVX2_Features = 42, + eAVX512F_Features = 43, }; const char* cpu_feature_names[] = @@ -170,6 +173,9 @@ namespace "SSE4.1 Instructions", "SSE4.2 Instructions", "SSE4a Instructions", + "AVX Instructions", // 41 + "AVX2 Instructions", // 42 + "AVX-512F Instructions", // 43 }; std::string intel_CPUFamilyName(int composed_family) @@ -283,6 +289,21 @@ class LLProcessorInfoImpl return hasExtension(cpu_feature_names[eSSE4a_Features]); } + bool hasAVX() const + { + return hasExtension(cpu_feature_names[eAVX_Features]); + } + + bool hasAVX2() const + { + return hasExtension(cpu_feature_names[eAVX2_Features]); + } + + bool hasAVX512F() const + { + return hasExtension(cpu_feature_names[eAVX512F_Features]); + } + bool hasAltivec() const { return hasExtension("Altivec"); @@ -572,6 +593,12 @@ class LLProcessorInfoWindowsImpl : public LLProcessorInfoImpl setExtension(cpu_feature_names[eSSE4_2_Features]); } + // AVX: CPUID leaf 1, ECX bit 28, OSXSAVE + XCR0[1:2] enabled + if ((cpu_info[2] & 0x18000000) == 0x18000000 && ((_xgetbv(0) & 0x6) == 0x6)) + { + setExtension(cpu_feature_names[eAVX_Features]); + } + unsigned int feature_info = (unsigned int) cpu_info[3]; for(unsigned int index = 0, bit = 1; index < eSSE3_Features; ++index, bit <<= 1) { @@ -581,6 +608,27 @@ class LLProcessorInfoWindowsImpl : public LLProcessorInfoImpl } } } + else if (i == 7) + { + // AVX2/AVX-512* require AVX to be usable (OSXSAVE + XCR0 state) + if (hasExtension(cpu_feature_names[eAVX_Features])) + { + int cpu_info7[4] = { -1 }; + __cpuidex(cpu_info7, 7, 0); + // AVX2: EBX bit 5 + if (cpu_info7[1] & 0x20) + { + setExtension(cpu_feature_names[eAVX2_Features]); + } + + // AVX-512F: EBX bit 16; ZMM state in XCR0 (bits 5-7) + const unsigned long long xcr0 = _xgetbv(0); + if ((cpu_info7[1] & 0x10000) && ((xcr0 & 0xE6) == 0xE6)) + { + setExtension(cpu_feature_names[eAVX512F_Features]); + } + } + } } // Calling __cpuid with 0x80000000 as the InfoType argument @@ -779,6 +827,29 @@ class LLProcessorInfoDarwinImpl : public LLProcessorInfoImpl // Not supposed to happen? setExtension(cpu_feature_names[eSSE4a_Features]); } + if (cpu_features_str.find(" AVX1.0 ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX_Features]); + } + + // AVX2 and AVX-512F are reported in machdep.cpu.leaf7_features on macOS + char cpu_leaf7_features[1024]; + len = sizeof(cpu_leaf7_features); + memset(cpu_leaf7_features, 0, len); + sysctlbyname("machdep.cpu.leaf7_features", (void*)cpu_leaf7_features, &len, NULL, 0); + + std::string cpu_leaf7_str(cpu_leaf7_features); + cpu_leaf7_str = " " + cpu_leaf7_str + " "; + + if (cpu_leaf7_str.find(" AVX2 ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX2_Features]); + } + + if (cpu_leaf7_str.find(" AVX512F ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX512F_Features]); + } } }; @@ -946,6 +1017,21 @@ class LLProcessorInfoLinuxImpl : public LLProcessorInfoImpl setExtension(cpu_feature_names[eSSE4a_Features]); } + if (flags.find(" avx ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX_Features]); + } + + if (flags.find(" avx2 ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX2_Features]); + } + + if (flags.find(" avx512f ") != std::string::npos) + { + setExtension(cpu_feature_names[eAVX512F_Features]); + } + # endif // LL_X86 } @@ -1010,6 +1096,9 @@ bool LLProcessorInfo::hasSSE3S() const { return mImpl->hasSSE3S(); } bool LLProcessorInfo::hasSSE41() const { return mImpl->hasSSE41(); } bool LLProcessorInfo::hasSSE42() const { return mImpl->hasSSE42(); } bool LLProcessorInfo::hasSSE4a() const { return mImpl->hasSSE4a(); } +bool LLProcessorInfo::hasAVX() const { return mImpl->hasAVX(); } +bool LLProcessorInfo::hasAVX2() const { return mImpl->hasAVX2(); } +bool LLProcessorInfo::hasAVX512F() const { return mImpl->hasAVX512F(); } bool LLProcessorInfo::hasAltivec() const { return mImpl->hasAltivec(); } std::string LLProcessorInfo::getCPUFamilyName() const { return mImpl->getCPUFamilyName(); } std::string LLProcessorInfo::getCPUBrandName() const { return mImpl->getCPUBrandName(); } diff --git a/indra/llcommon/llprocessor.h b/indra/llcommon/llprocessor.h index b955f7f55cf..da93a531f44 100644 --- a/indra/llcommon/llprocessor.h +++ b/indra/llcommon/llprocessor.h @@ -46,6 +46,9 @@ class LL_COMMON_API LLProcessorInfo bool hasSSE41() const; bool hasSSE42() const; bool hasSSE4a() const; + bool hasAVX() const; + bool hasAVX2() const; + bool hasAVX512F() const; bool hasAltivec() const; std::string getCPUFamilyName() const; std::string getCPUBrandName() const; diff --git a/indra/llcommon/llqueuedthread.cpp b/indra/llcommon/llqueuedthread.cpp index efeeb1340ed..8c740956b3c 100644 --- a/indra/llcommon/llqueuedthread.cpp +++ b/indra/llcommon/llqueuedthread.cpp @@ -222,6 +222,33 @@ void LLQueuedThread::waitOnPending() return; } +void LLQueuedThread::waitOnPending(F32 max_time_sec) +{ + LLTimer wait_timer; + wait_timer.reset(); + while (1) + { + update(0); + + if (mIdleThread) + { + break; + } + + if (wait_timer.getElapsedTimeF64() >= max_time_sec) + { + LL_WARNS() << "waitOnPending() timed out after " << max_time_sec + << " seconds in thread: " << mName << LL_ENDL; + break; + } + + if (mThreaded) + { + yield(); + } + } +} + // MAIN thread void LLQueuedThread::printQueueStats() { diff --git a/indra/llcommon/llqueuedthread.h b/indra/llcommon/llqueuedthread.h index de50b8ae95e..95f0e58fa80 100644 --- a/indra/llcommon/llqueuedthread.h +++ b/indra/llcommon/llqueuedthread.h @@ -141,6 +141,7 @@ class LL_COMMON_API LLQueuedThread : public LLThread size_t updateQueue(F32 max_time_ms); void waitOnPending(); + void waitOnPending(F32 max_time_sec); void printQueueStats(); virtual size_t getPending(); diff --git a/indra/llcommon/llsdserialize_xml.cpp b/indra/llcommon/llsdserialize_xml.cpp index 9b2e820aee3..2c3953309fa 100644 --- a/indra/llcommon/llsdserialize_xml.cpp +++ b/indra/llcommon/llsdserialize_xml.cpp @@ -57,16 +57,59 @@ LLSDXMLFormatter::~LLSDXMLFormatter() S32 LLSDXMLFormatter::format(const LLSD& data, std::ostream& ostr, EFormatterOptions options) const { - Sink sink(ostr); - std::string post; - if (options & LLSDFormatter::OPTIONS_PRETTY) + // A stream that has already failed cannot record anything we write, and + // the caller has no way to tell an empty document from a lost one. + if (ostr.rdstate() & (std::ios_base::badbit | std::ios_base::failbit)) { - post = "\n"; + LL_WARNS() << "LLSDXMLFormatter::format: Stream already in error state" << LL_ENDL; + return -1; + } + + S32 rv = 0; + try + { + // The Sink flushes from its destructor, so it has to be destroyed + // inside the try — a throw out of that flush would be a throw out of + // a destructor. + Sink sink(ostr); + std::string post; + if (options & LLSDFormatter::OPTIONS_PRETTY) + { + post = "\n"; + } + sink.put(""); + sink.put(post); + rv = format_impl(data, sink, options, 1); + sink.put("\n"); + } + catch (const std::bad_alloc&) + { + // we might be saving something massive, don't error or crash + LL_WARNS() << "LLSDXMLFormatter::format: Memory allocation failed during formatting" << LL_ENDL; + return -1; + } + catch (const std::exception& e) + { + LL_WARNS() << "LLSDXMLFormatter::format: Standard exception: " << e.what() << LL_ENDL; + return -1; + } + catch (...) + { + LL_WARNS() << "LLSDXMLFormatter::format: Unknown exception during formatting" << LL_ENDL; + return -1; + } + + // The Sink writes in blocks and does not check the stream as it goes, so + // a mid-document I/O failure only shows up in the stream's state here. + if (ostr.rdstate() & (std::ios_base::badbit | std::ios_base::failbit)) + { + LL_WARNS() << "LLSDXMLFormatter::format: Stream I/O failed" + << " - Stream state: good=" << ostr.good() + << " eof=" << ostr.eof() + << " fail=" << ostr.fail() + << " bad=" << ostr.bad() << LL_ENDL; + return -1; } - sink.put(""); - sink.put(post); - S32 rv = format_impl(data, sink, options, 1); - sink.put("\n"); return rv; } diff --git a/indra/llcommon/llsys.cpp b/indra/llcommon/llsys.cpp index 3d18d7863bc..a1ae290267a 100644 --- a/indra/llcommon/llsys.cpp +++ b/indra/llcommon/llsys.cpp @@ -576,6 +576,9 @@ LLCPUInfo::LLCPUInfo() mHasSSE41 = proc.hasSSE41(); mHasSSE42 = proc.hasSSE42(); mHasSSE4a = proc.hasSSE4a(); + mHasAVX = proc.hasAVX(); + mHasAVX2 = proc.hasAVX2(); + mHasAVX512F = proc.hasAVX512F(); mHasAltivec = proc.hasAltivec(); mCPUMHz = (F64)proc.getCPUFrequency(); mFamily = proc.getCPUFamilyName(); @@ -617,6 +620,18 @@ LLCPUInfo::LLCPUInfo() { mSSEVersions.append("4a"); } + if (mHasAVX) + { + mSIMDVersions.append("AVX"); + } + if (mHasAVX2) + { + mSIMDVersions.append("AVX2"); + } + if (mHasAVX512F) + { + mSIMDVersions.append("AVX-512F"); + } } bool LLCPUInfo::hasAltivec() const @@ -659,6 +674,21 @@ bool LLCPUInfo::hasSSE4a() const return mHasSSE4a; } +bool LLCPUInfo::hasAVX() const +{ + return mHasAVX; +} + +bool LLCPUInfo::hasAVX2() const +{ + return mHasAVX2; +} + +bool LLCPUInfo::hasAVX512F() const +{ + return mHasAVX512F; +} + F64 LLCPUInfo::getMHz() const { return mCPUMHz; @@ -674,6 +704,11 @@ const LLSD& LLCPUInfo::getSSEVersions() const return mSSEVersions; } +const LLSD& LLCPUInfo::getSIMDVersions() const +{ + return mSIMDVersions; +} + void LLCPUInfo::stream(std::ostream& s) const { // gather machine information. diff --git a/indra/llcommon/llsys.h b/indra/llcommon/llsys.h index 0abbe047ad5..aeba5563baa 100644 --- a/indra/llcommon/llsys.h +++ b/indra/llcommon/llsys.h @@ -81,6 +81,7 @@ class LL_COMMON_API LLCPUInfo std::string getCPUString() const; const LLSD& getSSEVersions() const; + const LLSD& getSIMDVersions() const; bool hasAltivec() const; bool hasSSE() const; @@ -90,6 +91,9 @@ class LL_COMMON_API LLCPUInfo bool hasSSE41() const; bool hasSSE42() const; bool hasSSE4a() const; + bool hasAVX() const; + bool hasAVX2() const; + bool hasAVX512F() const; F64 getMHz() const; // Family is "AMD Duron" or "Intel Pentium Pro" @@ -103,11 +107,15 @@ class LL_COMMON_API LLCPUInfo bool mHasSSE41; bool mHasSSE42; bool mHasSSE4a; + bool mHasAVX; + bool mHasAVX2; + bool mHasAVX512F; bool mHasAltivec; F64 mCPUMHz; std::string mFamily; std::string mCPUString; LLSD mSSEVersions; + LLSD mSIMDVersions; }; //============================================================================= diff --git a/indra/llcommon/llthread.cpp b/indra/llcommon/llthread.cpp index 0f774564bec..e4ec12dfabd 100644 --- a/indra/llcommon/llthread.cpp +++ b/indra/llcommon/llthread.cpp @@ -372,11 +372,13 @@ void LLThread::shutdown() // The thread isn't already stopped // First, set the flag that indicates that we're ready to die setQuitting(); + if (!isStopped()) + { + // Give the thread a chance to update status. + yield(); + } //LL_INFOS() << "LLThread::~LLThread() Killing thread " << mName << " Status: " << mStatus << LL_ENDL; - // Now wait a bit for the thread to exit - // It's unclear whether I should even bother doing this - this destructor - // should never get called unless we're already stopped, really... S32 counter = 0; const S32 MAX_WAIT = 600; while (counter < MAX_WAIT) @@ -385,9 +387,9 @@ void LLThread::shutdown() { break; } - // Sleep for a tenth of a second - ms_sleep(100); - yield(); + // Sleep for 10ms, 6 seconds total, then give up and kill the thread + // Warning: This can be called from the main thread + ms_sleep(10); counter++; } } @@ -504,6 +506,14 @@ void LLThread::checkPause() void LLThread::setQuitting() { + // shutdown() frees mDataLock and mRunCondition once the worker is joined. + // A later setQuitting() has nothing left to signal, and dereferencing + // them here would fault rather than report the double call. + if (!mDataLock || !mRunCondition) + { + return; + } + mDataLock->lock(); if (mStatus == RUNNING) { diff --git a/indra/llcommon/llwatchdog.cpp b/indra/llcommon/llwatchdog.cpp index bb6d6fadcee..d64efd1b8a1 100644 --- a/indra/llcommon/llwatchdog.cpp +++ b/indra/llcommon/llwatchdog.cpp @@ -59,6 +59,7 @@ class LLWatchdogTimerThread : public LLThread { mStopping = true; mSleepMsecs = 1; + setQuitting(); } void run() override @@ -124,6 +125,11 @@ bool LLWatchdogTimeout::isAlive() const return (mTimer.getStarted() && !mTimer.hasExpired()); } +bool LLWatchdogTimeout::hasExpired() const +{ + return mTimer.hasExpired(); +} + bool LLWatchdogTimeout::started() const { return mTimer.getStarted(); @@ -233,11 +239,23 @@ void LLWatchdog::init( mCrashOnFreeze = crash_on_freeze; } -void LLWatchdog::cleanup() +void LLWatchdog::shutdown() { if (mTimer) { mTimer->stop(); + } +} + +void LLWatchdog::cleanup() +{ + LL_PROFILE_ZONE_SCOPED; + if (mTimer) + { + // ~LLWatchdogTimerThread signals and joins the worker itself, so the + // stop/shutdown pair belongs to exactly one of the two -- calling it + // here as well would run setQuitting() again after shutdown() had + // already freed the thread's mutex. delete mTimer; mTimer = nullptr; } diff --git a/indra/llcommon/llwatchdog.h b/indra/llcommon/llwatchdog.h index 45458e271b8..ca3a59692d2 100644 --- a/indra/llcommon/llwatchdog.h +++ b/indra/llcommon/llwatchdog.h @@ -47,6 +47,7 @@ class LL_COMMON_API LLWatchdogEntry // This may mean that resources used by // isAlive and other method may need synchronization. virtual bool isAlive() const = 0; + virtual bool hasExpired() const = 0; virtual bool started() const = 0; virtual void reset() = 0; virtual void start(); @@ -67,6 +68,7 @@ class LL_COMMON_API LLWatchdogTimeout : public LLWatchdogEntry virtual ~LLWatchdogTimeout(); bool isAlive() const override; + bool hasExpired() const override; bool started() const override; void reset() override; void start() override { start(""); } @@ -89,6 +91,12 @@ class LL_COMMON_API LLWatchdog : public LLSingleton { LLSINGLETON(LLWatchdog); ~LLWatchdog(); + + LLWatchdog(const LLWatchdog&) = delete; + LLWatchdog(LLWatchdog&&) = delete; + LLWatchdog& operator=(const LLWatchdog&) = delete; + LLWatchdog& operator=(LLWatchdog&&) = delete; + public: // Add an entry to the watchdog. void add(LLWatchdogEntry* e); @@ -106,6 +114,7 @@ class LL_COMMON_API LLWatchdog : public LLSingleton bool crash_on_freeze); void run(); void cleanup(); + void shutdown(); private: diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index 1b440098756..21339e7cbf7 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -17,7 +17,9 @@ #include #include // std headers +#include #include +#include // external library headers #include "llapr.h" #include "apr_thread_proc.h" @@ -108,6 +110,19 @@ static std::string readfile(const std::filesystem::path& pathname, const std::st return output; } +#if LL_WINDOWS +static std::string readfile_if_present(const std::string& pathname) +{ + std::ifstream inf(pathname.c_str()); + if (!inf.is_open()) + { + return ""; + } + return std::string((std::istreambuf_iterator(inf)), + std::istreambuf_iterator()); +} +#endif + /// Looping on LLProcess::isRunning() must now be accompanied by pumping /// "mainloop" -- otherwise the status won't update and you get an infinite /// loop. @@ -118,6 +133,9 @@ void yield(int seconds=1) LLEventPumps::instance().obtain("mainloop").post(LLSD()); } +constexpr int EOF_EVENT_RETRY_COUNT = 20; +constexpr auto EOF_EVENT_RETRY_DELAY = std::chrono::milliseconds(50); + void waitfor(LLProcess& proc, int timeout=60) { int i = 0; @@ -246,6 +264,215 @@ struct PythonProcessLauncher NamedExtTempFile mScript; }; +#if LL_WINDOWS +namespace +{ + static constexpr const char* AUTOKILL_HELPER_SCRIPT_ENV = "LLPROCESS_AUTOKILL_HELPER_SCRIPT"; + static constexpr const char* AUTOKILL_HELPER_PIDFILE_ENV = "LLPROCESS_AUTOKILL_HELPER_PIDFILE"; + static constexpr const char* AUTOKILL_HELPER_RELEASE_ENV = "LLPROCESS_AUTOKILL_HELPER_RELEASE"; + static constexpr const char* AUTOKILL_HELPER_COUNT_ENV = "LLPROCESS_AUTOKILL_HELPER_COUNT"; + static constexpr int AUTOKILL_HELPER_RELEASE_TIMEOUT_SECONDS = 60; + static constexpr int AUTOKILL_HELPER_PID_TIMEOUT_SECONDS = 15; + static constexpr DWORD AUTOKILL_HELPER_POLL_INTERVAL_MS = 100; + static constexpr DWORD AUTOKILL_CHILD_TERMINATION_TIMEOUT_MS = 5000; + static constexpr int AUTOKILL_HELPER_INVALID_ENV_EXIT = 2; + static constexpr int AUTOKILL_HELPER_INVALID_COUNT_EXIT = 3; + static constexpr int AUTOKILL_HELPER_LAUNCH_FAILURE_EXIT = 4; + + struct ScopedEnvironmentVariable + { + ScopedEnvironmentVariable(const char* name, const std::string& value): + mName(name), + mHadValue(false) + { + DWORD size = GetEnvironmentVariableA(name, nullptr, 0); + if (size > 0) + { + std::vector buffer(size); + DWORD copied = GetEnvironmentVariableA(name, buffer.data(), size); + if (copied > 0) + { + mHadValue = true; + mOldValue.assign(buffer.data(), copied); + } + } + SetEnvironmentVariableA(name, value.c_str()); + } + + ~ScopedEnvironmentVariable() + { + SetEnvironmentVariableA(mName.c_str(), mHadValue ? mOldValue.c_str() : nullptr); + } + + std::string mName; + std::string mOldValue; + bool mHadValue; + }; + + std::string get_current_executable_path() + { + std::vector buffer(MAX_PATH); + for (;;) + { + DWORD length = GetModuleFileNameA(nullptr, buffer.data(), static_cast(buffer.size())); + tut::ensure("GetModuleFileNameA() failed", length > 0); + if (length < buffer.size() && + (length < buffer.size() - 1 || buffer[length] == '\0')) + { + return std::string(buffer.data(), length); + } + buffer.resize(buffer.size() * 2); + } + } + + void run_autokill_helper_from_environment() + { + const std::string script = LLStringUtil::getenv(AUTOKILL_HELPER_SCRIPT_ENV); + if (script.empty()) + { + return; + } + + const std::string pidfile = LLStringUtil::getenv(AUTOKILL_HELPER_PIDFILE_ENV); + const std::string releasefile = LLStringUtil::getenv(AUTOKILL_HELPER_RELEASE_ENV); + const std::string countstr = LLStringUtil::getenv(AUTOKILL_HELPER_COUNT_ENV); + const std::string python = LLStringUtil::getenv("PYTHON"); + int child_count = 1; + if (!countstr.empty()) + { + try + { + child_count = std::stoi(countstr); + } + catch (const std::exception& err) + { + LL_WARNS("LLProcess") << "Invalid autokill helper child count '" + << countstr << "': " << err.what() << LL_ENDL; + std::exit(AUTOKILL_HELPER_INVALID_COUNT_EXIT); + } + } + + if (pidfile.empty() || releasefile.empty() || python.empty() || child_count < 1) + { + std::exit(AUTOKILL_HELPER_INVALID_ENV_EXIT); + } + + std::vector children; + children.reserve(child_count); + std::ofstream out(pidfile.c_str(), std::ios::trunc); + for (int i = 0; i < child_count; ++i) + { + LLProcess::Params params; + params.executable = python; + params.args.add(script); + params.autokill = true; + params.attached = false; + LLProcessPtr child = LLProcess::create(params); + if (!child) + { + std::exit(AUTOKILL_HELPER_LAUNCH_FAILURE_EXIT); + } + children.push_back(child); + out << child->getProcessID() << '\n'; + } + out.flush(); + out.close(); + + for (DWORD elapsed_ms = 0; + elapsed_ms < AUTOKILL_HELPER_RELEASE_TIMEOUT_SECONDS * 1000; + elapsed_ms += AUTOKILL_HELPER_POLL_INTERVAL_MS) + { + if (readfile_if_present(releasefile) == "exit") + { + break; + } + Sleep(AUTOKILL_HELPER_POLL_INTERVAL_MS); + } + + // Exit the helper process itself so the job handle closes and Windows + // terminates the autokilled children. + std::exit(0); + } + + std::vector wait_for_helper_pids( + const std::string& pidfile, + int expected_count, + int timeout = AUTOKILL_HELPER_PID_TIMEOUT_SECONDS) + { + for (int i = 0; + i < (timeout * 1000) / static_cast(AUTOKILL_HELPER_POLL_INTERVAL_MS); + ++i) + { + std::ifstream inf(pidfile.c_str()); + std::vector pids; + DWORD pid = 0; + while (inf >> pid) + { + pids.push_back(pid); + } + if (static_cast(pids.size()) == expected_count) + { + return pids; + } + Sleep(AUTOKILL_HELPER_POLL_INTERVAL_MS); + LLEventPumps::instance().obtain("mainloop").post(LLSD()); + } + tut::ensure(STRINGIZE("expected " << expected_count + << " child pids within " << timeout + << " seconds"), false); + return {}; + } + + void verify_autokill_on_helper_exit(const std::string& desc, int child_count) + { + NamedExtTempFile child_script("py", + "import time\n" + "time.sleep(30)\n"); + NamedTempFile pidfile("pid", ""); + NamedTempFile releasefile("release", ""); + + ScopedEnvironmentVariable helper_script(AUTOKILL_HELPER_SCRIPT_ENV, child_script.getPath().string()); + ScopedEnvironmentVariable helper_pidfile(AUTOKILL_HELPER_PIDFILE_ENV, pidfile.getPath().string()); + ScopedEnvironmentVariable helper_release(AUTOKILL_HELPER_RELEASE_ENV, releasefile.getPath().string()); + ScopedEnvironmentVariable helper_count(AUTOKILL_HELPER_COUNT_ENV, std::to_string(child_count)); + + LLProcess::Params params; + params.executable = get_current_executable_path(); + params.desc = desc + " helper"; + LLProcessPtr helper = LLProcess::create(params); + tut::ensure("helper launched", bool(helper)); + + std::vector pids = wait_for_helper_pids(pidfile.getPath().string(), child_count); + std::vector handles; + handles.reserve(pids.size()); + for (DWORD pid : pids) + { + // SYNCHRONIZE lets the test wait for the child to terminate, while + // PROCESS_QUERY_LIMITED_INFORMATION keeps the requested access minimal. + HANDLE handle = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, false, pid); + tut::ensure(STRINGIZE("opened child process handle for pid " << pid), handle != nullptr); + handles.push_back(handle); + } + + { + std::ofstream out(releasefile.getPath(), std::ios::trunc); + out << "exit"; + } + + waitfor(*helper); + tut::ensure_equals("helper exited", helper->getStatus().mState, LLProcess::EXITED); + + for (HANDLE handle : handles) + { + tut::ensure_equals("autokilled child exited", + WaitForSingleObject(handle, AUTOKILL_CHILD_TERMINATION_TIMEOUT_MS), + WAIT_OBJECT_0); + CloseHandle(handle); + } + } +} +#endif + /// convenience function for PythonProcessLauncher::run() template static void python(const std::string& desc, const CONTENT& script) @@ -298,6 +525,13 @@ namespace tut { struct llprocess_data { + llprocess_data() + { +#if LL_WINDOWS + run_autokill_helper_from_environment(); +#endif + } + LLAPRPool pool; }; typedef test_group llprocess_group; @@ -335,7 +569,7 @@ namespace tut WaitInfo(apr_proc_t* child_): child(child_), rv(-1), // we haven't yet called apr_proc_wait() - rc(0), + rc(0), // child's exit code why(apr_exit_why_e(0)) {} apr_proc_t* child; // which subprocess @@ -1042,9 +1276,6 @@ namespace tut template<> template<> void object::test<16>() { -#ifdef LL_DISABLE_DEBUG_LOGGING - skip("Debug messages disabled"); -#endif set_test_name("get*Pipe() validation"); PythonProcessLauncher py(get_test_name(), "from __future__ import print_function\n" @@ -1221,6 +1452,14 @@ namespace tut LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); EventListener listener(childout.getPump()); waitfor(*py.mPy); + // On Windows the pipe-close EOF notification can trail the process exit + // status by a short interval, so keep pumping for up to 1 second + // (20 * 50 ms) until it arrives. + for (int i = 0; i < EOF_EVENT_RETRY_COUNT && listener.mHistory.empty(); ++i) + { + std::this_thread::sleep_for(EOF_EVENT_RETRY_DELAY); + LLEventPumps::instance().obtain("mainloop").post(LLSD()); + } // We can't be positive there will only be a single event, if the OS // (or any other intervening layer) does crazy buffering. What we want // to ensure is that there was exactly ONE event with "eof" true, and @@ -1432,4 +1671,396 @@ namespace tut waitfor(*py.mPy); ensure("postend never triggered", listener.mTriggered); } + + template<> template<> + void object::test<25>() + { + set_test_name("large stdin write"); + PythonProcessLauncher py(get_test_name(), + "import sys\n" + "expected = int(sys.argv[1])\n" + "data = sys.stdin.buffer.read(expected)\n" + "if len(data) != expected:\n" + " print('short read %s' % len(data))\n" + "elif data != (b'x' * expected):\n" + " print('payload mismatch')\n" + "else:\n" + " print('ok')\n"); + const std::size_t payload_size = 1024 * 1024; + const std::string payload(payload_size, 'x'); + py.mParams.args.add(stringize(payload_size)); + py.mParams.files.add(LLProcess::FileParam("pipe")); // stdin + py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout + py.launch(); + + LLProcess::ReadPipe& childout(py.mPy->getReadPipe(LLProcess::STDOUT)); + std::ostream& childin(py.mPy->getWritePipe(LLProcess::STDIN).get_ostream()); + childin.write(payload.data(), payload.size()); + childin.flush(); + + int i, timeout = 20; + for (i = 0; i < timeout && py.mPy->isRunning() && ! childout.contains("\n"); ++i) + { + yield(); + } + ensure("large stdin write timed out", i < timeout); + ensure("child never replied", childout.contains("\n")); + ensure_equals("large stdin ack", childout.getline(), "ok"); + waitfor(*py.mPy); + ensure_equals("bad child termination", py.mPy->getStatus().mState, LLProcess::EXITED); + ensure_equals("bad child exit code", py.mPy->getStatus().mData, 0); + } + + template<> template<> + void object::test<26>() + { + set_test_name("all three pipes active"); + PythonProcessLauncher py(get_test_name(), + "import sys\n" + "sys.stdout.write('stdout message\\n')\n" + "sys.stderr.write('stderr message\\n')\n" + "sys.stdout.flush()\n" + "sys.stderr.flush()\n" + "input_data = sys.stdin.readline()\n" + "sys.stdout.write('received: ' + input_data)\n"); + py.mParams.files.add(LLProcess::FileParam("pipe")); // stdin + py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout + py.mParams.files.add(LLProcess::FileParam("pipe")); // stderr + py.launch(); + + LLProcess::ReadPipe& childout = py.mPy->getReadPipe(LLProcess::STDOUT); + LLProcess::ReadPipe& childerr = py.mPy->getReadPipe(LLProcess::STDERR); + + // Wait for initial output + int i, timeout = 60; + for (i = 0; i < timeout && (!childout.contains("\n") || !childerr.contains("\n")); ++i) + { + yield(); + } + ensure("initial output timeout", i < timeout); + + ensure_equals("stdout message", childout.getline(), "stdout message"); + ensure_equals("stderr message", childerr.getline(), "stderr message"); + + py.mPy->getWritePipe().get_ostream() << "test input" << std::endl; + + waitfor(*py.mPy); + ensure("script never replied", childout.contains("\n")); + ensure_equals("echo response", childout.getline(), "received: test input"); + } + + template<> template<> + void object::test<27>() + { + set_test_name("process state transitions"); + PythonProcessLauncher py(get_test_name(), + "import time\n" + "time.sleep(1)\n"); + + py.launch(); + + // Immediately after launch + LLProcess::Status status = py.mPy->getStatus(); + ensure_equals("post-launch state", status.mState, LLProcess::RUNNING); + ensure("process is running", py.mPy->isRunning()); + + // After completion + waitfor(*py.mPy); + status = py.mPy->getStatus(); + ensure_equals("post-completion state", status.mState, LLProcess::EXITED); + ensure("process is not running", !py.mPy->isRunning()); + } + + template<> template<> + void object::test<28>() + { + set_test_name("ReadPipe limit with rapid data"); + PythonProcessLauncher py(get_test_name(), + "import sys\n" + "for i in range(100):\n" + " sys.stdout.write('line %d\\n' % i)\n" + " sys.stdout.flush()\n"); + py.mParams.files.add(LLProcess::FileParam()); // stdin + py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout + py.launch(); + + LLProcess::ReadPipe& childout = py.mPy->getReadPipe(LLProcess::STDOUT); + childout.setLimit(50); // Small limit + + EventListener listener(childout.getPump()); + waitfor(*py.mPy); + + // Verify that events respect the limit + listener.checkHistory( + [](const EventListener::Listory& history) + { + bool saw_data = false; + for (const LLSD& event : history) + { + const std::string data = event["data"].asString(); + ensure("event data within limit", data.length() <= 50); + saw_data = saw_data || !data.empty(); + } + ensure("saw at least one data event", saw_data); + }); + } + + template<> template<> + void object::test<29>() + { + set_test_name("nonexistent executable"); + LLProcess::Params params; + params.executable = "/path/to/nonexistent/executable"; + std::string pumpname("postend_invalid"); + params.postend = pumpname; + + EventListener listener(LLEventPumps::instance().obtain(pumpname)); + + LLProcessPtr child = LLProcess::create(params); + ensure("should not create invalid process", !child); + + listener.checkHistory( + [](const EventListener::Listory& history) + { + ensure_equals("got failure event", history.size(), 1); + LLSD event = history.front(); + ensure_equals("state is UNSTARTED", + event["state"].asInteger(), LLProcess::UNSTARTED); + ensure("has error string", !event["string"].asString().empty()); + }); + } + + template<> template<> + void object::test<30>() + { + set_test_name("process ID and handle validity"); + PythonProcessLauncher py(get_test_name(), + "import time\n" + "time.sleep(1)\n"); + py.launch(); + + LLProcess::id pid = py.mPy->getProcessID(); + LLProcess::handle handle = py.mPy->getProcessHandle(); + + ensure("PID is valid", pid != 0); + ensure("handle is valid", handle != 0); + +#if LL_WINDOWS + // On Windows, verify handle is a valid process handle + DWORD exitCode; + ensure("GetExitCodeProcess succeeds", + GetExitCodeProcess(handle, &exitCode) != 0); + ensure_equals("process still running", exitCode, STILL_ACTIVE); +#else + // On POSIX, PID and handle should be the same + ensure_equals("PID equals handle", pid, handle); + ensure("process still running", py.mPy->isRunning()); +#endif + + waitfor(*py.mPy); + } + + template<> template<> + void object::test<31>() + { + set_test_name("ReadPipe search at boundaries"); + PythonProcessLauncher py(get_test_name(), + "import sys\n" + "sys.stdout.write('abcdefghijklmnopqrstuvwxyz')\n"); + py.mParams.files.add(LLProcess::FileParam()); // stdin + py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout + py.launch(); + + LLProcess::ReadPipe& childout = py.mPy->getReadPipe(LLProcess::STDOUT); + waitfor(*py.mPy); + + // Test find at exact end + ensure("find at end succeeds", + childout.find("xyz", 23) != LLProcess::ReadPipe::npos); + ensure("find past end returns npos", + childout.find("a", 30) == LLProcess::ReadPipe::npos); + + // Test empty string search + ensure("contains empty string", childout.contains("")); + + // Test single char at boundaries + ensure_equals("find 'a' at 0", childout.find('a', 0), 0); + ensure_equals("find 'z' at end", childout.find('z'), 25); + + // Test peek at boundaries + ensure_equals("peek at exact size", childout.peek(26), ""); + ensure_equals("peek past end", childout.peek(30, 10), ""); + } + + template<> template<> + void object::test<32>() + { + set_test_name("rapid process lifecycle"); + const int ITERATIONS = 10; + + for (int i = 0; i < ITERATIONS; ++i) + { + PythonProcessLauncher py(STRINGIZE(get_test_name() << " " << i), + "import sys\n" + "sys.exit(0)\n"); + py.run(); + ensure_equals("quick exit status", + py.mPy->getStatus().mState, LLProcess::EXITED); + ensure_equals("quick exit code", + py.mPy->getStatus().mData, 0); + // Let the process object destroy + } + + // Give time for cleanup + yield(0); + } + + template<> template<> + void object::test<33>() + { + set_test_name("process with no output"); + PythonProcessLauncher py(get_test_name(), + "import time\n" + "time.sleep(1)\n" + "# Produce no output\n"); + py.mParams.files.add(LLProcess::FileParam()); // stdin + py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout + py.mParams.files.add(LLProcess::FileParam("pipe")); // stderr + py.launch(); + + LLProcess::ReadPipe& childout = py.mPy->getReadPipe(LLProcess::STDOUT); + LLProcess::ReadPipe& childerr = py.mPy->getReadPipe(LLProcess::STDERR); + + EventListener outListener(childout.getPump()); + EventListener errListener(childerr.getPump()); + + waitfor(*py.mPy); + // On Windows the pipe-close EOF notification can trail the process exit + // status by a short interval, so keep pumping for up to 1 second + // (20 * 50 ms) until both pipes report it. + for (int i = 0; + i < EOF_EVENT_RETRY_COUNT && + (outListener.mHistory.empty() || errListener.mHistory.empty()); + ++i) + { + std::this_thread::sleep_for(EOF_EVENT_RETRY_DELAY); + LLEventPumps::instance().obtain("mainloop").post(LLSD()); + } + + ensure_equals("stdout size", childout.size(), 0); + ensure_equals("stderr size", childerr.size(), 0); + ensure_equals("process exited", py.mPy->getStatus().mState, LLProcess::EXITED); + + auto check_eof = [](const EventListener::Listory& history, const std::string& which) + { + ensure_equals(STRINGIZE(which << " events"), history.size(), 1); + const LLSD& event = history.front(); + ensure(STRINGIZE(which << " eof event"), event["eof"].asBoolean()); + ensure_equals(STRINGIZE(which << " len"), event["len"].asInteger(), 0); + }; + + outListener.checkHistory( + [&](const EventListener::Listory& history) + { + check_eof(history, "stdout"); + }); + + errListener.checkHistory( + [&](const EventListener::Listory& history) + { + check_eof(history, "stderr"); + }); + } + + template<> template<> + void object::test<34>() + { + set_test_name("tick() completes quickly after kill()"); + // Regression test: tick() must not block after kill() is called. + // The old Windows code called WaitForSingleObject(..., 100) in tick(), + // causing a 100 ms stall on the main thread. This test ensures that a + // mainloop tick completes well under that threshold even when the child + // process has been killed and may still be exiting. + PythonProcessLauncher py(get_test_name(), + "import time\n" + "time.sleep(120)\n"); + py.launch(); + + // Wait for the process to start up + yield(); + ensure("process started", py.mPy->isRunning()); + + // Send the kill signal + py.mPy->kill(); + + // Sleep longer than the tick threshold to ensure the child has had + // time to exit at the OS level, so the next tick is likely to enter + // the "process just exited" code path that used to block for 100 ms. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Time a single mainloop tick: it must not block + auto start = std::chrono::steady_clock::now(); + LLEventPumps::instance().obtain("mainloop").post(LLSD()); + auto elapsed_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start).count(); + + ensure(STRINGIZE("tick() took " << elapsed_ms << " ms, expected < 75 ms"), + elapsed_ms < 75); + + // Let the process fully exit so cleanup is orderly + waitfor(*py.mPy); + } + + template<> template<> + void object::test<35>() + { + set_test_name("LLProcess destructor completes quickly after kill()"); + // Regression test: after an explicit kill() call the destructor must + // not perform a blocking wait. The old Windows code called + // WaitForSingleObject(..., 100) in the destructor, causing a 100 ms + // stall on the main thread. With mKillCalled set to true, the + // destructor skips the termination/wait block entirely. + PythonProcessLauncher py(get_test_name(), + "import time\n" + "time.sleep(120)\n"); + py.launch(); + + // Wait for the process to start up + yield(); + ensure("process started", py.mPy->isRunning()); + + // Kill the process (sets mKillCalled = true) + py.mPy->kill(); + + // Time how long the destructor takes + auto start = std::chrono::steady_clock::now(); + py.mPy.reset(); // explicit destruction + auto elapsed_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start).count(); + + ensure(STRINGIZE("destructor took " << elapsed_ms << " ms, expected < 75 ms"), + elapsed_ms < 75); + } + + template<> template<> + void object::test<36>() + { + set_test_name("autokill ensures child termination on parent exit"); +#if !LL_WINDOWS + skip("Windows-specific test"); +#else + verify_autokill_on_helper_exit(get_test_name(), 1); +#endif + } + + template<> template<> + void object::test<37>() + { + set_test_name("multiple processes with autokill"); +#if !LL_WINDOWS + skip("Windows-specific test"); +#else + verify_autokill_on_helper_exit(get_test_name(), 2); +#endif + } } // namespace tut diff --git a/indra/llcommon/tests/llsdserialize_test.cpp b/indra/llcommon/tests/llsdserialize_test.cpp index 4eb20fb9315..f8222692e9f 100644 --- a/indra/llcommon/tests/llsdserialize_test.cpp +++ b/indra/llcommon/tests/llsdserialize_test.cpp @@ -280,6 +280,58 @@ namespace tut deserialize_no_spurious_error("\n", LLSD::emptyArray()); } + template<> template<> + void sd_xml_object::test<8>() + { + // Test that stream state (precision and exceptions) is correctly restored after format() + { + std::ostringstream ostr; + + // Set custom precision + ostr.precision(10); + + // Set some exception bits + ostr.exceptions(std::ios_base::badbit); + std::ios_base::iostate original_exceptions = ostr.exceptions(); + + // Format some LLSD data + mSD = 3.141592653589793; + S32 result = mFormatter->format(mSD, ostr); + + // Verify formatting succeeded + ensure("format should succeed", result >= 0); + + // Verify precision was restored + ensure_equals("precision should be restored", + ostr.precision(), 10); + + // Verify exceptions were restored + ensure_equals("exception bits should be restored", + ostr.exceptions(), original_exceptions); + } + + // Test with no bits set + { + std::ostringstream ostr; + ostr.precision(5); + std::ios_base::iostate original_exceptions = ostr.exceptions(); // 0 + + mSD = "test"; + S32 result = mFormatter->format(mSD, ostr); + + // Verify formatting succeeded + ensure("format should succeed", result >= 0); + + // Verify exceptions were removed + ensure_equals("exception bits should remain unchanged", + ostr.exceptions(), original_exceptions); + + // Verify precision was still restored even on failure + ensure_equals("precision should be restored even on stream failure", + ostr.precision(), (std::streamsize)5); + } + } + class TestLLSDSerializeData { public: diff --git a/indra/llcorehttp/CMakeLists.txt b/indra/llcorehttp/CMakeLists.txt index a11a75f7335..46a8b387974 100644 --- a/indra/llcorehttp/CMakeLists.txt +++ b/indra/llcorehttp/CMakeLists.txt @@ -96,6 +96,7 @@ if (BUILD_TESTING AND LLCOREHTTP_TESTS) tests/test_httpheaders.hpp tests/test_bufferarray.hpp tests/test_bufferstream.hpp + tests/test_jsonrpcws.hpp ) list(APPEND llcorehttp_TEST_SOURCE_FILES ${llcorehttp_TEST_HEADER_FILES}) diff --git a/indra/llcorehttp/httpstats.cpp b/indra/llcorehttp/httpstats.cpp index dc2ac35c711..f64752e91e1 100644 --- a/indra/llcorehttp/httpstats.cpp +++ b/indra/llcorehttp/httpstats.cpp @@ -41,6 +41,7 @@ HTTPStats::~HTTPStats() void HTTPStats::resetStats() { + std::lock_guard lock(mStatsMutex); mResutCodes.clear(); mDataDown.reset(); mDataUp.reset(); @@ -50,6 +51,7 @@ void HTTPStats::resetStats() void HTTPStats::recordResultCode(S32 code) { + std::lock_guard lock(mStatsMutex); std::map::iterator it; it = mResutCodes.find(code); @@ -86,6 +88,7 @@ namespace void HTTPStats::dumpStats() { + std::lock_guard lock(mStatsMutex); std::stringstream out; out << "HTTP DATA SUMMARY" << std::endl; diff --git a/indra/llcorehttp/httpstats.h b/indra/llcorehttp/httpstats.h index 5c0f26d34e7..08a537cda74 100644 --- a/indra/llcorehttp/httpstats.h +++ b/indra/llcorehttp/httpstats.h @@ -47,17 +47,23 @@ namespace LLCore void recordDataDown(size_t bytes) { + std::lock_guard lock(mStatsMutex); mDataDown.push((F32)bytes); } void recordDataUp(size_t bytes) { + std::lock_guard lock(mStatsMutex); mDataUp.push((F32)bytes); } - void recordHTTPRequest() { ++mRequests; } + void recordHTTPRequest() + { + std::lock_guard lock(mStatsMutex); + ++mRequests; + } - void recordResultCode(S32 code); + void recordResultCode(S32 code); // http thread void dumpStats(); private: @@ -67,6 +73,8 @@ namespace LLCore S32 mRequests; std::map mResutCodes; + + std::mutex mStatsMutex; }; diff --git a/indra/llcorehttp/lljsonrpcws.cpp b/indra/llcorehttp/lljsonrpcws.cpp index 96f05733c27..725ab7e75b2 100644 --- a/indra/llcorehttp/lljsonrpcws.cpp +++ b/indra/llcorehttp/lljsonrpcws.cpp @@ -451,6 +451,24 @@ void LLJSONRPCConnection::sweepTimeouts() } } +void LLJSONRPCConnection::testInjectPendingRequest(const std::string& id, F64 deadline, ResponseCallback callback) +{ + LLMutexLock lock(&mMutex); + mPendingRequests[id] = std::move(callback); + mPendingDeadlines.push({ deadline, id }); +} + +void LLJSONRPCConnection::testSweepTimeouts() +{ + sweepTimeouts(); +} + +size_t LLJSONRPCConnection::testPendingRequestCount() const +{ + LLMutexLock lock(&mMutex); + return mPendingRequests.size(); +} + LLSD LLJSONRPCConnection::generateId() { // Server-wide atomic counter for efficient unique ID generation. diff --git a/indra/llcorehttp/lljsonrpcws.h b/indra/llcorehttp/lljsonrpcws.h index cd71473a473..7cb26b1fb26 100644 --- a/indra/llcorehttp/lljsonrpcws.h +++ b/indra/llcorehttp/lljsonrpcws.h @@ -401,6 +401,11 @@ class LLJSONRPCConnection : public LLWebsocketMgr::WSConnection /// Invoked by the sweep timer; fires the timeout callback for any /// request whose deadline has passed. Safe to call from the main thread. void sweepTimeouts(); + +public: + void testInjectPendingRequest(const std::string& id, F64 deadline, ResponseCallback callback); + void testSweepTimeouts(); + size_t testPendingRequestCount() const; }; /** diff --git a/indra/llcorehttp/tests/llcorehttp_test.cpp b/indra/llcorehttp/tests/llcorehttp_test.cpp index c7c50e61664..65d7fe93a9a 100644 --- a/indra/llcorehttp/tests/llcorehttp_test.cpp +++ b/indra/llcorehttp/tests/llcorehttp_test.cpp @@ -44,6 +44,7 @@ #include "test_httprequest.hpp" #include "test_httpheaders.hpp" #include "test_httprequestqueue.hpp" +#include "test_jsonrpcws.hpp" #include "_httpservice.h" #include "llproxy.h" diff --git a/indra/llcorehttp/tests/test_jsonrpcws.hpp b/indra/llcorehttp/tests/test_jsonrpcws.hpp new file mode 100644 index 00000000000..6cf7932aa0e --- /dev/null +++ b/indra/llcorehttp/tests/test_jsonrpcws.hpp @@ -0,0 +1,146 @@ +/** + * @file test_jsonrpcws.hpp + * @brief unit tests for LLJSONRPCConnection helpers and dispatch behavior + */ + +#ifndef TEST_LLCORE_JSONRPCWS_H_ +#define TEST_LLCORE_JSONRPCWS_H_ + +#include "lljsonrpcws.h" +#include "lltimer.h" + +namespace +{ +class TestJSONRPCConnection : public LLJSONRPCConnection +{ +public: + TestJSONRPCConnection() + : LLJSONRPCConnection(LLWebsocketMgr::WSServer::ptr_t(), LLWebsocketMgr::connection_h()) + { + } + + using LLJSONRPCConnection::processMessage; + using LLJSONRPCConnection::validateMessage; + using LLJSONRPCConnection::testInjectPendingRequest; + using LLJSONRPCConnection::testPendingRequestCount; + using LLJSONRPCConnection::testSweepTimeouts; +}; +} + +namespace tut +{ + struct JSONRPCWSTestData + { + }; + + typedef test_group JSONRPCWSTestGroupType; + typedef JSONRPCWSTestGroupType::object JSONRPCWSTestObjectType; + JSONRPCWSTestGroupType JSONRPCWSTestGroup("LLJSONRPCConnection Tests"); + + template<> template<> + void JSONRPCWSTestObjectType::test<1>() + { + set_test_name("makeEnvelope notification omits id"); + + LLSD params; + params["value"] = 42; + LLSD env = LLJSONRPCConnection::makeEnvelope(LLSD(), "runtime.debug", params, LLSD(), LLSD()); + + ensure("jsonrpc field should exist", env.has("jsonrpc")); + ensure_equals("jsonrpc version", env["jsonrpc"].asString(), "2.0"); + ensure("notification should omit id", !env.has("id")); + ensure_equals("method", env["method"].asString(), "runtime.debug"); + ensure_equals("param round trip", env["params"]["value"].asInteger(), 42); + } + + template<> template<> + void JSONRPCWSTestObjectType::test<2>() + { + set_test_name("makeEnvelope response keeps id slot"); + + LLSD env = LLJSONRPCConnection::makeEnvelope(LLSD(), std::string(), LLSD(), LLSD("ok"), LLSD()); + + ensure("response should include id", env.has("id")); + ensure("response should not include method", !env.has("method")); + ensure_equals("result", env["result"].asString(), "ok"); + } + + template<> template<> + void JSONRPCWSTestObjectType::test<3>() + { + set_test_name("validateMessage accepts valid request and rejects invalid params"); + + TestJSONRPCConnection conn; + + LLSD valid; + valid["jsonrpc"] = "2.0"; + valid["method"] = "session.ping"; + LLSD params = LLSD::emptyMap(); + params["timestamp"] = 123; + valid["params"] = params; + ensure("valid request should pass", conn.validateMessage(valid, true)); + + LLSD invalid = valid; + invalid["params"] = "not-an-array-or-object"; + ensure("invalid params type should fail", !conn.validateMessage(invalid, true)); + } + + template<> template<> + void JSONRPCWSTestObjectType::test<4>() + { + set_test_name("processMessage dispatches notification handler"); + + TestJSONRPCConnection conn; + + bool called = false; + LLSD seen_id; + LLSD seen_params; + conn.registerMethod("runtime.debug", + [&](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD + { + called = true; + ensure_equals("method propagated", method, "runtime.debug"); + seen_id = id; + seen_params = params; + return LLSD(); + }); + + LLSD params; + params["message"] = "hello"; + LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "runtime.debug", params, LLSD(), LLSD()); + conn.processMessage(notification); + + ensure("handler should be called", called); + ensure("notification id should be undefined", seen_id.isUndefined()); + ensure_equals("payload should be forwarded", seen_params["message"].asString(), "hello"); + } + + template<> template<> + void JSONRPCWSTestObjectType::test<5>() + { + set_test_name("sweepTimeouts expires overdue callbacks"); + + TestJSONRPCConnection conn; + + bool callback_called = false; + conn.testInjectPendingRequest( + "req_1", + LLTimer::getTotalSeconds() - 1.0, + [&](const LLSD& result, const LLSD& error) + { + callback_called = true; + ensure("timed out result should be undefined", result.isUndefined()); + ensure_equals( + "timeout code", + error["code"].asInteger(), + LLJSONRPCConnection::RPCError::REQUEST_TIMEOUT); + }); + + ensure_equals("pending request should be tracked", conn.testPendingRequestCount(), (size_t)1); + conn.testSweepTimeouts(); + ensure("timeout callback should be called", callback_called); + ensure_equals("pending request should be removed", conn.testPendingRequestCount(), (size_t)0); + } +} + +#endif \ No newline at end of file diff --git a/indra/llfilesystem/lldiskcache.cpp b/indra/llfilesystem/lldiskcache.cpp index 965a428b4cb..6e7e8ad0ca8 100644 --- a/indra/llfilesystem/lldiskcache.cpp +++ b/indra/llfilesystem/lldiskcache.cpp @@ -84,9 +84,9 @@ LLDiskCache::LLDiskCache(const std::string& cache_dir, // WARNING: purge() is called by LLPurgeDiskCacheThread. As such it must // NOT touch any LLDiskCache data without introducing and locking a mutex! -// Interaction through the filesystem itself should be safe. Let’s say thread +// Interaction through the filesystem itself should be safe. Let's say thread // A is accessing the cache file for reading/writing and thread B is trimming -// the cache. Let’s also assume using llifstream to open a file and +// the cache. Let's also assume using llifstream to open a file and // std::filesystem::remove are not atomic (which will be pretty much the // case). diff --git a/indra/llfilesystem/lllfsthread.cpp b/indra/llfilesystem/lllfsthread.cpp index c73a64f2701..5d382470596 100644 --- a/indra/llfilesystem/lllfsthread.cpp +++ b/indra/llfilesystem/lllfsthread.cpp @@ -50,6 +50,7 @@ S32 LLLFSThread::updateClass(U32 ms_elapsed) //static void LLLFSThread::cleanupClass() { + LL_PROFILE_ZONE_SCOPED; llassert(sLocal != NULL); sLocal->setQuitting(); while (sLocal->getPending()) diff --git a/indra/llinventory/llinventory.cpp b/indra/llinventory/llinventory.cpp index 17f20c6ddf4..be66b784ad1 100644 --- a/indra/llinventory/llinventory.cpp +++ b/indra/llinventory/llinventory.cpp @@ -33,6 +33,7 @@ #include "llxorcipher.h" #include "llsd.h" #include "llsdserialize.h" +#include "llstreamtools.h" #include "message.h" #include @@ -870,6 +871,42 @@ bool LLInventoryItem::importLegacyStream(std::istream& input_stream) } mDescription.assign(valuestr); + + // Currently server side doesn't handle escaped newline right + // and they end up unescaped. + // And even if we do copy the data correctly, moving the item + // from notecard using CopyInventoryFromNotecard drops the + // content after first the new line. + // + // Check if the description ends with | on this line + if (strchr(buffer, '|') == nullptr) + { + // No | found, continue reading lines until we find one + char next_buffer[MAX_STRING]; + while (input_stream.good()) + { + input_stream.getline(next_buffer, MAX_STRING); + + // Look for | delimiter + char* pipe_pos = strchr(next_buffer, '|'); + if (pipe_pos != nullptr) + { + // Found the delimiter, add text up to it + *pipe_pos = '\0'; // Terminate at the pipe + mDescription += '\n'; + mDescription += next_buffer; + break; + } + else + { + // No delimiter yet, add the whole line + mDescription += '\n'; + mDescription += next_buffer; + } + } + } + + unescape_string(mDescription); LLStringUtil::replaceNonstandardASCII(mDescription, ' '); /* TODO -- ask Ian about this code const char *donkey = mDescription.c_str(); @@ -963,7 +1000,12 @@ bool LLInventoryItem::exportLegacyStream(std::ostream& output_stream, bool inclu output_stream << buffer; mSaleInfo.exportLegacyStream(output_stream); output_stream << "\t\tname\t" << mName.c_str() << "|\n"; - output_stream << "\t\tdesc\t" << mDescription.c_str() << "|\n"; + // Note: Currently server side doesn't handle newline right for notecards. + // Even if we escape or replace all newlines with spaces, notecard's + // import buffer still ends up with newlines. + std::string desc = mDescription; + escape_string(desc); + output_stream << "\t\tdesc\t" << desc.c_str() << "|\n"; output_stream << "\t\tcreation_date\t" << mCreationDate << "\n"; output_stream << "\t}\n"; return true; @@ -984,12 +1026,14 @@ void LLInventoryItem::asLLSD( LLSD& sd ) const if (mThumbnailUUID.notNull()) { - sd[INV_THUMBNAIL_LABEL] = LLSD().with(INV_ASSET_ID_LABEL, mThumbnailUUID); + LLSD& thumbnail = sd[INV_THUMBNAIL_LABEL]; + thumbnail[INV_ASSET_ID_LABEL] = mThumbnailUUID; } if (mFavorite) { - sd[INV_FAVORITE_LABEL] = LLSD().with(INV_TOGGLED_LABEL, mFavorite); + LLSD& favorite = sd[INV_FAVORITE_LABEL]; + favorite[INV_TOGGLED_LABEL] = mFavorite; } if (!mRuntime.empty()) @@ -1011,7 +1055,7 @@ void LLInventoryItem::asLLSD( LLSD& sd ) const cipher.encrypt(shadow_id.mData, UUID_BYTES); sd[INV_SHADOW_ID_LABEL] = shadow_id; } - sd[INV_ASSET_TYPE_LABEL] = std::string(LLAssetType::lookup(mType)); + sd[INV_ASSET_TYPE_LABEL] = LLAssetType::lookup(mType); const std::string inv_type_str = LLInventoryType::lookup(mInventoryType); if(!inv_type_str.empty()) { @@ -1050,217 +1094,231 @@ bool LLInventoryItem::fromLLSD(const LLSD& sd, bool is_new) end = sd.endMap(); for (i = sd.beginMap(); i != end; ++i) { - if (i->first == INV_ITEM_ID_LABEL) - { - mUUID = i->second; - continue; - } + // Use string length as a fast pre-filter before string comparison + const std::string& key = i->first; + const LLSD& value = i->second; + const size_t key_len = key.length(); - if (i->first == INV_PARENT_ID_LABEL) + switch (key_len) { - mParentUUID = i->second; - continue; - } - - if (i->first == INV_THUMBNAIL_LABEL) - { - const LLSD &thumbnail_map = i->second; - if (thumbnail_map.has(INV_ASSET_ID_LABEL)) - { - mThumbnailUUID = thumbnail_map[INV_ASSET_ID_LABEL]; - } - /* Example: - asset_id - acc0ec86 - 17f2 - 4b92 - ab41 - 6718b1f755f7 - perms - 8 - service - 3 - version - 1 - */ - continue; - } + case 4: // "name", "desc", "type" + if (key == INV_NAME_LABEL) // "name" + { + mName = value.asString(); + LLStringUtil::replaceNonstandardASCII(mName, ' '); + LLStringUtil::replaceChar(mName, '|', ' '); + continue; + } + if (key == INV_DESC_LABEL) // "desc" + { + mDescription = value.asString(); + LLStringUtil::replaceNonstandardASCII(mDescription, ' '); + continue; + } + if (key == INV_ASSET_TYPE_LABEL) // "type" + { + if (value.isString()) + { + mType = LLAssetType::lookup(value.asStringRef().c_str()); + } + else if (value.isInteger()) + { + S8 type = (U8)value.asInteger(); + mType = static_cast(type); + } + continue; + } + break; - if (i->first == INV_THUMBNAIL_ID_LABEL) - { - mThumbnailUUID = i->second.asUUID(); - continue; - } + case 5: // "flags" + if (key == INV_FLAGS_LABEL) + { + if (value.isBinary()) + { + mFlags = ll_U32_from_sd(value); + } + else if (value.isInteger()) + { + mFlags = value.asInteger(); + } + continue; + } + break; - if (i->first == INV_FAVORITE_LABEL) - { - const LLSD& favorite_map = i->second; - if (favorite_map.has(INV_TOGGLED_LABEL)) - { - mFavorite = favorite_map[INV_TOGGLED_LABEL].asBoolean(); - } - continue; - } + case 6: // "script" + if (key == INV_SCRIPT_LABEL) + { + const LLSD& script_map = value; + if (script_map.has(INV_RUNTIME_LABEL)) + { + mRuntime = script_map[INV_RUNTIME_LABEL].asString(); + } + else + { + // Clear stale runtime data when a script block is + // present without an explicit runtime value. + mRuntime.clear(); + } + continue; + } + break; - if (i->first == INV_SCRIPT_LABEL) - { - const LLSD& script_map = i->second; - const std::string w = INV_RUNTIME_LABEL; - if (script_map.has(w)) - { - mRuntime = script_map[w].asString(); - } - else - { - // Clear any stale runtime when a script block is present - // but no explicit runtime value is provided. - mRuntime.clear(); - } - continue; - } + case 7: // "item_id" + if (key == INV_ITEM_ID_LABEL) + { + mUUID = value; + continue; + } + break; - if (i->first == INV_METADATA_LABEL) - { - // Server (non-AIS) exports thumbnail/favorite/script nested under - // a "metadata" wrapper; mirror the legacy-stream parser behavior. - const LLSD& metadata = i->second; - if (metadata.has(INV_THUMBNAIL_LABEL)) - { - const LLSD& thumbnail = metadata[INV_THUMBNAIL_LABEL]; - if (thumbnail.has(INV_ASSET_ID_LABEL)) + case 8: // "asset_id", "inv_type" + if (key == INV_ASSET_ID_LABEL) { - mThumbnailUUID = thumbnail[INV_ASSET_ID_LABEL].asUUID(); + mAssetUUID = value; + continue; } - } - if (metadata.has(INV_FAVORITE_LABEL)) - { - const LLSD& favorite = metadata[INV_FAVORITE_LABEL]; - if (favorite.has(INV_TOGGLED_LABEL)) + if (key == INV_INVENTORY_TYPE_LABEL) // "inv_type" { - mFavorite = favorite[INV_TOGGLED_LABEL].asBoolean(); + if (value.isString()) + { + mInventoryType = LLInventoryType::lookup(value.asStringRef().c_str()); + } + else if (value.isInteger()) + { + S8 type = (U8)value.asInteger(); + mInventoryType = static_cast(type); + } + continue; } - } - if (metadata.has(INV_SCRIPT_LABEL) - && metadata[INV_SCRIPT_LABEL].has(INV_RUNTIME_LABEL)) - { - mRuntime = metadata[INV_SCRIPT_LABEL][INV_RUNTIME_LABEL].asString(); - } - continue; - } - - if (i->first == INV_PERMISSIONS_LABEL) - { - mPermissions.importLLSD(i->second); - continue; - } - - if (i->first == INV_SALE_INFO_LABEL) - { - // Sale info used to contain next owner perm. It is now in - // the permissions. Thus, we read that out, and fix legacy - // objects. It's possible this op would fail, but it - // should pick up the vast majority of the tasks. - bool has_perm_mask = false; - U32 perm_mask = 0; - if (!mSaleInfo.fromLLSD(i->second, has_perm_mask, perm_mask)) - { - return false; - } - if (has_perm_mask) - { - if (perm_mask == PERM_NONE) + if (key == INV_FAVORITE_LABEL) // "favorite" { - perm_mask = mPermissions.getMaskOwner(); + if (value.has(INV_TOGGLED_LABEL)) + { + mFavorite = value[INV_TOGGLED_LABEL].asBoolean(); + } + continue; } - // fair use fix. - if (!(perm_mask & PERM_COPY)) + if (key == INV_METADATA_LABEL) { - perm_mask |= PERM_TRANSFER; + // Server (non-AIS) exports thumbnail, favorite, and + // script metadata under a "metadata" wrapper. + const LLSD& metadata = value; + if (metadata.has(INV_THUMBNAIL_LABEL)) + { + const LLSD& thumbnail = metadata[INV_THUMBNAIL_LABEL]; + if (thumbnail.has(INV_ASSET_ID_LABEL)) + { + mThumbnailUUID = thumbnail[INV_ASSET_ID_LABEL].asUUID(); + } + } + if (metadata.has(INV_FAVORITE_LABEL)) + { + const LLSD& favorite = metadata[INV_FAVORITE_LABEL]; + if (favorite.has(INV_TOGGLED_LABEL)) + { + mFavorite = favorite[INV_TOGGLED_LABEL].asBoolean(); + } + } + if (metadata.has(INV_SCRIPT_LABEL) + && metadata[INV_SCRIPT_LABEL].has(INV_RUNTIME_LABEL)) + { + mRuntime = metadata[INV_SCRIPT_LABEL][INV_RUNTIME_LABEL].asString(); + } + continue; } - mPermissions.setMaskNext(perm_mask); - } - continue; - } - - if (i->first == INV_SHADOW_ID_LABEL) - { - mAssetUUID = i->second; - LLXORCipher cipher(MAGIC_ID.mData, UUID_BYTES); - cipher.decrypt(mAssetUUID.mData, UUID_BYTES); - continue; - } - - if (i->first == INV_ASSET_ID_LABEL) - { - mAssetUUID = i->second; - continue; - } - - if (i->first == INV_LINKED_ID_LABEL) - { - mAssetUUID = i->second; - continue; - } - - if (i->first == INV_ASSET_TYPE_LABEL) - { - LLSD const &label = i->second; - if (label.isString()) - { - mType = LLAssetType::lookup(label.asStringRef().c_str()); - } - else if (label.isInteger()) - { - S8 type = (U8) label.asInteger(); - mType = static_cast(type); - } - continue; - } + break; - if (i->first == INV_INVENTORY_TYPE_LABEL) - { - LLSD const &label = i->second; - if (label.isString()) - { - mInventoryType = LLInventoryType::lookup(label.asStringRef().c_str()); - } - else if (label.isInteger()) - { - S8 type = (U8) label.asInteger(); - mInventoryType = static_cast(type); - } - continue; - } - - if (i->first == INV_FLAGS_LABEL) - { - LLSD const &label = i->second; - if (label.isBinary()) - { - mFlags = ll_U32_from_sd(label); - } - else if (label.isInteger()) - { - mFlags = label.asInteger(); - } - continue; - } + case 9: // "parent_id", "shadow_id", "linked_id", "sale_info", "thumbnail" + if (key == INV_PARENT_ID_LABEL) + { + mParentUUID = value; + continue; + } + if (key == INV_SHADOW_ID_LABEL) + { + mAssetUUID = value; + LLXORCipher cipher(MAGIC_ID.mData, UUID_BYTES); + cipher.decrypt(mAssetUUID.mData, UUID_BYTES); + continue; + } + if (key == INV_LINKED_ID_LABEL) + { + mAssetUUID = value; + continue; + } + if (key == INV_SALE_INFO_LABEL) + { + // Sale info used to contain next owner perm. It is now in + // the permissions. Thus, we read that out, and fix legacy + // objects. It's possible this op would fail, but it + // should pick up the vast majority of the tasks. + bool has_perm_mask = false; + U32 perm_mask = 0; + if (!mSaleInfo.fromLLSD(value, has_perm_mask, perm_mask)) + { + return false; + } + if (has_perm_mask) + { + if (perm_mask == PERM_NONE) + { + perm_mask = mPermissions.getMaskOwner(); + } + // fair use fix. + if (!(perm_mask & PERM_COPY)) + { + perm_mask |= PERM_TRANSFER; + } + mPermissions.setMaskNext(perm_mask); + } + continue; + } + if (key == INV_THUMBNAIL_LABEL) + { + if (value.has(INV_ASSET_ID_LABEL)) + { + mThumbnailUUID = value[INV_ASSET_ID_LABEL]; + } + /* Example: + asset_id + acc0ec86 - 17f2 - 4b92 - ab41 - 6718b1f755f7 + perms + 8 + service + 3 + version + 1 + */ + continue; + } + break; + case 10: // "created_at" + if (key == INV_CREATION_DATE_LABEL) + { + mCreationDate = value.asInteger(); + continue; + } + break; - if (i->first == INV_NAME_LABEL) - { - mName = i->second.asString(); - LLStringUtil::replaceNonstandardASCII(mName, ' '); - LLStringUtil::replaceChar(mName, '|', ' '); - continue; - } + case 11: // "permissions" + if (key == INV_PERMISSIONS_LABEL) + { + mPermissions.importLLSD(value); + continue; + } + break; - if (i->first == INV_DESC_LABEL) - { - mDescription = i->second.asString(); - LLStringUtil::replaceNonstandardASCII(mDescription, ' '); - continue; - } + case 12: // "thumbnail_id" + if (key == INV_THUMBNAIL_ID_LABEL) + { + mThumbnailUUID = value.asUUID(); + continue; + } + break; - if (i->first == INV_CREATION_DATE_LABEL) - { - mCreationDate = i->second.asInteger(); - continue; + default: + // Unknown field - skip + break; } } @@ -1336,12 +1394,14 @@ LLSD LLInventoryCategory::asLLSD() const if (mThumbnailUUID.notNull()) { - sd[INV_THUMBNAIL_LABEL] = LLSD().with(INV_ASSET_ID_LABEL, mThumbnailUUID); + LLSD& thumbnail = sd[INV_THUMBNAIL_LABEL]; + thumbnail[INV_ASSET_ID_LABEL] = mThumbnailUUID; } if (mFavorite) { - sd[INV_FAVORITE_LABEL] = LLSD().with(INV_TOGGLED_LABEL, mFavorite); + LLSD& favorite = sd[INV_FAVORITE_LABEL]; + favorite[INV_TOGGLED_LABEL] = mFavorite; } return sd; @@ -1603,17 +1663,19 @@ void LLInventoryCategory::exportLLSD(LLSD& cat_data) const { cat_data[INV_FOLDER_ID_LABEL] = mUUID; cat_data[INV_PARENT_ID_LABEL] = mParentUUID; - cat_data[INV_ASSET_TYPE_LABEL] = std::string(LLAssetType::lookup(mType)); + cat_data[INV_ASSET_TYPE_LABEL] = LLAssetType::lookup(mType); cat_data[INV_PREFERRED_TYPE_LABEL] = LLFolderType::lookup(mPreferredType); cat_data[INV_NAME_LABEL] = mName; if (mThumbnailUUID.notNull()) { - cat_data[INV_THUMBNAIL_LABEL] = LLSD().with(INV_ASSET_ID_LABEL, mThumbnailUUID); + LLSD& thumbnail = cat_data[INV_THUMBNAIL_LABEL]; + thumbnail[INV_ASSET_ID_LABEL] = mThumbnailUUID; } if (mFavorite) { - cat_data[INV_FAVORITE_LABEL] = LLSD().with(INV_TOGGLED_LABEL, mFavorite); + LLSD& favorite = cat_data[INV_FAVORITE_LABEL]; + favorite[INV_TOGGLED_LABEL] = mFavorite; } } diff --git a/indra/llkdu/llimagej2ckdu.cpp b/indra/llkdu/llimagej2ckdu.cpp index b0f4f2c2f1d..5445a42b7f2 100644 --- a/indra/llkdu/llimagej2ckdu.cpp +++ b/indra/llkdu/llimagej2ckdu.cpp @@ -231,11 +231,11 @@ struct LLKDUMessageError : public LLKDUMessage { // According to the documentation nat found: // http://pirlwww.lpl.arizona.edu/resources/guide/software/Kakadu/html_pages/globals__kdu$mize_errors.html - // "If a kdu_error object is destroyed, handler→flush will be called with + // "If a kdu_error object is destroyed, handler->flush will be called with // an end_of_message argument equal to true and the process will // subsequently be terminated through exit. The termination may be // avoided, however, by throwing an exception from within the message - // terminating handler→flush call." + // terminating handler->flush call." // So throwing an exception here isn't arbitrary: we MUST throw an // exception if we want to recover from a KDU error. // Because this confused me: the above quote specifically refers to diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index 3fe24179287..f70cff88a90 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -52,6 +52,7 @@ #include "llmeshoptimizer.h" #include "lltimer.h" #include "llvolumeoctree.h" +#include "workqueue.h" #include "mikktspace/mikktspace.hh" @@ -1971,6 +1972,8 @@ LLVolume::LLVolume(const LLVolumeParams ¶ms, const F32 detail, const bool ge mNumHullIndices = 0; // set defaults + mSculptValidationCache.fill(LLSculptValidationState::Unvalidated); + if (mParams.getPathParams().getCurveType() == LL_PCODE_PATH_FLEXIBLE) { mPathp = new LLDynamicPath(); @@ -1998,6 +2001,13 @@ void LLVolume::resizePath(S32 length) setDirty(); } +void LLVolume::setDirty() +{ + mPathp->setDirty(); + mProfilep->setDirty(); + mSculptValidationCache.fill(LLSculptValidationState::Unvalidated); +} + void LLVolume::regen() { generate(); @@ -2293,30 +2303,149 @@ bool LLVolume::unpackVolumeFaces(std::istream& is, S32 size) { LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; + // Sanity-check before even trying to decompress + constexpr S32 MAX_MESH_COMPRESSED_SIZE = 128 * 1024 * 1024; // 128 MB + const LLUUID& mesh_id = getParams().getSculptID(); + + if (size <= 0 || size > MAX_MESH_COMPRESSED_SIZE) + { + LL_WARNS("MeshStreaming") << "Rejecting implausible compressed mesh size " << size + << " for mesh id " << mesh_id << LL_ENDL; + return false; + } + //input stream is now pointing at a zlib compressed block of LLSD //decompress block - LLSD mdl; - U32 uzip_result = LLUZipHelper::unzip_llsd(mdl, is, size); - if (uzip_result != LLUZipHelper::ZR_OK) + try + { + LLSD mdl; + U32 uzip_result = LLUZipHelper::unzip_llsd(mdl, is, size); + if (uzip_result != LLUZipHelper::ZR_OK) + { + LL_DEBUGS("MeshStreaming") << "Failed to unzip LLSD blob for LoD with code " << uzip_result << " , will probably fetch from sim again." << LL_ENDL; + return false; + } + return unpackVolumeFacesInternal(mdl); + } + catch (const std::bad_alloc&) + { + constexpr S32 SMALL_MESH_THRESHOLD = 4000; + if (size < SMALL_MESH_THRESHOLD) + { + // showOutOfMemory and LL_ERRS must run on the main thread. + // Post to mainloop WorkQueue, mirroring LLThread::tryRun(). + LL::WorkQueue::ptr_t main_queue = LL::WorkQueue::getInstance("mainloop"); + bool done = false; + if (main_queue) + { + const LLUUID mesh_id_copy = mesh_id; // capture by value for the lambda + done = main_queue->post([mesh_id_copy, size]() + { + LLError::LLUserWarningMsg::showOutOfMemory(); + LL_ERRS("MeshStreaming") << "Out of memory unpacking mesh id " << mesh_id_copy + << " of compressed size " << size << LL_ENDL; + }); + } + if (!done) + { + // No main queue available (e.g. during shutdown) + LL_WARNS("MeshStreaming") << "Out of memory unpacking mesh id " << mesh_id + << " of compressed size " << size << " (main queue unavailable)" << LL_ENDL; + } + } + else + { + LL_WARNS("MeshStreaming") << "Out of memory unpacking mesh id " << mesh_id + << " of compressed size " << size << LL_ENDL; + } + return false; + } + catch (const std::exception& e) { - LL_DEBUGS("MeshStreaming") << "Failed to unzip LLSD blob for LoD with code " << uzip_result << " , will probably fetch from sim again." << LL_ENDL; + LL_WARNS("MeshStreaming") << "Exception unpacking mesh id " << mesh_id + << " of compressed size " << size << ": " << e.what() << LL_ENDL; + return false; + } + catch (...) + { + LL_WARNS("MeshStreaming") << "Unknown exception unpacking mesh id " << mesh_id + << " of compressed size " << size << LL_ENDL; return false; } - return unpackVolumeFacesInternal(mdl); } bool LLVolume::unpackVolumeFaces(U8* in_data, S32 size) { + LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; + constexpr S32 MAX_MESH_COMPRESSED_SIZE = 128 * 1024 * 1024; // 128 MB + const LLUUID& mesh_id = getParams().getSculptID(); + + if (!in_data || size <= 0 || size > MAX_MESH_COMPRESSED_SIZE) + { + LL_WARNS("MeshStreaming") << "Rejecting implausible compressed mesh size " << size + << " for mesh id " << mesh_id << LL_ENDL; + return false; + } + //input data is now pointing at a zlib compressed block of LLSD //decompress block - LLSD mdl; - U32 uzip_result = LLUZipHelper::unzip_llsd(mdl, in_data, size); - if (uzip_result != LLUZipHelper::ZR_OK) + try { - LL_DEBUGS("MeshStreaming") << "Failed to unzip LLSD blob for LoD with code " << uzip_result << " , will probably fetch from sim again." << LL_ENDL; + LLSD mdl; + U32 uzip_result = LLUZipHelper::unzip_llsd(mdl, in_data, size); + if (uzip_result != LLUZipHelper::ZR_OK) + { + LL_DEBUGS("MeshStreaming") << "Failed to unzip LLSD blob for LoD, mesh id " << mesh_id + << ", code " << uzip_result << " , will probably fetch from sim again." << LL_ENDL; + return false; + } + return unpackVolumeFacesInternal(mdl); + } + catch (const std::bad_alloc&) + { + constexpr S32 SMALL_MESH_THRESHOLD = 4000; + if (size < SMALL_MESH_THRESHOLD) + { + // showOutOfMemory and LL_ERRS must run on the main thread. + // Post to mainloop WorkQueue, mirroring LLThread::tryRun(). + LL::WorkQueue::ptr_t main_queue = LL::WorkQueue::getInstance("mainloop"); + bool done = false; + if (main_queue) + { + const LLUUID mesh_id_copy = mesh_id; // capture by value for the lambda + done = main_queue->post([mesh_id_copy, size]() + { + LLError::LLUserWarningMsg::showOutOfMemory(); + LL_ERRS("MeshStreaming") << "Out of memory unpacking mesh id " << mesh_id_copy + << " of compressed size " << size << LL_ENDL; + }); + } + if (!done) + { + // No main queue available (e.g. during shutdown) + LL_WARNS("MeshStreaming") << "Out of memory unpacking mesh id " << mesh_id + << " of compressed size " << size << " (main queue unavailable)" << LL_ENDL; + } + } + else + { + LL_WARNS("MeshStreaming") << "Out of memory unpacking mesh id " << mesh_id + << " of compressed size " << size << LL_ENDL; + } + return false; + } + catch (const std::exception& e) + { + LL_WARNS("MeshStreaming") << "Exception unpacking mesh id " << mesh_id + << " of compressed size " << size << ": " << e.what() << LL_ENDL; + return false; + } + catch (...) + { + LL_WARNS("MeshStreaming") << "Unknown exception unpacking mesh id " << mesh_id + << " of compressed size " << size << LL_ENDL; return false; } - return unpackVolumeFacesInternal(mdl); } bool LLVolume::unpackVolumeFacesInternal(const LLSD& mdl) @@ -2393,11 +2522,31 @@ bool LLVolume::unpackVolumeFacesInternal(const LLSD& mdl) //copy out vertices U32 num_verts = static_cast(pos.size())/(3*2); + if (num_verts == 0) + { + LL_WARNS() << "Zero vertices for face index: " << i << LL_ENDL; + face.resizeIndices(3); + face.resizeVertices(1); + face.mPositions->clear(); + face.mNormals->clear(); + face.mTexCoords->setZero(); + memset(face.mIndices, 0, sizeof(U16) * 3); + continue; + } + + if (num_verts > 65535) // U16 indices + { + LL_WARNS() << "Invalid vertex count " << num_verts << " exceeds maximum for face index: " << i << LL_ENDL; + mVolumeFaces.clear(); + return false; + } + face.resizeVertices(num_verts); if (num_verts > 0 && !face.mPositions) { LL_WARNS() << "Failed to allocate " << num_verts << " vertices for face index: " << i << " Total: " << face_count << LL_ENDL; + face.resizeVertices(0); face.resizeIndices(0); continue; } @@ -3169,10 +3318,100 @@ S32 sculpt_sides(F32 detail) } } +static bool validate_sculpt_geometry(const LLVolume* volume) +{ + if (!volume || volume->getNumVolumeFaces() == 0) + { + return true; + } + + // Validate each face in the sculpt + for (S32 face_idx = 0; face_idx < volume->getNumVolumeFaces(); ++face_idx) + { + const LLVolumeFace& face = volume->getVolumeFace(face_idx); + + // Skip validation for degenerate faces + if (face.mNumVertices < 3 || face.mNumIndices < 3) + { + continue; + } + + // Calculate 'bounding' surface + LLVector4a extent_size; + extent_size.setSub(face.mExtents[1], face.mExtents[0]); + + F32 width = extent_size[0]; + F32 height = extent_size[1]; + F32 depth = extent_size[2]; + + F32 bounding_surface = width * height + width * depth + height * depth; + bounding_surface *= 2; + + if (!llfinite(bounding_surface) || bounding_surface <= FLT_EPSILON) + { + return false; + } + + // Calculate total surface area of all triangles + F32 total_triangle_area = 0.0f; + S32 num_triangles = face.mNumIndices / 3; + + for (S32 i = 0; i < num_triangles; ++i) + { + S32 idx_base = i * 3; + U16 idx0 = face.mIndices[idx_base]; + U16 idx1 = face.mIndices[idx_base + 1]; + U16 idx2 = face.mIndices[idx_base + 2]; + + // Validate indices + if (idx0 >= face.mNumVertices || idx1 >= face.mNumVertices || idx2 >= face.mNumVertices) + { + continue; + } + const LLVector4a& v0 = face.mPositions[idx0]; + const LLVector4a& v1 = face.mPositions[idx1]; + const LLVector4a& v2 = face.mPositions[idx2]; + + // Calculate triangle area + LLVector4a edge1, edge2, cross; + edge1.setSub(v1, v0); + edge2.setSub(v2, v0); + cross.setCross3(edge1, edge2); + + F32 triangle_area = cross.getLength3().getF32() * 0.5f; + total_triangle_area += triangle_area; + } + + if (!llfinite(total_triangle_area)) + { + return false; + } + + // The idea here is that the total area of a sculpt is normally comparable + // to the area of a bounding box. But a random overlapping collection of + // triangles has a significantly larger area than a sculpt normally has. + F32 area_ratio = total_triangle_area / bounding_surface; + + constexpr F32 MAX_AREA_TO_AREA_RATIO = 16.0f; + if (area_ratio > MAX_AREA_TO_AREA_RATIO) + { + LL_DEBUGS("LLVOLUME") << "Sculpt rejected: excessive triangle area." + << " Face index: " << face_idx + << " Total triangle area: " << total_triangle_area << " sq m" + << " Bounding surface area: " << bounding_surface << " sq m" + << " Ratio: " << area_ratio + << " Triangle count: " << num_triangles + << " Bounds: [" << width << " x " << height << " x " << depth << "]" << LL_ENDL; + return false; + } + } + + return true; +} // determine the number of vertices in both s and t direction for this sculpt -void sculpt_calc_mesh_resolution(U16 width, U16 height, U8 type, F32 detail, S32& s, S32& t) +static void sculpt_calc_mesh_resolution(U16 width, U16 height, U8 type, F32 detail, S32& s, S32& t) { // this code has the following properties: // 1) the aspect ratio of the mesh is as close as possible to the ratio of the map @@ -3282,10 +3521,62 @@ void LLVolume::sculpt(U16 sculpt_width, U16 sculpt_height, S8 sculpt_components, mSculptLevel = sculpt_level; - // Delete any existing faces so that they get regenerated - mVolumeFaces.clear(); + LLSculptValidationState cached_state = LLSculptValidationState::Valid; + if (!data_is_empty && (sculpt_level >= 0) && sculpt_level < SCULPT_CACHE_SIZE) + { + // validation might be expensive, so we do it only once per lod. + cached_state = mSculptValidationCache[sculpt_level]; + } - createVolumeFaces(); + switch (cached_state) + { + case LLSculptValidationState::Valid: + { + // Delete any existing faces, then regenerate + mVolumeFaces.clear(); + createVolumeFaces(); + break; + } + case LLSculptValidationState::Invalid: + { + // Invalid geometry + // Regenerate mesh with an empty placeholder + mVolumeFaces.clear(); + sculptGenerateEmptyPlaceholder(); + createVolumeFaces(); + break; + } + case LLSculptValidationState::Unvalidated: + { + // First time at this LOD + mVolumeFaces.clear(); + createVolumeFaces(); + + bool valid_geometry = true; + + // Todo: can this be backed into createVolumeFaces? + // Or calculated without having to call createVolumeFaces first? + // Todo 2: should the same be done for meshes? + valid_geometry = validate_sculpt_geometry(this); + mSculptValidationCache[sculpt_level] = valid_geometry ? LLSculptValidationState::Valid : LLSculptValidationState::Invalid; + + if (!valid_geometry) + { + LL_WARNS("LLVOLUME") << "Sculpt failed geometry validation - either invalid image or malicious. " + << "Replacing with placeholder. Image: " << mParams.getSculptID() << LL_ENDL; + + // Clear the malicious faces + mVolumeFaces.clear(); + + // Regenerate mesh with empty placeholder + sculptGenerateEmptyPlaceholder(); + + // Recreate faces with placeholder geometry + createVolumeFaces(); + } + break; + } + } //switch (cached_state) } @@ -5707,7 +5998,40 @@ bool LLVolumeFace::cacheOptimize(bool gen_tangents) mOptimized = true; if (gen_tangents && mNormals && mTexCoords) - { // generate mikkt space tangents before cache optimizing since the index buffer may change + { + if (!mPositions || !mIndices || mNumVertices <= 0 || mNumIndices <= 0) + { + LL_WARNS_ONCE("LLVolume") << "Invalid volume face data for tangent generation: " + << "mPositions=" << (void*)mPositions + << ", mIndices=" << (void*)mIndices + << ", mNumVertices=" << mNumVertices + << ", mNumIndices=" << mNumIndices << LL_ENDL; + return false; + } + + if (mNumIndices % 3 != 0) + { + LL_WARNS_ONCE("LLVolume") << "Non-triangulated mesh, mNumIndices=" << mNumIndices << LL_ENDL; + return false; + } + + for (S32 i = 0; i < mNumIndices; ++i) + { + if (mIndices[i] >= mNumVertices) + { + LL_WARNS_ONCE("LLVolume") << "Out of bounds index detected: mIndices[" << i << "]=" + << mIndices[i] << " >= mNumVertices=" << mNumVertices << LL_ENDL; + return false; + } + } + + if (mNormalizedScale.mV[0] == 0.0f || mNormalizedScale.mV[1] == 0.0f || mNormalizedScale.mV[2] == 0.0f) + { + LL_WARNS_ONCE("LLVolume") << "Invalid normalized scale: " << mNormalizedScale << LL_ENDL; + return false; + } + + // generate mikkt space tangents before cache optimizing since the index buffer may change // a bit of a hack to do this here, but this function gets called exactly once for the lifetime of a mesh // and is executed on a background thread MikktData data(this); diff --git a/indra/llmath/llvolume.h b/indra/llmath/llvolume.h index d08a961db69..34acf196192 100644 --- a/indra/llmath/llvolume.h +++ b/indra/llmath/llvolume.h @@ -60,6 +60,8 @@ class LLVolumeOctree; #include "llalignedarray.h" #include "llrigginginfo.h" +#include + //============================================================================ constexpr S32 MIN_DETAIL_FACES = 6; @@ -1041,7 +1043,7 @@ class LLVolume : public LLRefCount const LLVector4a& getMeshPt(const U32 i) const { return mMesh[i]; } - void setDirty() { mPathp->setDirty(); mProfilep->setDirty(); } + void setDirty(); void regen(); void genTangents(S32 face); @@ -1132,6 +1134,11 @@ class LLVolume : public LLRefCount bool mIsMeshAssetLoaded; bool mIsMeshAssetUnavaliable; + // Cache for sculpt geometry validation results + static constexpr S32 SCULPT_CACHE_SIZE = 6; // 0 to 5 inclusive + enum class LLSculptValidationState : S8 { Unvalidated, Valid, Invalid }; + std::array mSculptValidationCache; + const LLVolumeParams mParams; LLPath *mPathp; LLProfile *mProfilep; diff --git a/indra/llmessage/CMakeLists.txt b/indra/llmessage/CMakeLists.txt index b7a6fa0eac7..cefaa5bf16f 100644 --- a/indra/llmessage/CMakeLists.txt +++ b/indra/llmessage/CMakeLists.txt @@ -150,6 +150,7 @@ target_sources(llmessage llxfer_mem.h llxfer_vfile.h llxorcipher.h + llzerocode.h machine.h mean_collision_data.h message.h @@ -208,6 +209,7 @@ if (BUILD_TESTING) LL_ADD_INTEGRATION_TEST(lltemplatemessagebuilder "" "${test_libs}" "${test_project}") LL_ADD_INTEGRATION_TEST(llxfer_file "" "${test_libs}" "${test_project}") LL_ADD_INTEGRATION_TEST(llxorcipher "" "${test_libs}" "${test_project}") + LL_ADD_INTEGRATION_TEST(llzerocode "" "${test_libs}" "${test_project}") LL_ADD_INTEGRATION_TEST(message "" "${test_libs}" "${test_project}") endif () diff --git a/indra/llmessage/llcircuit.cpp b/indra/llmessage/llcircuit.cpp index 62ea5475625..694896fb806 100644 --- a/indra/llmessage/llcircuit.cpp +++ b/indra/llmessage/llcircuit.cpp @@ -346,8 +346,7 @@ S32 LLCircuitData::resendUnackedPackets(const F64Seconds now) packetp->mBuffer[0] |= LL_RESENT_FLAG; // tag packet id as being a resend - gMessageSystem->mPacketRing.sendPacket(packetp->mSocket, - (char *)packetp->mBuffer, packetp->mBufferLength, + gMessageSystem->sendPacketToSocket((char *)packetp->mBuffer, packetp->mBufferLength, packetp->mHost); mThrottles.throttleOverflow(TC_RESEND, packetp->mBufferLength * 8.f); @@ -976,7 +975,7 @@ bool LLCircuitData::updateWatchDogTimers(LLMessageSystem *msgsys) { // let's call this one a loss! mPacketsLost++; - gMessageSystem->mDroppedPackets++; + gMessageSystem->mLostPackets++; if(gMessageSystem->mVerboseLog) { std::ostringstream str; diff --git a/indra/llmessage/llpacketbuffer.h b/indra/llmessage/llpacketbuffer.h index ac4012d330d..a08e6c22e5e 100644 --- a/indra/llmessage/llpacketbuffer.h +++ b/indra/llmessage/llpacketbuffer.h @@ -38,6 +38,9 @@ class LLPacketBuffer LLPacketBuffer(S32 hSocket); // receive a packet ~LLPacketBuffer(); + LLPacketBuffer(const LLPacketBuffer&) = default; + LLPacketBuffer& operator=(const LLPacketBuffer&) = default; + S32 getSize() const { return mSize; } const char *getData() const { return mData; } LLHost getHost() const { return mHost; } @@ -46,11 +49,18 @@ class LLPacketBuffer void init(S32 hSocket); void init(const char* buffer, S32 data_size, const LLHost& host); + // Whether LLCircuitData::checkPacketInID() has already been run for this + // packet (done at socket-read time, before this packet was sorted into + // the high/low priority inbound queue). + bool getPacketIDChecked() const { return mPacketIDChecked; } + void setPacketIDChecked(bool checked) { mPacketIDChecked = checked; } + protected: char mData[NET_BUFFER_SIZE]; // packet data /* Flawfinder : ignore */ S32 mSize; // size of buffer in bytes LLHost mHost; // source/dest IP and port LLHost mReceivingIF; // source/dest IP and port + bool mPacketIDChecked = false; }; #endif diff --git a/indra/llmessage/llpacketring.cpp b/indra/llmessage/llpacketring.cpp index 7fdd0e8b25e..bade413e61e 100644 --- a/indra/llmessage/llpacketring.cpp +++ b/indra/llmessage/llpacketring.cpp @@ -28,367 +28,121 @@ #include "llpacketring.h" -#if LL_WINDOWS - #include -#else - #include - #include -#endif - -// linden library includes #include "llerror.h" -#include "lltimer.h" -#include "llproxy.h" -#include "llrand.h" -#include "message.h" -#include "u64.h" -#include "llmessagelog.h" -constexpr S16 MAX_BUFFER_RING_SIZE = 1024; +constexpr S16 MAX_BUFFER_RING_SIZE = 8192; + +// DANGER: don't adjust DEFAULT_BUFFER_RING_SIZE unless you know what +// you're doing. Its value affects the "buffer load rate" which is used +// to supply backpressure to an overloaded nework queue. constexpr S16 DEFAULT_BUFFER_RING_SIZE = 256; -LLPacketRing::LLPacketRing () - : mPacketRing(DEFAULT_BUFFER_RING_SIZE, nullptr) +LLPacketRing::LLPacketRing() + : mRing(DEFAULT_BUFFER_RING_SIZE, nullptr) { LLHost invalid_host; - for (size_t i = 0; i < mPacketRing.size(); ++i) + for (size_t i = 0; i < mRing.size(); ++i) { - mPacketRing[i] = new LLPacketBuffer(invalid_host, nullptr, 0); + mRing[i] = new LLPacketBuffer(invalid_host, nullptr, 0); } } -LLPacketRing::~LLPacketRing () +LLPacketRing::~LLPacketRing() { - for (auto packet : mPacketRing) + for (auto* packet : mRing) { delete packet; } - mPacketRing.clear(); + mRing.clear(); mNumBufferedPackets = 0; mNumBufferedBytes = 0; mHeadIndex = 0; } -S32 LLPacketRing::receivePacket (S32 socket, char *datap) -{ - bool drop = computeDrop(); - return (mNumBufferedPackets > 0) ? - receiveOrDropBufferedPacket(datap, drop) : - receiveOrDropPacket(socket, datap, drop); -} - -bool send_packet_helper(int socket, const char * datap, S32 data_size, LLHost host) +void LLPacketRing::pushPacket(const LLPacketBuffer& packet) { - if (!LLProxy::isSOCKSProxyEnabled()) + S16 ring_size = (S16)mRing.size(); + if (mNumBufferedPackets >= ring_size && ring_size < MAX_BUFFER_RING_SIZE) { - return send_packet(socket, datap, data_size, host.getAddress(), host.getPort()); + expandRing(); + ring_size = (S16)mRing.size(); } - char headered_send_buffer[NET_BUFFER_SIZE + SOCKS_HEADER_SIZE]; - - proxywrap_t *socks_header = static_cast(static_cast(&headered_send_buffer)); - socks_header->rsv = 0; - socks_header->addr = host.getAddress(); - socks_header->port = htons(host.getPort()); - socks_header->atype = ADDRESS_IPV4; - socks_header->frag = 0; - - memcpy(headered_send_buffer + SOCKS_HEADER_SIZE, datap, data_size); + LLPacketBuffer* slot = mRing[mHeadIndex]; + S32 old_size = slot->getSize(); - return send_packet( socket, - headered_send_buffer, - data_size + SOCKS_HEADER_SIZE, - LLProxy::getInstance()->getUDPProxy().getAddress(), - LLProxy::getInstance()->getUDPProxy().getPort()); -} - -bool LLPacketRing::sendPacket(int socket, const char * datap, S32 data_size, LLHost host) -{ -#define LOCALHOST_ADDR 16777343 - LLMessageLog::log(LLHost(LOCALHOST_ADDR, gMessageSystem->getListenPort()), host, (U8*)datap, data_size); -#undef LOCALHOST_ADDR - mActualBytesOut += data_size; - return send_packet_helper(socket, datap, data_size, host); -} + *slot = packet; -void LLPacketRing::dropPackets (U32 num_to_drop) -{ - mPacketsToDrop += num_to_drop; -} + mHeadIndex = (mHeadIndex + 1) % ring_size; -void LLPacketRing::setDropPercentage (F32 percent_to_drop) -{ - mDropPercentage = percent_to_drop; -} - -bool LLPacketRing::computeDrop() -{ - bool drop= (mDropPercentage > 0.0f && (ll_frand(100.f) < mDropPercentage)); - if (drop) + if (mNumBufferedPackets < ring_size) { - ++mPacketsToDrop; + ++mNumBufferedPackets; + mNumBufferedBytes += packet.getSize(); } - if (mPacketsToDrop > 0) + else { - --mPacketsToDrop; - drop = true; + // Ring is at maximum capacity; oldest packet was overwritten. + // This is VERY BAD because we've already ACKed the packet we're loosing + // (if it was "reliable"). + LL_WARNS("PacketRing") << "buffer overflow at " << mNumBufferedPackets << " packets" << LL_ENDL; + mNumBufferedBytes += packet.getSize() - old_size; } - return drop; } -S32 LLPacketRing::receiveOrDropPacket(S32 socket, char *datap, bool drop) +bool LLPacketRing::popPacket(LLPacketBuffer& packet) { - S32 packet_size = 0; - - // pull straight from socket - if (LLProxy::isSOCKSProxyEnabled()) - { - char buffer[NET_BUFFER_SIZE + SOCKS_HEADER_SIZE]; /* Flawfinder ignore */ - packet_size = receive_packet(socket, buffer); - if (packet_size > 0) - { - mActualBytesIn += packet_size; - } - - if (packet_size > SOCKS_HEADER_SIZE) - { - if (drop) - { - packet_size = 0; - } - else - { - // *FIX We are assuming ATYP is 0x01 (IPv4), not 0x03 (hostname) or 0x04 (IPv6) - packet_size -= SOCKS_HEADER_SIZE; // The unwrapped packet size - memcpy(datap, buffer + SOCKS_HEADER_SIZE, packet_size); - proxywrap_t * header = static_cast(static_cast(buffer)); - mLastSender.setAddress(header->addr); - mLastSender.setPort(ntohs(header->port)); - mLastReceivingIF = ::get_receiving_interface(); - } - } - else - { - packet_size = 0; - } - } - else + if (mNumBufferedPackets <= 0) { - packet_size = receive_packet(socket, datap); - if (packet_size > 0) - { - mActualBytesIn += packet_size; - if (drop) - { - packet_size = 0; - } - else - { - mLastSender = ::get_sender(); - mLastReceivingIF = ::get_receiving_interface(); - } - } + return false; } - return packet_size; -} -S32 LLPacketRing::receiveOrDropBufferedPacket(char *datap, bool drop) -{ - // receivePacket() only routes here when packets are buffered. - llassert(mNumBufferedPackets > 0); + S16 ring_size = (S16)mRing.size(); + S16 tail_index = (mHeadIndex + ring_size - mNumBufferedPackets) % ring_size; - S32 packet_size = 0; + LLPacketBuffer* slot = mRing[tail_index]; + S32 packet_size = slot->getSize(); - S16 ring_size = (S16)(mPacketRing.size()); - S16 packet_index = (mHeadIndex + ring_size - mNumBufferedPackets) % ring_size; - LLPacketBuffer* packet = mPacketRing[packet_index]; - packet_size = packet->getSize(); - mLastSender = packet->getHost(); - mLastReceivingIF = packet->getReceivingInterface(); + packet = *slot; --mNumBufferedPackets; mNumBufferedBytes -= packet_size; - if (mNumBufferedPackets == 0) - { - // Byte accounting nets to zero once the ring drains. Held now that - // bufferInboundPacket no longer clobbers a live slot on empty receives. - llassert(mNumBufferedBytes == 0); - } - if (!drop) - { - if (packet_size > 0) - { - memcpy(datap, packet->getData(), packet_size); - } - else - { - // Unreachable: bufferInboundPacket only commits a slot for a real - // (size > 0) packet, so a buffered packet never reads back as 0. - // Assert in debug; fall through returning 0 in release. - llassert(false); - } - } - else - { - packet_size = 0; - } - return packet_size; -} - -S32 LLPacketRing::bufferInboundPacket(S32 socket) -{ - if (mNumBufferedPackets == mPacketRing.size() && mNumBufferedPackets < MAX_BUFFER_RING_SIZE) - { - expandRing(); - } - - LLPacketBuffer* packet = mPacketRing[mHeadIndex]; - S32 old_packet_size = packet->getSize(); - S32 packet_size = 0; - if (LLProxy::isSOCKSProxyEnabled()) - { - char buffer[NET_BUFFER_SIZE + SOCKS_HEADER_SIZE]; /* Flawfinder ignore */ - packet_size = receive_packet(socket, buffer); - if (packet_size > 0) - { - mActualBytesIn += packet_size; - if (packet_size > SOCKS_HEADER_SIZE) - { - // *FIX We are assuming ATYP is 0x01 (IPv4), not 0x03 (hostname) or 0x04 (IPv6) - - proxywrap_t * header = static_cast(static_cast(buffer)); - LLHost sender; - sender.setAddress(header->addr); - sender.setPort(ntohs(header->port)); - - packet_size -= SOCKS_HEADER_SIZE; // The unwrapped packet size - packet->init(buffer + SOCKS_HEADER_SIZE, packet_size, sender); - - mHeadIndex = (mHeadIndex + 1) % (S16)(mPacketRing.size()); - if (mNumBufferedPackets < MAX_BUFFER_RING_SIZE) - { - ++mNumBufferedPackets; - mNumBufferedBytes += packet_size; - } - else - { - // we overwrote an older packet - mNumBufferedBytes += packet_size - old_packet_size; - } - } - else - { - // Runt SOCKS wrapper (no payload past the header): discard - // it, but keep the raw size so drainSocket() keeps draining - // instead of misreading one bad datagram as an empty socket. - // The drain accounting counts it as received-but-dropped. - } - } - } - else - { - // Receive into a scratch buffer first rather than straight into the - // ring slot. When the ring is full, mPacketRing[mHeadIndex] is the - // oldest *unread* packet; a would-block or zero-length datagram makes - // receive_packet() return <= 0, and receiving directly into the slot - // would clobber that unread packet and desync the byte/packet - // accounting. Only commit the slot once we know we have a real - // packet. (The SOCKS branch above already works this way.) - char buffer[NET_BUFFER_SIZE]; /* Flawfinder: ignore */ - packet_size = receive_packet(socket, buffer); - if (packet_size > 0) - { - mActualBytesIn += packet_size; - - packet->init(buffer, packet_size, ::get_sender()); - - mHeadIndex = (mHeadIndex + 1) % (S16)(mPacketRing.size()); - if (mNumBufferedPackets < MAX_BUFFER_RING_SIZE) - { - ++mNumBufferedPackets; - mNumBufferedBytes += packet_size; - } - else - { - // we overwrote an older packet - mNumBufferedBytes += packet_size - old_packet_size; - } - } - } - return packet_size; -} + llassert(mNumBufferedPackets > 0 || mNumBufferedBytes == 0); -S32 LLPacketRing::drainSocket(S32 socket) -{ - // drain into buffer - S32 packet_size = 1; - S32 num_loops = 0; - S32 old_num_packets = mNumBufferedPackets; - while (packet_size > 0) - { - packet_size = bufferInboundPacket(socket); - ++num_loops; - } - S32 num_dropped_packets = (num_loops - 1 + old_num_packets) - mNumBufferedPackets; - if (num_dropped_packets > 0) - { - // It will eventually be accounted by mDroppedPackets - // and mPacketsLost, but track it here for logging purposes. - mNumDroppedPackets += num_dropped_packets; - } - return (S32)(mNumBufferedPackets); + return true; } bool LLPacketRing::expandRing() { - // compute larger size - constexpr S16 BUFFER_RING_EXPANSION = 256; - S16 old_size = (S16)(mPacketRing.size()); + constexpr S16 BUFFER_RING_EXPANSION = 512; + S16 old_size = (S16)mRing.size(); S16 new_size = llmin(old_size + BUFFER_RING_EXPANSION, MAX_BUFFER_RING_SIZE); if (new_size == old_size) { - // mPacketRing is already maxed out return false; } - // make a larger ring and copy packet pointers + // Lay existing entries out linearly in FIFO order starting at index 0. std::vector new_ring(new_size, nullptr); for (S16 i = 0; i < old_size; ++i) { S16 j = (mHeadIndex + i) % old_size; - new_ring[i] = mPacketRing[j]; + new_ring[i] = mRing[j]; } - // allocate new packets for the remainder of new_ring LLHost invalid_host; for (S16 i = old_size; i < new_size; ++i) { new_ring[i] = new LLPacketBuffer(invalid_host, nullptr, 0); } - // swap the rings and reset mHeadIndex - mPacketRing.swap(new_ring); + mRing.swap(new_ring); mHeadIndex = mNumBufferedPackets; return true; } F32 LLPacketRing::getBufferLoadRate() const { - // goes up to MAX_BUFFER_RING_SIZE return (F32)mNumBufferedPackets / (F32)DEFAULT_BUFFER_RING_SIZE; } - -void LLPacketRing::dumpPacketRingStats() -{ - mNumDroppedPacketsTotal += mNumDroppedPackets; - LL_INFOS("Messaging") << "Packet ring stats: " << std::endl - << "Buffered packets: " << mNumBufferedPackets << std::endl - << "Buffered bytes: " << mNumBufferedBytes << std::endl - << "Dropped packets current: " << mNumDroppedPackets << std::endl - << "Dropped packets total: " << mNumDroppedPacketsTotal << std::endl - << "Dropped packets percentage: " << mDropPercentage << "%" << std::endl - << "Actual in bytes: " << mActualBytesIn << std::endl - << "Actual out bytes: " << mActualBytesOut << LL_ENDL; - mNumDroppedPackets = 0; -} diff --git a/indra/llmessage/llpacketring.h b/indra/llmessage/llpacketring.h index 572dcbd271d..5315e5a5bda 100644 --- a/indra/llmessage/llpacketring.h +++ b/indra/llmessage/llpacketring.h @@ -1,7 +1,16 @@ /** * @file llpacketring.h - * @brief definition of LLPacketRing class for implementing a resend, - * drop, or delay in packet transmissions + * @brief LLPacketRing: a simple ring buffer for LLPacketBuffers. + * + * LLPacketRing stores incoming UDP packets that have already been received + * from the network socket. It has no socket or proxy awareness; callers + * push packets in with pushPacket() and retrieve them in FIFO order with + * popPacket(). + * + * The ring starts at DEFAULT_BUFFER_RING_SIZE slots and grows in increments + * of BUFFER_RING_EXPANSION up to MAX_BUFFER_RING_SIZE. Once at the ceiling, + * pushPacket() silently overwrites the oldest queued packet to make room for + * the incoming one. * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code @@ -29,9 +38,7 @@ #include -#include "llhost.h" #include "llpacketbuffer.h" -#include "llthrottle.h" class LLPacketRing @@ -40,71 +47,26 @@ class LLPacketRing LLPacketRing(); ~LLPacketRing(); - // receive one packet: either buffered or from the socket - S32 receivePacket (S32 socket, char *datap); - - // send one packet - bool sendPacket(int h_socket, const char * send_buffer, S32 buf_size, LLHost host); - - // drains packets from socket and returns final mNumBufferedPackets - S32 drainSocket(S32 socket); + // Copy 'packet' onto the tail of the ring, growing the ring or + // overwriting the oldest entry when the ring is at capacity. + void pushPacket(const LLPacketBuffer& packet); - void dropPackets(U32); - void setDropPercentage (F32 percent_to_drop); - - inline LLHost getLastSender() const; - inline LLHost getLastReceivingInterface() const; - - S32 getActualInBytes() const { return mActualBytesIn; } - S32 getActualOutBytes() const { return mActualBytesOut; } - S32 getAndResetActualInBits() { S32 bits = mActualBytesIn * 8; mActualBytesIn = 0; return bits;} - S32 getAndResetActualOutBits() { S32 bits = mActualBytesOut * 8; mActualBytesOut = 0; return bits;} + // Copy the head (oldest) packet into 'packet'. + // Returns true if a packet was available, false if the ring was empty. + bool popPacket(LLPacketBuffer& packet); S32 getNumBufferedPackets() const { return (S32)(mNumBufferedPackets); } - S32 getNumBufferedBytes() const { return mNumBufferedBytes; } - S32 getNumDroppedPackets() const { return mNumDroppedPacketsTotal + mNumDroppedPackets; } - - F32 getBufferLoadRate() const; // from 0 to 4 (0 - empty, 1 - default size is full) - void dumpPacketRingStats(); -protected: - // returns 'true' if we should intentionally drop a packet - bool computeDrop(); - - // returns packet_size of received packet, zero or less if no packet found - S32 receiveOrDropPacket(S32 socket, char *datap, bool drop); - S32 receiveOrDropBufferedPacket(char *datap, bool drop); + S32 getNumBufferedBytes() const { return mNumBufferedBytes; } - // returns packet_size of packet buffered - S32 bufferInboundPacket(S32 socket); + // Ratio of buffered packets to DEFAULT_BUFFER_RING_SIZE (0 = empty, 1 = nominal full). + F32 getBufferLoadRate() const; - // returns 'true' if ring was expanded +private: + // Returns true if the ring was expanded, false if already at the ceiling. bool expandRing(); -protected: - std::vector mPacketRing; - S16 mHeadIndex { 0 }; + std::vector mRing; + S16 mHeadIndex { 0 }; S16 mNumBufferedPackets { 0 }; - S32 mNumDroppedPackets { 0 }; - S32 mNumDroppedPacketsTotal { 0 }; - S32 mNumBufferedBytes { 0 }; - - S32 mActualBytesIn { 0 }; - S32 mActualBytesOut { 0 }; - F32 mDropPercentage { 0.0f }; // % of inbound packets to drop - U32 mPacketsToDrop { 0 }; // drop next inbound n packets - - // These are the sender and receiving_interface for the last packet delivered by receivePacket() - LLHost mLastSender; - LLHost mLastReceivingIF; + S32 mNumBufferedBytes { 0 }; }; - - -inline LLHost LLPacketRing::getLastSender() const -{ - return mLastSender; -} - -inline LLHost LLPacketRing::getLastReceivingInterface() const -{ - return mLastReceivingIF; -} diff --git a/indra/llmessage/llqueryflags.h b/indra/llmessage/llqueryflags.h index 227d28ba5c5..8dfb74aab08 100644 --- a/indra/llmessage/llqueryflags.h +++ b/indra/llmessage/llqueryflags.h @@ -56,8 +56,8 @@ const U32 DFQ_NAME_SORT = 0x1 << 19; const U32 DFQ_LIMIT_BY_PRICE = 0x1 << 20; const U32 DFQ_LIMIT_BY_AREA = 0x1 << 21; -const U32 DFQ_FILTER_MATURE = 0x1 << 22; -const U32 DFQ_PG_PARCELS_ONLY = 0x1 << 23; +const U32 DFQ_FILTER_MATURE = 0x1 << 22; // legacy, will not work with any from DFQ_INC_NEW_VIEWER +const U32 DFQ_PG_PARCELS_ONLY = 0x1 << 23; // legacy, will not work with any from DFQ_INC_NEW_VIEWER const U32 DFQ_INC_PG = 0x1 << 24; // Flags appear in 1.23 viewer or later const U32 DFQ_INC_MATURE = 0x1 << 25; diff --git a/indra/llmessage/lltemplatemessagebuilder.cpp b/indra/llmessage/lltemplatemessagebuilder.cpp index 69d9ed93651..3ac2aa79763 100644 --- a/indra/llmessage/lltemplatemessagebuilder.cpp +++ b/indra/llmessage/lltemplatemessagebuilder.cpp @@ -29,6 +29,7 @@ #include "lltemplatemessagebuilder.h" #include "llmessagetemplate.h" +#include "llzerocode.h" #include "llmath.h" #include "llquaternion.h" #include "u64.h" @@ -439,87 +440,26 @@ void LLTemplateMessageBuilder::addUUID(const char *varname, const LLUUID& uuid) addData(varname, uuid.mData, MVT_LLUUID, sizeof(uuid.mData)); } -static S32 zero_code(U8 **data, U32 *data_size) +void LLTemplateMessageBuilder::compressMessage(U8*& buf_ptr, U32& buffer_length) { - // Encoded send buffer needs to be slightly larger since the zero - // coding can potentially increase the size of the send data. - static U8 encodedSendBuffer[2 * MAX_BUFFER_SIZE]; - - S32 count = *data_size; - - S32 net_gain = 0; - U8 num_zeroes = 0; - - U8 *inptr = (U8 *)*data; - U8 *outptr = (U8 *)encodedSendBuffer; - -// skip the packet id field - - for (U32 ii = 0; ii < LL_PACKET_ID_SIZE ; ++ii) + if(ME_ZEROCODED != mCurrentSMessageTemplate->getEncoding()) { - count--; - *outptr++ = *inptr++; - } - -// build encoded packet, keeping track of net size gain - -// sequential zero bytes are encoded as 0 [U8 count] -// with 0 0 [count] representing wrap (>256 zeroes) - - while (count--) - { - if (!(*inptr)) // in a zero count - { - if (num_zeroes) - { - if (++num_zeroes > 254) - { - *outptr++ = num_zeroes; - num_zeroes = 0; - } - net_gain--; // subseqent zeroes save one - } - else - { - *outptr++ = 0; - net_gain++; // starting a zero count adds one - num_zeroes = 1; - } - inptr++; - } - else - { - if (num_zeroes) - { - *outptr++ = num_zeroes; - num_zeroes = 0; - } - *outptr++ = *inptr++; - } + return; } - if (num_zeroes) - { - *outptr++ = num_zeroes; - } + // Encoded send buffer needs to be slightly larger since the zero + // coding can potentially increase the size of the send data. + static U8 encodedSendBuffer[2 * MAX_BUFFER_SIZE]; - if (net_gain < 0) + S32 encoded_size = LLZeroCode::encode(buf_ptr, buffer_length, + encodedSendBuffer, sizeof(encodedSendBuffer), + LL_PACKET_ID_SIZE); + if (encoded_size >= 0) { // Compression stats are accounted by LLMessageSystem::sendMessage(), // which detects this buffer swap. - *data = encodedSendBuffer; - *data_size += net_gain; - encodedSendBuffer[0] |= LL_ZERO_CODE_FLAG; // set the head bit to indicate zero coding - } - - return(net_gain); -} - -void LLTemplateMessageBuilder::compressMessage(U8*& buf_ptr, U32& buffer_length) -{ - if(ME_ZEROCODED == mCurrentSMessageTemplate->getEncoding()) - { - zero_code(&buf_ptr, &buffer_length); + buf_ptr = encodedSendBuffer; + buffer_length = (U32)encoded_size; } } diff --git a/indra/llmessage/llzerocode.h b/indra/llmessage/llzerocode.h new file mode 100644 index 00000000000..ac014dc5502 --- /dev/null +++ b/indra/llmessage/llzerocode.h @@ -0,0 +1,213 @@ +/** + * @file llzerocode.h + * @brief Zero-code run-length compression used by the LLMessageSystem UDP protocol. + * + * $LicenseInfo:firstyear=2001&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLZEROCODE_H +#define LL_LLZEROCODE_H + +#include + +#include "stdtypes.h" + +// Zero-coding compresses runs of zero bytes in a packet body, leaving the +// first header_size bytes of the buffer (the packet header - flags, +// sequence number, offset, etc.) untouched aside from the flag bit below. +// +// Runs of zero bytes in the body are replaced by a two-byte token: +// 0x00 N - represents N zero bytes, for N in 1..254 +// 0x00 0x00 N - represents (255 + N) zero bytes (wrap/overflow case, +// produced by decode()'s wire format but never emitted +// by encode(), which instead starts a fresh 0x00 token +// every 255 zero bytes) +namespace LLZeroCode +{ + // High bit of the first header byte: set by encode() and cleared by + // decode() to indicate whether the body that follows is zero-coded. + const U8 FLAG = 0x80; + + // Zero-codes src (src_size bytes, the first header_size of which are the + // packet header and are copied verbatim) into dst. + // + // dst_capacity must be at least 2 * src_size: a pathological body of + // isolated zero bytes can nearly double in size when encoded. + // + // Returns the encoded size (with FLAG set in dst[0]) if doing so made the + // packet smaller. Returns -1 if compression would not help (or the + // arguments are invalid), in which case dst is left untouched and the + // caller should keep using the original, uncompressed buffer. + inline S32 encode(const U8* src, U32 src_size, U8* dst, U32 dst_capacity, U32 header_size) + { + if (src_size < header_size || dst_capacity < 2 * src_size) + { + return -1; + } + + S32 count = (S32)(src_size - header_size); + S32 net_gain = 0; + U8 num_zeroes = 0; + + const U8* inptr = src; + U8* outptr = dst; + + // copy the header verbatim + for (U32 ii = 0; ii < header_size; ++ii) + { + *outptr++ = *inptr++; + } + + // sequential zero bytes are encoded as 0 [U8 count]; a run longer + // than 254 bytes is split into consecutive 0 [U8 count] tokens. + while (count--) + { + if (!(*inptr)) // in a zero count + { + if (num_zeroes) + { + if (++num_zeroes > 254) + { + *outptr++ = num_zeroes; + num_zeroes = 0; + } + net_gain--; // subsequent zeroes save one + } + else + { + *outptr++ = 0; + net_gain++; // starting a zero count adds one + num_zeroes = 1; + } + inptr++; + } + else + { + if (num_zeroes) + { + *outptr++ = num_zeroes; + num_zeroes = 0; + } + *outptr++ = *inptr++; + } + } + + if (num_zeroes) + { + *outptr++ = num_zeroes; + } + + if (net_gain >= 0) + { + // compression did not shrink the packet; caller should keep the original + return -1; + } + + dst[0] |= FLAG; + return (S32)src_size + net_gain; + } + + // Expands a zero-coded src (src_size bytes) into dst. + // + // If FLAG is not set in src[0], the body is not zero-coded: no work is + // done and the function returns 0. + // + // On success, returns the number of bytes written to dst (always includes + // the header_size header bytes, copied verbatim except for FLAG being + // cleared from dst[0]). + // + // If expansion would write past dst_capacity - which only a malformed or + // malicious packet should cause - decoding is aborted, *overflow is set + // true and 0 is returned; the caller should treat the packet as invalid. + inline U32 decode(const U8* src, U32 src_size, U8* dst, U32 dst_capacity, U32 header_size, bool& overflow) + { + overflow = false; + + if (src_size < header_size || !(src[0] & FLAG)) + { + return 0; + } + + S32 count = (S32)(src_size - header_size); + + const U8* inptr = src; + U8* outptr = dst; + + for (U32 ii = 0; ii < header_size; ++ii) + { + *outptr++ = *inptr++; + } + dst[0] &= ~FLAG; + + // reconstruct the body: a 0x00 byte starts a run; the byte(s) that + // follow give its length (see the wire format described above). + while (count--) + { + if ((U32)(outptr - dst) >= dst_capacity) + { + overflow = true; + outptr = dst; + break; + } + + if (!((*outptr++ = *inptr++))) + { + while ((count--) && (!(*inptr))) + { + if ((U32)(outptr - dst) + 256 > dst_capacity) + { + overflow = true; + outptr = dst; + count = -1; + break; + } + *outptr++ = *inptr++; + memset(outptr, 0, 255); + outptr += 255; + } + + if (count < 0) + { + break; + } + else + { + if ((U32)(outptr - dst) + (*inptr) > dst_capacity) + { + // Discard rather than decode on into a scrambled + // buffer, the same as the two cases above. + overflow = true; + outptr = dst; + break; + } + memset(outptr, 0, (*inptr) - 1); + outptr += ((*inptr) - 1); + inptr++; + } + } + } + + return (U32)(outptr - dst); + } +} + +#endif // LL_LLZEROCODE_H diff --git a/indra/llmessage/message.cpp b/indra/llmessage/message.cpp index 074edfa9ac8..77c1aaa9660 100644 --- a/indra/llmessage/message.cpp +++ b/indra/llmessage/message.cpp @@ -67,6 +67,7 @@ #include "llsdmessagereader.h" #include "llsdserialize.h" #include "llstring.h" +#include "llzerocode.h" #include "lltransfermanager.h" #include "lluuid.h" #include "llxfermanager.h" @@ -80,6 +81,7 @@ #include "llrand.h" #include "llmessagelog.h" #include "llpounceable.h" +#include "llproxy.h" // Constants //const char* MESSAGE_LOG_FILENAME = "message.log"; @@ -175,7 +177,7 @@ void LLMessageSystem::init() mTotalBytesIn = 0; mTotalBytesOut = 0; - mDroppedPackets = 0; // total dropped packets in + mLostPackets = 0; // total lost packets out mResentPackets = 0; // total resent packets out mFailedResendPackets = 0; // total resend failure packets out mOffCircuitPackets = 0; // total # of off-circuit packets rejected @@ -186,6 +188,13 @@ void LLMessageSystem::init() mIncomingCompressedSize = 0; mCurrentRecvPacketID = 0; + mActualBytesIn = 0; + mActualBytesOut = 0; + mDropPercentage = 0.0f; + mPacketsToDrop = 0; + mNumDroppedPackets = 0; + mNumDroppedPacketsTotal = 0; + mMessageFileVersionNumber = 0.f; mTimingCallback = NULL; @@ -511,30 +520,36 @@ bool LLMessageSystem::checkMessages(LockMessageChecker&, S64 frame_count, bool recv_reliable = false; bool recv_resent = false; - S32 acks = 0; + S32 num_acks = 0; S32 true_rcv_size = 0; + bool recv_packet_id_checked = false; U8* buffer = mTrueReceiveBuffer; - if(!faked_message) + if (!faked_message) { - mTrueReceiveSize = mPacketRing.receivePacket(mSocket, (char *)mTrueReceiveBuffer); - + mTrueReceiveSize = receivePacketOrDrop((char *)mTrueReceiveBuffer, recv_packet_id_checked); receive_size = mTrueReceiveSize; - mLastSender = mPacketRing.getLastSender(); - mLastReceivingIF = mPacketRing.getLastReceivingInterface(); - } else { + } + else + { buffer = fake_buffer; //true my ass. mTrueReceiveSize = fake_size; receive_size = mTrueReceiveSize; mLastSender = fake_host; - mLastReceivingIF = mPacketRing.getLastReceivingInterface(); //don't really give two tits about the interface, just leave it + // A synthetic packet carries no real sequence number, so the + // circuit's inbound sequence state must not be advanced from it. + // Reporting the check as already done is how upstream's own + // buffered path skips it. + recv_packet_id_checked = true; + // clearReceiveState() invalidated mLastReceivingIF at the top of + // this iteration, and an injected packet has no interface to name. } // If you want to dump all received packets into Alchemy.log, uncomment this //dumpPacketToLog(); - if(mTrueReceiveSize && receive_size >= (S32) LL_MINIMUM_VALID_PACKET_SIZE && !faked_message) + if (mTrueReceiveSize && receive_size >= (S32) LL_MINIMUM_VALID_PACKET_SIZE && !faked_message) { #define LOCALHOST_ADDR 16777343 LLMessageLog::log(mLastSender, LLHost(LOCALHOST_ADDR, mPort), buffer, mTrueReceiveSize); @@ -558,21 +573,23 @@ bool LLMessageSystem::checkMessages(LockMessageChecker&, S64 frame_count, LLHost host; LLCircuitData* cdp; - // note if packet acks are appended. + // handle any packet ACKs (for outbound messages) found at tail of inbound message if((buffer[0] & LL_ACK_FLAG) && !faked_message) { - acks += buffer[--receive_size]; + // Note: these ACKs may have already been handled if this was a buffered message + // but it doesn't hurt to handle them again. + num_acks += buffer[--receive_size]; true_rcv_size = receive_size; - if(receive_size >= ((S32)(acks * sizeof(TPACKETID) + LL_MINIMUM_VALID_PACKET_SIZE))) + if(receive_size >= ((S32)(num_acks * sizeof(TPACKETID) + LL_MINIMUM_VALID_PACKET_SIZE))) { - receive_size -= acks * sizeof(TPACKETID); + receive_size -= num_acks * sizeof(TPACKETID); } else { // mal-formed packet. ignore it and continue with // the next one LL_WARNS("Messaging") << "Malformed packet received. Packet size " - << receive_size << " with invalid no. of acks " << acks + << receive_size << " with invalid no. of acks " << num_acks << LL_ENDL; valid_packet = false; continue; @@ -597,11 +614,11 @@ bool LLMessageSystem::checkMessages(LockMessageChecker&, S64 frame_count, // this message came in on if it's valid, and NULL if the // circuit was bogus. - if(cdp && (acks > 0) && ((S32)(acks * sizeof(TPACKETID)) < (true_rcv_size)) && !faked_message) + if(cdp && (num_acks > 0) && ((S32)(num_acks * sizeof(TPACKETID)) < (true_rcv_size)) && !faked_message) { TPACKETID packet_id; U32 mem_id=0; - for(S32 i = 0; i < acks; ++i) + for(S32 i = 0; i < num_acks; ++i) { true_rcv_size -= sizeof(TPACKETID); memcpy(&mem_id, &mTrueReceiveBuffer[true_rcv_size], /* Flawfinder: ignore*/ @@ -656,7 +673,7 @@ bool LLMessageSystem::checkMessages(LockMessageChecker&, S64 frame_count, str << tbuf << "(unknown)" << (recv_reliable ? " reliable" : "") << " resent " - << ((acks > 0) ? "acks" : "") + << ((num_acks > 0) ? "acks" : "") << " DISCARD DUPLICATE"; LL_INFOS("Messaging") << str.str() << LL_ENDL; } @@ -706,7 +723,7 @@ bool LLMessageSystem::checkMessages(LockMessageChecker&, S64 frame_count, if ( valid_packet ) { - logValidMsg(cdp, host, recv_reliable, recv_resent, acks>0 ); + logValidMsg(cdp, host, recv_reliable, recv_resent, num_acks>0, recv_packet_id_checked ); valid_packet = mTemplateMessageReader->readMessage(buffer, host); } @@ -750,7 +767,7 @@ bool LLMessageSystem::checkMessages(LockMessageChecker&, S64 frame_count, // Check to see if we need to print debug info if ((mt_sec - mCircuitPrintTime) > mCircuitPrintFreq) { - mPacketRing.dumpPacketRingStats(); + dumpPacketRingStats(); dumpCircuitInfo(); mCircuitPrintTime = mt_sec; } @@ -775,6 +792,10 @@ S32 LLMessageSystem::getReceiveBytes() const } } +F32 LLMessageSystem::getBufferLoadRate() const +{ + return llmax(mHighPriorityInbound.getBufferLoadRate(), mLowPriorityInbound.getBufferLoadRate()); +} void LLMessageSystem::processAcks(LockMessageChecker&, F32 collect_time) { @@ -848,7 +869,312 @@ void LLMessageSystem::processAcks(LockMessageChecker&, F32 collect_time) S32 LLMessageSystem::drainUdpSocket() { - return mPacketRing.drainSocket(mSocket); + S32 packet_size = 1; + S32 num_loops = 0; + S32 old_num_buffered_packets = getNumBufferedPackets(); + while (packet_size > 0) + { + packet_size = bufferInboundPacket(); + ++num_loops; + } + S32 num_dropped_packets = (num_loops - 1 + old_num_buffered_packets) - getNumBufferedPackets(); + if (num_dropped_packets > 0) + { + mNumDroppedPackets += num_dropped_packets; + } + return getNumBufferedPackets(); +} + +bool LLMessageSystem::computeDrop() +{ + bool drop = (mDropPercentage > 0.0f && (ll_frand(100.f) < mDropPercentage)); + if (drop) + { + ++mPacketsToDrop; + } + if (mPacketsToDrop > 0) + { + --mPacketsToDrop; + drop = true; + } + return drop; +} + +bool LLMessageSystem::isHighPriorityMessage(const LLPacketBuffer& pkt) const +{ + S32 size = pkt.getSize(); + + // avoid stepping out of bounds + const S32 MIN_VALID_PACKET_SIZE = LL_PACKET_ID_SIZE + 4; + if (size < MIN_VALID_PACKET_SIZE) + { + return false; + } + + // We want to prioritize crucial messages used to establish viewer <--> simulator connection, + // which are all low-frequency. A simple approximation is to just prioritize all non high- + // frequency messages. + // + // High frequency messages use a single byte for message_id whereas medium- and low- + // frequency messages have 255 at the first byte (which is after the LL_PACKET_ID_SIZE + // bytes of packet_id). + const U8* header = (const U8*)pkt.getData() + LL_PACKET_ID_SIZE; + if (header[0] != 255) + { + // high-frequency message + return false; + } + + // BUG: The low-frequency AvatarAppearance message will be ignored if its agent is unknown + // and the agent is created upon receipt of its first high-frequency ObjectUpdate message. + // This race condition will be exacerbated by our default prioritization strategy. + // + // WORKAROUND: All middle- and low-frequency messages are high-priority except AvatarAppearance + // + // AvatarAppearance is "Low 158" which means it is stored in four bytes: 0xff 0xff 0x00 0x9E + // the last two bytes represent 158 in a BigEndian U16 + return header[1] != 255 && header[2] != 0 && header[3] != 158; +} + +void LLMessageSystem::dropPackets(U32 num_to_drop) +{ + mPacketsToDrop += num_to_drop; +} + +void LLMessageSystem::setDropPercentage(F32 percent_to_drop) +{ + mDropPercentage = percent_to_drop; +} + +S32 LLMessageSystem::receivePacketOrDrop(char* datap, bool& packet_id_already_checked) +{ + packet_id_already_checked = false; + + if (getNumBufferedPackets() > 0) + { + LLHost invalid_host; + LLPacketBuffer pkt(invalid_host, nullptr, 0); + if (!mHighPriorityInbound.popPacket(pkt)) + { + mLowPriorityInbound.popPacket(pkt); + } + + S32 packet_size = pkt.getSize(); + mLastSender = pkt.getHost(); + mLastReceivingIF = pkt.getReceivingInterface(); + packet_id_already_checked = pkt.getPacketIDChecked(); + + if (packet_size > 0) + { + memcpy(datap, pkt.getData(), packet_size); + } + return packet_size; + } + + // Read directly from the socket. checkPacketInID() has not run yet for + // this packet. + bool drop = computeDrop(); + S32 packet_size = 0; + if (LLProxy::isSOCKSProxyEnabled()) + { + char buffer[NET_BUFFER_SIZE + SOCKS_HEADER_SIZE]; /* Flawfinder ignore */ + packet_size = receive_packet(mSocket, buffer); + if (packet_size > 0) + { + mActualBytesIn += packet_size; + } + if (packet_size > SOCKS_HEADER_SIZE) + { + if (drop) + { + packet_size = 0; + } + else + { + // *FIX We are assuming ATYP is 0x01 (IPv4), not 0x03 (hostname) or 0x04 (IPv6) + packet_size -= SOCKS_HEADER_SIZE; + memcpy(datap, buffer + SOCKS_HEADER_SIZE, packet_size); + proxywrap_t* header = static_cast(static_cast(buffer)); + mLastSender.setAddress(header->addr); + mLastSender.setPort(ntohs(header->port)); + mLastReceivingIF = ::get_receiving_interface(); + } + } + else + { + packet_size = 0; + } + } + else + { + packet_size = receive_packet(mSocket, datap); + if (packet_size > 0) + { + mActualBytesIn += packet_size; + if (drop) + { + packet_size = 0; + } + else + { + mLastSender = ::get_sender(); + mLastReceivingIF = ::get_receiving_interface(); + } + } + } + return packet_size; +} + +S32 LLMessageSystem::bufferInboundPacket() +{ + LLHost invalid_host; + LLPacketBuffer pkt(invalid_host, nullptr, 0); + S32 packet_size = 0; + // What the socket handed us, which is not the usable payload size once a + // SOCKS header is stripped. drainUdpSocket() loops on this: reporting 0 + // for a datagram that did arrive would end the drain and leave everything + // behind it sitting in the kernel buffer. + S32 socket_read_size = 0; + + if (LLProxy::isSOCKSProxyEnabled()) + { + char buffer[NET_BUFFER_SIZE + SOCKS_HEADER_SIZE]; /* Flawfinder ignore */ + packet_size = receive_packet(mSocket, buffer); + socket_read_size = packet_size; + if (packet_size > 0) + { + mActualBytesIn += packet_size; + if (packet_size > SOCKS_HEADER_SIZE) + { + // *FIX We are assuming ATYP is 0x01 (IPv4), not 0x03 (hostname) or 0x04 (IPv6) + proxywrap_t* header = static_cast(static_cast(buffer)); + LLHost sender; + sender.setAddress(header->addr); + sender.setPort(ntohs(header->port)); + packet_size -= SOCKS_HEADER_SIZE; + pkt.init(buffer + SOCKS_HEADER_SIZE, packet_size, sender); + } + else + { + // Runt wrapper with no payload past the header: discard the + // packet, but socket_read_size keeps the drain going. + packet_size = 0; + } + } + } + else + { + pkt.init(mSocket); + packet_size = pkt.getSize(); + socket_read_size = packet_size; + if (packet_size > 0) + { + mActualBytesIn += packet_size; + } + } + + if (packet_size >= (S32)LL_MINIMUM_VALID_PACKET_SIZE && !computeDrop()) + { + const char* data = pkt.getData(); + LLCircuitData* cdp = mCircuitInfo.findCircuit(pkt.getHost()); + TPACKETID recv_packet_id = ntohl(*((U32*)(&data[1]))); + + // Harvest piggybacked ACKs for outbound messages from the packet tail of this inbound message + if (cdp && (data[0] & LL_ACK_FLAG)) + { + U8 num_acks = (U8)data[packet_size - 1]; + S32 true_rcv_size = packet_size - 1; + if (true_rcv_size >= (S32)(num_acks * sizeof(TPACKETID) + LL_MINIMUM_VALID_PACKET_SIZE)) + { + TPACKETID ack_id; + U32 mem_id = 0; + for (S32 i = 0; i < num_acks; ++i) + { + true_rcv_size -= sizeof(TPACKETID); + memcpy(&mem_id, &data[true_rcv_size], sizeof(TPACKETID)); /* Flawfinder: ignore */ + ack_id = ntohl(mem_id); + cdp->ackReliablePacket(ack_id); + } + if (!cdp->getUnackedPacketCount()) + { + mCircuitInfo.mUnackedCircuitMap.erase(cdp->mHost); + } + } + } + + if (cdp) + { + // ACK inbound reliable packet ASAP + if ((data[0] & LL_RELIABLE_FLAG)) + { + cdp->collectRAck(recv_packet_id); + } + + // Check packet sequencing here, in true socket-arrival order, before + // this packet is sorted into the high/low priority inbound queue. + // Skip genuine duplicate resends, same as checkMessages()/logValidMsg() + // would do further downstream. + bool recv_resent = (data[0] & LL_RESENT_FLAG) != 0; + if (!recv_resent || !cdp->isDuplicateResend(recv_packet_id)) + { + cdp->checkPacketInID(recv_packet_id, recv_resent); + pkt.setPacketIDChecked(true); + } + } + + if (isHighPriorityMessage(pkt)) + { + mHighPriorityInbound.pushPacket(pkt); + } + else + { + mLowPriorityInbound.pushPacket(pkt); + } + } + + return socket_read_size; +} + +bool LLMessageSystem::sendPacketToSocket(const char* datap, S32 data_size, LLHost host) +{ +#define LOCALHOST_ADDR 16777343 + LLMessageLog::log(LLHost(LOCALHOST_ADDR, mPort), host, (U8*)datap, data_size); +#undef LOCALHOST_ADDR + mActualBytesOut += data_size; + if (!LLProxy::isSOCKSProxyEnabled()) + { + return send_packet(mSocket, datap, data_size, host.getAddress(), host.getPort()); + } + + char headered_send_buffer[NET_BUFFER_SIZE + SOCKS_HEADER_SIZE]; + + proxywrap_t* socks_header = static_cast(static_cast(&headered_send_buffer)); + socks_header->rsv = 0; + socks_header->addr = host.getAddress(); + socks_header->port = htons(host.getPort()); + socks_header->atype = ADDRESS_IPV4; + socks_header->frag = 0; + + memcpy(headered_send_buffer + SOCKS_HEADER_SIZE, datap, data_size); + + return send_packet(mSocket, + headered_send_buffer, + data_size + SOCKS_HEADER_SIZE, + LLProxy::getInstance()->getUDPProxy().getAddress(), + LLProxy::getInstance()->getUDPProxy().getPort()); +} + +void LLMessageSystem::dumpPacketRingStats() +{ + mNumDroppedPacketsTotal += mNumDroppedPackets; + LL_INFOS("Messaging") << "buffered_packets=" << getNumBufferedPackets() + << "buffered_bytes=" << (mHighPriorityInbound.getNumBufferedBytes() + mLowPriorityInbound.getNumBufferedBytes()) + << "recently_dropped=" << mNumDroppedPackets + << "total_dropped=" << mNumDroppedPacketsTotal + << "dropped_percentage=" << mDropPercentage << "%" + << "bytes_IN=" << mActualBytesIn + << "bytes_OUT=" << mActualBytesOut << LL_ENDL; + mNumDroppedPackets = 0; } void LLMessageSystem::copyMessageReceivedToSend() @@ -1306,7 +1632,7 @@ S32 LLMessageSystem::sendMessage(const LLHost &host) } bool success; - success = mPacketRing.sendPacket(mSocket, (char *)buf_ptr, buffer_length, host); + success = sendPacketToSocket((char *)buf_ptr, buffer_length, host); if (!success) { @@ -1429,7 +1755,13 @@ void LLMessageSystem::logTrustedMsgFromUntrustedCircuit( const LLHost& host ) } } -void LLMessageSystem::logValidMsg(LLCircuitData *cdp, const LLHost& host, bool recv_reliable, bool recv_resent, bool recv_acks ) +void LLMessageSystem::logValidMsg( + LLCircuitData *cdp, + const LLHost& host, + bool recv_reliable, + bool recv_resent, + bool recv_acks, + bool skip_packet_id_check ) { if (mNumMessageCounts >= MAX_MESSAGE_COUNT_NUM) { @@ -1446,8 +1778,13 @@ void LLMessageSystem::logValidMsg(LLCircuitData *cdp, const LLHost& host, bool r if (cdp) { - // update circuit packet ID tracking (missing/out of order packets) - cdp->checkPacketInID( mCurrentRecvPacketID, recv_resent ); + if (!skip_packet_id_check) + { + // update circuit packet ID tracking (missing/out of order packets) + // Already done in bufferInboundPacket(), in true socket-arrival + // order, if this packet came off the high/low priority queues. + cdp->checkPacketInID( mCurrentRecvPacketID, recv_resent ); + } cdp->addBytesIn( (S32Bytes)mTrueReceiveSize ); } @@ -2637,7 +2974,7 @@ void LLMessageSystem::summarizeLogs(std::ostream& str) str << buffer << std::endl << std::endl; buffer = llformat( "SendPacket failures: %20d", mSendPacketFailureCount); str << buffer << std::endl; - buffer = llformat( "Dropped packets: %20d", mDroppedPackets); + buffer = llformat( "Dropped packets: %20d", getTotalNumDroppedPackets()); str << buffer << std::endl; buffer = llformat( "Resent packets: %20d", mResentPackets); str << buffer << std::endl; @@ -2669,6 +3006,7 @@ void LLMessageSystem::summarizeLogs(std::ostream& str) void end_messaging_system(bool print_summary) { + LL_PROFILE_ZONE_SCOPED; gTransferManager.cleanup(); LLTransferTargetVFile::updateQueue(true); // shutdown LLTransferTargetVFile if (gMessageSystem) @@ -2794,77 +3132,18 @@ S32 LLMessageSystem::zeroCodeExpand(U8** data, S32* data_size) mCompressedPacketsIn++; mCompressedBytesIn += *data_size; - *data[0] &= (~LL_ZERO_CODE_FLAG); - - S32 count = (*data_size); - - U8 *inptr = (U8 *)*data; - U8 *outptr = (U8 *)mEncodedRecvBuffer; - -// skip the packet id field - - for (U32 ii = 0; ii < LL_PACKET_ID_SIZE; ++ii) - { - count--; - *outptr++ = *inptr++; - } - -// reconstruct encoded packet, keeping track of net size gain - -// sequential zero bytes are encoded as 0 [U8 count] -// with 0 0 [count] representing wrap (>256 zeroes) - - while (count--) + bool overflow = false; + U32 decoded_size = LLZeroCode::decode(*data, (U32)*data_size, + mEncodedRecvBuffer, sizeof(mEncodedRecvBuffer), + LL_PACKET_ID_SIZE, overflow); + if (overflow) { - if (outptr > (&mEncodedRecvBuffer[MAX_BUFFER_SIZE-1])) - { - LL_WARNS("Messaging") << "attempt to write past reasonable encoded buffer size 1" << LL_ENDL; - callExceptionFunc(MX_WROTE_PAST_BUFFER_SIZE); - outptr = mEncodedRecvBuffer; - break; - } - if (!((*outptr++ = *inptr++))) - { - while (((count--)) && (!(*inptr))) - { - *outptr++ = *inptr++; - if (outptr > (&mEncodedRecvBuffer[MAX_BUFFER_SIZE-256])) - { - LL_WARNS("Messaging") << "attempt to write past reasonable encoded buffer size 2" << LL_ENDL; - callExceptionFunc(MX_WROTE_PAST_BUFFER_SIZE); - outptr = mEncodedRecvBuffer; - count = -1; - break; - } - memset(outptr,0,255); - outptr += 255; - } - - if (count < 0) - { - break; - } - - else - { - if (outptr > (&mEncodedRecvBuffer[MAX_BUFFER_SIZE-(*inptr)])) - { - LL_WARNS("Messaging") << "attempt to write past reasonable encoded buffer size 3" << LL_ENDL; - callExceptionFunc(MX_WROTE_PAST_BUFFER_SIZE); - // discard the malformed packet instead of continuing to - // decode into a scrambled buffer (mirrors cases 1 and 2) - outptr = mEncodedRecvBuffer; - break; - } - memset(outptr,0,(*inptr) - 1); - outptr += ((*inptr) - 1); - inptr++; - } - } + LL_WARNS("Messaging") << "attempt to write past reasonable encoded buffer size" << LL_ENDL; + callExceptionFunc(MX_WROTE_PAST_BUFFER_SIZE); } *data = mEncodedRecvBuffer; - *data_size = (S32)(outptr - mEncodedRecvBuffer); + *data_size = (S32)decoded_size; mUncompressedBytesIn += *data_size; return(in_size); @@ -3300,7 +3579,7 @@ void LLMessageSystem::establishBidirectionalTrust(const LLHost &host, S64 frame_ void LLMessageSystem::dumpPacketToLog() { - LL_WARNS("Messaging") << "Packet Dump from:" << mPacketRing.getLastSender() << LL_ENDL; + LL_WARNS("Messaging") << "Packet Dump from:" << mLastSender << LL_ENDL; LL_WARNS("Messaging") << "Packet Size:" << mTrueReceiveSize << LL_ENDL; char line_buffer[256]; /* Flawfinder: ignore */ S32 i; diff --git a/indra/llmessage/message.h b/indra/llmessage/message.h index fa797eb19db..bad0836c341 100644 --- a/indra/llmessage/message.h +++ b/indra/llmessage/message.h @@ -120,6 +120,49 @@ class LLMessageStringTable : public LLSingleton // Repeat for number of messages in file // +// UDP Packet Buffer Layout +// +// Every UDP message sent or received by LLMessageSystem uses the following +// on-wire layout. Offsets are defined in EPacketHeaderLayout below. +// +// Byte(s) Name Description +// ------- ---- ----------- +// 0 Flags Bit-field (see flag constants below): +// 0x80 LL_ZERO_CODE_FLAG – body is zero-run-length encoded +// 0x40 LL_RELIABLE_FLAG – sender expects a packet ACK +// 0x20 LL_RESENT_FLAG – this is a retransmission +// 0x10 LL_ACK_FLAG – piggybacked ACKs are appended +// at the tail of this packet +// 1-4 Packet ID Sequence number, U32 in network (big-endian) byte order. +// 5 Offset Byte offset from PHL_NAME to the start of the message +// body (past the message-ID bytes). Zero for most messages. +// 6+ Message ID Variable-length message type identifier: +// High-frequency (1 byte, values 0x01–0xFE) +// Medium-frequency (2 bytes, 0xFF hh) +// Low-frequency (4 bytes, 0xFF 0xFF hh ll) +// ... Body Message block data, described by the message template. +// Present only when LL_ZERO_CODE_FLAG is clear; otherwise +// the body (everything after byte 5) is zero-coded (see +// below). The 6-byte header is never zero-coded. +// +// Optional ACK tail (present when LL_ACK_FLAG is set in the flags byte): +// +// ... ACK IDs N packet-sequence IDs being acknowledged, each a U32 in +// network byte order, packed contiguously immediately before +// the ACK count byte. Read in reverse: walk backwards from +// just before the count byte, 4 bytes at a time. +// last ACK Count U8 giving N, the number of appended ACK IDs (max 255). +// This is the very last byte of the UDP payload. +// +// Zero-coding (applied when LL_ZERO_CODE_FLAG is set): +// +// Runs of zero bytes in the body are replaced by a two-byte token: +// 0x00 N – represents (N + 1) zero bytes, for N in 1..254 +// 0x00 0x00 N – represents (256 + N) zero bytes (wrap/overflow case) +// A literal 0x00 byte that starts no run is encoded as 0x00 0x00 0x00. +// The six-byte packet header is excluded from zero-coding and is always +// transmitted as-is. + // Constants const S32 MAX_MESSAGE_INTERNAL_NAME_SIZE = 255; const S32 MAX_BUFFER_SIZE = NET_BUFFER_SIZE; @@ -292,8 +335,11 @@ class LLMessageSystem : public LLMessageSenderInterface bool mBlockUntrustedInterface; LLHost mUntrustedInterface; + protected: + LLPacketRing mHighPriorityInbound; + LLPacketRing mLowPriorityInbound; + public: - LLPacketRing mPacketRing; LLReliablePacketParams mReliablePacketParams; // Set this flag to true when you want *very* verbose logs. @@ -333,7 +379,7 @@ class LLMessageSystem : public LLMessageSenderInterface U32 mReliablePacketsIn; // total reliable packets in U32 mReliablePacketsOut; // total reliable packets out - U32 mDroppedPackets; // total dropped packets in + U32 mLostPackets; // total reliable outbound packets declared lost U32 mResentPackets; // total resent packets out U32 mFailedResendPackets; // total resend failure packets out U32 mOffCircuitPackets; // total # of off-circuit packets rejected @@ -426,6 +472,25 @@ class LLMessageSystem : public LLMessageSenderInterface // returns total number of buffered packets after the drain S32 drainUdpSocket(); + // Inbound Packet-loss simulation controls + void dropPackets(U32 num_to_drop); + void setDropPercentage(F32 percent_to_drop); + + // UDP byte-accounting + S32 getActualInBytes() const { return mActualBytesIn; } + S32 getActualOutBytes() const { return mActualBytesOut; } + S32 getAndResetActualInBits() { S32 bits = mActualBytesIn * 8; mActualBytesIn = 0; return bits; } + S32 getAndResetActualOutBits() { S32 bits = mActualBytesOut * 8; mActualBytesOut = 0; return bits; } + + // Get number of "dropped" inbound packets + S32 getTotalNumDroppedPackets() const { return mNumDroppedPacketsTotal + mNumDroppedPackets; } + + S32 getNumBufferedPackets() const { return mHighPriorityInbound.getNumBufferedPackets() + mLowPriorityInbound.getNumBufferedPackets(); } + void dumpPacketRingStats(); + + // Send datap to host via mSocket (with SOCKS proxy support if enabled). + bool sendPacketToSocket(const char* datap, S32 data_size, LLHost host); + bool isMessageFast(const char *msg); bool isMessage(const char *msg) { @@ -759,7 +824,7 @@ class LLMessageSystem : public LLMessageSenderInterface S32 getReceiveBytes() const; S32 getUnackedListSize() const { return mUnackedListSize; } - F32 getBufferLoadRate() const { return mPacketRing.getBufferLoadRate(); } + F32 getBufferLoadRate() const; //const char* getCurrentSMessageName() const { return mCurrentSMessageName; } //const char* getCurrentSBlockName() const { return mCurrentSBlockName; } @@ -850,7 +915,7 @@ class LLMessageSystem : public LLMessageSenderInterface void logMsgFromInvalidCircuit( const LLHost& sender, bool recv_reliable ); void logTrustedMsgFromUntrustedCircuit( const LLHost& sender ); - void logValidMsg(LLCircuitData *cdp, const LLHost& sender, bool recv_reliable, bool recv_resent, bool recv_acks ); + void logValidMsg(LLCircuitData *cdp, const LLHost& sender, bool recv_reliable, bool recv_resent, bool recv_acks, bool skip_packet_id_check ); struct LLMessageCountInfo { @@ -902,6 +967,34 @@ class LLMessageSystem : public LLMessageSenderInterface S32 mIncomingCompressedSize; // original size of compressed msg (0 if uncomp.) TPACKETID mCurrentRecvPacketID; // packet ID of current receive packet (for reporting) + // Socket I/O helpers + + // Receive one packet: pop from ring if buffered, else read from mSocket. + // Sets mLastSender and mLastReceivingIF. + // Sets packet_id_already_checked to whether checkPacketInID() was already + // run for this packet back when it was buffered (see bufferInboundPacket()). + // Returns packet_size, or 0 if no packet or packet was dropped. + S32 receivePacketOrDrop(char* datap, bool& packet_id_already_checked); + + // Read one raw packet from mSocket into inbound message queues + // Returns packet_size (0 if no packet was available). + S32 bufferInboundPacket(); + + // Returns true if the next inbound packet should be intentionally dropped. + bool computeDrop(); + + // Returns true if pkt carries a high-priority message and should be queued + // in mHighPriorityInbound. + bool isHighPriorityMessage(const LLPacketBuffer& pkt) const; + + // Packet-loss simulation and byte-accounting state + S32 mActualBytesIn; + S32 mActualBytesOut; + F32 mDropPercentage; // % of inbound packets to drop + U32 mPacketsToDrop; // drop next N inbound packets + S32 mNumDroppedPackets; // inbound + S32 mNumDroppedPacketsTotal;// inbound + public: LLMessageBuilder* mMessageBuilder; LLTemplateMessageBuilder* mTemplateMessageBuilder; diff --git a/indra/llmessage/net.cpp b/indra/llmessage/net.cpp index 2be5a9e5b63..0df38d040af 100644 --- a/indra/llmessage/net.cpp +++ b/indra/llmessage/net.cpp @@ -326,7 +326,7 @@ S32 receive_packet(int hSocket, char * receiveBuffer) return 0; if (WSAECONNRESET == WSAGetLastError()) return 0; - LL_INFOS() << "receivePacket() failed, Error: " << WSAGetLastError() << LL_ENDL; + LL_INFOS() << "receive_packet() failed, Error: " << WSAGetLastError() << LL_ENDL; } return nRet; diff --git a/indra/llmessage/tests/llzerocode_test.cpp b/indra/llmessage/tests/llzerocode_test.cpp new file mode 100644 index 00000000000..7a52c90b46b --- /dev/null +++ b/indra/llmessage/tests/llzerocode_test.cpp @@ -0,0 +1,253 @@ +/** + * @file llzerocode_test.cpp + * @brief LLZeroCode test cases. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "../llzerocode.h" + +#include "../test/lltut.h" + +#include + +namespace tut +{ + struct zerocode_data + { + }; + typedef test_group zerocode_test; + typedef zerocode_test::object zerocode_object; + tut::zerocode_test zerocode_testcase("LLZeroCode"); + + // Builds header_size bytes of header (with header[0] == header0) followed by body. + static std::vector makeBuffer(U32 header_size, U8 header0, const std::vector& body) + { + std::vector buf(header_size, 0); + if (header_size) + { + buf[0] = header0; + } + buf.insert(buf.end(), body.begin(), body.end()); + return buf; + } + + // Runs src through encode() then decode(), and (when compression is expected to help) + // asserts the round trip reproduces src exactly, including untouched header bits other + // than the zero-code flag. When compression is not expected to help, asserts encode() + // refuses it. + static void ensureRoundTrip(const char* msg, U32 header_size, U8 header0, + const std::vector& body, bool expect_compressed) + { + std::vector src = makeBuffer(header_size, header0, body); + std::vector enc(2 * src.size() + 16, 0xAA); + + S32 enc_size = LLZeroCode::encode(src.data(), (U32)src.size(), + enc.data(), (U32)enc.size(), header_size); + + if (!expect_compressed) + { + ensure(std::string(msg) + ": encode should refuse (no benefit)", enc_size < 0); + return; + } + + ensure(std::string(msg) + ": encode should succeed", enc_size >= 0); + ensure(std::string(msg) + ": encoded size should be smaller", (U32)enc_size < src.size()); + ensure(std::string(msg) + ": FLAG should be set on encoded output", (enc[0] & LLZeroCode::FLAG) != 0); + if (header_size > 0) + { + ensure_equals(std::string(msg) + ": non-flag header bits preserved on encode", + (U8)(enc[0] & ~LLZeroCode::FLAG), (U8)(header0 & ~LLZeroCode::FLAG)); + } + + std::vector dec(src.size() + 16, 0xBB); + bool overflow = false; + U32 dec_size = LLZeroCode::decode(enc.data(), (U32)enc_size, + dec.data(), (U32)dec.size(), header_size, overflow); + ensure(std::string(msg) + ": decode should not overflow", !overflow); + ensure_equals(std::string(msg) + ": decoded size should match original", dec_size, (U32)src.size()); + ensure(std::string(msg) + ": decoded bytes should match original", + memcmp(dec.data(), src.data(), src.size()) == 0); + } + + // Basic mixed body; header carries an unrelated flag bit (0x40) that must survive untouched. + template<> template<> + void zerocode_object::test<1>() + { + ensureRoundTrip("mixed body", 6, 0x40, + {0,0,0,5,0,0,0,0,0,0,7,8,9,0,0}, true); + } + + // A body with no zero bytes at all cannot benefit from zero-coding. + template<> template<> + void zerocode_object::test<2>() + { + ensureRoundTrip("no zero bytes", 1, 0x00, {1,2,3,4,5,6,7,8,9}, false); + } + + // Isolated zero-byte runs of length 1 or 2 cost as much or more than they save + // (marker + terminator == 2 bytes), so encode must refuse them. + template<> template<> + void zerocode_object::test<3>() + { + ensureRoundTrip("isolated single zero", 1, 0x00, {5, 0}, false); + ensureRoundTrip("isolated double zero", 1, 0x00, {5, 0, 0, 9}, false); + } + + // A long enough zero run followed by other data pays off. + template<> template<> + void zerocode_object::test<4>() + { + std::vector body(20, 0); + body.push_back(99); + ensureRoundTrip("long zero run", 1, 0x00, body, true); + } + + // Wrap boundary: runs are split into chunks of at most 255 zero bytes each + // (encode never emits the doubled-0x00 wire form; see test<7> for that). + // A run only shrinks the packet once it is longer than 2 bytes. + template<> template<> + void zerocode_object::test<5>() + { + static const U32 lengths[] = {1, 2, 253, 254, 255, 256, 257, 300, 509, 510, 511, 1000}; + for (U32 n : lengths) + { + std::vector body(n, 0); + body.push_back(42); // trailing nonzero byte + ensureRoundTrip("wrap boundary (with trailing byte)", 1, 0x00, body, n > 2); + } + } + + // Same boundary check, but with the zero run flushed at end-of-buffer + // (no trailing nonzero byte to force the terminator write mid-loop). + template<> template<> + void zerocode_object::test<6>() + { + static const U32 lengths[] = {1, 2, 254, 255, 256, 300}; + for (U32 n : lengths) + { + std::vector body(n, 0); + ensureRoundTrip("wrap boundary (end-of-buffer flush)", 1, 0x00, body, n > 2); + } + } + + // decode() must also accept the "0x00 0x00 N" doubled-marker wire format + // (worth +255 zero bytes per extra marker) for compatibility with any + // encoder other than this one's chunking strategy, even though encode() + // itself never emits it. + template<> template<> + void zerocode_object::test<7>() + { + // header (1 byte, FLAG set) + [0x00 marker][0x00 extra-wrap-marker][terminator=5] + // decoded body length = 1 (marker) + 1 (extra marker) + 255 (memset) + (5 - 1) = 261 + std::vector encoded = {(U8)LLZeroCode::FLAG, 0x00, 0x00, 0x05}; + std::vector dec(1024, 0xDD); + bool overflow = false; + U32 dec_size = LLZeroCode::decode(encoded.data(), (U32)encoded.size(), + dec.data(), (U32)dec.size(), 1, overflow); + ensure("wrap format: no overflow", !overflow); + ensure_equals("wrap format: decoded size", dec_size, (U32)262); + ensure_equals("wrap format: FLAG cleared on header", dec[0], (U8)0x00); + for (U32 i = 1; i < dec_size; ++i) + { + ensure_equals("wrap format: decoded byte is zero", dec[i], (U8)0); + } + } + + // decode() is a no-op (returns 0, does not touch dst) when FLAG is not set. + template<> template<> + void zerocode_object::test<8>() + { + std::vector src = makeBuffer(6, 0x00, {1,2,3,0,0,0}); + std::vector dec(64, 0xCC); + bool overflow = false; + U32 dec_size = LLZeroCode::decode(src.data(), (U32)src.size(), + dec.data(), (U32)dec.size(), 6, overflow); + ensure_equals("not zero-coded: decode returns 0", dec_size, (U32)0); + ensure("not zero-coded: no overflow", !overflow); + } + + // encode() refuses to write into an undersized destination buffer. + template<> template<> + void zerocode_object::test<9>() + { + std::vector body(50, 0); + std::vector src = makeBuffer(1, 0, body); + std::vector enc(src.size(), 0); // smaller than the required 2 * src_size + S32 r = LLZeroCode::encode(src.data(), (U32)src.size(), enc.data(), (U32)enc.size(), 1); + ensure("encode: capacity guard rejects undersized dst", r < 0); + } + + // decode() reports overflow (rather than writing out of bounds) when dst is too small, + // whether the destination is smaller than a single byte's worth of headroom... + template<> template<> + void zerocode_object::test<10>() + { + std::vector body(500, 0); + std::vector src = makeBuffer(1, 0, body); + std::vector enc(2 * src.size() + 16, 0); + S32 enc_size = LLZeroCode::encode(src.data(), (U32)src.size(), enc.data(), (U32)enc.size(), 1); + ensure("decode overflow setup: encode succeeded", enc_size >= 0); + + std::vector dec(1, 0); // capacity == header_size exactly + bool overflow = false; + U32 dec_size = LLZeroCode::decode(enc.data(), (U32)enc_size, + dec.data(), (U32)dec.size(), 1, overflow); + ensure("decode overflow (minimal capacity): overflow flagged", overflow); + ensure_equals("decode overflow (minimal capacity): partial output discarded", dec_size, 0u); + } + + // ...or comfortably larger than 256 bytes but still short of the true decoded size. + template<> template<> + void zerocode_object::test<11>() + { + std::vector body(500, 0); + std::vector src = makeBuffer(1, 0, body); + std::vector enc(2 * src.size() + 16, 0); + S32 enc_size = LLZeroCode::encode(src.data(), (U32)src.size(), enc.data(), (U32)enc.size(), 1); + ensure("decode overflow setup: encode succeeded", enc_size >= 0); + + std::vector dec(300, 0); // > 256, but less than the true decoded size (~501) + bool overflow = false; + U32 dec_size = LLZeroCode::decode(enc.data(), (U32)enc_size, + dec.data(), (U32)dec.size(), 1, overflow); + ensure("decode overflow (insufficient capacity): overflow flagged", overflow); + ensure_equals("decode overflow (insufficient capacity): partial output discarded", dec_size, 0u); + } + + // A pathological alternating zero/non-zero pattern grows under zero-coding + // (every isolated zero costs 2 output bytes for 1 input byte), so encode + // must refuse it and leave the original buffer in use. + template<> template<> + void zerocode_object::test<12>() + { + std::vector body; + for (int i = 0; i < 200; ++i) + { + body.push_back(0); + body.push_back((U8)(i + 1)); + } + ensureRoundTrip("alternating pattern", 1, 0x00, body, false); + } +} diff --git a/indra/llphysicsextensionsos/llphysicsextensions.cpp b/indra/llphysicsextensionsos/llphysicsextensions.cpp index 3bb8ffbf1a5..295ab41a17f 100644 --- a/indra/llphysicsextensionsos/llphysicsextensions.cpp +++ b/indra/llphysicsextensionsos/llphysicsextensions.cpp @@ -72,6 +72,7 @@ //============================================================================= /*static */bool LLPhysicsExtensions::quitSystem() { + LL_PROFILE_ZONE_SCOPED; return LLPhysicsExtensionsImpl::quitSystem(); } //============================================================================= diff --git a/indra/llplugin/llpluginclassmedia.cpp b/indra/llplugin/llpluginclassmedia.cpp index 7d4de31d3a6..e3b4ad15b7c 100644 --- a/indra/llplugin/llpluginclassmedia.cpp +++ b/indra/llplugin/llpluginclassmedia.cpp @@ -38,6 +38,7 @@ #else #include // getpid #endif +#include extern LLControlGroup gSavedSettings; #if LL_DARWIN || LL_LINUX @@ -45,6 +46,8 @@ extern bool gHiDPISupport; #endif static int LOW_PRIORITY_TEXTURE_SIZE_DEFAULT = 256; +static const U32 MIN_DEBUG_PORT = 1024; +static const U32 MAX_DEBUG_PORT = 65535; static int nextPowerOf2( int value ) { @@ -118,6 +121,7 @@ bool LLPluginClassMedia::init(const std::string &launcher_filename, const std::s void LLPluginClassMedia::reset() { + LL_PROFILE_ZONE_SCOPED; if(mPlugin) { mPlugin->requestShutdown(); @@ -979,6 +983,34 @@ void LLPluginClassMedia::showPageSource() sendMessage(message); } + +static U32 assignCefDebuggingPort() +{ + U32 base_port = gSavedSettings.getU32("CEFRemoteDebuggingPort"); + if (base_port == 0) + { + return 0; + } + base_port = llclamp(base_port, MIN_DEBUG_PORT, MAX_DEBUG_PORT); + + static U32 last_base_port = 0; + static U32 offset = 0; + if (base_port != last_base_port) + { + last_base_port = base_port; + offset = 0; + } + + U32 new_port = base_port + offset; + if (new_port > MAX_DEBUG_PORT) + { + return 0; + } + + ++offset; + return new_port; +} + void LLPluginClassMedia::setUserDataPath(const std::string &user_data_path_cache, const std::string &username, const std::string &user_data_path_cef_log) @@ -990,6 +1022,10 @@ void LLPluginClassMedia::setUserDataPath(const std::string &user_data_path_cache bool cef_verbose_log = gSavedSettings.getBOOL("CefVerboseLog"); message.setValueBoolean("cef_verbose_log", cef_verbose_log); + + U32 cef_remote_debugging_port = assignCefDebuggingPort(); + message.setValueU32("cef_remote_debugging_port", cef_remote_debugging_port); + mCefRemoteDebuggingPort = cef_remote_debugging_port; sendMessage(message); } @@ -1678,7 +1714,13 @@ void LLPluginClassMedia::setLoop(bool loop) void LLPluginClassMedia::setVolume(float volume) { - if(volume != mRequestedVolume) + // VLC's volume is integer, 0 to 100 range. When converted from float, + // changes below 0.01 won't be noticed by VLC. + // CEF's audio range is DWORD, 0 to 65535. But changes so small are + // inaudible and don't warrant the overhead, so use a bigger epsilon + // to avoid extra messages and locking. + constexpr float VOLUME_EPSILON = 0.002f; + if (std::abs(volume - mRequestedVolume) > VOLUME_EPSILON) { mRequestedVolume = volume; diff --git a/indra/llplugin/llpluginclassmedia.h b/indra/llplugin/llpluginclassmedia.h index c2df9c98839..8a451c250d0 100644 --- a/indra/llplugin/llpluginclassmedia.h +++ b/indra/llplugin/llpluginclassmedia.h @@ -169,6 +169,11 @@ class LLPluginClassMedia : public LLPluginProcessParentOwner std::string getPluginVersion() { return mPlugin?mPlugin->getPluginVersion():std::string(""); }; + int getProcessID() { return mPlugin ? (int)mPlugin->getProcessID() : 0; }; + + // 0 means remote debugging is disabled for this plugin instance. + U32 getCefRemoteDebuggingPort() const { return mCefRemoteDebuggingPort; }; + bool getDisableTimeout() { return mPlugin?mPlugin->getDisableTimeout():false; }; void setDisableTimeout(bool disable) { if(mPlugin) mPlugin->setDisableTimeout(disable); }; @@ -495,6 +500,8 @@ class LLPluginClassMedia : public LLPluginProcessParentOwner F64 mSleepTime; + U32 mCefRemoteDebuggingPort {0}; + bool mCanUndo; bool mCanRedo; bool mCanCut; diff --git a/indra/llplugin/llpluginprocessparent.cpp b/indra/llplugin/llpluginprocessparent.cpp index c5d7ba311e7..584b8b49c29 100644 --- a/indra/llplugin/llpluginprocessparent.cpp +++ b/indra/llplugin/llpluginprocessparent.cpp @@ -578,6 +578,16 @@ void LLPluginProcessParent::idle(void) else if (!mProcess && !mProcessCreationRequested) { mProcessCreationRequested = true; +#if LL_DARWIN + // On darwin process should be created from main thread, otherwise it can + // cause a fork-lock deadlock. Relevant to both, boost and apr. + // Alternatively can remake process creation to use posix_spawn + if (!(mProcess = LLProcess::create(mProcessParams))) + { + mProcessCreationRequested = false; + errorState(); + } +#else LL::WorkQueue::ptr_t main_queue = LL::WorkQueue::getInstance("mainloop"); // *NOTE: main_queue->postTo casts this refcounted smart pointer to a weak // pointer @@ -628,6 +638,7 @@ void LLPluginProcessParent::idle(void) errorState(); } } +#endif } if (mProcess) diff --git a/indra/llplugin/llpluginprocessparent.h b/indra/llplugin/llpluginprocessparent.h index d0a75f2a682..940f31c017d 100644 --- a/indra/llplugin/llpluginprocessparent.h +++ b/indra/llplugin/llpluginprocessparent.h @@ -131,6 +131,8 @@ class LLPluginProcessParent : public LLPluginMessagePipeOwner void setUseDaemon(bool use_daemon, const std::string& rendezvous_path = std::string()) { mUseDaemon = use_daemon; mDaemonRendezvous = rendezvous_path; }; + LLProcess::id getProcessID() const { return mProcess ? mProcess->getProcessID() : 0; } + void setLaunchTimeout(F32 timeout) { mPluginLaunchTimeout = timeout; }; void setLockupTimeout(F32 timeout) { mPluginLockupTimeout = timeout; }; diff --git a/indra/llprimitive/llprimitive.cpp b/indra/llprimitive/llprimitive.cpp index 7a638dd6254..fd755e35dd9 100644 --- a/indra/llprimitive/llprimitive.cpp +++ b/indra/llprimitive/llprimitive.cpp @@ -1369,6 +1369,12 @@ bool LLPrimitive::packTEMessage(LLDataPacker &dp) const } S32 LLPrimitive::parseTEMessage(LLMessageSystem* mesgsys, char const* block_name, const S32 block_num, LLTEContents& tec) +{ + return parseTEMessage(mesgsys, block_name, block_num, tec, getNumTEs()); +} + +// static +S32 LLPrimitive::parseTEMessage(LLMessageSystem* mesgsys, char const* block_name, const S32 block_num, LLTEContents& tec, U8 face_count) { S32 retval = 0; // temp buffer for material ID processing @@ -1403,7 +1409,7 @@ S32 LLPrimitive::parseTEMessage(LLMessageSystem* mesgsys, char const* block_name tec.packed_buffer[tec.size] = 0x00; ++tec.size; - tec.face_count = llmin((U32)getNumTEs(),(U32)LLTEContents::MAX_TES); + tec.face_count = llmin((U32)face_count,(U32)LLTEContents::MAX_TES); U8 *cur_ptr = tec.packed_buffer; LL_DEBUGS("TEXTUREENTRY") << "Texture Entry with buffere sized: " << tec.size << LL_ENDL; diff --git a/indra/llprimitive/llprimitive.h b/indra/llprimitive/llprimitive.h index c3e3e19ee94..21bf52610ef 100644 --- a/indra/llprimitive/llprimitive.h +++ b/indra/llprimitive/llprimitive.h @@ -502,6 +502,9 @@ class LLPrimitive : public LLXform S32 unpackTEMessage(LLMessageSystem* mesgsys, char const* block_name, const S32 block_num); // Variable num of blocks S32 unpackTEMessage(LLDataPacker &dp); S32 parseTEMessage(LLMessageSystem* mesgsys, char const* block_name, const S32 block_num, LLTEContents& tec); + // Same as above, but usable before an instance exists to derive the face count from + // (e.g. decoding a message for an object that hasn't been created yet). + static S32 parseTEMessage(LLMessageSystem* mesgsys, char const* block_name, const S32 block_num, LLTEContents& tec, U8 face_count); S32 applyParsedTEMessage(LLTEContents& tec); #ifdef CHECK_FOR_FINITE diff --git a/indra/llrender/alfontshaping.cpp b/indra/llrender/alfontshaping.cpp index 887f64bf4c4..435b28ecf0c 100644 --- a/indra/llrender/alfontshaping.cpp +++ b/indra/llrender/alfontshaping.cpp @@ -229,6 +229,15 @@ namespace static const hb_feature_t kFixedWidthLigaturesOk[] = { { HB_TAG('k','e','r','n'), 0, 0, (unsigned)-1 }, }; + // Tabular figures for variable faces instantiated at an explicit + // weight. Proportional digits make any column of changing numbers + // (L$ balances, statistics bars, timers) jitter as the digits + // change; 'tnum' asks the font for the equal-advance digit set it + // was designed with, which is what the UI wants everywhere it + // shows a number. Faces without a 'tnum' lookup ignore it. + static const hb_feature_t kTabularFigures[] = { + { HB_TAG('t','n','u','m'), 1, 0, (unsigned)-1 }, + }; const hb_feature_t* features = nullptr; unsigned int num_features = 0; if (face->isFixedWidth()) @@ -244,6 +253,11 @@ namespace num_features = (unsigned int)(sizeof(kFixedWidthStrict) / sizeof(kFixedWidthStrict[0])); } } + else if (face->wghtAxisSet()) + { + features = kTabularFigures; + num_features = (unsigned int)(sizeof(kTabularFigures) / sizeof(kTabularFigures[0])); + } // VS-16 stripping for faces whose cmap lacks U+FE0F (notably Noto- // COLRv1, which uses 'ccmp' / pre-shape normalization in its design diff --git a/indra/llrender/llfontfreetype.cpp b/indra/llrender/llfontfreetype.cpp index 47cbcaec554..e689c0b2dbb 100644 --- a/indra/llrender/llfontfreetype.cpp +++ b/indra/llrender/llfontfreetype.cpp @@ -213,6 +213,11 @@ bool LLFontFreetype::isFixedWidth() const return mFace && mFace->isFixedWidth(); } +bool LLFontFreetype::wghtAxisSet() const +{ + return mFace && mFace->wghtAxisSet(); +} + namespace { // Walk a fallback list and return the (face, glyph_index) for the first @@ -1098,15 +1103,31 @@ void LLFontFreetype::renderGlyph(EFontGlyphType bitmap_type, U32 glyph_index, ll llassert_always_msg(FT_Err_Ok == error, message.c_str()); } - // FT_Render_Glyph's mode arg overrides the load_flags' target. For - // LIGHT we want the lighter stem-weight filter to match the LIGHT - // autohinter's no-horizontal-fit output; everything else uses NORMAL. - const FT_Render_Mode render_mode = (mHinting == EFontHinting::LIGHT) - ? FT_RENDER_MODE_LIGHT - : FT_RENDER_MODE_NORMAL; - if (FT_Render_Glyph(getFTFace()->glyph, render_mode) != 0) + // An embedded bitmap strike and sbix / CBDT colour glyphs arrive already + // rasterized, with the slot's format left as FT_GLYPH_FORMAT_BITMAP. + // Rendering those again would throw the bitmap away. FT_Load_Glyph clears + // the slot on entry, so a buffer present here always belongs to this load. + if (!getFTFace()->glyph->bitmap.buffer) { - LL_WARNS() << "Failed to render glyph for character " << llformat("U+%X", U32(wch)) << " at glyph index " << glyph_index << LL_ENDL; + // FT_Render_Glyph's mode arg overrides the load_flags' target. For + // LIGHT we want the lighter stem-weight filter to match the LIGHT + // autohinter's no-horizontal-fit output; everything else uses NORMAL. + const FT_Render_Mode render_mode = (mHinting == EFontHinting::LIGHT) + ? FT_RENDER_MODE_LIGHT + : FT_RENDER_MODE_NORMAL; + if (FT_Render_Glyph(getFTFace()->glyph, render_mode) != 0) + { + LL_WARNS() << "Failed to render glyph for character " << llformat("U+%X", U32(wch)) + << " at glyph index " << glyph_index << LL_ENDL; + // LIGHT is the only non-default target we ask for; fall back to + // NORMAL rather than leaving the slot without a bitmap. + if (render_mode != FT_RENDER_MODE_NORMAL + && FT_Render_Glyph(getFTFace()->glyph, FT_RENDER_MODE_NORMAL) != 0) + { + LL_WARNS() << "Fallback to FT_RENDER_MODE_NORMAL also failed for character " + << llformat("U+%X", U32(wch)) << LL_ENDL; + } + } } mRenderGlyphCount++; diff --git a/indra/llrender/llfontfreetype.h b/indra/llrender/llfontfreetype.h index 0e70e580e22..793689b98f4 100644 --- a/indra/llrender/llfontfreetype.h +++ b/indra/llrender/llfontfreetype.h @@ -242,6 +242,12 @@ class LLFontFreetype : public LLRefCount bool getAllowMonospaceLigatures() const { return mAllowMonospaceLigatures; } void setAllowMonospaceLigatures(bool allow) { mAllowMonospaceLigatures = allow; } + // True when the face actually applied a "wght" variation axis value, i.e. + // it is a variable font instantiated at a weight - not merely a face whose + // fonts.xml entry named one. Shaping asks such faces for tabular figures + // (see alfontshaping.cpp). + bool wghtAxisSet() const; + // True when the pen position should accumulate fractionally through a // glyph run, with each glyph's destination rect snapped at draw time. // False (HINTING_DEFAULT) keeps the legacy per-glyph round so native- diff --git a/indra/llrender/llfontgl.cpp b/indra/llrender/llfontgl.cpp index 15f29ededbc..eac99f671d9 100644 --- a/indra/llrender/llfontgl.cpp +++ b/indra/llrender/llfontgl.cpp @@ -901,7 +901,7 @@ S32 LLFontGL::render(const LLWString &wstr, S32 begin_offset, F32 x, F32 y, cons } chars_drawn++; - cur_x += fgi->mXAdvance; + cur_x += mFontFreetype->getXAdvance(fgi); cur_y += fgi->mYAdvance; llwchar next_char = wstr[i+1]; diff --git a/indra/llrender/llgl.cpp b/indra/llrender/llgl.cpp index bbabf3fad12..63d48935379 100644 --- a/indra/llrender/llgl.cpp +++ b/indra/llrender/llgl.cpp @@ -1539,11 +1539,6 @@ void LLGLManager::initWGL() { LL_WARNS("RenderInit") << "No ARB WGL PBuffer extensions" << LL_ENDL; } - - if(!mGLExtensions.contains("WGL_ARB_render_texture")) - { - LL_WARNS("RenderInit") << "No ARB WGL render texture extensions" << LL_ENDL; - } #endif } @@ -1790,7 +1785,7 @@ bool LLGLManager::initGL() } if (mVRAM != 0) { - LL_WARNS("RenderInit") << "VRAM Detected (AMDAssociations):" << mVRAM << LL_ENDL; + LL_INFOS("RenderInit") << "VRAM Detected (AMDAssociations):" << mVRAM << LL_ENDL; } } else #endif @@ -1805,7 +1800,7 @@ bool LLGLManager::initGL() if (mVRAM != 0) { - LL_WARNS("RenderInit") << "VRAM Detected (GLXMesaQueryRenderer):" << mVRAM << LL_ENDL; + LL_INFOS("RenderInit") << "VRAM Detected (GLXMesaQueryRenderer):" << mVRAM << LL_ENDL; } } } @@ -1821,7 +1816,7 @@ bool LLGLManager::initGL() if (mVRAM != 0) { - LL_WARNS("RenderInit") << "VRAM Detected (NVXGpuMemoryInfo):" << mVRAM << LL_ENDL; + LL_INFOS("RenderInit") << "VRAM Detected (NVXGpuMemoryInfo):" << mVRAM << LL_ENDL; } } diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index c423890c467..8d6d97e3fea 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -724,6 +724,7 @@ void LLImageGL::init(bool usemipmaps) mIsMask = false; mNeedsAlphaAndPickMask = true ; + mAlphaAnalysisSerial = 0; mAlphaStride = 0 ; mAlphaOffset = 0 ; @@ -2209,6 +2210,9 @@ void LLImageGL::destroyGLTexture() mTexName = 0; mGLTextureCreated = false ; } + + // Invalidate pending jobs + ++mAlphaAnalysisSerial; } //force to invalidate the gl texture, most likely a sculpty texture @@ -2346,6 +2350,9 @@ void LLImageGL::setNeedsAlphaAndPickMask(bool need_mask) { mAlphaOffset = INVALID_OFFSET ; mIsMask = false; + + // Invalidate pending jobs + ++mAlphaAnalysisSerial; } } } @@ -2493,11 +2500,16 @@ void LLImageGL::resolveDeprecatedFormat() } } -void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h) +bool LLImageGL::analyzeAlphaData( + const void* data_in, + U32 w, + U32 h, + S8 alpha_offset, + S8 alpha_stride) { - if(!data_in || sSkipAnalyzeAlpha || !mNeedsAlphaAndPickMask) + if (!data_in || alpha_stride < 1) { - return ; + return false; } LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; @@ -2506,7 +2518,7 @@ void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h) U32 alphatotal = 0; U32 sample[16]; - memset(sample, 0, sizeof(U32)*16); + memset(sample, 0, sizeof(U32) * 16); // generate histogram of quantized alpha. // also add-in the histogram of a 2x2 box-sampled version. The idea is @@ -2517,13 +2529,13 @@ void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h) { // Walk only complete 2x2 quads. Odd dimensions previously hit // asserts in debug and read OOB on the trailing row/column in - // release (`current[w * mAlphaStride]` indexes one row down, + // release (`current[w * alpha_stride]` indexes one row down, // which doesn't exist for the last odd row). Trailing odd row // and column are dropped from the histogram; the remaining // sample is still representative for the mask classifier. const U32 paired_w = w & ~1u; const U32 paired_h = h & ~1u; - const GLubyte* rowstart = ((const GLubyte*) data_in) + mAlphaOffset; + const GLubyte* rowstart = ((const GLubyte*)data_in) + alpha_offset; for (U32 y = 0; y < paired_h; y += 2) { const GLubyte* current = rowstart; @@ -2531,38 +2543,39 @@ void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h) { const U32 s1 = current[0]; alphatotal += s1; - const U32 s2 = current[w * mAlphaStride]; + const U32 s2 = current[w * alpha_stride]; alphatotal += s2; - current += mAlphaStride; + current += alpha_stride; const U32 s3 = current[0]; alphatotal += s3; - const U32 s4 = current[w * mAlphaStride]; + const U32 s4 = current[w * alpha_stride]; alphatotal += s4; - current += mAlphaStride; + current += alpha_stride; - ++sample[s1/16]; - ++sample[s2/16]; - ++sample[s3/16]; - ++sample[s4/16]; + ++sample[s1 / 16]; + ++sample[s2 / 16]; + ++sample[s3 / 16]; + ++sample[s4 / 16]; - const U32 asum = (s1+s2+s3+s4); + const U32 asum = (s1 + s2 + s3 + s4); alphatotal += asum; - sample[asum/(16*4)] += 4; + sample[asum / (16 * 4)] += 4; } - rowstart += 2 * w * mAlphaStride; + rowstart += 2 * w * alpha_stride; } - length *= 2; // we sampled everything twice, essentially + // Two histogram entries per source texel over the paired region. + length = 2 * paired_w * paired_h; } else { - const GLubyte* current = ((const GLubyte*) data_in) + mAlphaOffset; + const unsigned char* current = ((const unsigned char*)data_in) + alpha_offset; for (U32 i = 0; i < length; i++) { const U32 s1 = *current; alphatotal += s1; - ++sample[s1/16]; - current += mAlphaStride; + ++sample[s1 / 16]; + current += alpha_stride; } } @@ -2589,15 +2602,103 @@ void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h) upperhalftotal += sample[i]; } - if (midrangetotal > length/48 || // lots of midrange, or - (lowerhalftotal == length && alphatotal != 0) || // all close to transparent but not all totally transparent, or - (upperhalftotal == length && alphatotal != 255*length)) // all close to opaque but not all totally opaque + if (midrangetotal > length / 48 || + (lowerhalftotal == length && alphatotal != 0) || + (upperhalftotal == length && alphatotal != 255 * length)) { - mIsMask = false; // not suitable for masking + return false; // not suitable for masking } else { - mIsMask = true; + return true; // is a mask + } +} + +void LLImageGL::analyzeAlpha(const void* data_in, U32 w, U32 h) +{ + // if mNeedsAlphaAndPickMask is true, then offset and stride are supposed to be valid. + if (!data_in || sSkipAnalyzeAlpha || !mNeedsAlphaAndPickMask) + return; + + // Already on a worker thread or a small image - analyze immediately + if (!on_main_thread() || (w < 64 && h < 64)) + { + LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; + // Should have been incremented by destroyGLTexture, + // but increment either way, for extra safety. + ++mAlphaAnalysisSerial; + + mIsMask = analyzeAlphaData(data_in, w, h, mAlphaOffset, mAlphaStride); + return; + } + + // On main thread - defer to worker thread + + // Capture context + const S8 alpha_offset = mAlphaOffset; + const S8 alpha_stride = mAlphaStride; + const U32 request_serial = ++mAlphaAnalysisSerial; + + // Copy data for worker thread + const size_t data_size = size_t(w) * size_t(h) * size_t(alpha_stride); + U8* data_copy = new (std::nothrow) U8[data_size]; + + if (!data_copy) + { + LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; + mIsMask = analyzeAlphaData(data_in, w, h, alpha_offset, alpha_stride); + return; + } + memcpy(data_copy, static_cast(data_in), data_size); + + // Viewer can rapidly switch between lods, which would invalidate + // previous analysis results. + // Use a serial number to filter out obsolete analysis results. + + ref(); // Keep texture alive + + auto mainq = mMainQueue.lock(); + + // Get the GL thread queue + auto workerq = LLImageGLThread::sEnabledTextures ? + LL::WorkQueue::getInstance("LLImageGL") : // Use the image processing queue if available + LL::WorkQueue::getInstance("General"); // Fallback to general + + bool posted_job = false; + if (mainq && workerq) + { + posted_job = mainq->postTo( + workerq, + // Worker thread: analyze alpha + [data_copy, w, h, alpha_offset, alpha_stride]() -> bool + { + LL_PROFILE_ZONE_NAMED("Deffered alpha mask analysis"); + bool is_mask = LLImageGL::analyzeAlphaData(data_copy, w, h, alpha_offset, alpha_stride); + delete[] data_copy; + return is_mask; + }, + // Main thread: apply result + [this, request_serial](bool is_mask) + { + // Only apply if no newer analysis has been requested + if (mAlphaAnalysisSerial == request_serial) + { + mIsMask = is_mask; + } + unref(); + } + ); + + // Conservative default until analysis completes + mIsMask = false; + } + if (!posted_job) + { + LL_PROFILE_ZONE_SCOPED_CATEGORY_TEXTURE; + // Queues not available - fall back to synchronous analysis + delete[] data_copy; + mIsMask = analyzeAlphaData(data_in, w, h, mAlphaOffset, mAlphaStride); + unref(); } } diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index 6f944cf9c68..ae75b313990 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -150,6 +150,7 @@ class LLImageGL : public LLThreadSafeRefCount protected: virtual ~LLImageGL(); + static bool analyzeAlphaData(const void* data_in, U32 w, U32 h, S8 alpha_offset, S8 alpha_stride); void analyzeAlpha(const void* data_in, U32 w, U32 h); void calcAlphaChannelOffsetAndStride(); @@ -351,6 +352,7 @@ class LLImageGL : public LLThreadSafeRefCount bool mIsMask; bool mNeedsAlphaAndPickMask; + LLAtomicU32 mAlphaAnalysisSerial; // for request tracking. S8 mAlphaStride ; S8 mAlphaOffset ; diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp index d9e365413df..2b40c594806 100644 --- a/indra/llrender/llshadermgr.cpp +++ b/indra/llrender/llshadermgr.cpp @@ -1270,7 +1270,14 @@ void LLShaderMgr::persistShaderCacheMetadata() return; } - LL_INFOS("ShaderMgr") << "Persisting shader cache metadata to disk" << LL_ENDL; + if (mShaderCacheDir.empty() || !LLFile::isdir(mShaderCacheDir)) + { + LL_WARNS("ShaderMgr") << "Invalid shader cache directory: " << mShaderCacheDir << LL_ENDL; + return; + } + + size_t total_entries = mShaderBinaryCache.size(); + LL_INFOS("ShaderMgr") << "Persisting shader " << (S32)total_entries << " cache metadata entries to disk" << LL_ENDL; LLSD out; // Settings and shader cache get saved at different time, thus making @@ -1281,6 +1288,8 @@ void LLShaderMgr::persistShaderCacheMetadata() out["shaders"] = LLSD::emptyMap(); LLSD &shaders = out["shaders"]; + size_t removed = 0; + static const F32 LRU_TIME = (60.f * 60.f) * 24.f * 7.f; // 14 days const F32 current_time = (F32)LLTimer::getTotalSeconds(); for (auto it = mShaderBinaryCache.begin(); it != mShaderBinaryCache.end();) @@ -1289,8 +1298,9 @@ void LLShaderMgr::persistShaderCacheMetadata() if ((shader_metadata.mLastUsedTime + LRU_TIME) < current_time) { std::string shader_path = gDirUtilp->add(mShaderCacheDir, it->first.asString() + ".shaderbin"); - LLFile::remove(shader_path); + LLFile::remove(shader_path, ENOENT); it = mShaderBinaryCache.erase(it); + removed++; } else { @@ -1304,14 +1314,32 @@ void LLShaderMgr::persistShaderCacheMetadata() } std::string meta_out_path = gDirUtilp->add(mShaderCacheDir, "shaderdata.llsd"); + if (shaders.size() == 0) + { + LL_WARNS("ShaderMgr") << "No shader cache entries to persist, removing cache metadata file" << LL_ENDL; + LLFile::remove(meta_out_path); + return; + } + llofstream outstream(meta_out_path, std::ios_base::out | std::ios_base::binary); if (!outstream.is_open()) { LL_WARNS("ShaderMgr") << "Failed to open file. Unable to save shader cache to: " << mShaderCacheDir << LL_ENDL; return; } + LLSDSerialize::toBinary(out, outstream); + if (outstream.fail()) + { + LL_WARNS("ShaderMgr") << "Failed to serialize shader cache metadata" << LL_ENDL; + outstream.close(); + LLFile::remove(meta_out_path); // Clean up partial write + return; + } outstream.close(); + + LL_INFOS("ShaderMgr") << "Persisted " << (S32)shaders.size() + << " entries. Removed " << (S32)removed << " entries." << LL_ENDL; } bool LLShaderMgr::loadCachedProgramBinary(LLGLSLShader* shader) @@ -1333,34 +1361,56 @@ bool LLShaderMgr::loadCachedProgramBinary(LLGLSLShader* shader) { std::string in_path = gDirUtilp->add(mShaderCacheDir, shader->mShaderHash.asString() + ".shaderbin"); auto& shader_info = binary_iter->second; - if (shader_info.mBinaryLength > 0) + + try { - std::vector in_data; - in_data.resize(shader_info.mBinaryLength); - std::error_code ec; - LLFile filep = LLFile(in_path, LLFile::in | LLFile::binary, ec); - if (!ec && (bool)filep) + constexpr GLsizei MAX_SHADER_BINARY_SIZE = 1024 * 1024; // 1 MB, normally around 10KB + if (shader_info.mBinaryLength > 0 && shader_info.mBinaryLength <= MAX_SHADER_BINARY_SIZE) { - size_t result = filep.read(in_data.data(), in_data.size(), ec); - filep.close(); + std::vector in_data; + in_data.resize(shader_info.mBinaryLength); - if (result == in_data.size()) + std::error_code ec; + LLFile filep = LLFile(in_path, LLFile::in | LLFile::binary, ec); + if (!ec && (bool)filep) { - GLenum error = glGetError(); // Clear current error - glProgramBinary(shader->mProgramObject, shader_info.mBinaryFormat, in_data.data(), shader_info.mBinaryLength); + size_t result = filep.read(in_data.data(), in_data.size(), ec); + filep.close(); - error = glGetError(); - GLint success = GL_TRUE; - glGetProgramiv(shader->mProgramObject, GL_LINK_STATUS, &success); - if (error == GL_NO_ERROR && success == GL_TRUE) + if (result == in_data.size()) + { + GLenum error = glGetError(); // Clear current error + glProgramBinary(shader->mProgramObject, shader_info.mBinaryFormat, in_data.data(), shader_info.mBinaryLength); + + error = glGetError(); + GLint success = GL_TRUE; + glGetProgramiv(shader->mProgramObject, GL_LINK_STATUS, &success); + if (error == GL_NO_ERROR && success == GL_TRUE) + { + binary_iter->second.mLastUsedTime = (F32)LLTimer::getTotalSeconds(); + LL_INFOS() << "Loaded cached binary for shader: " << shader->mName << LL_ENDL; + return true; + } + } + else { - binary_iter->second.mLastUsedTime = (F32)LLTimer::getTotalSeconds(); - LL_INFOS() << "Loaded cached binary for shader: " << shader->mName << LL_ENDL; - return true; + LL_WARNS("ShaderMgr") << "Incomplete read of shader binary. Expected: " + << in_data.size() << ", read: " << result << LL_ENDL; } } } } + catch (const std::bad_alloc&) + { + LL_WARNS("ShaderMgr") << "Failed to allocate memory for shader binary (" + << shader_info.mBinaryLength << " bytes) for: " + << shader->mName << LL_ENDL; + } + catch (const std::exception& err) + { + LL_WARNS("ShaderMgr") << "Caught exception " << err.what() << " while loading shader binary for: " << shader->mName << LL_ENDL; + } + //an error occured, normally we would print log but in this case it means the shader needs recompiling. LL_INFOS() << "Failed to load cached binary for shader: " << shader->mName << " falling back to compilation" << LL_ENDL; LLFile::remove(in_path); diff --git a/indra/llrender/lluiimage.cpp b/indra/llrender/lluiimage.cpp index d4e9ef874eb..e8eb56fc45f 100644 --- a/indra/llrender/lluiimage.cpp +++ b/indra/llrender/lluiimage.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -31,6 +31,12 @@ // Project includes #include "lluiimage.h" +#include + +// Static member initialization +std::vector > LLUIImage::sImageList; +size_t LLUIImage::sCleanupIndex = 0; +bool LLUIImage::sEnableDisplayListsCollection = true; LLUIImage::LLUIImage(const std::string& name, LLPointer image) : mName(name), @@ -63,6 +69,12 @@ LLUIImage::LLUIImage(std::string&& name, LLPointer image) : LLUIImage::~LLUIImage() { delete mImageLoaded; + + // Unregister from global cleanup list (sanity check) + // But it's supposed to be cleared already, else we wouldn't + // be destructing this object. + unregisterFromGlobalCleanup(); + mDisplayLists.clear(); } S32 LLUIImage::getWidth() const @@ -77,6 +89,199 @@ S32 LLUIImage::getHeight() const return ll_round((F32)mImage->getHeight(0) * mClipRegion.getHeight()); } +buffer_data_list_t* LLUIImage::findDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const +{ + LLImageGL* gl_image = mImage->getGLTexture(); + if (!gl_image) + { + return nullptr; + } + + LLVector3 ui_translation = gGL.getUITranslation(); + LLVector3 ui_scale = gGL.getUIScale(); + LLGLuint tex_name = gl_image->getTexName(); + + auto key = PackedKey::create(x, y, width, height, color, solid_color, ui_translation, ui_scale, tex_name); + + auto it = mDisplayLists.find(key); + if (it != mDisplayLists.end()) + { + it->second.last_used = std::chrono::steady_clock::now(); + return &it->second.list; + } + return nullptr; +} + +buffer_data_list_t* LLUIImage::genDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const +{ + LL_PROFILE_ZONE_SCOPED; + + LLImageGL* gl_image = mImage->getGLTexture(); + if (!gl_image) + { + // Don't cache when texture hasn't been created yet + // draw just aborts in this case, so don't draw either. + return nullptr; + } + + LLVector3 ui_translation = gGL.getUITranslation(); + LLVector3 ui_scale = gGL.getUIScale(); + + // Get the GL texture name - this uniquely identifies the current texture state + // including discard level changes, texture recreation, etc. + LLGLuint tex_name = gl_image->getTexName(); + + auto key = PackedKey::create(x, y, width, height, color, solid_color, ui_translation, ui_scale, tex_name); + + CachedDisplayList cached; + cached.last_used = std::chrono::steady_clock::now(); + + // Generate the display list by capturing the draw commands + gGL.beginList(&cached.list); + + gl_draw_scaled_image_with_border( + x, y, + width, height, + mImage, + color, + solid_color, + mClipRegion, + mScaleRegion, + mScaleStyle == SCALE_INNER); + + gGL.endList(); + + // Insert into cache + // emplace, since we only call genDisplayList if key was not found. + auto result = mDisplayLists.emplace(key, std::move(cached)); + + // Register for cleanup on first buffer creation + if (mDisplayLists.size() == 1) + { + sImageList.push_back(const_cast(this)); + } + + return &result.first->second.list; +} + +void LLUIImage::invalidateDisplayLists() +{ + mDisplayLists.clear(); + + unregisterFromGlobalCleanup(); +} + +void LLUIImage::cleanupDisplayLists() +{ + if (mDisplayLists.empty()) + { + llassert(false); //it shouldn't be in this list + unregisterFromGlobalCleanup(); + // marks current position for a recheck. Increments after cleanupDisplayLists. + if (sCleanupIndex > 0) + { + sCleanupIndex--; + } + else if (sImageList.empty()) + { + sCleanupIndex = 0; + } + else + { + sCleanupIndex = sImageList.size() - 1; + } + return; + } + + // Time threshold for cleaning up unused display lists (global cleanup) + constexpr std::chrono::seconds DISPLAY_LIST_TIMEOUT{ 2 }; + auto now = std::chrono::steady_clock::now(); + + // Remove display lists that haven't been used recently + for (auto it = mDisplayLists.begin(); it != mDisplayLists.end(); ) + { + if (now - it->second.last_used > DISPLAY_LIST_TIMEOUT) + { + it = mDisplayLists.erase(it); + } + else + { + ++it; + } + } + + // Unregister from cleanup list if all display lists were removed + if (mDisplayLists.empty()) + { + unregisterFromGlobalCleanup(); + // marks current position for a recheck. Increments after cleanupDisplayLists. + if (sCleanupIndex > 0) + { + sCleanupIndex--; + } + else if (sImageList.empty()) + { + sCleanupIndex = 0; + } + else + { + sCleanupIndex = sImageList.size() - 1; + } + } +} + +void LLUIImage::unregisterFromGlobalCleanup() +{ + auto list_it = std::find(sImageList.begin(), sImageList.end(), this); + if (list_it != sImageList.end()) + { + // Swap with last element and pop (O(1) removal) + *list_it = sImageList.back(); + sImageList.pop_back(); + } +} + +// static +void LLUIImage::updateClass() +{ + if (sImageList.empty()) + { + return; + } + + // Clean up a batch of images each frame to amortize the cost + // ensuring all images are checked regularly + // Note: buffers often get obsolete in batches, perhaps + // increase rate of cleanup after a buffer was removed? + // and decrease rate if no buffer were removed and creates + // for a while? + constexpr size_t BATCH_SIZE = 8; + size_t images_to_process = std::min(BATCH_SIZE, sImageList.size()); + + for (size_t i = 0; i < images_to_process; ++i) + { + if (sCleanupIndex >= sImageList.size()) + { + sCleanupIndex = 0; + } + + sImageList[sCleanupIndex]->cleanupDisplayLists(); + ++sCleanupIndex; + } +} + +void LLUIImage::cleanupClass() +{ + std::vector > list_copy(sImageList); + sImageList.clear(); + for (LLUIImage* image : list_copy) + { + // invalidateDisplayLists will attempt to clear sImageList + image->invalidateDisplayLists(); + } + sCleanupIndex = 0; +} + void LLUIImage::draw3D(const LLVector3& origin_agent, const LLVector3& x_axis, const LLVector3& y_axis, const LLRect& rect, const LLColor4& color) { @@ -91,7 +296,7 @@ void LLUIImage::draw3D(const LLVector3& origin_agent, const LLVector3& x_axis, c } else { - border_scale = (F32)rect.getWidth() / border_width; + border_scale = (F32)rect.getWidth() / border_width; } } @@ -138,6 +343,8 @@ void LLUIImage::onImageLoaded() { (*mImageLoaded)(); } + + invalidateDisplayLists(); } namespace LLInitParam diff --git a/indra/llrender/lluiimage.h b/indra/llrender/lluiimage.h index 2806e10015c..d41d74d4e0c 100644 --- a/indra/llrender/lluiimage.h +++ b/indra/llrender/lluiimage.h @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -34,10 +34,15 @@ #include "llinitparam.h" #include "lltexture.h" #include "llrender2dutils.h" +#include "llvertexbuffer.h" #include +#include +#include #include +#include +#include extern const LLColor4 UI_VERTEX_COLOR; @@ -59,21 +64,28 @@ class LLUIImage : public LLRefCount LL_FORCE_INLINE void setClipRegion(const LLRectf& region) { mClipRegion = region; + // This happens when image becomes loaded + invalidateDisplayLists(); } LL_FORCE_INLINE void setScaleRegion(const LLRectf& region) { mScaleRegion = region; + // This happens when image becomes loaded + invalidateDisplayLists(); } LL_FORCE_INLINE void setScaleStyle(EScaleStyle style) { mScaleStyle = style; + // This happens when image becomes loaded + invalidateDisplayLists(); } LL_FORCE_INLINE LLPointer getImage() { return mImage; } LL_FORCE_INLINE const LLPointer& getImage() const { return mImage; } + LL_FORCE_INLINE void draw(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const; LL_FORCE_INLINE void draw(S32 x, S32 y, S32 width, S32 height, const LLColor4& color = UI_VERTEX_COLOR) const; LL_FORCE_INLINE void draw(S32 x, S32 y, const LLColor4& color = UI_VERTEX_COLOR) const; LL_FORCE_INLINE void draw(const LLRect& rect, const LLColor4& color = UI_VERTEX_COLOR) const { draw(rect.mLeft, rect.mBottom, rect.getWidth(), rect.getHeight(), color); } @@ -86,6 +98,10 @@ class LLUIImage : public LLRefCount LL_FORCE_INLINE void drawBorder(const LLRect& rect, const LLColor4& color, S32 border_width) const { drawBorder(rect.mLeft, rect.mBottom, rect.getWidth(), rect.getHeight(), color, border_width); } LL_FORCE_INLINE void drawBorder(S32 x, S32 y, const LLColor4& color, S32 border_width) const { drawBorder(x, y, getWidth(), getHeight(), color, border_width); } + // Note: draw3D is not cached with display lists because it uses world-space rendering + // with dynamic transforms (gl_segmented_rect_3d_tex). These calls are infrequent and + // highly dynamic, making caching ineffective. The 2D UI methods benefit from caching + // because they're called many times per frame with the same dimensions. void draw3D(const LLVector3& origin_agent, const LLVector3& x_axis, const LLVector3& y_axis, const LLRect& rect, const LLColor4& color); LL_FORCE_INLINE const std::string& getName() const { return mName; } @@ -101,7 +117,107 @@ class LLUIImage : public LLRefCount void onImageLoaded(); + // Global cleanup of unused display lists across all LLUIImage instances + // Should be called periodically (e.g., once per frame or when memory pressure is detected) + static void updateClass(); + static void cleanupClass(); + + static void enableDisplayListsCollection(bool enable) { sEnableDisplayListsCollection = enable; } + protected: + // Packed key for identifying unique display list configurations + struct PackedKey + { + uint64_t position; // x and y coordinates (32 bits each) + uint64_t color_flags; // RGBA color (8 bits each) + solid_color flag (1 bit) + uint64_t dimensions; // width and height (32 bits each) + uint64_t translate; // UI translation (32 bits each for X and Y) + uint64_t scale; // UI scale (32 bits each for X and Y) + uint64_t tex_name; // OpenGL texture name (32 bits) + padding + + constexpr bool operator==(const PackedKey& other) const + { + return position == other.position && + color_flags == other.color_flags && + dimensions == other.dimensions && + translate == other.translate && + scale == other.scale && + tex_name == other.tex_name; + } + + struct Hash + { + std::size_t operator()(const PackedKey& key) const + { + return static_cast(key.position ^ key.color_flags ^ + key.dimensions ^ key.translate ^ + key.scale ^ key.tex_name); + } + }; + + // Static factory function to create PackedKey from parameters + static constexpr PackedKey create(S32 x, S32 y, S32 width, S32 height, + const LLColor4& color, bool solid_color, + const LLVector3& translate, const LLVector3& scale, + LLGLuint texture_name) + { + auto float_to_u8 = [](F32 f) -> uint8_t { + return static_cast(llclamp(f * 255.0f, 0.0f, 255.0f)); + }; + + auto float_to_bits = [](F32 f) -> uint32_t { + return std::bit_cast(f); + }; + + uint8_t r = float_to_u8(color.mV[VRED]); + uint8_t g = float_to_u8(color.mV[VGREEN]); + uint8_t b = float_to_u8(color.mV[VBLUE]); + uint8_t a = float_to_u8(color.mV[VALPHA]); + + uint64_t pos = (static_cast(static_cast(x)) << 32) | + static_cast(static_cast(y)); + + uint64_t col = (static_cast(r) << 56) | + (static_cast(g) << 48) | + (static_cast(b) << 40) | + (static_cast(a) << 32) | + (solid_color ? 1ULL : 0ULL); + + uint64_t dim = (static_cast(static_cast(width)) << 32) | + static_cast(static_cast(height)); + + uint64_t trns = (static_cast(float_to_bits(translate.mV[VX])) << 32) | + static_cast(float_to_bits(translate.mV[VY])); + + uint64_t scl = (static_cast(float_to_bits(scale.mV[VX])) << 32) | + static_cast(float_to_bits(scale.mV[VY])); + + // Store full 32-bit texture name in lower 32 bits (upper 32 bits unused/zero) + uint64_t tex = static_cast(texture_name); + + return PackedKey{ pos, col, dim, trns, scl, tex }; + } + }; + + // Cached display list for a specific configuration + struct CachedDisplayList + { + buffer_data_list_t list; + std::chrono::steady_clock::time_point last_used; + }; + + // Get a display list for the given configuration + buffer_data_list_t* findDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const; + // Generate a new display list for the given configuration, draws immediately. + buffer_data_list_t* genDisplayList(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const; + + // Invalidate all cached display lists (called when image properties change) + void invalidateDisplayLists(); + + // Clean up old display lists for this image (called by updateClass) + void cleanupDisplayLists(); + void unregisterFromGlobalCleanup(); + image_loaded_signal_t* mImageLoaded; std::string mName; @@ -111,6 +227,14 @@ class LLUIImage : public LLRefCount EScaleStyle mScaleStyle; mutable S32 mCachedW; mutable S32 mCachedH; + + // Display list cache - now using PackedKey directly + mutable std::unordered_map mDisplayLists; + + // Track all LLUIImage cache instances for global cleanup + static std::vector > sImageList; + static size_t sCleanupIndex; // Round-robin cleanup position + static bool sEnableDisplayListsCollection; }; #include "lluiimage.inl" diff --git a/indra/llrender/lluiimage.inl b/indra/llrender/lluiimage.inl index dff1fcdfccb..cf155909dd0 100644 --- a/indra/llrender/lluiimage.inl +++ b/indra/llrender/lluiimage.inl @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -29,30 +29,70 @@ void LLUIImage::draw(S32 x, S32 y, const LLColor4& color) const draw(x, y, getWidth(), getHeight(), color); } +void LLUIImage::draw(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, bool solid_color) const +{ + if (sEnableDisplayListsCollection) + { + // Get display list for this configuration + buffer_data_list_t* display_list = findDisplayList(x, y, width, height, color, solid_color); + + if (display_list && !display_list->empty()) + { + // Deliberately empty pending verts. + // They aren't related to the image, so don't register them under draw zone + gGL.flush(); + LL_PROFILE_ZONE_SCOPED; + //gGL.pushUIMatrix(); + + if (solid_color) + { + gSolidColorProgram.bind(); + } + + gGL.color4fv(color.mV); // for the shader + + // Replay the cached display list + for (LLVertexBufferData& buffer : *display_list) + { + buffer.draw(); + } + + if (solid_color) + { + gUIProgram.bind(); + } + //gGL.popUIMatrix(); + } + else + { + // Create, draw and capture display list. + // Basically a wrapper around gl_draw_scaled_image_with_border + // that records the output into a list. + genDisplayList(x, y, width, height, color, solid_color); + } + } + else + { + gl_draw_scaled_image_with_border( + x, y, + width, height, + mImage, + color, + solid_color, + mClipRegion, + mScaleRegion, + mScaleStyle == SCALE_INNER); + } +} + void LLUIImage::draw(S32 x, S32 y, S32 width, S32 height, const LLColor4& color) const { - gl_draw_scaled_image_with_border( - x, y, - width, height, - mImage, - color, - false, - mClipRegion, - mScaleRegion, - mScaleStyle == SCALE_INNER); + draw(x, y, width, height, color, false); } void LLUIImage::drawSolid(S32 x, S32 y, S32 width, S32 height, const LLColor4& color) const { - gl_draw_scaled_image_with_border( - x, y, - width, height, - mImage, - color, - true, - mClipRegion, - mScaleRegion, - mScaleStyle == SCALE_INNER); + draw(x, y, width, height, color, true); } void LLUIImage::drawBorder(S32 x, S32 y, S32 width, S32 height, const LLColor4& color, S32 border_width) const diff --git a/indra/llrender/llvertexbuffer.cpp b/indra/llrender/llvertexbuffer.cpp index 9f6978ad4bf..7538b6c877a 100644 --- a/indra/llrender/llvertexbuffer.cpp +++ b/indra/llrender/llvertexbuffer.cpp @@ -298,18 +298,21 @@ static void delete_buffers(S32 count, GLuint* buffers) LL_PROFILE_ZONE_SCOPED_CATEGORY_VERTEX; // wait a few frames before actually deleting the buffers to avoid // synchronization issues with the GPU - static std::vector sFreeList[4]; + constexpr U32 BUCKET_COUNT = 4; + static std::vector sFreeList[BUCKET_COUNT]; if (gGLManager.mInited) { - U32 idx = LLImageGL::sFrameCount % 4; + // Move current frame to free list + U32 idx = LLImageGL::sFrameCount % BUCKET_COUNT; for (S32 i = 0; i < count; ++i) { sFreeList[idx].push_back(buffers[i]); } - idx = (LLImageGL::sFrameCount + 3) % 4; + // Clear frame -3 (equals +1), this idx will be written over on the next call + idx = (LLImageGL::sFrameCount + 1) % BUCKET_COUNT; if (!sFreeList[idx].empty()) { @@ -951,12 +954,14 @@ void LLVertexBuffer::initClass(LLWindow* window) { llassert(sVBOPool == nullptr); +#if LL_DARWIN || LL_ARM64 if (gGLManager.mIsApple) { LL_INFOS() << "VBO Pooling Disabled" << LL_ENDL; sVBOPool = new LLAppleVBOPool(); } else +#endif { LL_INFOS() << "VBO Pooling Enabled" << LL_ENDL; sVBOPool = new LLDefaultVBOPool(); @@ -1294,7 +1299,12 @@ U8* LLVertexBuffer::mapVertexBuffer(LLVertexBuffer::AttributeType type, U32 inde count = mNumVerts - index; } +#if LL_DARWIN || LL_ARM64 + // Region tracking not needed on apple silicon - it recreates entire buffer + // While mIsApple can be encountered under windows, this is a + // macOS OpenGL behavior workaround. LL_ARM64 check might be not needed if (!gGLManager.mIsApple) +#endif { U32 start = mOffsets[type] + sTypeSize[type] * index; U32 end = start + sTypeSize[type] * count-1; @@ -1331,7 +1341,9 @@ U8* LLVertexBuffer::mapIndexBuffer(U32 index, S32 count) count = mNumIndices-index; } +#if LL_DARWIN || LL_ARM64 if (!gGLManager.mIsApple) +#endif { U32 start = sizeof(U16) * index; U32 end = start + sizeof(U16) * count-1; @@ -1366,10 +1378,13 @@ U8* LLVertexBuffer::mapIndexBuffer(U32 index, S32 count) // dst -- mMappedData or mMappedIndexData void LLVertexBuffer::flush_vbo(GLenum target, U32 start, U32 end, void* data, U8* dst) { - // Callers compute end = start + size - 1; when size == 0 this underflows to (start - 1). - // Without this guard the non-Apple loop below iterates ~65k times against an underflowed end. + // Callers compute end = start + size - 1; when size == 0 this underflows to + // (start - 1), which then passes the "end != 0" test below and issues a + // glBufferSubData with a nonsense size. if (end + 1 == start) return; + +#if LL_DARWIN || LL_ARM64 if (gGLManager.mIsApple) { // on OS X, flush_vbo doesn't actually write to the GL buffer, so be sure to call @@ -1381,6 +1396,7 @@ void LLVertexBuffer::flush_vbo(GLenum target, U32 start, U32 end, void* data, U8 memcpy(dst+start, data, end-start+1); } else +#endif { llassert(target == GL_ARRAY_BUFFER ? sGLRenderBuffer == mGLBuffer : sGLRenderIndices == mGLIndices); @@ -1430,6 +1446,7 @@ void LLVertexBuffer::_unmapBuffer() } }; +#if LL_DARWIN || LL_ARM64 if (gGLManager.mIsApple) { STOP_GLERROR; @@ -1472,6 +1489,7 @@ void LLVertexBuffer::_unmapBuffer() STOP_GLERROR; } else +#endif // LL_DARWIN || LL_ARM64 { if (!mMappedVertexRegions.empty()) { diff --git a/indra/llui/CMakeLists.txt b/indra/llui/CMakeLists.txt index ec918aa66c8..36e077393e2 100644 --- a/indra/llui/CMakeLists.txt +++ b/indra/llui/CMakeLists.txt @@ -35,6 +35,7 @@ target_sources(llui llfloaterreglistener.cpp llflyoutbutton.cpp llfocusmgr.cpp + llgestureautocompletehelper.cpp llfolderview.cpp llfolderviewitem.cpp llfolderviewmodel.cpp @@ -145,6 +146,7 @@ target_sources(llui llfloaterreglistener.h llflyoutbutton.h llfocusmgr.h + llgestureautocompletehelper.h llfolderview.h llfolderviewitem.h llfolderviewmodel.h diff --git a/indra/llui/llflyoutbutton.cpp b/indra/llui/llflyoutbutton.cpp index 40d08d689a9..dfcfff3b0ca 100644 --- a/indra/llui/llflyoutbutton.cpp +++ b/indra/llui/llflyoutbutton.cpp @@ -53,6 +53,10 @@ LLFlyoutButton::LLFlyoutButton(const Params& p) // [/SL:KB] bp.click_callback.function(boost::bind(&LLFlyoutButton::onActionButtonClick, this, _2)); bp.follows.flags(FOLLOWS_ALL); + if (p.font.isProvided()) + { + bp.font(p.font); + } mActionButton = LLUICtrlFactory::create(bp); addChild(mActionButton); @@ -101,4 +105,3 @@ void LLFlyoutButton::setToggleState(bool state) mToggleState = state; } - diff --git a/indra/llui/llfolderview.cpp b/indra/llui/llfolderview.cpp index af9f771e0ba..f97f8508998 100644 --- a/indra/llui/llfolderview.cpp +++ b/indra/llui/llfolderview.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -651,9 +651,11 @@ bool LLFolderView::startDrag() void LLFolderView::commitRename( const LLSD& data ) { + // Intentionally doesn't check mRenamer->getVisible(), + // since this can be called from 'focus lost' event. + // Ex: clicking inworld should commit the rename. finishRenamingItem(); arrange( NULL, NULL ); - } void LLFolderView::draw() @@ -727,13 +729,17 @@ void LLFolderView::draw() void LLFolderView::finishRenamingItem( void ) { - if(!mRenamer) + if (!mRenamer) { return; } if( mRenameItem ) { mRenameItem->rename( mRenamer->getText() ); + // Clear text to avoid duplicate renames. + // Ex: 'return' key will trigger rename from handleKeyHere + // and might trigger commitRename from focus lost event. + mRenamer->setText(LLStringUtil::null); } closeRenamer(); @@ -1423,6 +1429,7 @@ bool LLFolderView::search(LLFolderViewItem* first_item, const std::string &searc } } + // Note: for inventory getSearchableName should already be 'upper' case. std::string current_item_label(search_item->getViewModelItem()->getSearchableName()); LLStringUtil::toUpper(current_item_label); auto search_string_length = llmin(upper_case_string.size(), current_item_label.size()); diff --git a/indra/llui/llfolderviewitem.cpp b/indra/llui/llfolderviewitem.cpp index 34e001ac193..ff6cf2102da 100644 --- a/indra/llui/llfolderviewitem.cpp +++ b/indra/llui/llfolderviewitem.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code -* Copyright (C) 2010, Linden Research, Inc. +* Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -234,7 +234,7 @@ LLFolderViewItem::LLFolderViewItem(const LLFolderViewItem::Params& p) mIndentation(0), mControlLabelRotation(0.f), mDragAndDropTarget(false), - mLabel(utf8str_to_wstring(p.name)), + mLabel(utf8str_to_wstring(p.name)), // will be immediately reset in postBuild() mRoot(p.root), mViewModelItem(p.listener), mIsMouseOverTitle(false), @@ -277,21 +277,19 @@ bool LLFolderViewItem::postBuild() llassert(vmi); // not supposed to happen, if happens, find out why and fix if (vmi) { - // getDisplayName() is expensive (due to internal getLabelSuffix() and name building) - // it also sets search strings so it requires a filter reset + // First getDisplayName() is expensive due to internal + // lazy getLabelSuffix(), it is however needed as it sets + // search string, which can later determine visibility. + // Refreshing a search string also requires a filter reset. mLabel = utf8str_to_wstring(vmi->getDisplayName()); mIsFavorite = vmi->isFavorite() && !vmi->isItemInTrash(); - setToolTip(vmi->getName()); // Dirty the filter flag of the model from the view (CHUI-849) vmi->dirtyFilter(); } - // Don't do full refresh on constructor if it is possible to avoid + // Don't do full refresh on constructor if it is possible to avoid, // it significantly slows down bulk view creation. - // Todo: Ideally we need to move getDisplayName() out of constructor as well. - // Like: make a logic that will let filter update search string, - // while LLFolderViewItem::arrange() updates visual part mSuffixNeedsRefresh = true; mLabelWidthDirty = true; return true; @@ -399,7 +397,6 @@ void LLFolderViewItem::refresh() mLabelFontBuffer->reset(); } mIsFavorite = vmi.isFavorite() && !vmi.isItemInTrash(); - setToolTip(vmi.getName()); // icons are slightly expensive to get, can be optimized // see LLInventoryIcon::getIcon() mIcon = vmi.getIcon(); @@ -673,6 +670,19 @@ const std::string& LLFolderViewItem::getName( void ) const return getViewModelItem() ? getViewModelItem()->getName() : noName; } +const std::string LLFolderViewItem::getToolTip() const +{ + // Return the item name as tooltip without storing it + if (!LLView::sDebugUnicode) + { + if (const LLFolderViewModelItem* vmi = getViewModelItem()) + { + return vmi->getName(); + } + } + return LLView::getToolTip(); +} + // LLView functionality bool LLFolderViewItem::handleRightMouseDown( S32 x, S32 y, MASK mask ) { diff --git a/indra/llui/llfolderviewitem.h b/indra/llui/llfolderviewitem.h index 958a8ab43e8..826b70f38e1 100644 --- a/indra/llui/llfolderviewitem.h +++ b/indra/llui/llfolderviewitem.h @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code -* Copyright (C) 2010, Linden Research, Inc. +* Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -264,6 +264,11 @@ class LLFolderViewItem : public LLView // viewed. This method will ask the viewed object itself. const std::string& getName( void ) const; + // Override to provide lazy tooltip generation without memory overhead + // Inventory can consist of millions of items, yet most stay invisible, + // much less need to show a tooltip, so avoid storing tooltips. + virtual const std::string getToolTip() const; + // This method returns the label displayed on the view. This // method was primarily added to allow sorting on the folder // contents possible before the entire view has been constructed. diff --git a/indra/llui/llgestureautocompletehelper.cpp b/indra/llui/llgestureautocompletehelper.cpp new file mode 100644 index 00000000000..f00f1406e5f --- /dev/null +++ b/indra/llui/llgestureautocompletehelper.cpp @@ -0,0 +1,165 @@ +/** + * @file llgestureautocompletehelper.cpp + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "llgestureautocompletehelper.h" + +#include "llfloater.h" +#include "llfloaterreg.h" +#include "llfocusmgr.h" +#include "lluictrl.h" + +constexpr char GESTURE_AUTOCOMPLETE_FLOATER[] = "gesture_autocomplete_picker"; + +bool LLGestureAutocompleteHelper::isActive(const LLUICtrl* ctrl) const +{ + return mHostHandle.get() == ctrl; +} + +void LLGestureAutocompleteHelper::showHelper( + LLUICtrl* host_ctrl, + const std::vector& rows, + size_t total, + std::function commit_cb) +{ + if (mHelperHandle.isDead()) + { + LLFloater* helper_floater = LLFloaterReg::getInstance(GESTURE_AUTOCOMPLETE_FLOATER); + mHelperHandle = helper_floater->getHandle(); + mHelperCommitConn = helper_floater->setCommitCallback( + [this](LLUICtrl*, const LLSD& param) { onCommitGesture(param.asString()); }); + } + + setHostCtrl(host_ctrl); + mRows = rows; + mTotal = total; + mGestureCommitCb = commit_cb; + + S32 floater_x, floater_y; + LLRect host_rect = host_ctrl->getRect(); + if (!host_ctrl->localPointToOtherView(0, host_rect.getHeight(), &floater_x, &floater_y, gFloaterView)) + { + LL_WARNS() << "Cannot show gesture autocomplete helper for non-floater controls." << LL_ENDL; + return; + } + + LLFloater* helper_floater = mHelperHandle.get(); + LLRect rect = helper_floater->getRect(); + rect.setLeftTopAndSize(floater_x, floater_y + rect.getHeight(), rect.getWidth(), rect.getHeight()); + helper_floater->setRect(rect); + + refreshPicker(); +} + +void LLGestureAutocompleteHelper::hideHelper(const LLUICtrl* ctrl) +{ + if (ctrl && !isActive(ctrl)) + { + return; + } + + setHostCtrl(nullptr); +} + +bool LLGestureAutocompleteHelper::handleKey(const LLUICtrl* ctrl, KEY key, MASK mask) +{ + if (mHelperHandle.isDead() || !isActive(ctrl)) + { + return false; + } + + return mHelperHandle.get()->handleKey(key, mask, true); +} + +void LLGestureAutocompleteHelper::onCommitGesture(const std::string& trigger) +{ + if (!mHostHandle.isDead() && mGestureCommitCb) + { + mGestureCommitCb(trigger); + } + + hideHelper(getHostCtrl()); +} + +void LLGestureAutocompleteHelper::refreshPicker() +{ + if (mHelperHandle.isDead()) + { + return; + } + + LLFloater* helper_floater = mHelperHandle.get(); + + if (helper_floater->isShown()) + { + helper_floater->onOpen(LLSD()); + } + else + { + helper_floater->openFloater(LLSD()); + } +} + +void LLGestureAutocompleteHelper::setHostCtrl(LLUICtrl* host_ctrl) +{ + const LLUICtrl* cur_host_ctrl = mHostHandle.get(); + + if (cur_host_ctrl != host_ctrl) + { + mHostCtrlFocusLostConn.disconnect(); + mHostHandle.markDead(); + mGestureCommitCb = {}; + mRows.clear(); + mTotal = 0; + + if (!mHelperHandle.isDead()) + { + mHelperHandle.get()->closeFloater(); + } + + if (host_ctrl) + { + mHostHandle = host_ctrl->getHandle(); + mHostCtrlFocusLostConn = host_ctrl->setFocusLostCallback( + [this](auto*) + { + // Scroll list grabs focus on click. + // Keep focus on the host when the click was ours. + LLFloater* helper_floater = mHelperHandle.get(); + if (helper_floater && gFocusMgr.childHasKeyboardFocus(helper_floater)) + { + if (LLUICtrl* host = getHostCtrl()) + { + host->setFocus(true); + } + return; + } + + hideHelper(getHostCtrl()); + }); + } + } +} diff --git a/indra/llui/llgestureautocompletehelper.h b/indra/llui/llgestureautocompletehelper.h new file mode 100644 index 00000000000..6292655fc75 --- /dev/null +++ b/indra/llui/llgestureautocompletehelper.h @@ -0,0 +1,80 @@ +/** + * @file llgestureautocompletehelper.h + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "llhandle.h" +#include "llsingleton.h" + +#include +#include +#include +#include + +class LLFloater; +class LLUICtrl; + +class LLGestureAutocompleteHelper : public LLSingleton +{ + LLSINGLETON(LLGestureAutocompleteHelper) {} + ~LLGestureAutocompleteHelper() override {} + +public: + struct Row + { + std::string value; + std::string trigger; + std::string name; + }; + + bool isActive(const LLUICtrl* ctrl) const; + void showHelper( + LLUICtrl* host_ctrl, + const std::vector& rows, + size_t total, + std::function commit_cb); + void hideHelper(const LLUICtrl* ctrl = nullptr); + bool handleKey(const LLUICtrl* ctrl, KEY key, MASK mask); + void onCommitGesture(const std::string& trigger); + + const std::vector& rows() const { return mRows; } + size_t total() const { return mTotal; } + +protected: + void setHostCtrl(LLUICtrl* host_ctrl); + LLUICtrl* getHostCtrl() const { return mHostHandle.get(); } + +private: + void refreshPicker(); + + LLHandle mHostHandle; + LLHandle mHelperHandle; + boost::signals2::connection mHostCtrlFocusLostConn; + boost::signals2::connection mHelperCommitConn; + std::function mGestureCommitCb; + + std::vector mRows; + size_t mTotal = 0; +}; diff --git a/indra/llui/lltextbase.cpp b/indra/llui/lltextbase.cpp index 38b29fce6de..1fad8714ce8 100644 --- a/indra/llui/lltextbase.cpp +++ b/indra/llui/lltextbase.cpp @@ -2368,6 +2368,7 @@ void LLTextBase::createUrlContextMenu(S32 x, S32 y, const std::string &in_url) registrar.add("Url.AddFriend", boost::bind(&LLUrlAction::addFriend, url)); registrar.add("Url.RemoveFriend", boost::bind(&LLUrlAction::removeFriend, url)); registrar.add("Url.ReportAbuse", boost::bind(&LLUrlAction::reportAbuse, url)); + registrar.add("Url.ReportAbuseObj", boost::bind(&LLUrlAction::reportAbuseObj, url)); registrar.add("Url.SendIM", boost::bind(&LLUrlAction::sendIM, url)); registrar.add("Url.ZoomInObject", boost::bind(&LLUrlAction::zoomInObject, url)); registrar.add("Url.ShowOnMap", boost::bind(&LLUrlAction::showLocationOnMap, url)); diff --git a/indra/llui/lltextbase.h b/indra/llui/lltextbase.h index 0a7f74cc343..cad23aa28e1 100644 --- a/indra/llui/lltextbase.h +++ b/indra/llui/lltextbase.h @@ -318,6 +318,7 @@ class LLTextBase public: friend class LLTextSegment; friend class LLNormalTextSegment; + friend class LLEmbeddedItemSegment; friend class LLUICtrlFactory; typedef boost::signals2::signal is_friend_signal_t; diff --git a/indra/llui/lltexteditor.cpp b/indra/llui/lltexteditor.cpp index 2b87fd1fe7a..4f885b27165 100644 --- a/indra/llui/lltexteditor.cpp +++ b/indra/llui/lltexteditor.cpp @@ -61,6 +61,7 @@ #include "lltooltip.h" #include "llmenugl.h" #include "llchatmentionhelper.h" +#include "llgestureautocompletehelper.h" #include #include "llcombobox.h" @@ -1823,28 +1824,25 @@ void LLTextEditor::pasteTextWithLinebreaks(LLWString & clean_string) LLWString::size_type start = 0; LLWString::size_type pos = clean_string.find('\n', start); - while((pos != LLWString::npos) && (pos != clean_string.length() -1)) + while (pos != LLWString::npos) { - if(pos!=start) + if (pos != start) { std::basic_string str = std::basic_string(clean_string,start,pos-start); setCursorPos(mCursorPos + insert(mCursorPos, str, true, LLTextSegmentPtr())); } - addLineBreakChar(true); // Add a line break and group with the next addition. + const bool trailing_linebreak = (pos == clean_string.length() - 1); + addLineBreakChar(!trailing_linebreak); start = pos+1; pos = clean_string.find('\n',start); } - if (pos != start) + if (start < clean_string.length()) { std::basic_string str = std::basic_string(clean_string,start,clean_string.length()-start); setCursorPos(mCursorPos + insert(mCursorPos, str, false, LLTextSegmentPtr())); } - else - { - addLineBreakChar(false); // Add a line break and end the grouping. - } } // copy selection to primary @@ -2093,7 +2091,8 @@ bool LLTextEditor::handleKeyHere(KEY key, MASK mask ) // not handled and let the parent take care of field movement. if (KEY_TAB == key && mTabsToNextField) { - return mShowChatMentionPicker && LLChatMentionHelper::instance().handleKey(this, key, mask); + return (mShowChatMentionPicker && LLChatMentionHelper::instance().handleKey(this, key, mask)) + || LLGestureAutocompleteHelper::instance().handleKey(this, key, mask); } if (mReadOnly && mScroller) @@ -2107,7 +2106,8 @@ bool LLTextEditor::handleKeyHere(KEY key, MASK mask ) if (!mReadOnly) { if ((mShowEmojiHelper && LLEmojiHelper::instance().handleKey(this, key, mask)) || - (mShowChatMentionPicker && LLChatMentionHelper::instance().handleKey(this, key, mask))) + (mShowChatMentionPicker && LLChatMentionHelper::instance().handleKey(this, key, mask)) || + LLGestureAutocompleteHelper::instance().handleKey(this, key, mask)) { return true; } diff --git a/indra/llui/lltexteditor.h b/indra/llui/lltexteditor.h index dae74dc9fbf..fbc7de90fe5 100644 --- a/indra/llui/lltexteditor.h +++ b/indra/llui/lltexteditor.h @@ -219,6 +219,7 @@ class LLTextEditor : void setShowContextMenu(bool show) { mShowContextMenu = show; } bool getShowContextMenu() const { return mShowContextMenu; } + void showContextMenu(S32 x, S32 y); void showEmojiHelper(); void hideEmojiHelper(); @@ -233,7 +234,6 @@ class LLTextEditor : LLWString getConvertedText() const; protected: - void showContextMenu(S32 x, S32 y); void drawPreeditMarker(); void removeCharOrTab(); diff --git a/indra/llui/lltextvalidate.cpp b/indra/llui/lltextvalidate.cpp index 9a087d82307..1f2e7e65dbf 100644 --- a/indra/llui/lltextvalidate.cpp +++ b/indra/llui/lltextvalidate.cpp @@ -434,7 +434,7 @@ class ValidatorASCIINoLeadingSpace : public ValidatorASCII } validatorASCIINoLeadingSpaceImpl; Validator validateASCIINoLeadingSpace(validatorASCIINoLeadingSpaceImpl); -class ValidatorASCIIWithNewLine : public ValidatorImpl +class ValidatorASCIIWithNewLineNoPipe : public ValidatorImpl { // Used for multiline text stored on the server. // Example is landmark description in Places SP. @@ -446,9 +446,9 @@ class ValidatorASCIIWithNewLine : public ValidatorImpl { CHAR ch = str[len]; - if ((ch < 0x20 && ch != 0xA) || ch > 0x7f) + if ((ch < 0x20 && ch != 0xA) || ch > 0x7f || ch == '|') { - return setError("Validator_ShouldBeNewLineOrASCII", LLSD().with("NR", len + 1).with("CH", llsd(ch))); + return setError("Validator_ShouldBeNewLineOrASCIINoPipe", LLSD().with("NR", len + 1).with("CH", llsd(ch))); } } @@ -458,8 +458,8 @@ class ValidatorASCIIWithNewLine : public ValidatorImpl public: /*virtual*/ bool validate(const std::string& str) override { return validate(str); } /*virtual*/ bool validate(const LLWString& str) override { return validate(str); } -} validatorASCIIWithNewLineImpl; -Validator validateASCIIWithNewLine(validatorASCIIWithNewLineImpl); +} validatorASCIIWithNewLineNoPipeImpl; +Validator validateASCIIWithNewLineNoPipe(validatorASCIIWithNewLineNoPipeImpl); void Validators::declareValues() { @@ -472,7 +472,7 @@ void Validators::declareValues() declare("alpha_num_space", validateAlphaNumSpace); declare("ascii_printable_no_pipe", validateASCIIPrintableNoPipe); declare("ascii_printable_no_space", validateASCIIPrintableNoSpace); - declare("ascii_with_newline", validateASCIIWithNewLine); + declare("ascii_with_newline_no_pipe", validateASCIIWithNewLineNoPipe); } } // namespace LLTextValidate diff --git a/indra/llui/lltextvalidate.h b/indra/llui/lltextvalidate.h index 096c28b4481..a95f79bfcb9 100644 --- a/indra/llui/lltextvalidate.h +++ b/indra/llui/lltextvalidate.h @@ -88,7 +88,7 @@ namespace LLTextValidate extern Validator validateASCIIPrintableNoSpace; extern Validator validateASCII; extern Validator validateASCIINoLeadingSpace; - extern Validator validateASCIIWithNewLine; + extern Validator validateASCIIWithNewLineNoPipe; // Add available validators to the internal map struct Validators : public LLInitParam::TypeValuesHelper diff --git a/indra/llui/lltoolbar.cpp b/indra/llui/lltoolbar.cpp index 0b181785e85..94b9ed96cae 100644 --- a/indra/llui/lltoolbar.cpp +++ b/indra/llui/lltoolbar.cpp @@ -939,6 +939,11 @@ void LLToolBar::updateLayoutAsNeeded() mNeedsLayout = false; } +bool LLToolBar::postBuild() +{ + mCaretIcon = getChild("caret"); + return LLUICtrl::postBuild(); +} void LLToolBar::draw() { @@ -982,35 +987,36 @@ void LLToolBar::draw() LLUI::translate((F32)getRect().mLeft, (F32)getRect().mBottom); // Position the caret - if (!mCaretIcon) - { - mCaretIcon = getChild("caret"); - } - - LLIconCtrl* caret = mCaretIcon; - caret->setVisible(false); - if (mDragAndDropTarget && !mButtonCommands.empty()) + // Todo: This shouldn't be on draw, but, as example, on hover + if (mCaretIcon) { - LLRect caret_rect = caret->getRect(); - if (getOrientation(mSideType) == LLLayoutStack::HORIZONTAL) - { - caret->setRect(LLRect(mDragx-caret_rect.getWidth()/2+1, - mDragy, - mDragx+caret_rect.getWidth()/2+1, - mDragy-mDragGirth)); - } - else + mCaretIcon->setVisible(false); + if (mDragAndDropTarget && !mButtonCommands.empty()) { - caret->setRect(LLRect(mDragx, - mDragy+caret_rect.getHeight()/2, - mDragx+mDragGirth, - mDragy-caret_rect.getHeight()/2)); + LLRect caret_rect = mCaretIcon->getRect(); + if (getOrientation(mSideType) == LLLayoutStack::HORIZONTAL) + { + mCaretIcon->setRect(LLRect(mDragx - caret_rect.getWidth() / 2 + 1, + mDragy, + mDragx + caret_rect.getWidth() / 2 + 1, + mDragy - mDragGirth)); + } + else + { + mCaretIcon->setRect(LLRect(mDragx, + mDragy + caret_rect.getHeight() / 2, + mDragx + mDragGirth, + mDragy - caret_rect.getHeight() / 2)); + } + mCaretIcon->setVisible(true); } - caret->setVisible(true); } LLUICtrl::draw(); - caret->setVisible(false); + if (mCaretIcon) + { + mCaretIcon->setVisible(false); + } mDragAndDropTarget = false; } diff --git a/indra/llui/lltoolbar.h b/indra/llui/lltoolbar.h index d64f637e721..1a24a7df7ae 100644 --- a/indra/llui/lltoolbar.h +++ b/indra/llui/lltoolbar.h @@ -234,6 +234,7 @@ class LLToolBar }; // virtuals + bool postBuild(); void draw(); void reshape(S32 width, S32 height, bool called_from_parent = true); bool handleRightMouseDown(S32 x, S32 y, MASK mask); diff --git a/indra/llui/llurlaction.cpp b/indra/llui/llurlaction.cpp index c13d19c9acd..4bf8e88f947 100644 --- a/indra/llui/llurlaction.cpp +++ b/indra/llui/llurlaction.cpp @@ -278,13 +278,22 @@ void LLUrlAction::reportAbuse(std::string url) } } +void LLUrlAction::reportAbuseObj(std::string url) +{ + std::string object_id = getObjectId(url); + if (LLUUID::validate(object_id)) + { + executeSLURL("secondlife:///app/object/" + object_id + "/reportAbuse"); + } +} + void LLUrlAction::blockObject(std::string url) { std::string object_id = getObjectId(url); std::string object_name = getObjectName(url); if (LLUUID::validate(object_id)) { - executeSLURL("secondlife:///app/agent/" + object_id + "/block/" + LLURI::escape(object_name)); + executeSLURL("secondlife:///app/object/" + object_id + "/block/" + LLURI::escape(object_name)); } } @@ -294,6 +303,6 @@ void LLUrlAction::unblockObject(std::string url) std::string object_name = getObjectName(url); if (LLUUID::validate(object_id)) { - executeSLURL("secondlife:///app/agent/" + object_id + "/unblock/" + object_name); + executeSLURL("secondlife:///app/object/" + object_id + "/unblock/" + LLURI::escape(object_name)); } } diff --git a/indra/llui/llurlaction.h b/indra/llui/llurlaction.h index 8227ab5b77e..252c74f0d42 100644 --- a/indra/llui/llurlaction.h +++ b/indra/llui/llurlaction.h @@ -92,6 +92,7 @@ class LLUrlAction static void addFriend(std::string url); static void removeFriend(std::string url); static void reportAbuse(std::string url); + static void reportAbuseObj(std::string url); static void blockObject(std::string url); static void unblockObject(std::string url); diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp index 6864a37fee6..9ed1a7f0521 100644 --- a/indra/llwebrtc/llwebrtc.cpp +++ b/indra/llwebrtc/llwebrtc.cpp @@ -26,6 +26,9 @@ #include "llwebrtc_impl.h" #include +#include +#include +#include #include #include "api/audio_codecs/audio_decoder_factory.h" #include "api/audio_codecs/audio_encoder_factory.h" @@ -393,47 +396,153 @@ void LLWebRTCImpl::init() } -void LLWebRTCImpl::terminate() +bool LLWebRTCImpl::terminate() { - mWorkerThread->BlockingCall( - [this]() + // Run all blocking WebRTC shutdown calls on a separate thread so that a + // hung BlockingCall cannot block the viewer shutdown indefinitely. + // Webrtc is not mission critical, we need to save personal data. + auto done_promise = std::make_shared >(); + std::future done_future = done_promise->get_future(); + + // Hand ownership of the connections to the shutdown thread. Nothing on + // this thread may touch them afterwards -- in the timeout case below the + // shutdown thread is detached and may still be working through them. + std::vector> connections; + connections.swap(mPeerConnections); + + // Explicitely unregister observers before shutting down the threads. + // shutdown_thread, if detached, can outlive observer. + mWorkerThread->BlockingCall([this]() + { + if (mDeviceModule) { - if (mDeviceModule) + mDeviceModule->SetObserver(nullptr); + } + }); + mVoiceDevicesObserverList.clear(); + + // shutdown_thread can be detached, then LLWebRTCImpl will be nulled out. + // Capture what's needed in lambda, don't rely on [this]. + std::thread shutdown_thread( + [networkThread = std::move(mNetworkThread), + workerThread = std::move(mWorkerThread), + signalingThread = std::move(mSignalingThread), + deviceModule = std::move(mDeviceModule), + factory = std::move(mPeerConnectionFactory), + connections = std::move(connections), + done_promise]() mutable + { + // Stop the capture/render devices alongside the connection teardown + // below rather than ahead of it. Both of these calls end in a + // WaitForSingleObject on a WASAPI thread with a 2s timeout apiece + // (AudioDeviceWindowsCore::StopRecording / StopPlayout), so blocking on + // them here can spend most of the shutdown budget before the + // connections have been touched at all -- and after an OS sleep they + // tend to hit the full timeout. + // + // This work has to stay on the worker thread: the device module was + // created there and its AudioDeviceBuffer is guarded by a sequence + // checker bound to that thread. Posting instead of blocking lets the + // signaling close below get on with its network-thread work (data + // channel close, transport teardown) while the device stop is still + // waiting on WASAPI. + // + // No explicit join is needed: Thread's task queue is FIFO and + // BlockingCall posts through it, so the ForceTerminate call at the end + // of this lambda can't run until this task has finished. Any + // worker-thread work the signaling close does is likewise ordered + // after it, so nothing sees the device module half torn down. + workerThread->PostTask( + [&deviceModule]() + { + if (deviceModule) { - mDeviceModule->ForceStopRecording(); - mDeviceModule->StopPlayout(); + deviceModule->ForceStopRecording(); + deviceModule->StopPlayout(); } }); - for (auto &connection : mPeerConnections) - { - connection->terminate(); - } + // Close the connections inline on the signaling thread. This can't be + // connection->terminate(), which only *posts* the close: that queues the + // real work behind everything below, so the connections would be closed + // after the factory and the device module are gone -- or not at all, if + // the thread is destroyed with the task still queued. + // + // It matters that the close completes here because closing a peer + // connection flushes any in-flight GetStats request and runs its + // callback inline, and that callback calls back into the viewer's + // signaling observers. Those observers are only valid until + // llwebrtc::terminate() returns. + signalingThread->BlockingCall( + [&connections]() + { + for (auto& connection : connections) + { + connection->closeOnSignalingThread(); + } + // Destroy the connections here, on the signaling thread, while + // it's still running. + connections.clear(); + }); - // connection->terminate() above spawns a number of Signaling thread calls to - // shut down the connection. The following Blocking Call will wait - // until they're done before it's executed, allowing time to clean up. + // Drain anything the closes posted before dropping the factory. + signalingThread->BlockingCall([]() {}); - mSignalingThread->BlockingCall([this]() { mPeerConnectionFactory = nullptr; }); + signalingThread->BlockingCall([&factory]() { + factory = nullptr; + }); - mWorkerThread->BlockingCall( - [this]() + workerThread->BlockingCall( + [&deviceModule]() { - if (mDeviceModule) + if (deviceModule) { - mDeviceModule->ForceTerminate(); + deviceModule->ForceTerminate(); } - mDeviceModule = nullptr; + deviceModule = nullptr; }); - // In case peer connections still somehow have jobs in workers, - // only clear connections up after clearing workers. - mNetworkThread = nullptr; - mWorkerThread = nullptr; - mSignalingThread = nullptr; + // Explicitly clean WebRTC threads in dependency order before signalling completion. + // The connections were closed and destroyed on the signaling thread, so it's safe + // to clean. + signalingThread.reset(); + workerThread.reset(); + networkThread.reset(); + + done_promise->set_value(); + }); + + constexpr auto WEBRTC_TERMINATE_TIMEOUT = std::chrono::seconds(10); + if (done_future.wait_for(WEBRTC_TERMINATE_TIMEOUT) == std::future_status::timeout) + { + RTC_LOG(LS_WARNING) << __FUNCTION__ + << ": timed out waiting for WebRTC thread shutdown." + " Detaching — some WebRTC resources will be leaked."; + shutdown_thread.detach(); + + // Leave every member exactly as it is. The detached thread is still + // running the lambda above, which reads mSignalingThread, mWorkerThread, + // mDeviceModule and mPeerConnectionFactory through `this` -- clearing or + // releasing them here would pull them out from under it mid-shutdown + // (a null mSignalingThread is an immediate segfault at the next + // BlockingCall). Instead we report the failure so the caller leaks this + // object rather than deleting it; the process is exiting anyway and our + // priority is saving cache and personal data. + // + // mPeerConnections is already empty -- the detached thread owns the + // connections now and must be left to finish with them. + // + // The log sink is unhooked here (and deliberately not deleted, since the + // detached thread may still log) because the viewer-side log callback + // behind it doesn't outlive this call. + webrtc::LogMessage::RemoveLogToStream(mLogSink); + return false; + } + + shutdown_thread.join(); - mPeerConnections.clear(); webrtc::LogMessage::RemoveLogToStream(mLogSink); + return true; } @@ -783,7 +892,9 @@ void LLWebRTCImpl::updateDevices() void LLWebRTCImpl::OnDevicesUpdated() { - updateDevices(); + // OnDevicesUpdated() is called on macOS CoreAudio's device-change callback + // thread. Calling updateDevices() on that thread causes a deadlock. + mWorkerThread->PostTask([this] { updateDevices(); }); } @@ -982,6 +1093,8 @@ LLWebRTCPeerConnectionImpl::LLWebRTCPeerConnectionImpl(const webrtc::Environment mAnswerReceived(false), mPeerConnectionState(webrtc::PeerConnectionInterface::PeerConnectionState::kNew), mDisconnectCount(0), + mStatsRequestPending(false), + mShuttingDown(false), mPendingJobs(0) { } @@ -1014,47 +1127,70 @@ void LLWebRTCPeerConnectionImpl::terminate() mWebRTCImpl->PostSignalingTask( [self]() { - if (self->mPeerConnection) - { - if (self->mDataChannel) - { - { - self->mDataChannel->Close(); - self->mDataChannel = nullptr; - } - } + self->closeOnSignalingThread(); + self->mPendingJobs--; + }); +} - // to remove 'Secondlife is recording' icon from taskbar - // if user was speaking - auto senders = self->mPeerConnection->GetSenders(); - for (auto& sender : senders) - { - auto track = sender->track(); - if (track) - { - track->set_enabled(false); - } - } +// Signaling thread only. +void LLWebRTCPeerConnectionImpl::closeOnSignalingThread() +{ + // Stop issuing stats requests; one may already be in flight, and + // Close() below will flush it. + mShuttingDown = true; - self->mPeerConnection->Close(); - if (self->mLocalStream) - { - auto tracks = self->mLocalStream->GetAudioTracks(); - for (auto& track : tracks) - { - self->mLocalStream->RemoveTrack(track); - } - self->mLocalStream = nullptr; - } - self->mPeerConnection = nullptr; + if (mPeerConnection) + { + if (mDataChannel) + { + mDataChannel->Close(); + mDataChannel = nullptr; + } - for (auto &observer : self->mSignalingObserverList) - { - observer->OnPeerConnectionClosed(); - } + // to remove 'Secondlife is recording' icon from taskbar + // if user was speaking + auto senders = mPeerConnection->GetSenders(); + for (auto& sender : senders) + { + auto track = sender->track(); + if (track) + { + track->set_enabled(false); } - self->mPendingJobs--; - }); + } + + // NOTE: Close() delivers any pending GetStats report inline, before it + // returns, so the observer list below must still be valid here. + mPeerConnection->Close(); + if (mLocalStream) + { + auto tracks = mLocalStream->GetAudioTracks(); + for (auto& track : tracks) + { + mLocalStream->RemoveTrack(track); + } + mLocalStream = nullptr; + } + mPeerConnection = nullptr; + } + + // Notify unconditionally, even if there was no peer connection to close -- + // a connection can be shut down before it ever finished initializing, and + // the caller is still waiting to hear that the close is done. Withholding + // this leaves the viewer's connection state machine parked in + // VOICE_STATE_WAIT_FOR_CLOSE, which has no timeout of its own. + for (auto &observer : mSignalingObserverList) + { + observer->OnPeerConnectionClosed(); + } + + // Nothing may call back into the viewer past this point. Connections + // closed while the viewer is still running unset themselves as observers + // when they're destroyed, but any that are left for llwebrtc::terminate() + // to close deliberately don't -- they're torn down as soon as it returns, + // so a late callback would be reaching into freed memory. + mSignalingObserverList.clear(); + mDataObserverList.clear(); } void LLWebRTCPeerConnectionImpl::setSignalingObserver(LLWebRTCSignalingObserver *observer) { mSignalingObserverList.emplace_back(observer); } @@ -1677,20 +1813,20 @@ void LLWebRTCPeerConnectionImpl::OnStateChange() switch (mDataChannel->state()) { case webrtc::DataChannelInterface::kOpen: - RTC_LOG(LS_INFO) << __FUNCTION__ << " Data Channel State Open"; + RTC_LOG(LS_VERBOSE) << __FUNCTION__ << " Data Channel State Open"; for (auto &observer : mSignalingObserverList) { observer->OnDataChannelReady(this); } break; case webrtc::DataChannelInterface::kConnecting: - RTC_LOG(LS_INFO) << __FUNCTION__ << " Data Channel State Connecting"; + RTC_LOG(LS_VERBOSE) << __FUNCTION__ << " Data Channel State Connecting"; break; case webrtc::DataChannelInterface::kClosing: - RTC_LOG(LS_INFO) << __FUNCTION__ << " Data Channel State closing"; + RTC_LOG(LS_VERBOSE) << __FUNCTION__ << " Data Channel State closing"; break; case webrtc::DataChannelInterface::kClosed: - RTC_LOG(LS_INFO) << __FUNCTION__ << " Data Channel State closed"; + RTC_LOG(LS_VERBOSE) << __FUNCTION__ << " Data Channel State closed"; break; default: break; @@ -1781,16 +1917,41 @@ void LLWebRTCPeerConnectionImpl::gatherConnectionStats() return; } - auto stats_callback = webrtc::make_ref_counted( - [this](const LLWebRTCStatsMap& generic_stats) + webrtc::scoped_refptr self(this); + mWebRTCImpl->PostSignalingTask( + [self]() + { + if (!self->mPeerConnection + || self->mShuttingDown + || self->mPeerConnectionState != webrtc::PeerConnectionInterface::PeerConnectionState::kConnected + || self->mStatsRequestPending) // signaling thread only { - for (auto& observer : mSignalingObserverList) + return; + } + + self->mStatsRequestPending = true; + + auto stats_callback = webrtc::make_ref_counted( + [self](const LLWebRTCStatsMap& generic_stats) + { + self->mStatsRequestPending = false; + + // This can be delivered inline from PeerConnection::Close(), which + // flushes pending stats requests as it tears down. Don't call out + // to the observers in that case -- we're on our way out. + if (!self->mPeerConnection || self->mShuttingDown) + { + return; + } + + for (auto& observer : self->mSignalingObserverList) { observer->OnStatsDelivered(generic_stats); } }); - mPeerConnection->GetStats(stats_callback.get()); + self->mPeerConnection->GetStats(stats_callback.get()); + }); } LLWebRTCImpl * gWebRTCImpl = nullptr; @@ -1824,8 +1985,14 @@ void terminate() { if (gWebRTCImpl) { - gWebRTCImpl->terminate(); - delete gWebRTCImpl; + if (gWebRTCImpl->terminate()) + { + delete gWebRTCImpl; + } + // Otherwise shutdown timed out and was left to a detached thread that is + // still using this object -- and the webrtc threads it owns -- so it's + // intentionally leaked. Deleting it would hand that thread a freed + // object to finish shutting down with. gWebRTCImpl = nullptr; } } diff --git a/indra/llwebrtc/llwebrtc_impl.h b/indra/llwebrtc/llwebrtc_impl.h index 28d25b8d515..ae3a6a49039 100644 --- a/indra/llwebrtc/llwebrtc_impl.h +++ b/indra/llwebrtc/llwebrtc_impl.h @@ -81,19 +81,28 @@ class LLWebRTCLogSink : public webrtc::LogSink { if (mCallback) { + // RTC_LOG prefixes each message with its "(file:line): " origin. Use it to demote + // libwebrtc's own chatty INFO/WARNING logging to verbose, keeping ours as-is. + static const std::string file_prefix("(llwebrtc.cpp:"); switch (severity) { case webrtc::LS_VERBOSE: mCallback->LogMessage(LLWebRTCLogCallback::LOG_LEVEL_VERBOSE, msg); break; case webrtc::LS_INFO: - mCallback->LogMessage(LLWebRTCLogCallback::LOG_LEVEL_VERBOSE, msg); + mCallback->LogMessage(msg.rfind(file_prefix, 0) == 0 + ? LLWebRTCLogCallback::LOG_LEVEL_INFO + : LLWebRTCLogCallback::LOG_LEVEL_VERBOSE, + msg); break; case webrtc::LS_WARNING: - mCallback->LogMessage(LLWebRTCLogCallback::LOG_LEVEL_VERBOSE, msg); + mCallback->LogMessage(msg.rfind(file_prefix, 0) == 0 + ? LLWebRTCLogCallback::LOG_LEVEL_WARNING + : LLWebRTCLogCallback::LOG_LEVEL_VERBOSE, + msg); break; case webrtc::LS_ERROR: - mCallback->LogMessage(LLWebRTCLogCallback::LOG_LEVEL_VERBOSE, msg); + mCallback->LogMessage(LLWebRTCLogCallback::LOG_LEVEL_ERROR, msg); break; default: break; @@ -413,7 +422,11 @@ class LLWebRTCImpl : public LLWebRTCDeviceInterface, public webrtc::AudioDeviceO } void init(); - void terminate(); + // Returns true if shutdown completed cleanly and this object may be + // destroyed. Returns false if it timed out: a detached thread is still + // using this object and its webrtc threads, so it must be leaked, not + // deleted. + bool terminate(); // // LLWebRTCDeviceInterface @@ -585,7 +598,12 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface, ~LLWebRTCPeerConnectionImpl(); void init(LLWebRTCImpl * webrtc_impl); + // Posts closeOnSignalingThread() and returns immediately. void terminate(); + // The actual close. Signaling thread only. Callable directly (via a + // BlockingCall) when the caller needs the connection to be fully closed + // before it continues -- see LLWebRTCImpl::terminate(). + void closeOnSignalingThread(); virtual void AddRef() const override = 0; virtual webrtc::RefCountReleaseStatus Release() const override = 0; @@ -688,6 +706,14 @@ class LLWebRTCPeerConnectionImpl : public LLWebRTCPeerConnectionInterface, webrtc::PeerConnectionInterface::PeerConnectionState mPeerConnectionState; uint32_t mDisconnectCount; + // Accessed only on the WebRTC signaling thread. + bool mStatsRequestPending; + + // Set by closeOnSignalingThread() so that no new stats request (or other + // callback into the viewer) is issued while we're tearing down. + // Accessed only on the WebRTC signaling thread. + bool mShuttingDown; + std::atomic mPendingJobs; }; diff --git a/indra/llwindow/CMakeLists.txt b/indra/llwindow/CMakeLists.txt index 34e28c94891..5c52754ec7e 100644 --- a/indra/llwindow/CMakeLists.txt +++ b/indra/llwindow/CMakeLists.txt @@ -89,6 +89,14 @@ if(USE_SDL_WINDOW) ) endif() +if(DARWIN) + # IOKit deployment-target shim, consumed by newview's llmachineid.cpp. + target_sources(llwindow + PUBLIC + llwindowmacosx_iokit.h + ) +endif(DARWIN) + if(WINDOWS) # lldxhardware (GPU/driver/VRAM queries) is used regardless of window # backend — the SDL backend calls LLDXHardware::updateVRAMBudgetFromDXGI() diff --git a/indra/llwindow/llwindowcallbacks.cpp b/indra/llwindow/llwindowcallbacks.cpp index 5722860d682..cf516fcacdd 100644 --- a/indra/llwindow/llwindowcallbacks.cpp +++ b/indra/llwindow/llwindowcallbacks.cpp @@ -68,6 +68,18 @@ void LLWindowCallbacks::handleMouseLeave(LLWindow *window) return; } +void LLWindowCallbacks::handlePreCloseRequest() +{ +} + +void LLWindowCallbacks::handleCloseRequestCanceled() +{ +} + +void LLWindowCallbacks::handleSuspendRequest() +{ +} + bool LLWindowCallbacks::handleCloseRequest(LLWindow *window, bool from_user) { //allow the window to close diff --git a/indra/llwindow/llwindowcallbacks.h b/indra/llwindow/llwindowcallbacks.h index 7b49a3c0f49..5faae01a480 100644 --- a/indra/llwindow/llwindowcallbacks.h +++ b/indra/llwindow/llwindowcallbacks.h @@ -42,6 +42,10 @@ class LLWindowCallbacks virtual bool handleMouseDown(LLWindow *window, LLCoordGL pos, MASK mask); virtual bool handleMouseUp(LLWindow *window, LLCoordGL pos, MASK mask); virtual void handleMouseLeave(LLWindow *window); + // Called before close request is processed (ex: to create marker file in case OS is about to kill app). + virtual void handlePreCloseRequest(); + virtual void handleCloseRequestCanceled(); + virtual void handleSuspendRequest(); // return true to allow window to close, which will then cause handleQuit to be called virtual bool handleCloseRequest(LLWindow *window, bool from_user); virtual bool handleSessionExit(LLWindow* window); diff --git a/indra/llwindow/llwindowmacosx_iokit.h b/indra/llwindow/llwindowmacosx_iokit.h new file mode 100644 index 00000000000..a6be2c86ef4 --- /dev/null +++ b/indra/llwindow/llwindowmacosx_iokit.h @@ -0,0 +1,35 @@ +/** + * @file llwindowmacosx_iokit.h + * @brief IOKit compatibility for macOS deployment target differences + * + * $LicenseInfo:firstyear=2025&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#pragma once +#include + +// kIOMainPortDefault is the macOS 12+ rename of kIOMasterPortDefault. +#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 120000 +static const mach_port_t kLLIOMainPort = kIOMainPortDefault; +#else +static const mach_port_t kLLIOMainPort = kIOMasterPortDefault; +#endif diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 3eae291f362..896b6355502 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -143,6 +143,19 @@ void show_window_creation_error(const std::string& title) LL_WARNS("Window") << title << LL_ENDL; } +static bool is_thread_from_current_process(DWORD thread_id) +{ + HANDLE thread_handle = OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, thread_id); + if (!thread_handle) + { + return false; + } + + const DWORD process_id = GetProcessIdOfThread(thread_handle); + CloseHandle(thread_handle); + return process_id == GetCurrentProcessId(); +} + HGLRC SafeCreateContext(HDC &hdc) { __try @@ -514,6 +527,7 @@ LLWindowWin32::LLWindowWin32(LLWindowCallbacks* callbacks, : LLWindow(callbacks, fullscreen, flags), mAbsoluteCursorPosition(false), + mReceivedSCClose(false), mMaxGLVersion(max_gl_version), mMaxCores(max_cores) { @@ -1437,7 +1451,7 @@ bool LLWindowWin32::switchContext(bool fullscreen, const LLCoordScreen& size, bo catch (...) { LOG_UNHANDLED_EXCEPTION("ChoosePixelFormat"); - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1448,7 +1462,7 @@ bool LLWindowWin32::switchContext(bool fullscreen, const LLCoordScreen& size, bo if (!DescribePixelFormat(mhDC, pixel_format, sizeof(PIXELFORMATDESCRIPTOR), &pfd)) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtDescErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtDescErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1486,7 +1500,7 @@ bool LLWindowWin32::switchContext(bool fullscreen, const LLCoordScreen& size, bo if (!SetPixelFormat(mhDC, pixel_format, &pfd)) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtSetErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtSetErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1494,14 +1508,14 @@ bool LLWindowWin32::switchContext(bool fullscreen, const LLCoordScreen& size, bo if (!(mhRC = SafeCreateContext(mhDC))) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } if (!wglMakeCurrent(mhDC, mhRC)) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextActErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextActErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1677,14 +1691,14 @@ const S32 max_format = (S32)num_formats - 1; if (!mhDC) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBDevContextErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBDevContextErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } if (!SetPixelFormat(mhDC, pixel_format, &pfd)) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtSetErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtSetErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1716,7 +1730,7 @@ const S32 max_format = (S32)num_formats - 1; { LL_WARNS("Window") << "No wgl_ARB_pixel_format extension!" << LL_ENDL; // cannot proceed without wgl_ARB_pixel_format extension, shutdown same as any other gGLManager.initGL() failure - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBVideoDrvErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBVideoDrvErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1725,7 +1739,7 @@ const S32 max_format = (S32)num_formats - 1; if (!DescribePixelFormat(mhDC, pixel_format, sizeof(PIXELFORMATDESCRIPTOR), &pfd)) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtDescErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBPixelFmtDescErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1747,7 +1761,7 @@ const S32 max_format = (S32)num_formats - 1; if (!wglMakeCurrent(mhDC, mhRC)) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextActErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextActErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1756,7 +1770,7 @@ const S32 max_format = (S32)num_formats - 1; if (!gGLManager.initGL()) { - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBVideoDrvErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBVideoDrvErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); close(); return false; } @@ -1971,7 +1985,7 @@ void* LLWindowWin32::createSharedContext() if (!rc && !(rc = wglCreateContext(mhDC))) { close(); - LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextErr"), 8/*LAST_EXEC_GRAPHICS_INIT*/); + LLError::LLUserWarningMsg::show(mCallbacks->translateString("MBGLContextErr"), LLError::LLUserWarningMsg::ERROR_INIT_FAILED); } return rc; @@ -2459,6 +2473,15 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ update_width, update_height)); break; } + case WM_ERASEBKGND: + { + RECT client_rect; + if (GetClientRect(h_wnd, &client_rect)) + { + FillRect((HDC)w_param, &client_rect, (HBRUSH)GetStockObject(BLACK_BRUSH)); + } + return 1; + } case WM_PARENTNOTIFY: { break; @@ -2493,23 +2516,27 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ break; } - case WM_POWERBROADCAST: - { - // Might need to register for power broadcast interface - // Todo: log monitor suspending and resuming. - LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_POWERBROADCAST"); - LL_INFOS("Window") << "Received WM_POWERBROADCAST with wParam: 0x" << std::hex << (uintptr_t)w_param << " lParam: 0x" << (uintptr_t)l_param << std::dec << LL_ENDL; - break; - } - case WM_ACTIVATEAPP: { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_ACTIVATEAPP"); + // Resolve ownership before deferring the work because thread IDs can + // be reused after a thread exits. + const bool activating_same_process_thread = + !w_param && is_thread_from_current_process(static_cast(l_param)); window_imp->post([=]() { // This message should be sent whenever the app gains or loses focus. BOOL activating = (BOOL)w_param; + // Native dialogs run on a worker thread. Moving focus between + // the viewer and one of those dialogs must not be treated as + // switching to another application: in fullscreen that would + // minimize the viewer and hide its owned dialog. + if (!activating && activating_same_process_thread) + { + activating = TRUE; + } + if (window_imp->mFullscreen) { // When we run fullscreen, restoring or minimizing the app needs @@ -2559,8 +2586,15 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ case WM_SYSCOMMAND: { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_SYSCOMMAND"); - switch (w_param) + switch (w_param & 0xFFF0) { + case SC_CLOSE: + // User clicked close from system menu/taskbar or 'end process' from task manager + // Do nothing, will cause WM_CLOSE. + // If we don't get this message before WM_CLOSE, we are likely getting + // a kill from some external program. Win11 task manager Does cause SC_CLOSE. + window_imp->mReceivedSCClose = true; + break; case SC_KEYMENU: // Disallow the ALT key from triggering the default system menu. return 0; @@ -2576,9 +2610,30 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_CLOSE"); window_imp->mWindowThread->pingWindowTimeout("WM_CLOSE"); - // todo: WM_CLOSE can be caused by user and by task manager, - // distinguish these cases. - // For now assume it is always user. + + window_imp->mCallbacks->handlePreCloseRequest(); // mark app as potentially closing + if (!window_imp->mReceivedSCClose) + { + // Some external program is trying to close the app. + // Assume that it's going to destroy process if it fails + // and try to fast-quit without confirmation or cleanup. + window_imp->post([=]() + { + // Check if app needs cleanup or can be closed immediately. + if (window_imp->mCallbacks->handleSessionExit(window_imp)) + { + // Get the app to initiate cleanup. + window_imp->mCallbacks->handleQuit(window_imp); + } + }); + return 0; + } + window_imp->mReceivedSCClose = false; + + // There is no way to tell the difference between a user issued + // WM_CLOSE or task manager's WM_CLOSE. + // Assume it is a user and ask for confirmation, but create a marker file. + // If App keeps doing something after a second, or gets 'destroy' message clear the marker. window_imp->post([=]() { // Will the app allow the window to close? @@ -2600,6 +2655,16 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ } return 0; } + case WM_NCDESTROY: + LL_INFOS("Window") << "Received WM_NCDESTROY" << LL_ENDL; + break; + case WM_WTSSESSION_CHANGE: + { + // Detects Remote Desktop disconnects, fast user switching, session logoff + // w_param: WTS_CONSOLE_CONNECT, WTS_CONSOLE_DISCONNECT, WTS_SESSION_LOGOFF, etc. + LL_INFOS("Window") << "Received WM_WTSSESSION_CHANGE with wParam: " << (U32)w_param << LL_ENDL; + break; + } case WM_QUERYENDSESSION: { // Generally means that OS is going to shut down or user is going to log off. @@ -2622,8 +2687,10 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ || (end_session_flags & ENDSESSION_CRITICAL) // will shutdown regardless of app state || (end_session_flags & ENDSESSION_LOGOFF)) // logoff, can delay shutdown { + window_imp->mCallbacks->handlePreCloseRequest(); // mark app as closing window_imp->post([=]() { + LL_INFOS("Window") << "Shutting down due to session terminating" << LL_ENDL; // Check if app needs cleanup or can be closed immediately. if (window_imp->mCallbacks->handleSessionExit(window_imp)) { @@ -2642,6 +2709,193 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ // if session is ending OS is going to take care of it. return 0; } + case WM_POWERBROADCAST: + { + LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_POWERBROADCAST"); + switch (w_param) + { + case PBT_APMSUSPEND: + LL_INFOS("Window") << "System is suspending (sleep/hibernate)" << LL_ENDL; + // System is about to enter sleep or hibernation + // Viewer can't function in hibernation, try to shut down. + // The system allows approximately two seconds for an + // application to handle this notification. + + // Mark app as potentially closing, to minimize issues if OS does not recover. + window_imp->mCallbacks->handlePreCloseRequest(); + window_imp->post([=]() + { + window_imp->mCallbacks->handleSuspendRequest(); + }); + // Window thread normally doesn't block main thread, but OS can suspend + // immediately if we don't wait. + // Keep OS from suspending to give a chance to send stats. + ms_sleep(1000); + return TRUE; + + case PBT_APMRESUMESUSPEND: + LL_INFOS("Window") << "System is resuming from suspend" << LL_ENDL; + window_imp->mCallbacks->handleCloseRequestCanceled(); + return TRUE; + + case PBT_APMPOWERSTATUSCHANGE: + LL_INFOS("Window") << "Power status has changed" << LL_ENDL; + // Power status change (AC/battery) + // Viewer requires high performance, not much we can do. + // about it, but log for diagnostic purposes (example: + // OS trying to throw viewer at an iGPU after this message) + return TRUE; + + default: + LL_INFOS("Window") << "Received WM_POWERBROADCAST with wParam: 0x" << std::hex << (uintptr_t)w_param << " lParam: 0x" << (uintptr_t)l_param << std::dec << LL_ENDL; + break; + } + break; + } + case WM_POST_UNINSTALL_: + { + LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_POST_UNINSTALL_"); + // Other instance, likely velopack, requested we quit. + // Don't trust PID alone (can be spoofed), verify the + // path for security purposes before processing. + // Verifying path isn't a strong varranty, if this turns + // up to be a risk, we will want something more secure. + // See sendShutdownToOtherInstances for the sender. + + // LPARAM contains message type. + DWORD message_type = static_cast(l_param); + if (message_type == WM_POST_UNINSTALL_MSG_SHUTDOWN || message_type == WM_POST_UNINSTALL_MSG_UPDATE) + { + DWORD sender_process_id = static_cast(w_param); + + // Make sure something didn't just send us our own process + DWORD our_process_id = GetCurrentProcessId(); + if (our_process_id == sender_process_id) + { + LL_WARNS("Window") << "Received WM_POST_UNINSTALL_ from our own process, ignoring" << LL_ENDL; + break; + } + + if (sender_process_id == 0) + { + LL_WARNS("Window") << "Received WM_POST_UNINSTALL_ but couldn't get sender process ID" << LL_ENDL; + break; + } + + // Open the existing sender process to verify its executable path + HANDLE hSenderProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, sender_process_id); + if (!hSenderProcess) + { + LL_WARNS("Window") << "Received WM_POST_UNINSTALL_ but couldn't open sender process" << LL_ENDL; + break; + } + + // Get the actual executable path of the sender + wchar_t sender_exe_path[MAX_PATH]; + DWORD size = MAX_PATH; + bool got_sender_path = QueryFullProcessImageNameW(hSenderProcess, 0, sender_exe_path, &size) != 0; + CloseHandle(hSenderProcess); + + if (!got_sender_path) + { + LL_WARNS("Window") << "Received WM_POST_UNINSTALL_ but couldn't query sender executable path" << LL_ENDL; + break; + } + + // Extract directory from sender's executable path + wchar_t sender_dir[MAX_PATH]; + wchar_t* file_part = nullptr; + DWORD result = GetFullPathNameW(sender_exe_path, MAX_PATH, sender_dir, &file_part); + + if (result == 0 || result >= MAX_PATH) + { + LL_WARNS("Window") << "Failed to normalize sender executable path" << LL_ENDL; + break; + } + + // Remove the filename to get just directory + if (file_part) + { + *file_part = L'\0'; + } + + // Remove trailing backslash + size_t sender_dir_len = wcslen(sender_dir); + if (sender_dir_len > 0 && sender_dir[sender_dir_len - 1] == L'\\') + { + sender_dir[sender_dir_len - 1] = L'\0'; + sender_dir_len--; + } + + // Remove "\current" suffix from sender's path if present + const std::wstring current_suffix = L"\\current"; + std::wstring sender_normalized_str(sender_dir); + if (sender_normalized_str.length() >= current_suffix.length() && + _wcsicmp(sender_normalized_str.c_str() + sender_normalized_str.length() - current_suffix.length(), + current_suffix.c_str()) == 0) + { + sender_normalized_str.resize(sender_normalized_str.length() - current_suffix.length()); + } + + // Get our executable directory for comparison + std::wstring our_wide = ll_convert(gDirUtilp->getExecutableDir()); + + // Normalize our path + wchar_t our_normalized[MAX_PATH]; + file_part = nullptr; + + DWORD result2 = GetFullPathNameW(our_wide.c_str(), MAX_PATH, our_normalized, &file_part); + + if (result2 == 0 || result2 >= MAX_PATH) + { + LL_WARNS("Window") << "Failed to normalize our executable path" << LL_ENDL; + break; + } + + // Remove trailing backslash + size_t our_len = wcslen(our_normalized); + if (our_len > 0 && our_normalized[our_len - 1] == L'\\') + { + our_normalized[our_len - 1] = L'\0'; + our_len--; + } + + // Remove "\current" suffix from our path if present + std::wstring our_normalized_str(our_normalized); + if (our_normalized_str.length() >= current_suffix.length() && + _wcsicmp(our_normalized_str.c_str() + our_normalized_str.length() - current_suffix.length(), + current_suffix.c_str()) == 0) + { + our_normalized_str.resize(our_normalized_str.length() - current_suffix.length()); + } + + // Compare the normalized base installation paths (case-insensitive) + if (_wcsicmp(sender_normalized_str.c_str(), our_normalized_str.c_str()) == 0) + { + window_imp->post([=]() + { + LL_INFOS("Window") << "Received valid shutdown request from verified same installation directory" << LL_ENDL; + // Check if app needs cleanup or can be closed immediately. + if (window_imp->mCallbacks->handleCloseRequest(window_imp, false)) + { + // Get the app to initiate cleanup. + window_imp->mCallbacks->handleQuit(window_imp); + } + }); + } + else + { + LL_WARNS("Window") << "Rejected shutdown request - sender not from our installation directory. " + << "Sender: " << ll_convert_wide_to_string(sender_normalized_str) + << " Our: " << ll_convert_wide_to_string(our_normalized_str) << LL_ENDL; + } + } + else + { + LL_WARNS("Window") << "Received invalid WM_POST_UNINSTALL_ message" << LL_ENDL; + } + break; + } case WM_COMMAND: { LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_COMMAND"); @@ -3166,9 +3420,19 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ case WM_DISPLAYCHANGE: { + LL_PROFILE_ZONE_NAMED_CATEGORY_WIN32("mwp - WM_DISPLAYCHANGE"); window_imp->mWindowThread->pingWindowTimeout("WM_DISPLAYCHANGE"); - WINDOW_IMP_POST(window_imp->mCallbacks->handleDisplayChanged()); - break; + window_imp->post([=]() { + window_imp->mCallbacks->handleDisplayChanged(); + // Note: WM_DISPLAYCHANGE was passing to WM_SETFOCUS + // which might have been unintended and was messing with zones. + // handleFocus was copied over and return 0 added, but + // handleFocus might be not needed here. + // handleFocus resets mouse, closes popups and keys, which + // we probablt should do on 'display change'. + window_imp->mCallbacks->handleFocus(window_imp); + }); + return 0; } case WM_SETFOCUS: diff --git a/indra/llwindow/llwindowwin32.h b/indra/llwindow/llwindowwin32.h index ab0324ea9bb..ef90122223d 100644 --- a/indra/llwindow/llwindowwin32.h +++ b/indra/llwindow/llwindowwin32.h @@ -40,6 +40,12 @@ // Hack for async host by name #define LL_WM_HOST_RESOLVED (WM_APP + 1) +// For requesting shutdown on uninstall, +// make sure it does not conflict with messages like WM_DUMMY_ +inline constexpr UINT WM_POST_UNINSTALL_ = WM_USER + 0x0019; +inline constexpr DWORD WM_POST_UNINSTALL_MSG_SHUTDOWN = 1; +inline constexpr DWORD WM_POST_UNINSTALL_MSG_UPDATE = 2; + typedef void (*LLW32MsgCallback)(const MSG &msg); class LLWindowWin32 : public LLWindow @@ -236,6 +242,7 @@ class LLWindowWin32 : public LLWindow LPWSTR mIconResource; bool mInputProcessingPaused; + bool mReceivedSCClose; // received SC_CLOSE and expecting WM_CLOSE // The following variables are for Language Text Input control. // They are all static, since one context is shared by all LLWindowWin32 diff --git a/indra/llxml/llcontrol.cpp b/indra/llxml/llcontrol.cpp index fc6a8c3cee3..89c7cb8f8c5 100644 --- a/indra/llxml/llcontrol.cpp +++ b/indra/llxml/llcontrol.cpp @@ -192,20 +192,6 @@ LLSD LLControlVariable::getComparableValue(const LLSD& value) storable_value = false; } } - else if (TYPE_LLSD == type() && value.isString()) - { - LLPointer parser = new LLSDNotationParser; - LLSD result; - std::stringstream value_stream(value.asString()); - if (parser->parse(value_stream, result, LLSDSerialize::SIZE_UNLIMITED) != LLSDParser::PARSE_FAILURE) - { - storable_value = result; - } - else - { - storable_value = value; - } - } else { storable_value = value; @@ -402,6 +388,7 @@ static bool compareRoutine(settings_pair_t lhs, settings_pair_t rhs) void LLControlGroup::cleanup() { + LL_PROFILE_ZONE_SCOPED; if(mSettingsProfile && getCount.size() != 0) { std::string file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, SETTINGS_PROFILE); @@ -734,6 +721,30 @@ void LLControlGroup::setLLSD(std::string_view name, const LLSD& val) set(name, val); } +bool LLControlVariable::setValueFromNotation(const std::string& notation, bool saved_value) +{ + if (mType == TYPE_LLSD) + { + LLPointer parser = new LLSDNotationParser; + LLSD result; + std::stringstream value_stream(notation); + S32 parse_count = parser->parse(value_stream, result, LLSDSerialize::SIZE_UNLIMITED); + if (parse_count != LLSDParser::PARSE_FAILURE) + { + setValue(result, saved_value); + return true; + } + LL_WARNS("Controls") << "Failed to parse LLSD notation for control '" + << mName << "': " << notation << LL_ENDL; + } + else + { + LL_WARNS("Controls") << "setValueFromNotation() called on non-LLSD control '" + << mName << "' (type " << LLControlGroup::typeEnumToString(mType) << "); ignoring." << LL_ENDL; + } + return false; +} + void LLControlGroup::setUntypedValue(std::string_view name, const LLSD& val) { if (name.empty()) @@ -959,6 +970,7 @@ U32 LLControlGroup::loadFromFileLegacy(const std::string& filename, bool require U32 LLControlGroup::saveToFile(const std::string& filename, bool nondefault_only) { + LL_PROFILE_ZONE_SCOPED; LLSD settings; int num_saved = 0; for (ctrl_name_table_t::iterator iter = mNameTable.begin(); diff --git a/indra/llxml/llcontrol.h b/indra/llxml/llcontrol.h index bf66c862b91..d27c114ec95 100644 --- a/indra/llxml/llcontrol.h +++ b/indra/llxml/llcontrol.h @@ -130,6 +130,7 @@ class LLControlVariable : public LLRefCount void setPersist(ePersist); void setHiddenFromSettingsEditor(bool hide); void setComment(const std::string& comment); + bool setValueFromNotation(const std::string& notation, bool saved_value = true); private: void firePropertyChanged(const LLSD &pPreviousValue) @@ -326,7 +327,7 @@ class LLControlCache final : public LLRefCount, public LLInstanceTracker mPickedFiles; VolumeCatcher mVolumeCatcher; F32 mCurVolume; @@ -358,6 +359,7 @@ MediaPluginBase(host_send_func, host_user_data) mCanSelectAll = false; mCefLogFile = ""; mCefLogVerbose = false; + mCefRemoteDebuggingPort = 0; mPickedFiles.clear(); mCurVolume = 0.0; @@ -1277,6 +1279,8 @@ void MediaPluginCEF::receiveMessage(const std::string& message_string) settings.webgl_enabled = true; settings.log_file = mCefLogFile; settings.log_verbose = mCefLogVerbose; + settings.enable_remote_debug = (mCefRemoteDebuggingPort != 0); + settings.remote_debugging_port = mCefRemoteDebuggingPort; settings.autoplay_without_gesture = true; std::vector custom_schemes(1, "secondlife"); @@ -1340,6 +1344,7 @@ void MediaPluginCEF::receiveMessage(const std::string& message_string) mCefLogFile = message_in.getValue("cef_log_file"); mCefLogVerbose = message_in.getValueBoolean("cef_verbose_log"); + mCefRemoteDebuggingPort = message_in.getValueU32("cef_remote_debugging_port"); } else if (message_name == "size_change") { diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 4a2d440ba44..25d70e0a574 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -380,6 +380,7 @@ set(viewer_SOURCE_FILES llfloaterfonttest.cpp llfloaterforgetuser.cpp llfloatergesture.cpp + llfloatergestureautocompletepicker.cpp llfloatergodtools.cpp llfloatergotoline.cpp llfloatergridstatus.cpp @@ -390,7 +391,6 @@ set(viewer_SOURCE_FILES llfloaterhelpbrowser.cpp llfloaterhexeditor.cpp llfloaterhoverheight.cpp - llfloaterhowto.cpp llfloaterhud.cpp llfloaterimagepreview.cpp llfloaterimsessiontab.cpp @@ -710,6 +710,7 @@ set(viewer_SOURCE_FILES llpreviewtexture.cpp llproductinforequest.cpp llprogressview.cpp + llpublishedobjectmgr.cpp llrecentpeople.cpp llreflectionmap.cpp llreflectionmapmanager.cpp @@ -1162,6 +1163,7 @@ set(viewer_HEADER_FILES llfloaterfonttest.h llfloaterforgetuser.h llfloatergesture.h + llfloatergestureautocompletepicker.h llfloatergodtools.h llfloatergotoline.h llfloatergridstatus.h @@ -1172,7 +1174,6 @@ set(viewer_HEADER_FILES llfloaterhelpbrowser.h llfloaterhexeditor.h llfloaterhoverheight.h - llfloaterhowto.h llfloaterhud.h llfloaterimagepreview.h llfloaterimnearbychat.h @@ -1480,6 +1481,7 @@ set(viewer_HEADER_FILES llpreviewtexture.h llproductinforequest.h llprogressview.h + llpublishedobjectmgr.h llrecentpeople.h llreflectionmap.h llreflectionmapmanager.h diff --git a/indra/newview/Info-AlchemySDL.plist b/indra/newview/Info-AlchemySDL.plist index d63ee1ac087..50cea134cd3 100644 --- a/indra/newview/Info-AlchemySDL.plist +++ b/indra/newview/Info-AlchemySDL.plist @@ -28,6 +28,8 @@ ${MACOSX_BUNDLE_COPYRIGHT} NSMicrophoneUsageDescription For voice chat, you must grant permission for Alchemy to use the microphone. + NSLocalNetworkUsageDescription + Alchemy uses WebRTC for voice chat, which may require local network access while establishing the voice connection. CFBundleDocumentTypes diff --git a/indra/newview/VIEWER_VERSION.txt b/indra/newview/VIEWER_VERSION.txt index c4697fd5666..bb6eac98a95 100644 --- a/indra/newview/VIEWER_VERSION.txt +++ b/indra/newview/VIEWER_VERSION.txt @@ -1 +1 @@ -26.3.0 +26.4.0 diff --git a/indra/newview/app_settings/cmd_line.xml b/indra/newview/app_settings/cmd_line.xml index 340334aee84..f4cf7976aa7 100644 --- a/indra/newview/app_settings/cmd_line.xml +++ b/indra/newview/app_settings/cmd_line.xml @@ -90,6 +90,12 @@ ConnectAsGod + gpubenchmark + + desc + Run GPU memory bandwidth benchmark, print result to stdout, and exit. Used internally by the viewer to isolate benchmark from the main process. + + graphicslevel desc diff --git a/indra/newview/app_settings/commands.xml b/indra/newview/app_settings/commands.xml index c7374b81d2a..5d426a3dfd5 100644 --- a/indra/newview/app_settings/commands.xml +++ b/indra/newview/app_settings/commands.xml @@ -105,16 +105,6 @@ is_running_function="Floater.IsOpen" is_running_parameters="gestures" /> - RecentJumpThresholdSecs Comment - Seconds after a jump input during which finish-anim is suppressed to avoid interrupting rapid successive jumps. + Seconds after jump input during which landing finish-anim is suppressed to avoid interrupting rapid successive jumps. Persist 1 Type @@ -3779,17 +3779,6 @@ Value https://viewer-help.secondlife.com/[LANGUAGE]/[CHANNEL]/[VERSION]/[TOPIC][DEBUG_MODE] - HowToHelpURL - - Comment - URL for How To help content - Persist - 1 - Type - String - Value - https://lecs-viewer-web-components.s3.amazonaws.com/v3.0/[GRID_LOWERCASE]/howto/index.html - HomeSidePanelURL Comment @@ -3812,17 +3801,6 @@ Value https://search.[GRID]/viewer/?query_term=[QUERY]&search_type=[TYPE][COLLECTION]&maturity=[MATURITY]&lang=[LANGUAGE]&g=[GODLIKE]&sid=[SESSION_ID]&rid=[REGION_ID]&pid=[PARCEL_ID]&channel=[CHANNEL]&version=[VERSION]&major=[VERSION_MAJOR]&minor=[VERSION_MINOR]&patch=[VERSION_PATCH]&build=[VERSION_BUILD] - GuidebookURL - - Comment - URL for Guidebook content - Persist - 1 - Type - String - Value - http://guidebooks.secondlife.io/welcome/index.html - HighResSnapshot Comment @@ -9538,6 +9516,17 @@ Value 0 + NametagOverWater + + Comment + Render name tag over the transparent water while camera is above the water + Persist + 1 + Type + Boolean + Value + 1 + RenderInitError Comment @@ -10423,6 +10412,17 @@ Value 1 + CollectUIImageVertexBuffers + + Comment + When enabled, images will cache vertex buffers and reuse them. When disabled, the general cache will be used with significant hash-lookup overhead, but vertices are regenerated each frame so they are always up to date. + Persist + 0 + Type + Boolean + Value + 1 + ShowMyComplexityChanges Comment @@ -13814,6 +13814,17 @@ Value 0 + SwitchToSharedEnvAfterTeleport + + Comment + Switch to Shared Environment after teleport + Persist + 1 + Type + Boolean + Value + 1 + PreferredBrowserBehavior Comment @@ -14995,7 +15006,7 @@ Type Boolean Value - 0 + 1 OutfitOperationsTimeout @@ -15008,6 +15019,17 @@ Value 180 + OSHibernationMode + + Comment + Whether to prevent OS from hibernating. 0 - can hibernate; 1 - can't hibernate, can turn screen off; 2 - can't hibernate, can't turn screen off + Persist + 1 + Type + S32 + Value + 0 + HeightUnits Comment @@ -16760,6 +16782,17 @@ Value 0 + CEFRemoteDebuggingPort + + Comment + Enable CEF remote debugging on the specified base port (1024 to 65535, 0 to disable). Each new SLPlugin gets base+N. + Persist + 0 + Type + U32 + Value + 0 + 360CaptureJPEGEncodeQuality Comment @@ -16943,7 +16976,7 @@ Persist 1 Type - Boolean + U32 Value 0 diff --git a/indra/newview/app_settings/shaders/class3/deferred/materialF.glsl b/indra/newview/app_settings/shaders/class3/deferred/materialF.glsl index 5b6e2ca4b5b..726711a66fb 100644 --- a/indra/newview/app_settings/shaders/class3/deferred/materialF.glsl +++ b/indra/newview/app_settings/shaders/class3/deferred/materialF.glsl @@ -188,7 +188,7 @@ void alphaMask(float alpha) #endif } -void waterClip() +void applyWaterClip() { #if (DIFFUSE_ALPHA_MODE == DIFFUSE_ALPHA_MODE_BLEND) waterClip(vary_position.xyz); @@ -210,17 +210,17 @@ float getShadow(vec3 pos, vec3 norm) #if (DIFFUSE_ALPHA_MODE == DIFFUSE_ALPHA_MODE_BLEND) return sampleDirectionalShadow(pos, norm, vary_texcoord0.xy); #else - return 1; + return 1.0; #endif #else - return 1; + return 1.0; #endif } void main() { mirrorClip(vary_position); - waterClip(); + applyWaterClip(); // diffcol == diffuse map combined with vertex color vec4 diffcol = texture(diffuseMap, vary_texcoord0.xy); @@ -384,5 +384,3 @@ void main() #endif } - - diff --git a/indra/newview/app_settings/toolbars.xml b/indra/newview/app_settings/toolbars.xml index de89113838d..c50ca35f542 100644 --- a/indra/newview/app_settings/toolbars.xml +++ b/indra/newview/app_settings/toolbars.xml @@ -10,7 +10,6 @@ - diff --git a/indra/newview/licenses-linux.txt b/indra/newview/licenses-linux.txt index a36551dde2d..ef543f7fe86 100644 --- a/indra/newview/licenses-linux.txt +++ b/indra/newview/licenses-linux.txt @@ -660,3 +660,37 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +============== +WebRTC +============== + +Copyright (c) 2011, The WebRTC project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/indra/newview/licenses-mac.txt b/indra/newview/licenses-mac.txt index 99ea210c78b..f5f63cd7e3d 100644 --- a/indra/newview/licenses-mac.txt +++ b/indra/newview/licenses-mac.txt @@ -548,7 +548,7 @@ tinygltf ============ MIT License -Copyright (c) 2017 Syoyo Fujita, Aurélien Chatelain and many contributors +Copyright (c) 2017 Syoyo Fujita, Aurélien Chatelain and many contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -621,3 +621,37 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +============== +WebRTC +============== + +Copyright (c) 2011, The WebRTC project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/indra/newview/licenses-win32.txt b/indra/newview/licenses-win32.txt index da66bc3d035..79d644b7328 100644 --- a/indra/newview/licenses-win32.txt +++ b/indra/newview/licenses-win32.txt @@ -623,7 +623,7 @@ tinygltf ============ MIT License -Copyright (c) 2017 Syoyo Fujita, Aurélien Chatelain and many contributors +Copyright (c) 2017 Syoyo Fujita, Aurélien Chatelain and many contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -695,3 +695,37 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +============== +WebRTC +============== + +Copyright (c) 2011, The WebRTC project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp index ec0844ea14b..d5256042321 100644 --- a/indra/newview/llagent.cpp +++ b/indra/newview/llagent.cpp @@ -44,6 +44,7 @@ #include "llchicletbar.h" #include "llconsole.h" #include "lldonotdisturbnotificationstorage.h" +#include "llenvironment.h" #include "llfirstuse.h" #include "llfloatercamera.h" #include "llfloaterimcontainer.h" @@ -1632,6 +1633,8 @@ void LLAgent::setAFK() setControlFlags(AGENT_CONTROL_AWAY | AGENT_CONTROL_STOP); gAwayTimer.start(); } + + LLAppViewer::instance()->setPermitOSHibernation(true); } //----------------------------------------------------------------------------- @@ -1650,6 +1653,13 @@ void LLAgent::clearAFK() sendAnimationRequest(ANIM_AGENT_AWAY, ANIM_REQUEST_STOP); clearControlFlags(AGENT_CONTROL_AWAY); } + + if (isAgentAvatarValid()) + { + // Only set this if agent is inworld, login screen + // shouldn't prevent hibernation. + LLAppViewer::instance()->setPermitOSHibernation(false); + } } //----------------------------------------------------------------------------- @@ -2778,17 +2788,20 @@ void LLAgent::onAnimStop(const LLUUID& id) else if (id == ANIM_AGENT_PRE_JUMP || id == ANIM_AGENT_LAND || id == ANIM_AGENT_MEDIUM_LAND) { // FIRE-34049/FIRE-34273/https://github.com/secondlife/viewer/issues/4218 - // Avoid forcing AGENT_CONTROL_FINISH_ANIM, which can short-circuit the next pre-jump - // during rapid successive jumps. + // Avoid forcing AGENT_CONTROL_FINISH_ANIM on landing, which can short-circuit the + // next pre-jump during rapid successive jumps. + // Do not suppress pre-jump finish, otherwise a quick tap from standing can stall. // TODO: a more robust fix would require knowing which specific animation finished, // information that is not currently provided by the simulator. + const bool is_landing_anim = (id == ANIM_AGENT_LAND || id == ANIM_AGENT_MEDIUM_LAND); const bool up_pos = (mControlFlags & AGENT_CONTROL_UP_POS) != 0; const F64 now = LLTimer::getTotalSeconds(); const F64 elapsed = now - mLastJumpInputTime; - static LLCachedControl recent_jump_threshold_secs(gSavedSettings, "RecentJumpThresholdSecs"); + static LLCachedControl recent_jump_threshold_secs(gSavedSettings, "RecentJumpThresholdSecs", 1.0); const bool recent_jump = (mLastJumpInputTime > 0.0) && (elapsed < recent_jump_threshold_secs); + const bool suppress_finish = is_landing_anim && recent_jump; - if (!up_pos && !recent_jump) + if (!up_pos && !suppress_finish) { setControlFlags(AGENT_CONTROL_FINISH_ANIM); } @@ -4292,6 +4305,7 @@ void LLAgent::handleTeleportFinished() } clearTeleportRequest(); mTeleportCanceled.reset(); + LLVOAvatar::resetEarlyAppearanceList(); if (mIsMaturityRatingChangingDuringTeleport) { // notify user that the maturity preference has been changed @@ -4323,6 +4337,11 @@ void LLAgent::handleTeleportFinished() mRegionp->setCapabilitiesReceivedCallback(boost::bind(&LLAgent::onCapabilitiesReceivedAfterTeleport)); } } + static LLCachedControl shared_env_on_teleport(gSavedSettings, "SwitchToSharedEnvAfterTeleport", true); + if (shared_env_on_teleport) + { + LLEnvironment::instance().setSharedEnvironment(); + } LLPerfStats::tunables.autoTuneTimeout = true; } diff --git a/indra/newview/llaisapi.cpp b/indra/newview/llaisapi.cpp index a1191e8a315..25b0dbc5505 100644 --- a/indra/newview/llaisapi.cpp +++ b/indra/newview/llaisapi.cpp @@ -1061,6 +1061,19 @@ AISUpdate::AISUpdate(const LLSD& update, AISAPI::COMMAND_TYPE type, const LLSD& mFetchDepth = request_body["depth"].asInteger(); } + // Some tasks are time sensitive, don't wait for them. + // Like FETCHCOF which happens on load and is needed for outfit + // loading. + // FETCHCATEGORYLINKS is used for wearing outfits and fetching + // a user selected outfit or for following an outfit link in COF. + // Other tasks, like FETCHCATEGORYSUBSET are general background + // fetches and can safely wait. + // Do count 'time sensitive' tasks in batch timer. + mUseTimeout = + type != AISAPI::UPDATECATEGORY + && type != AISAPI::UPDATEITEM + && type != AISAPI::FETCHCOF + && type != AISAPI::FETCHCATEGORYLINKS; mTaskTimer.setTimerExpirySec(AIS_TASK_EXPIRY_SECONDS); mTaskTimer.start(); @@ -1091,6 +1104,11 @@ void AISUpdate::clearParseResults() void AISUpdate::checkTimeout() { + if (!mUseTimeout) + { + // Priority task, don't wait. + return; + } if (mTaskTimer.hasExpired() || sBatchTimer.hasExpired()) { // If we are taking too long, don't starve other tasks, @@ -1780,35 +1798,41 @@ void AISUpdate::doUpdate() const LLUUID id = ucv_it->first; S32 version = static_cast(ucv_it->second); LLViewerInventoryCategory *cat = gInventory.getCategory(id); - LL_DEBUGS("Inventory") << "cat version update " << cat->getName() << " to version " << cat->getVersion() << LL_ENDL; - if (cat->getVersion() != version) - { - // the AIS version should be considered the true version. Adjust - // our local category model to reflect this version number. Otherwise - // it becomes possible to get stuck with the viewer being out of - // sync with the inventory system. Under normal circumstances - // inventory COF is maintained on the viewer through calls to - // LLInventoryModel::accountForUpdate when a changing operation - // is performed. This occasionally gets out of sync however. - if (version != LLViewerInventoryCategory::VERSION_UNKNOWN) - { - LL_WARNS() << "Possible version mismatch for category " << cat->getName() - << ", viewer version " << cat->getVersion() - << " AIS version " << version << " !!!Adjusting local version!!!" << LL_ENDL; - cat->setVersion(version); - } - else + // Update can be rather large and take time to process. + // By the time update gets to the category, it could + // could have been removed by the user + if (cat) + { + LL_DEBUGS("Inventory") << "cat " << cat->getName() << " version update from " << cat->getVersion() << " to AIS version " << version << LL_ENDL; + if (cat->getVersion() != version) { - // We do not account for update if version is UNKNOWN, so we shouldn't rise version - // either or viewer will get stuck on descendants count -1, try to refetch folder instead - // - // Todo: proper backoff? - - LL_WARNS() << "Possible version mismatch for category " << cat->getName() - << ", viewer version " << cat->getVersion() - << " AIS version " << version << " !!!Rerequesting category!!!" << LL_ENDL; - const S32 LONG_EXPIRY = 360; - cat->fetch(LONG_EXPIRY); + // the AIS version should be considered the true version. Adjust + // our local category model to reflect this version number. Otherwise + // it becomes possible to get stuck with the viewer being out of + // sync with the inventory system. Under normal circumstances + // inventory COF is maintained on the viewer through calls to + // LLInventoryModel::accountForUpdate when a changing operation + // is performed. This occasionally gets out of sync however. + if (version != LLViewerInventoryCategory::VERSION_UNKNOWN) + { + LL_WARNS() << "Possible version mismatch for category " << cat->getName() + << ", viewer version " << cat->getVersion() + << " AIS version " << version << " !!!Adjusting local version!!!" << LL_ENDL; + cat->setVersion(version); + } + else + { + // We do not account for update if version is UNKNOWN, so we shouldn't riase version + // either or viewer will get stuck on descendants count -1, try to refetch folder instead + // + // Todo: proper backoff? + + LL_WARNS() << "Possible version mismatch for category " << cat->getName() + << ", viewer version " << cat->getVersion() + << " AIS version " << version << " !!!Rerequesting category!!!" << LL_ENDL; + const S32 LONG_EXPIRY = 360; + cat->fetch(LONG_EXPIRY); + } } } } diff --git a/indra/newview/llaisapi.h b/indra/newview/llaisapi.h index cfe14a5c7eb..4828d13c5fb 100644 --- a/indra/newview/llaisapi.h +++ b/indra/newview/llaisapi.h @@ -109,6 +109,7 @@ class AISAPI class AISUpdate { + LOG_CLASS(AISUpdate); public: AISUpdate(const LLSD& update, AISAPI::COMMAND_TYPE type, const LLSD& request_body); void parseUpdate(const LLSD& update); @@ -161,6 +162,7 @@ class AISUpdate uuid_list_t mCategoryIds; bool mFetch; S32 mFetchDepth; + bool mUseTimeout; LLTimer mTaskTimer; static LLTimer sBatchTimer; static U32 sBatchFrameCount; diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index fba31edd961..98e55457cb6 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2012, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -68,6 +68,7 @@ #include "llconversationlog.h" #if LL_WINDOWS #include "lldxhardware.h" +#include #endif #include "lltexturestats.h" #include "lltrace.h" @@ -306,6 +307,13 @@ extern bool gDebugGL; extern bool gHiDPISupport; #endif +#if LL_WINDOWS +extern bool gGPUBenchmarkMode; +#else +static constexpr bool gGPUBenchmarkMode = false; +#endif // LL_WINDOWS + + //////////////////////////////////////////////////////////// // All from the last globals push... @@ -388,6 +396,8 @@ const std::string START_MARKER_FILE_NAME("Alchemy.start_marker"); const std::string ERROR_MARKER_FILE_NAME("Alchemy.error_marker"); const std::string LOGOUT_MARKER_FILE_NAME("Alchemy.logout_marker"); const std::string WATCHDOG_MARKER_FILE_NAME("Alchemy.watchdog_marker"); +const std::string INITED_MARKER_FILE_NAME("Alchemy.inited_marker"); +const std::string CLOSE_EVENT_MARKER_FILE_NAME("Alchemy.close_marker"); static std::string gLaunchFileOnQuit; //---------------------------------------------------------------------------- @@ -618,6 +628,12 @@ bool LLAppViewer::sendURLToOtherInstance(const std::string& url) return false; } +//virtual +void LLAppViewer::setOSHibernationMode(eHibernationMode mode) +{ + // See OS specific files +} + //---------------------------------------------------------------------------- // LLAppViewer definition @@ -675,6 +691,12 @@ LLAppViewer::LLAppViewer() gLoggedInTime.stop(); + // Locking this early is needed to prevent multiple instances and + // to log, but it also means that early paths such as SLURL handling + // can invoke processMarkerFiles() and potentially clear markers + // from previous runs before those stats are reported. + // Todo: improve this. Perhaps store stats 'permanently' to be reported + // on next login and only login cleans stats up? processMarkerFiles(); // // OK to write stuff to logs now, we've now crash reported if necessary @@ -867,6 +889,8 @@ bool LLAppViewer::init() // that use findSkinnedFilenames(), will include the localized files. gDirUtilp->setSkinFolder(gDirUtilp->getSkinFolder(), LLUI::getLanguage()); + loadLocalizedSettingsComments(); + // Setup LLTrans after LLUI::initClass has been called. initStrings(); @@ -965,6 +989,16 @@ bool LLAppViewer::init() LL_WARNS("InitInfo") << "initHardwareTest() failed." << LL_ENDL; // quit immediately LL_PROFILER_FRAME_END; + LLSingletonBase::deleteAll(); + cleanupConsole(); + delete mSettingsLocationList; + if (!mSecondInstance) + { + // Stats from previous session will likely be lost, but this should + // be fine as this is likely first run for this version. + // Todo: Might be smarter to have an exit code for a cleaner shutdown + removeMarkerFiles(); + } return false; } LL_INFOS("InitInfo") << "Hardware test initialization done." << LL_ENDL ; @@ -1172,7 +1206,7 @@ bool LLAppViewer::init() { LL_WARNS("InitInfo") << "Skipping updater check." << LL_ENDL; } -#endif //LL_RELEASE_FOR_DOWNLOAD +#endif { // Iterate over --leap command-line options. But this is a bit tricky: if @@ -1358,6 +1392,7 @@ bool LLAppViewer::doFrame() } #endif + LL_PROFILE_GPU_ZONE("Frame"); { // and now adjust the visuals from previous frame. if(LLPerfStats::tunables.userAutoTuneEnabled && LLPerfStats::tunables.tuningFlag != LLPerfStats::Tunables::Nothing) @@ -1766,7 +1801,15 @@ bool LLAppViewer::cleanup() // Give any remaining SLPlugin instances a chance to exit cleanly. LLPluginProcessParent::shutdown(); + if (LLWatchdog::instanceExists()) + { + // Signal a stop early, so that it will be out + // of 1s sleep loop by the time we get to clean it. + LLWatchdog::getInstance()->shutdown(); + } + disconnectViewer(); + LLViewerCamera::deleteSingleton(); LL_INFOS() << "Viewer disconnected" << LL_ENDL; @@ -2072,7 +2115,7 @@ bool LLAppViewer::cleanup() } LLSplashScreen::show(); - LLSplashScreen::update(LLTrans::getString("ShuttingDown")); + LLSplashScreen::update(LLTrans::getString("ShutdownCleanup")); LL_INFOS() << "Cleaning up Keyboard & Joystick" << LL_ENDL; @@ -2101,7 +2144,7 @@ bool LLAppViewer::cleanup() if (sTextureFetch) { sTextureFetch->shutdown(); - sTextureFetch->waitOnPending(); + sTextureFetch->waitOnPending(10.f); delete sTextureFetch; sTextureFetch = NULL; } @@ -2128,7 +2171,10 @@ bool LLAppViewer::cleanup() gSavedSettings.cleanup(); LLUIColorTable::instance().clear(); - LLWatchdog::getInstance()->cleanup(); + if (LLWatchdog::instanceExists()) + { + LLWatchdog::getInstance()->cleanup(); + } LLViewerAssetStatsFF::cleanup(); @@ -2153,6 +2199,10 @@ bool LLAppViewer::cleanup() SUBSYSTEM_CLEANUP(LLProxy); LLCore::LLHttp::cleanup(); + LLSplashScreen::update(LLTrans::getString("CompressingInventoryCache")); + LLInventoryModel::waitForPendingCacheWrites(); + LLSplashScreen::update(LLTrans::getString("ShuttingDown")); + ll_close_fail_log(); LLError::LLCallStacks::cleanup(); @@ -2163,6 +2213,8 @@ bool LLAppViewer::cleanup() LLWorld::deleteSingleton(); LLVoiceClient::deleteSingleton(); LLUI::deleteSingleton(); + LLGridManager::deleteSingleton(); + LLWatchdog::deleteSingleton(); // It's not at first obvious where, in this long sequence, a generic cleanup // call OUGHT to go. So let's say this: as we migrate cleanup from @@ -2300,6 +2352,9 @@ void errorHandler(const std::string& title_string, const std::string& message_st case LLError::LLUserWarningMsg::ERROR_MISSING_FILES: LLAppViewer::instance()->createErrorMarker(LAST_EXEC_MISSING_FILES); break; + case LLError::LLUserWarningMsg::ERROR_INIT_FAILED: + LLAppViewer::instance()->createErrorMarker(LAST_EXEC_INIT); + break; default: break; } @@ -2324,6 +2379,28 @@ void errorHandler(const std::string& title_string, const std::string& message_st } } +namespace +{ + // argv as handed to the entry point, stashed before LLAppViewer exists. + std::vector sStartupArgs; + + std::string getStartupLogFileName(); + std::string getOldLogFileName(const std::string& log_file); +} + +// static +void LLAppViewer::setStartupCommandLine(int argc, char** argv) +{ + sStartupArgs.clear(); + for (int i = 0; i < argc; ++i) + { + if (argv[i]) + { + sStartupArgs.emplace_back(argv[i]); + } + } +} + void LLAppViewer::initLoggingAndGetLastDuration() { // @@ -2340,22 +2417,25 @@ void LLAppViewer::initLoggingAndGetLastDuration() if (mSecondInstance) { - LLFile::mkdir(gDirUtilp->getDumpLogsDirPath()); + if (!gGPUBenchmarkMode) + { + LLFile::mkdir(gDirUtilp->getDumpLogsDirPath()); - LLUUID uid; - uid.generate(); - LLError::logToFile(gDirUtilp->getDumpLogsDirPath(uid.asString() + ".log")); + LLUUID uid; + uid.generate(); + // Is this even useful? + // Originally this wa used to store states, but I don't think it's practical with bugsplat attributes. + // So it just spams files now. + LLError::logToFile(gDirUtilp->getDumpLogsDirPath(uid.asString() + ".log")); + } } else { // Remove the last ".old" log file. - std::string old_log_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, - "Alchemy.old"); + std::string log_file = getStartupLogFileName(); + std::string old_log_file = getOldLogFileName(log_file); LLFile::remove(old_log_file); - // Get name of the log file - std::string log_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, - "Alchemy.log"); /* * Before touching any log files, compute the duration of the last run * by comparing the ctime of the previous start marker file with the ctime @@ -2403,7 +2483,7 @@ void LLAppViewer::initLoggingAndGetLastDuration() // Rename current log file to ".old" LLFile::rename(log_file, old_log_file); - // Set the log file to Alchemy.log + // Set the log file. LLError::logToFile(log_file); LL_INFOS() << "Started logging to " << log_file << LL_ENDL; if (!duration_log_msg.empty()) @@ -2542,6 +2622,105 @@ namespace LLStringUtil::null, OSMB_OK); } + + // Value of an option that takes one, in the spellings the viewer's own + // parser accepts: "--opt VALUE", "--opt=VALUE", and the "-opt" / "/opt" + // variants, with ':' allowed as the separator alongside '='. Last + // occurrence wins, which is what LLCommandLineParser does. + std::string findCommandLineValue(const std::vector& args, + const std::string& name) + { + const std::string forms[] = { "--" + name, "-" + name, "/" + name }; + std::string value; + + for (size_t i = 1; i < args.size(); ++i) + { + const std::string& arg = args[i]; + for (const std::string& form : forms) + { + if (arg == form) + { + if (i + 1 < args.size()) + { + value = args[i + 1]; + } + } + else if (arg.size() > form.size() + 1 && + arg.compare(0, form.size(), form) == 0 && + (arg[form.size()] == '=' || arg[form.size()] == ':')) + { + value = arg.substr(form.size() + 1); + } + } + } + return value; + } + + std::string getStartupLogFileName() + { + // This runs from LLAppViewer's constructor, so gSavedSettings has no + // controls yet and this cannot match today. Kept because it is the + // right answer if that ordering ever changes. + if (LLControlVariable* user_log_file = gSavedSettings.getControl("UserLogFile")) + { + std::string log_file = user_log_file->getValue().asString(); + if (!log_file.empty()) + { + return log_file; + } + } + + std::string log_file = findCommandLineValue(sStartupArgs, "logfile"); + +#if LL_WINDOWS + if (log_file.empty()) + { + // The native Win32 entry point is handed a command tail rather + // than argv, so setStartupCommandLine() was never called. Ask the + // OS instead. Keyed on an empty result rather than empty args, so + // this still covers an entry point that supplies only argv[0]. + int argc = 0; + if (LPWSTR* argw = CommandLineToArgvW(GetCommandLineW(), &argc)) + { + std::vector args; + args.reserve((size_t)argc); + for (int i = 0; i < argc; ++i) + { + args.push_back(ll_convert_wide_to_string(argw[i])); + } + LocalFree(argw); + log_file = findCommandLineValue(args, "logfile"); + } + } +#endif + + if (!log_file.empty()) + { + return log_file; + } + + return gDirUtilp->getExpandedFilename(LL_PATH_LOGS, "Alchemy.log"); + } + + std::string getOldLogFileName(const std::string& log_file) + { + std::string old_log_file = log_file; + size_t separator = old_log_file.find_last_of("/\\"); + size_t basename_start = (separator == std::string::npos) ? 0 : separator + 1; + size_t extension = old_log_file.find_last_of('.'); + + if (extension != std::string::npos && + extension > basename_start) + { + old_log_file.replace(extension, std::string::npos, ".old"); + } + else + { + old_log_file += ".old"; + } + + return old_log_file; + } } // anonymous namespace // Set a named control temporarily for this session, as when set via the command line --set option. @@ -2901,6 +3080,8 @@ bool LLAppViewer::initConfiguration() } } + LLGridManager::createInstance(); + LLSLURL start_slurl; if (!starting_location.empty()) { @@ -2943,19 +3124,39 @@ bool LLAppViewer::initConfiguration() { if (sendURLToOtherInstance(start_slurl.getSLURLString())) { - // successfully handed off URL to existing instance, exit + // Successfully handed off URL to existing instance. + // Returning 'false' gets treated as a failure to init, + // without cleanup, so instead clear markers and app here. + // Do not save settings. + // Might be smarter to have an exit code for a more reliable + // "early exit, needs cleanup" case. + LLGridManager::deleteSingleton(); + LLSingletonBase::deleteAll(); + cleanupConsole(); + delete mSettingsLocationList; + if (!mSecondInstance) + { + // Todo: Unfortunately, if we are doing this, stats and + // markers from previous session were already processed, + // cleared yet haven't been reported and will be lost. + // Consider a way to save those. + removeMarkerFiles(); + } return false; } } // Display splash screen. Must be after above check for previous // crash as this dialog is always frontmost. - std::string splash_msg; - LLStringUtil::format_map_t args; - args["[APP_NAME]"] = getSecondLifeTitle(); - splash_msg = LLTrans::getString("StartupLoading", args); - LLSplashScreen::show(); - LLSplashScreen::update(splash_msg); + if (!gGPUBenchmarkMode) + { + std::string splash_msg; + LLStringUtil::format_map_t args; + args["[APP_NAME]"] = getSecondLifeTitle(); + splash_msg = LLTrans::getString("StartupLoading", args); + LLSplashScreen::show(); + LLSplashScreen::update(splash_msg); + } //LLVolumeMgr::initClass(); LLVolumeMgr* volume_manager = new LLVolumeMgr(); @@ -2999,6 +3200,12 @@ bool LLAppViewer::initConfiguration() LLTrans::getString("MBAlreadyRunning"), LLStringUtil::null, OSMB_OK); + + // Since returning 'false' is basically an error without cleanup, + // do cleanup here. No need to worry about marker files here. + LLGridManager::deleteSingleton(); + LLSingletonBase::deleteAll(); + cleanupConsole(); return false; } @@ -3054,6 +3261,57 @@ bool LLAppViewer::initConfiguration() return true; // Config was successful. } +static void apply_localized_comments(LLControlGroup& group, const LLSD& comments) +{ + for (LLSD::map_const_iterator it = comments.beginMap(); it != comments.endMap(); ++it) + { + LLControlVariablePtr control = group.getControl(it->first); + if (control.notNull()) + { + const std::string comment = it->second.asString(); + if (!comment.empty()) + { + control->setComment(comment); + } + } + } +} + +void LLAppViewer::loadLocalizedSettingsComments() +{ + std::string lang = LLUI::getLanguage(); + if (lang.empty() || lang == "en") + { + return; + } + + std::string path = gDirUtilp->findSkinnedFilename( + LLDir::XUI, "settings_comments.xml", LLDir::CURRENT_SKIN); + if (path.empty()) + { + return; + } + + llifstream infile; + infile.open(path.c_str()); + if (!infile.is_open()) + { + return; + } + + LLSD comments; + if (LLSDParser::PARSE_FAILURE == LLSDSerialize::fromXML(comments, infile)) + { + infile.close(); + LL_WARNS("Settings") << "Failed to parse localized settings comments file: " << path << LL_ENDL; + return; + } + infile.close(); + + apply_localized_comments(gSavedSettings, comments); + apply_localized_comments(gSavedPerAccountSettings, comments); +} + // The following logic is replicated in initConfiguration() (to be able to get // some initial strings before we've finished initializing enough to know the // current language) and also in init() (to initialize for real). Somehow it @@ -3557,10 +3815,11 @@ LLSD LLAppViewer::getViewerInfo() const vlc_ver_codec << LIBVLC_VERSION_REVISION; info["LIBVLC_VERSION"] = vlc_ver_codec.str(); - S32 packets_in = (S32)LLViewerStats::instance().getRecording().getSum(LLStatViewer::PACKETS_IN); + LLTrace::Recording& recording = LLViewerStats::instance().getRecording(); + S32 packets_in = (S32)recording.getSum(LLStatViewer::PACKETS_IN); if (packets_in > 0) { - info["PACKETS_LOST"] = LLViewerStats::instance().getRecording().getSum(LLStatViewer::PACKETS_LOST); + info["PACKETS_LOST"] = recording.getSum(LLStatViewer::PACKETS_LOST); info["PACKETS_IN"] = packets_in; info["PACKETS_PCT"] = 100.f*info["PACKETS_LOST"].asReal() / info["PACKETS_IN"].asReal(); } @@ -3613,6 +3872,13 @@ std::string LLAppViewer::getViewerInfoString(bool default_string) const { args[ii->first] = LLTrans::getString("none_text", default_string); } + else if (ii->second.isBoolean()) + { + // LLSD intentionally renders false as an empty string to + // preserve string-to-boolean round trips. Viewer info is + // human-readable, so show both boolean states explicitly. + args[ii->first] = ii->second.asBoolean() ? "true" : "false"; + } else { // don't forget to render value asString() @@ -3748,8 +4014,12 @@ void LLAppViewer::writeSystemInfo() gDebugInfo["CPUInfo"]["CPUFamily"] = gSysCPU.getFamily(); gDebugInfo["CPUInfo"]["CPUMhz"] = (S32)gSysCPU.getMHz(); gDebugInfo["CPUInfo"]["CPUAltivec"] = gSysCPU.hasAltivec(); - gDebugInfo["CPUInfo"]["CPUSSE"] = gSysCPU.hasSSE(); - gDebugInfo["CPUInfo"]["CPUSSE2"] = gSysCPU.hasSSE2(); + gDebugInfo["CPUInfo"]["CPUSSE42"] = gSysCPU.hasSSE42(); + gDebugInfo["CPUInfo"]["CPUSSE4a"] = gSysCPU.hasSSE4a(); + gDebugInfo["CPUInfo"]["CPUAVX"] = gSysCPU.hasAVX(); + gDebugInfo["CPUInfo"]["CPUAVX2"] = gSysCPU.hasAVX2(); + gDebugInfo["CPUInfo"]["CPUAVX512F"] = gSysCPU.hasAVX512F(); + gDebugInfo["RAMInfo"]["Physical"] = LLSD::Integer(gSysMemory.getPhysicalMemoryKB().value()); gDebugInfo["RAMInfo"]["Allocated"] = LLSD::Integer(gMemoryAllocated.valueInUnits()); @@ -3997,12 +4267,24 @@ bool LLAppViewer::getMarkerData(const std::string& marker_name, std::string& dat void LLAppViewer::processMarkerFiles() { + if (gGPUBenchmarkMode) + { + // Skipping marker file processing in GPU benchmark mode + mSecondInstance = true; + initLoggingAndGetLastDuration(); + return; + } //We've got 4 things to test for here // - Other Process Running (Alchemy.exec_marker present, locked) // - Freeze (SecondLife.exec_marker present, not locked) // - LLError Crash (SecondLife.llerror_marker present) // - Other Crash (SecondLife.error_marker present) - // These checks should also remove these files for the last 2 cases if they currently exist + // - Watchdog freeze (SecondLife.watchdog_marker present) + // - Failed to initialize (SecondLife.inited_marker not present) + // - Potentially killed by task manager or computer + // didn't recover from hibernation (SecondLife.close_marker present) + // These checks should also remove these files for the last 2 cases + // if they currently exist std::ostringstream marker_log_stream; bool marker_is_same_version = true; @@ -4116,6 +4398,8 @@ void LLAppViewer::processMarkerFiles() // Bugsplat will set correct state in bugsplatSendLog. std::string error_marker_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, ERROR_MARKER_FILE_NAME); std::string watchdog_marker_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, WATCHDOG_MARKER_FILE_NAME); + std::string inited_marker_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, INITED_MARKER_FILE_NAME); + std::string close_marker_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, CLOSE_EVENT_MARKER_FILE_NAME); if(LLFile::isfile(error_marker_file)) { S32 marker_code = getMarkerErrorCode(error_marker_file); @@ -4154,11 +4438,13 @@ void LLAppViewer::processMarkerFiles() } else { - // so only check watchdog marker if there is no error marker. - if (LLFile::isfile(watchdog_marker_file)) + if (LAST_EXEC_UNKNOWN == gLastExecEvent + || LAST_EXEC_LOGOUT_UNKNOWN == gLastExecEvent) { - if (LAST_EXEC_UNKNOWN == gLastExecEvent - || LAST_EXEC_LOGOUT_UNKNOWN == gLastExecEvent) + // If viewer crashed after a freeze was detected, + // crash still takes precendence. + // So only check watchdog marker if there is no error marker. + if (LLFile::isfile(watchdog_marker_file)) { // watchdog marker gets created if we detect a freeze, // so if viwer did not stop gracefully, and we know it wasn't a crash, @@ -4170,9 +4456,49 @@ void LLAppViewer::processMarkerFiles() << LL_ENDL; } } - removeWatchdogMarker(); + // If 'close' marker is found, viewer either started shutdown but + // failed, OS did not recover from hibernation or viewer got + // killed by task manager. + // Marker does not indicate that viewer was closed or is closing, + // just that 'close' was requested before viewer died. + else if (LLFile::isfile(close_marker_file)) + { + // Unfortunately we can't reliably distinguish + // task manager's case from genuine shutdown, so we + // have to report all of them as the same thing. + // Todo: but we can distinguish hibernation, might want + // to simply not report it as an issue. + if (markerIsSameVersion(close_marker_file)) + { + gLastExecEvent = LAST_EXEC_OS_EVENT; + LL_INFOS("MarkerFile") << "'Close' marker '" << close_marker_file << "' found, setting LastExecEvent to OS_EVENT" + << LL_ENDL; + } + } + else if ((LAST_EXEC_UNKNOWN == gLastExecEvent) + && !LLFile::isfile(inited_marker_file)) + { + // Viewer didn't get to a login screen. + gLastExecEvent = LAST_EXEC_INIT; + LL_INFOS("MarkerFile") << "'Inited' marker '" + << inited_marker_file + << "' not found, assuming that init crashed." + << LL_ENDL; + } } } + if (LLFile::isfile(watchdog_marker_file)) + { + removeWatchdogMarker(); + } + if (LLFile::isfile(inited_marker_file)) + { + removeInitedMarker(); + } + if (LLFile::isfile(close_marker_file)) + { + removeCloseRequestMarker(); + } #if LL_DARWIN if (!mSecondInstance && gLastExecEvent != LAST_EXEC_NORMAL) @@ -4216,6 +4542,7 @@ void LLAppViewer::removeMarkerFiles() { LL_WARNS("MarkerFile") << "logout marker '"<capabilitiesReceived()) + { + constexpr bool include_preferences = true; + send_viewer_stats(include_preferences); + } sendLogoutRequest(); } @@ -4375,6 +4709,14 @@ void LLAppViewer::abortQuit() mClosingFloaters = false; } +void LLAppViewer::sendViewerStatistics(bool include_preferences) +{ + if (!gDisconnected) + { + send_viewer_stats(include_preferences); + } +} + void LLAppViewer::migrateCacheDirectory() { #if LL_WINDOWS || LL_DARWIN @@ -4996,6 +5338,7 @@ void LLAppViewer::idle() static LLCachedControl downscale_method(gSavedSettings, "RenderDownScaleMethod"); gGLManager.mDownScaleMethod = downscale_method; LLImageGL::updateClass(); + LLUIImage::updateClass(); // Service the WorkQueue we use for replies from worker threads. // Use function statics for the timeslice setting so we only have to fetch @@ -5639,6 +5982,61 @@ bool LLAppViewer::errorMarkerExists() const return LLFile::isfile(error_marker_file); } +void LLAppViewer::createCloseRequestMarker() const +{ + // WINDOW THREAD! since we need this to act fast. + // This does not indicate that viewer was closed or is closing, + // but that 'close' was requested. + if (!mSecondInstance) + { + std::string close_marker = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, CLOSE_EVENT_MARKER_FILE_NAME); + + LLFile file; + std::error_code ec; + file.open(close_marker, LLFile::out|LLFile::trunc|LLFile::binary, ec); + if (file) + { + recordMarkerVersion(file); + file.close(); + } + } +} + +void LLAppViewer::removeCloseRequestMarker() const +{ + if (!mSecondInstance) + { + std::string close_marker = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, CLOSE_EVENT_MARKER_FILE_NAME); + LLFile::remove(close_marker, ENOENT); + } +} + +void LLAppViewer::createInitedMarker() const +{ + if (!mSecondInstance) + { + std::string inited_marker = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, INITED_MARKER_FILE_NAME); + + LLFile file; + std::error_code ec; + file.open(inited_marker, LLFile::out|LLFile::trunc|LLFile::binary, ec); + if (file) + { + recordMarkerVersion(file); + file.close(); + } + } +} + +void LLAppViewer::removeInitedMarker() const +{ + if (!mSecondInstance) + { + std::string inited_marker = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, INITED_MARKER_FILE_NAME); + LLFile::remove(inited_marker, ENOENT); + } +} + void LLAppViewer::createWatchdogMarker() const { if (!mSecondInstance) @@ -5655,12 +6053,13 @@ void LLAppViewer::createWatchdogMarker() const } } } + void LLAppViewer::removeWatchdogMarker() const { if (!mSecondInstance) { std::string error_marker_file = gDirUtilp->getExpandedFilename(LL_PATH_LOGS, WATCHDOG_MARKER_FILE_NAME); - LLFile::remove(error_marker_file); + LLFile::remove(error_marker_file, ENOENT); } } @@ -5682,6 +6081,29 @@ void LLAppViewer::outOfMemorySoftQuit() } } +void LLAppViewer::setPermitOSHibernation(bool permit) +{ + if (permit) + { + if (mCurrentHibernationMode != LL_HIBERNATE_MODE_DEFAULT) + { + // Will call OS specific code to let OS hibernate when idle + setOSHibernationMode(LL_HIBERNATE_MODE_DEFAULT); + mCurrentHibernationMode = LL_HIBERNATE_MODE_DEFAULT; + } + } + else + { + static LLCachedControl os_hibernation_mode(gSavedSettings, "OSHibernationMode", 0); + eHibernationMode mode = static_cast(os_hibernation_mode()); + if (mode != LL_HIBERNATE_MODE_DEFAULT && mCurrentHibernationMode != mode) + { + setOSHibernationMode(mode); + mCurrentHibernationMode = mode; + } + } +} + void LLAppViewer::idleNameCache() { // Neither old nor new name cache can function before agent has a region @@ -5719,7 +6141,6 @@ void LLAppViewer::idleNetwork() pingMainloopTimeout("idleNetwork"); gObjectList.mNumNewObjects = 0; - S32 total_decoded = 0; static LLCachedControl speed_test(gSavedSettings, "SpeedTest", false); if (!speed_test()) @@ -5727,64 +6148,56 @@ void LLAppViewer::idleNetwork() LL_PROFILE_ZONE_NAMED_CATEGORY_NETWORK("idle network"); // decode LLTimer check_message_timer; - // Read all available packets from network const S64 frame_count = gFrameCount; // U32->S64 - F32 total_time = 0.0f; + S32 total_decoded = 0; + // Process packets from network + LockMessageChecker lmc(gMessageSystem); + while (lmc.checkAllMessages(frame_count, gServicePump)) { - bool needs_drain = false; - LockMessageChecker lmc(gMessageSystem); - while (lmc.checkAllMessages(frame_count, gServicePump)) - { - if (gDoDisconnect) - { - // We're disconnecting, don't process any more messages from the server - // We're usually disconnecting due to either network corruption or a - // server going down, so this is OK. - break; - } + ++total_decoded; - total_decoded++; - - if (total_decoded > MESSAGE_MAX_PER_FRAME) + // Time-box processing of network packets to prevent framerate catastrophe + if (check_message_timer.getElapsedTimeF32() >= CheckMessagesMaxTime) + { + // Drain the socket buffer so we know how many messages remain to process + S32 num_buffered_packets = gMessageSystem->drainUdpSocket(); + if (num_buffered_packets > total_decoded) { - needs_drain = true; - break; + // Grow CheckMessagesMaxTime until we process more packets each frame than arrive. + // This might spiral out of control on very slow computers on fast networks when + // the bandwidth settings are too high. There is a mechanism for providing backpressure + // to network bandwidth but it may be inadequate for the task + // (see LLViewerThrottle::updateDynamicThrottle() for more details). + CheckMessagesMaxTime *= 1.035f; // 3.5% ~= 2x in 20 frames, ~8x in 60 frames } - - // Prevent slow packets from completely destroying the frame rate. - // This usually happens due to clumps of avatars taking huge amount - // of network processing time (which needs to be fixed, but this is - // a good limit anyway). - total_time = check_message_timer.getElapsedTimeF32(); - if (total_time >= CheckMessagesMaxTime) + else if (num_buffered_packets == 0) { - needs_drain = true; - break; + // Reset CheckMessagesMaxTime to default value + CheckMessagesMaxTime = CHECK_MESSAGES_DEFAULT_MAX_TIME; } + break; } - if (needs_drain || gMessageSystem->mPacketRing.getNumBufferedPackets() > 0) + + if (total_decoded > MESSAGE_MAX_PER_FRAME) { - // Rather than allow packets to silently backup on the socket - // we drain them into our own buffer so we know how many exist. - S32 num_buffered_packets = gMessageSystem->drainUdpSocket(); - if (num_buffered_packets > 0) - { - // Increase CheckMessagesMaxTime so that we will eventually catch up - CheckMessagesMaxTime *= 1.035f; // 3.5% ~= 2x in 20 frames, ~8x in 60 frames - } + // MESSAGE_MAX_PER_FRAME is very high (400) + // We expect to run out of time before reaching here, but just in case... + gMessageSystem->drainUdpSocket(); + break; } - else + + if (gDoDisconnect) { - // Reset CheckMessagesMaxTime to default value - CheckMessagesMaxTime = CHECK_MESSAGES_DEFAULT_MAX_TIME; + // We're disconnecting so no need to process packets. + break; } + } - // Handle per-frame message system processing. + // Handle per-frame message system processing. - static LLCachedControl ack_collection_time(gSavedSettings, "AckCollectTime", 0.1f); - lmc.processAcks(ack_collection_time()); - } + static LLCachedControl ack_collection_time(gSavedSettings, "AckCollectTime", 0.1f); + lmc.processAcks(ack_collection_time()); } add(LLStatViewer::NUM_NEW_OBJECTS, gObjectList.mNumNewObjects); @@ -5901,6 +6314,9 @@ void LLAppViewer::disconnectViewer() // Pass the connection state to LLUrlEntryParcel not to attempt // parcel info requests while disconnected. LLUrlEntryParcel::setDisconnected(gDisconnected); + + // Restore default OS hibernation mode + setPermitOSHibernation(true); } void LLAppViewer::forceErrorLLError() @@ -6115,6 +6531,32 @@ F32 LLAppViewer::getMainloopTimeoutSec() const } } +std::string LLAppViewer::getMainloopWatchdogState() const +{ + if (!mMainloopTimeout) + { + return std::string(); + } + std::string state = mMainloopTimeout->getState(); + + if (mMainloopTimeout->hasExpired()) + { + return "Expired at " + state; + } + + // Check if the watchdog is currently active (timer started) + if (!mMainloopTimeout->isAlive()) + { + // Timer is not running, meaning watchdog is paused/stopped + if (state.empty()) + { + return "Paused"; + } + return "Paused at " + state; + } + return state; +} + void LLAppViewer::handleLoginComplete() { gLoggedInTime.start(); @@ -6177,6 +6619,15 @@ void LLAppViewer::handleLoginComplete() // we logged in successfully, so save settings on logout LL_INFOS() << "Login successful, per account settings will be saved on log out." << LL_ENDL; mSavePerAccountSettings=true; + + // Don't allow hibernation while we're running + setPermitOSHibernation(false); + // Track 'hibernation' mode changes + mOSHibernationModeChangeConnection = gSavedSettings.getControl("OSHibernationMode")->getSignal()->connect([](LLControlVariable* control, const LLSD& new_val, const LLSD& old_val) + { + // setPermitOSHibernation will sort itself out based on new mode. + LLAppViewer::instance()->setPermitOSHibernation(false); + }); } //virtual diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h index f8c0a831bed..162d62367d9 100644 --- a/indra/newview/llappviewer.h +++ b/indra/newview/llappviewer.h @@ -17,7 +17,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -74,9 +74,10 @@ typedef enum LAST_EXEC_LOGOUT_CRASH, LAST_EXEC_BAD_ALLOC, LAST_EXEC_MISSING_FILES, - LAST_EXEC_GRAPHICS_INIT, + LAST_EXEC_INIT, LAST_EXEC_UNKNOWN, LAST_EXEC_LOGOUT_UNKNOWN, + LAST_EXEC_OS_EVENT, LAST_EXEC_COUNT } eLastExecEvent; @@ -111,6 +112,13 @@ class LLAppViewer : public LLApp const LLSD& substitutions = LLSD()); // Display an error dialog and forcibly quit. void earlyExitNoNotify(); // Do not display error dialog then forcibly quit. void abortQuit(); // Called to abort a quit request. + void sendViewerStatistics(bool include_preferences); + + // Hand the process's argv to the viewer. Must be called from the entry + // point BEFORE LLAppViewer is constructed: the startup log file name is + // decided in the constructor, long before gSavedSettings has any controls, + // so --logfile can only be read from the raw command line. + static void setStartupCommandLine(int argc, char** argv); bool quitRequested() { return mQuitRequested; } bool logoutRequestSent() { return mLogoutRequestSent; } @@ -208,6 +216,7 @@ class LLAppViewer : public LLApp void pingMainloopTimeout(std::string_view state); F32 getMainloopTimeoutSec() const; + std::string getMainloopWatchdogState() const; // Handle the 'login completed' event. // *NOTE:Mani Fix this for login abstraction!! @@ -252,6 +261,10 @@ class LLAppViewer : public LLApp void createErrorMarker(eLastExecEvent error_code) const; bool errorMarkerExists() const; + void createCloseRequestMarker() const; + void removeCloseRequestMarker() const; + void createInitedMarker() const; + void removeInitedMarker() const; void createWatchdogMarker() const; void removeWatchdogMarker() const; @@ -261,6 +274,8 @@ class LLAppViewer : public LLApp // Note: mQuitRequested can be aborted by user. void outOfMemorySoftQuit(); + virtual void setPermitOSHibernation(bool permit); + #ifdef LL_DISCORD static void initDiscordSocial(); static void updateDiscordActivity(); @@ -272,10 +287,19 @@ class LLAppViewer : public LLApp virtual bool initWindow(); // Initialize the viewer's window. virtual void initLoggingAndGetLastDuration(); // Initialize log files, logging system virtual void initConsole() {}; // Initialize OS level debugging console. + virtual void cleanupConsole() {}; // Cleanup OS level debugging console. virtual bool initHardwareTest() { return true; } // A false result indicates the app should quit. virtual bool initSLURLHandler(); virtual bool sendURLToOtherInstance(const std::string& url); + typedef enum + { + LL_HIBERNATE_MODE_DEFAULT = 0, // Use the platform's default behavior. + LL_HIBERNATE_MODE_PREVENT = 1, + LL_HIBERNATE_MODE_PREVENT_SCREEN = 2, + } eHibernationMode; + virtual void setOSHibernationMode(eHibernationMode mode); + virtual bool initParseCommandLine(LLCommandLineParser& clp) { return true; } // Allow platforms to specify the command line args. @@ -301,6 +325,7 @@ class LLAppViewer : public LLApp bool initThreads(); // Initialize viewer threads, return false on failure. bool initConfiguration(); // Initialize settings from the command line/config file. void initStrings(); // Initialize LLTrans machinery + void loadLocalizedSettingsComments(); // Override Debug Settings comments for current locale bool initCache(); // Initialize local client cache. // We have switched locations of both Mac and Windows cache, make sure @@ -379,6 +404,9 @@ class LLAppViewer : public LLApp LLAppCoreHttp mAppCoreHttp; bool mIsFirstRun; + + eHibernationMode mCurrentHibernationMode = LL_HIBERNATE_MODE_DEFAULT; + boost::signals2::scoped_connection mOSHibernationModeChangeConnection; }; // Globals with external linkage. From viewer.h diff --git a/indra/newview/llappviewersdl.cpp b/indra/newview/llappviewersdl.cpp index 29e5c8ebdc8..3b3a87057b3 100644 --- a/indra/newview/llappviewersdl.cpp +++ b/indra/newview/llappviewersdl.cpp @@ -100,6 +100,7 @@ static void handleUrl(const char* url_utf8); #if LL_LINUX && LL_DBUS #include +#include // close() for the logind inhibitor fd #define VIEWERAPI_SERVICE "com.secondlife.ViewerAppAPIService" #define VIEWERAPI_PATH "/com/secondlife/ViewerAppAPI" @@ -112,6 +113,82 @@ static void handleUrl(const char* url_utf8); extern void default_unix_signal_handler(int, siginfo_t *, void *); #endif +#if LL_DARWIN +#include +static IOPMAssertionID sPowerAssertionID = kIOPMNullAssertionID; +#elif LL_LINUX && LL_DBUS +// Held open for as long as system sleep is inhibited; -1 when it is not. +static int sSleepInhibitFd = -1; + +// org.freedesktop.login1.Manager.Inhibit returns a file descriptor that keeps +// the inhibition alive until it is closed. +// +// "idle" rather than "sleep": we want to stop the machine idling to sleep +// under the viewer, which is what Windows' ES_SYSTEM_REQUIRED and macOS' +// NoIdleSleep do. Blocking "sleep" would also refuse a lid close or an +// explicit suspend, which no other platform here does. +static int take_logind_idle_inhibitor() +{ + DBusError err; + dbus_error_init(&err); + + DBusConnection* bus = dbus_bus_get(DBUS_BUS_SYSTEM, &err); + if (!bus) + { + LL_WARNS("OS") << "Cannot connect to system bus: " << err.message << LL_ENDL; + dbus_error_free(&err); + return -1; + } + + // We pump nothing on this connection and must not die with it. + dbus_connection_set_exit_on_disconnect(bus, false); + + DBusMessage* message = dbus_message_new_method_call("org.freedesktop.login1", + "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", + "Inhibit"); + if (!message) + { + dbus_connection_unref(bus); + return -1; + } + + const char* what = "idle"; + const char* who = "Alchemy Viewer"; + const char* why = "Viewer is running"; + const char* how = "block"; + dbus_message_append_args(message, + DBUS_TYPE_STRING, &what, + DBUS_TYPE_STRING, &who, + DBUS_TYPE_STRING, &why, + DBUS_TYPE_STRING, &how, + DBUS_TYPE_INVALID); + + DBusMessage* reply = dbus_connection_send_with_reply_and_block(bus, message, 2000, &err); + dbus_message_unref(message); + + if (!reply) + { + LL_WARNS("OS") << "logind Inhibit failed: " << err.message << LL_ENDL; + dbus_error_free(&err); + dbus_connection_unref(bus); + return -1; + } + + int fd = -1; + if (!dbus_message_get_args(reply, &err, DBUS_TYPE_UNIX_FD, &fd, DBUS_TYPE_INVALID)) + { + LL_WARNS("OS") << "logind Inhibit returned no fd: " << err.message << LL_ENDL; + dbus_error_free(&err); + fd = -1; + } + dbus_message_unref(reply); + dbus_connection_unref(bus); + + return fd; +} +#endif + namespace { int gArgC = 0; @@ -577,6 +654,10 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) gArgC = argc; gArgV = argv; + // Before the viewer is constructed: its constructor picks the startup log + // file, and --logfile is only readable from here at that point. + LLAppViewer::setStartupCommandLine(argc, argv); + #if LL_WINDOWS // Note: gIconResource (consumed by the native LLWindowWin32 window proc) // is not set here — SDL_RegisterApp sources the window-class icon from the @@ -1034,6 +1115,70 @@ bool LLAppViewerSDL::sendURLToOtherInstance(const std::string& url) } #endif // LL_DBUS +//virtual +void LLAppViewerSDL::setOSHibernationMode(eHibernationMode mode) +{ + // Deliberately no SDL_{Disable,Enable}ScreenSaver here. That flag is a + // single absolute global which llwindowsdl.cpp already drives from window + // focus, so writing it from here as well would have the two stomp each + // other. The screen half is handled by the platform assertion below. + +#if LL_DARWIN + // An IOPMAssertion is released by name, so drop the previous one before + // taking a new one - otherwise switching modes leaks the old assertion and + // the strongest one ever taken wins forever. + if (sPowerAssertionID != kIOPMNullAssertionID) + { + IOPMAssertionRelease(sPowerAssertionID); + sPowerAssertionID = kIOPMNullAssertionID; + } + + if (mode != LL_HIBERNATE_MODE_DEFAULT) + { + // NoDisplaySleep implies NoIdleSleep, so the screen mode needs only + // the one assertion. + CFStringRef type = (mode == LL_HIBERNATE_MODE_PREVENT_SCREEN) + ? kIOPMAssertionTypeNoDisplaySleep + : kIOPMAssertionTypeNoIdleSleep; + IOReturn result = IOPMAssertionCreateWithName(type, kIOPMAssertionLevelOn, + CFSTR("Alchemy Viewer"), + &sPowerAssertionID); + if (result != kIOReturnSuccess) + { + sPowerAssertionID = kIOPMNullAssertionID; + LL_WARNS("OS") << "IOPMAssertionCreateWithName failed: " << result << LL_ENDL; + } + } +#elif LL_LINUX && LL_DBUS + // logind hands back a file descriptor; holding it open is the inhibition, + // closing it releases it. Drop any previous one first, same reason as the + // macOS assertion above. + if (sSleepInhibitFd >= 0) + { + close(sSleepInhibitFd); + sSleepInhibitFd = -1; + } + + if (mode != LL_HIBERNATE_MODE_DEFAULT) + { + sSleepInhibitFd = take_logind_idle_inhibitor(); + if (sSleepInhibitFd < 0) + { + LL_WARNS("OS") << "Could not inhibit idle sleep via logind." << LL_ENDL; + } + else if (mode == LL_HIBERNATE_MODE_PREVENT_SCREEN) + { + // Keeping the display awake is the session screensaver's business, + // not logind's, and this backend does not speak to it yet. + LL_INFOS("OS") << "Idle sleep inhibited; keeping the display awake " + "is not implemented on this platform." << LL_ENDL; + } + } +#endif + + LL_INFOS("OS") << "OS hibernation mode set to " << (S32)mode << LL_ENDL; +} + void LLAppViewerSDL::initCrashReporting(bool reportFreeze) { } diff --git a/indra/newview/llappviewersdl.h b/indra/newview/llappviewersdl.h index c52d962d88e..e25bf486959 100644 --- a/indra/newview/llappviewersdl.h +++ b/indra/newview/llappviewersdl.h @@ -58,6 +58,8 @@ class LLAppViewerSDL final : public LLAppViewer bool initSLURLHandler() override; bool sendURLToOtherInstance(const std::string& url) override; + + void setOSHibernationMode(eHibernationMode mode) override; }; #endif // LL_LLAPPVIEWERSDL_H diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index aada10e8802..a94243adc7a 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -57,12 +57,11 @@ #include -#include "llversioninfovars.h" - // Velopack installer and update framework #if LL_VELOPACK #include "llvelopack.h" #endif +#include "llversioninfovars.h" // Sentry (https://sentry.io) crash reporting tool #if AL_SENTRY @@ -197,6 +196,13 @@ namespace LLAppViewer* app = LLAppViewer::instance(); + // Include mainloop watchdog state if available + std::string watchdog_state = app->getMainloopWatchdogState(); + if (!watchdog_state.empty()) + { + sBugSplatSender->setAttribute(WCSTR(L"WatchdogState"), WCSTR(watchdog_state)); + } + if (!app->isSecondInstance() && !app->errorMarkerExists()) { // If marker doesn't exist, create a marker with 'other' or 'logout' code for next launch @@ -218,6 +224,8 @@ namespace } #endif // LL_BUGSPLAT +extern bool gGPUBenchmarkMode; + namespace { void (*gOldTerminateHandler)() = NULL; @@ -525,12 +533,21 @@ int APIENTRY WINMAIN(HINSTANCE hInstance, // *FIX: global gIconResource = MAKEINTRESOURCE(IDI_LL_ICON); + // Benchmark subprocess mode before full init for LLFeatureManager::loadGPUClass(). + { + std::wstring cmdLineStr(pCmdLine ? pCmdLine : L""); + if (cmdLineStr.find(L"--gpubenchmark") != std::wstring::npos) + { + gGPUBenchmarkMode = true; + } + } + LLAppViewerWin32* viewer_app_ptr = new LLAppViewerWin32(ll_convert_wide_to_string(pCmdLine).c_str()); gOldTerminateHandler = std::set_terminate(exceptionTerminateHandler); // Set a debug info flag to indicate if multiple instances are running. - bool found_other_instance = !create_app_mutex(); + bool found_other_instance = gGPUBenchmarkMode || !create_app_mutex(); gDebugInfo["FoundOtherInstanceAtStartup"] = LLSD::Boolean(found_other_instance); bool ok = viewer_app_ptr->init(); @@ -842,12 +859,7 @@ bool LLAppViewerWin32::cleanup() bool result = LLAppViewer::cleanup(); gDXHardware.cleanup(); - - if (mIsConsoleAllocated) - { - FreeConsole(); - mIsConsoleAllocated = false; - } + cleanupConsole(); return result; } @@ -927,10 +939,22 @@ void LLAppViewerWin32::initLoggingAndGetLastDuration() void LLAppViewerWin32::initConsole() { // pop up debug console - mIsConsoleAllocated = create_console(); + if (!gGPUBenchmarkMode) + { + mIsConsoleAllocated = create_console(); + } return LLAppViewer::initConsole(); } +void LLAppViewerWin32::cleanupConsole() +{ + if (mIsConsoleAllocated) + { + FreeConsole(); + mIsConsoleAllocated = false; + } +} + void write_debug_dx(const char* str) { std::string value = gDebugInfo["DXInfo"].asString(); @@ -1003,7 +1027,7 @@ bool LLAppViewerWin32::sendURLToOtherInstance(const std::string& url) if (other_window != NULL) { - LL_DEBUGS() << "Found other window with the name '" << getWindowTitle() << "'" << LL_ENDL; + LL_DEBUGS("AppInit") << "Found other window with the name '" << getWindowTitle() << "'" << LL_ENDL; COPYDATASTRUCT cds; const S32 SLURL_MESSAGE_TYPE = 0; cds.dwData = SLURL_MESSAGE_TYPE; @@ -1011,13 +1035,281 @@ bool LLAppViewerWin32::sendURLToOtherInstance(const std::string& url) cds.lpData = (void*)url.c_str(); LRESULT msg_result = SendMessage(other_window, WM_COPYDATA, NULL, (LPARAM)&cds); - LL_DEBUGS() << "SendMessage(WM_COPYDATA) to other window '" + LL_DEBUGS("AppInit") << "SendMessage(WM_COPYDATA) to other window '" << getWindowTitle() << "' returned " << msg_result << LL_ENDL; return true; } return false; } +void LLAppViewerWin32::setOSHibernationMode(eHibernationMode mode) +{ + // ES_CONTINUOUS tells Windows to reset the idle timer + // and restore normal operation + // ES_SYSTEM_REQUIRED prevents system sleep/hibernation + // ES_DISPLAY_REQUIRED prevents display sleep + + if (mode == LL_HIBERNATE_MODE_DEFAULT) + { + // Allow OS to hibernate - clear the previous execution state flags + // ES_CONTINUOUS without other flags allows the system to idle normally + SetThreadExecutionState(ES_CONTINUOUS); + LL_INFOS("OS") << "Permitted OS hibernation/sleep" << LL_ENDL; + } + else if (mode == LL_HIBERNATE_MODE_PREVENT) + { + // Prevent OS from hibernating while viewer is running + // ES_CONTINUOUS | ES_SYSTEM_REQUIRED keeps the system awake + EXECUTION_STATE result = SetThreadExecutionState( + ES_CONTINUOUS | ES_SYSTEM_REQUIRED + ); + if (result == NULL) + { + LL_WARNS("OS") << "Failed to prevent OS hibernation, error: " << GetLastError() << LL_ENDL; + } + else + { + LL_INFOS("OS") << "Prevented OS hibernation, but allowed display sleep" << LL_ENDL; + } + } + else if (mode == LL_HIBERNATE_MODE_PREVENT_SCREEN) + { + // Prevent OS from hibernating or turning screen off while viewer is running + // ES_CONTINUOUS | ES_SYSTEM_REQUIRED keeps the system awake + // ES_DISPLAY_REQUIRED keeps the display on + EXECUTION_STATE result = SetThreadExecutionState( + ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED + ); + + if (result == NULL) + { + LL_WARNS("OS") << "Failed to prevent OS hibernation and display sleep, error: " << GetLastError() << LL_ENDL; + } + else + { + LL_INFOS("OS") << "Prevented OS hibernation/sleep" << LL_ENDL; + } + } +} + +bool LLAppViewerWin32::sendShutdownToOtherInstances(const std::wstring& install_dir) +{ + // Velopack installs viewer like this: + // %appdata%\Local\ChannelNameViewer\Update.exe // which is our uninstaller + // %appdata%\Local\ChannelNameViewer\SecondLifeViewer.exe // wrapper, redirects to main executable + // %appdata%\Local\ChannelNameViewer\current\SecondLifeViewer.exe // main executable + // For reliability don't expect install_dir to be actually in the base path, strip 'current' + + const std::wstring current_suffix = L"\\current"; + std::wstring normalized_path(install_dir); + if (normalized_path.length() >= current_suffix.length() && + _wcsicmp(normalized_path.c_str() + normalized_path.length() - current_suffix.length(), + current_suffix.c_str()) == 0) + { + normalized_path.resize(normalized_path.length() - current_suffix.length()); + } + + wchar_t window_class[256]; // Assume max length < 255 chars. + mbstowcs(window_class, sWindowClass, 255); + window_class[255] = 0; + + // Normalize the directory path + wchar_t our_dir_normalized[MAX_PATH]; + wchar_t* file_part = nullptr; + DWORD result = GetFullPathNameW(normalized_path.c_str(), MAX_PATH, our_dir_normalized, &file_part); + if (result == 0 || result >= MAX_PATH) + { + LL_WARNS() << "Failed to normalize our executable path" << LL_ENDL; + return false; + } + + // Remove trailing backslash if present + size_t dir_len = wcslen(our_dir_normalized); + if (dir_len > 0 && our_dir_normalized[dir_len - 1] == L'\\') + { + our_dir_normalized[dir_len - 1] = L'\0'; + dir_len--; + } + + // This message is meant for velopack, so we don't expect to have + // a window of our own, store any matching windows. + struct EnumData + { + const wchar_t* target_class; + const wchar_t* our_dir_normalized; + std::vector found_windows; + }; + + EnumData enum_data; + enum_data.target_class = window_class; + enum_data.our_dir_normalized = our_dir_normalized; + + // Callback function to find all matching windows + auto find_windows_callback = [](HWND hwnd, LPARAM lParam) -> BOOL + { + EnumData* data = reinterpret_cast(lParam); + wchar_t class_name[256]; + + if (GetClassName(hwnd, class_name, 256) > 0) + { + if (wcscmp(class_name, data->target_class) == 0) + { + // Get the process ID for this window + DWORD process_id = 0; + GetWindowThreadProcessId(hwnd, &process_id); + + // Open the process to query its executable path + HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, process_id); + if (hProcess) + { + wchar_t exe_path[MAX_PATH]; + DWORD size = MAX_PATH; + if (QueryFullProcessImageNameW(hProcess, 0, exe_path, &size)) + { + // Normalize the other process's path + wchar_t other_dir_normalized[MAX_PATH]; + wchar_t* other_file_part = nullptr; + DWORD result = GetFullPathNameW(exe_path, MAX_PATH, other_dir_normalized, &other_file_part); + + if (result > 0 && result < MAX_PATH) + { + // Remove the filename part to get just the directory + // We are doing this to avoid incidents, like having + // multiple viewer version exes in the same folder. + if (other_file_part) + { + *other_file_part = L'\0'; + } + + // Remove trailing backslash if present + size_t other_dir_len = wcslen(other_dir_normalized); + if (other_dir_len > 0 && other_dir_normalized[other_dir_len - 1] == L'\\') + { + other_dir_normalized[other_dir_len - 1] = L'\0'; + other_dir_len--; + } + + // Strip "\current" suffix if present to normalize comparison + // This handles both release (with \current) and debug builds (without) + const std::wstring current_suffix = L"\\current"; + if (other_dir_len >= current_suffix.length()) + { + size_t offset = other_dir_len - current_suffix.length(); + if (_wcsicmp(other_dir_normalized + offset, current_suffix.c_str()) == 0) + { + other_dir_normalized[offset] = L'\0'; + } + } + + // Compare directories (case-insensitive) + if (_wcsicmp(other_dir_normalized, data->our_dir_normalized) == 0) + { + data->found_windows.push_back(hwnd); + } + } + } + CloseHandle(hProcess); + } + } + } + + return TRUE; // Continue enumeration + }; + + // Find all matching windows and send shutdown messages + EnumWindows(find_windows_callback, reinterpret_cast(&enum_data)); + + if (enum_data.found_windows.empty()) + { + LL_DEBUGS("AppInit") << "No other instances found" << LL_ENDL; + return false; + } + + LL_INFOS("AppInit") << "Found " << (S32)(enum_data.found_windows.size()) << " other instance(s), sending shutdown messages" << LL_ENDL; + + // Get our own process ID to include in the message + DWORD our_process_id = GetCurrentProcessId(); + + constexpr UINT timeout_ms = 2000; // 2s. Viewer's message thread is supposed to be fast. + for (HWND other_window : enum_data.found_windows) + { + if (IsWindow(other_window)) + { + DWORD_PTR result = 0; + LRESULT send_result = SendMessageTimeout( + other_window, + WM_POST_UNINSTALL_, + static_cast(our_process_id), + static_cast(WM_POST_UNINSTALL_MSG_SHUTDOWN), + SMTO_ABORTIFHUNG | SMTO_BLOCK, + timeout_ms, + &result + ); + + if (send_result == 0) + { + DWORD error = GetLastError(); + if (error == ERROR_TIMEOUT) + { + LL_WARNS("AppInit") << "Shutdown message timed out for window " << std::hex << other_window << std::dec << LL_ENDL; + } + else + { + LL_WARNS("AppInit") << "Failed to send shutdown message to window " << std::hex << other_window + << ", error: " << error << std::dec << LL_ENDL; + } + + PostMessage(other_window, WM_CLOSE, 0, 0); + } + else + { + LL_DEBUGS("AppInit") << "Shutdown message sent successfully to window " << std::hex << other_window << std::dec << LL_ENDL; + } + } + } + + // Poll for up to 30 seconds, checking every 5 seconds + const S32 MAX_WAIT_TIME_MS = 60000; // 30 seconds + const S32 POLL_INTERVAL_MS = 5000; // 5 seconds + S32 elapsed_time_ms = 0; + size_t still_open_count = enum_data.found_windows.size(); + + while (elapsed_time_ms < MAX_WAIT_TIME_MS) + { + LL_INFOS("AppInit") << "Waiting for " << (S32)still_open_count << " instance(s) to close... (" + << (S32)(elapsed_time_ms / 1000) << "s elapsed)" << LL_ENDL; + + ms_sleep(POLL_INTERVAL_MS); + elapsed_time_ms += POLL_INTERVAL_MS; + + // Check if the specific windows we found still exist + // Don't enumerate all windows for new ones, assume that + // no instances were reused and assume user won't open + // the app again. For now just check our list. + still_open_count = 0; + for (HWND hwnd : enum_data.found_windows) + { + if (IsWindow(hwnd)) + { + still_open_count++; + } + } + + if (still_open_count == 0) + { + LL_INFOS("AppInit") << "All other instances have closed after " << (S32)(elapsed_time_ms / 1000) << " seconds" << LL_ENDL; + return false; + } + } + + if (still_open_count != 0) + { + LL_WARNS("AppInit") << "Proceeding with uninstall with " << (S32)still_open_count << " instance(s) still open." << LL_ENDL; + } + + return true; +} + std::string LLAppViewerWin32::generateSerialNumber() { diff --git a/indra/newview/llappviewerwin32.h b/indra/newview/llappviewerwin32.h index 46ec7a8e333..563fce16db0 100644 --- a/indra/newview/llappviewerwin32.h +++ b/indra/newview/llappviewerwin32.h @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2007&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -46,10 +46,15 @@ class LLAppViewerWin32 : public LLAppViewer bool reportCrashToBugsplat(void* pExcepInfo) override; bool reportCustomToBugsplat(const std::string& description) override; + // returns true if other windows were found and are still running. + static bool sendShutdownToOtherInstances(const std::wstring& install_dir); + protected: bool initWindow() override; // Override to initialize the viewer's window. void initLoggingAndGetLastDuration() override; // Override to clean stack_trace info. void initConsole() override; // Initialize OS level debugging console. + void cleanupConsole() override; + bool initHardwareTest() override; // Win32 uses DX9 to test hardware. bool initParseCommandLine(LLCommandLineParser& clp) override; @@ -57,6 +62,7 @@ class LLAppViewerWin32 : public LLAppViewer bool restoreErrorTrap() override; bool sendURLToOtherInstance(const std::string& url) override; + void setOSHibernationMode(eHibernationMode mode) override; std::string generateSerialNumber(); diff --git a/indra/newview/llavataractions.cpp b/indra/newview/llavataractions.cpp index 384b1215595..f05b2239200 100644 --- a/indra/newview/llavataractions.cpp +++ b/indra/newview/llavataractions.cpp @@ -392,6 +392,17 @@ void LLAvatarActions::showProfile(const LLUUID& avatar_id) } } +bool LLAvatarActions::myProfileVisible() +{ + LLFloater* floaterp = findProfileFloater(gAgentID); + return floaterp && floaterp->isInVisibleChain(); +} + +bool LLAvatarActions::myPicksTabVisible() +{ + return myProfileVisible() && isPickTabSelected(gAgentID); +} + // static void LLAvatarActions::showPicks(const LLUUID& avatar_id) { @@ -1026,6 +1037,13 @@ namespace action_give_inventory break; } LLViewerInventoryItem* inv_item = gInventory.getItem(*it); + if (!inv_item) + { + shared = false; + LL_WARNS() << "Failed to share an item " << *it + << ". Item was not found in inventory." << LL_ENDL; + continue; + } if (!inv_item->getPermissions().allowCopyBy(gAgentID)) { if (!noncopy_item_names.empty()) diff --git a/indra/newview/llavataractions.h b/indra/newview/llavataractions.h index 49d059a49c1..c6797f828de 100644 --- a/indra/newview/llavataractions.h +++ b/indra/newview/llavataractions.h @@ -110,6 +110,12 @@ class LLAvatarActions static bool isPickTabSelected(const LLUUID& avatar_id); static LLFloater* findProfileFloater(const LLUUID& avatar_id); + /** + * Profile helpers. + */ + static bool myProfileVisible(); + static bool myPicksTabVisible(); + /** * Show avatar on world map. */ diff --git a/indra/newview/llchathistory.cpp b/indra/newview/llchathistory.cpp index 3d7a12eecfe..f8decba2857 100644 --- a/indra/newview/llchathistory.cpp +++ b/indra/newview/llchathistory.cpp @@ -193,6 +193,49 @@ class LLChatHistoryHeader: public LLPanel LLMuteList::getInstance()->add(LLMute(getAvatarId(), mFrom, LLMute::OBJECT)); ALFloaterBlocked::showMuteAndSelect(getAvatarId()); } + else if (level == "report_abuse") + { + std::string time_string; + if (mTime > 0) // have frame time + { + time_t current_time = time_corrected(); + time_t message_time = (time_t)(current_time - LLFrameTimer::getElapsedSeconds() + mTime); + + // Report abuse shouldn't use AM/PM, use 24-hour time + time_string = "[" + LLTrans::getString("TimeMonth") + "]/[" + + LLTrans::getString("TimeDay") + "]/[" + + LLTrans::getString("TimeYear") + "] [" + + LLTrans::getString("TimeHour") + "]:[" + + LLTrans::getString("TimeMin") + "]"; + + LLSD substitution; + + substitution["datetime"] = (S32)message_time; + LLStringUtil::format(time_string, substitution); + } + else + { + // From history. This might be empty or not full. + // See LLChatLogParser::parse + time_string = getChild("time_box")->getValue().asString(); + + // Just add current date if not full. + // Should be fine since both times are supposed to be SLT. + if (!time_string.empty() && time_string.size() < 7) + { + time_string = "[" + LLTrans::getString("TimeMonth") + "]/[" + + LLTrans::getString("TimeDay") + "]/[" + + LLTrans::getString("TimeYear") + "] " + time_string; + + LLSD substitution; + // To avoid adding today's date to yesterday's timestamp, + // use creation time instead of current time + substitution["datetime"] = (S32)mCreationTime; + LLStringUtil::format(time_string, substitution); + } + } + LLFloaterReporter::showFromChatObj(getAvatarId(), time_string, mText); + } else if (level == "unblock") { LLMuteList::getInstance()->remove(LLMute(getAvatarId(), mFrom, LLMute::OBJECT)); @@ -479,7 +522,7 @@ class LLChatHistoryHeader: public LLPanel time_string = getChild("time_box")->getValue().asString(); // Just add current date if not full. - // Should be fine since both times are supposed to be stl + // Should be fine since both times are supposed to be SLT. if (!time_string.empty() && time_string.size() < 7) { time_string = "[" + LLTrans::getString("TimeMonth") + "]/[" @@ -493,7 +536,7 @@ class LLChatHistoryHeader: public LLPanel LLStringUtil::format(time_string, substitution); } } - LLFloaterReporter::showFromChat(mAvatarID, mFrom, time_string, mText); + LLFloaterReporter::showFromChatAv(mAvatarID, mFrom, time_string, mText); } else if(level == "block_unblock") { diff --git a/indra/newview/llchatitemscontainerctrl.cpp b/indra/newview/llchatitemscontainerctrl.cpp index 0fe05950a15..3f689b5d294 100644 --- a/indra/newview/llchatitemscontainerctrl.cpp +++ b/indra/newview/llchatitemscontainerctrl.cpp @@ -36,8 +36,10 @@ #include "llcommandhandler.h" #include "llfloaterreg.h" #include "lllocalcliprect.h" +#include "alfloaterblocked.h" #include "lltrans.h" #include "llfloaterimnearbychat.h" +#include "llfloaterreporter.h" #include "llfloaterworldmap.h" #include "llviewermenu.h" @@ -101,6 +103,32 @@ class LLObjectHandler : public LLCommandHandler } return true; } + if (verb == "block") + { + if (params.size() > 2) + { + const std::string object_name = LLURI::unescape(params[2].asString()); + LLMute mute(object_id, object_name, LLMute::OBJECT); + LLMuteList::getInstance()->add(mute); + ALFloaterBlocked::showMuteAndSelect(mute.mID); + } + return true; + } + if (verb == "unblock") + { + if (params.size() > 2) + { + const std::string object_name = params[2].asString(); + LLMute mute(object_id, object_name, LLMute::OBJECT); + LLMuteList::getInstance()->remove(mute); + } + return true; + } + if (verb == "reportAbuse" && web == NULL) + { + LLFloaterReporter::showFromObject(object_id, LLUUID::null); + return true; + } return false; } diff --git a/indra/newview/llcommandlineparser.cpp b/indra/newview/llcommandlineparser.cpp index 0734b125313..7e7d528199f 100644 --- a/indra/newview/llcommandlineparser.cpp +++ b/indra/newview/llcommandlineparser.cpp @@ -60,7 +60,7 @@ namespace // List of command-line switches that can't map-to settings variables. // Going forward, we want every new command-line switch to map-to some // settings variable. This list is used to validate that. - const std::set unmapped_options = { "help", "set", "setdefault", "settings", "sessionsettings", "usersessionsettings" }; + const std::set unmapped_options = { "help", "set", "setdefault", "settings", "sessionsettings", "usersessionsettings", "gpubenchmark" }; po::options_description gOptionsDesc; po::positional_options_description gPositionalOptions; @@ -611,6 +611,11 @@ void setControlValueCB(const LLCommandLineParser::token_vector_t& value, ctrl->setValue(llsdArray, false); } + else if (ctrl->isType(TYPE_LLSD)) + { + // Command-line LLSD should support a notation format string + ctrl->setValueFromNotation(onevalue(option, value), false); + } else { ctrl->setValue(onevalue(option, value), false); diff --git a/indra/newview/llcompilequeue.cpp b/indra/newview/llcompilequeue.cpp index c6cdee941f6..d93a9b61efc 100644 --- a/indra/newview/llcompilequeue.cpp +++ b/indra/newview/llcompilequeue.cpp @@ -53,7 +53,6 @@ #include "lldir.h" #include "llnotificationsutil.h" #include "llviewerstats.h" -#include "llfilesystem.h" #include "lluictrlfactory.h" #include "lltrans.h" @@ -62,7 +61,6 @@ #include "llviewerassetupload.h" #include "llcorehttputil.h" -#include "llpreviewscript.h" namespace { @@ -471,31 +469,22 @@ bool LLFloaterCompileQueue::processScript(LLHandle hfloat LLUUID assetId = result["asset_id"]; - // Check if this is a SLua script that shouldn't be recompiled to Mono/LSL - if (compile_target == "mono" || compile_target == "lsl2") - { - // Read the script from cache to check its type - LLFileSystem file(assetId, LLAssetType::AT_LSL_TEXT, LLFileSystem::READ); - if (file.getSize() > 0) - { - S32 file_length = file.getSize(); - std::vector buffer(file_length + 1); - file.read((U8*)&buffer[0], file_length); - buffer[file_length] = 0; - std::string script_text(&buffer[0]); + const bool script_is_lua = item->getInventorySubType() == SST_LUA; + const bool target_is_lua = compile_target == "luau"; + const bool incompatible_language = target_is_lua != script_is_lua; - if (is_lua_script(script_text)) - { - // This is a SLua script - skip it with a warning - LLStringUtil::format_map_t args; - args["[SCRIPT_NAME]"] = inventory->getName(); - args["[TARGET]"] = (compile_target == "mono") ? "Mono" : "LSL"; - std::string buffer = floater->getString("SkippingSluaScript", args); - floater->addStringMessage(buffer); - LL_INFOS("SCRIPTQ") << "Skipping SLua script: " << inventory->getName() << LL_ENDL; - return true; - } - } + // Lua and LSL use different source languages, so do not send an incompatible + // script to the compiler. The inventory subtype identifies the source language. + if (incompatible_language) + { + LLStringUtil::format_map_t args; + args["[SCRIPT_NAME]"] = inventory->getName(); + args["[TARGET]"] = compile_target; + std::string buffer = floater->getString("SkippingIncompatibleScript", args); + floater->addStringMessage(buffer); + LL_INFOS("SCRIPTQ") << "Skipping incompatible script: " << inventory->getName() + << " (target " << compile_target << ")" << LL_ENDL; + return true; } std::string url = object->getRegion()->getCapability("UpdateScriptTask"); diff --git a/indra/newview/lldrawable.cpp b/indra/newview/lldrawable.cpp index eef20cec763..a8eadd4cbee 100644 --- a/indra/newview/lldrawable.cpp +++ b/indra/newview/lldrawable.cpp @@ -665,10 +665,15 @@ F32 LLDrawable::updateXform(bool undamped) // snap to final position (only if no target omega is applied) dist_squared = 0.0f; //set target scale here, because of dist_squared = 0.0f remove object from move list - mCurrentScale = target_scale; - - if (getVOVolume() && !isRoot()) - { //child prim snapping to some position, needs a rebuild + if (mCurrentScale != target_scale) + { + mCurrentScale = target_scale; + // Final scale change needs a rebuild + gPipeline.markRebuild(this, LLDrawable::REBUILD_POSITION); + } + else if (getVOVolume() && !isRoot()) + { + //child prim snapping to some position, needs a rebuild gPipeline.markRebuild(this, LLDrawable::REBUILD_POSITION); } } diff --git a/indra/newview/lldrawpool.cpp b/indra/newview/lldrawpool.cpp index 5de21586d85..2c6f0c64d6f 100644 --- a/indra/newview/lldrawpool.cpp +++ b/indra/newview/lldrawpool.cpp @@ -443,7 +443,11 @@ void LLRenderPass::pushBatches(U32 type, bool texture, bool batch_textures) LLDrawInfo* pparams = *i; LLCullResult::increment_iterator(i, end); - pushBatch(*pparams, texture, batch_textures); + llassert(pparams); // figure out how null got here, it shouldn't be happening + if (pparams) + { + pushBatch(*pparams, texture, batch_textures); + } } } else @@ -462,7 +466,10 @@ void LLRenderPass::pushUntexturedBatches(U32 type) LLDrawInfo* pparams = *i; LLCullResult::increment_iterator(i, end); - pushUntexturedBatch(*pparams); + if (pparams) + { + pushUntexturedBatch(*pparams); + } } } @@ -482,7 +489,7 @@ void LLRenderPass::pushRiggedBatches(U32 type, bool texture, bool batch_textures LLDrawInfo* pparams = *i; LLCullResult::increment_iterator(i, end); - if (uploadMatrixPalette(pparams->mAvatar, pparams->mSkinInfo, lastAvatar, lastMeshId, skipLastSkin)) + if (pparams && uploadMatrixPalette(pparams->mAvatar, pparams->mSkinInfo, lastAvatar, lastMeshId, skipLastSkin)) { pushBatch(*pparams, texture, batch_textures); } @@ -507,7 +514,7 @@ void LLRenderPass::pushUntexturedRiggedBatches(U32 type) LLDrawInfo* pparams = *i; LLCullResult::increment_iterator(i, end); - if (uploadMatrixPalette(pparams->mAvatar, pparams->mSkinInfo, lastAvatar, lastMeshId, skipLastSkin)) + if (pparams && uploadMatrixPalette(pparams->mAvatar, pparams->mSkinInfo, lastAvatar, lastMeshId, skipLastSkin)) { pushUntexturedBatch(*pparams); } @@ -523,6 +530,10 @@ void LLRenderPass::pushMaskBatches(U32 type, bool texture, bool batch_textures) { LLDrawInfo* pparams = *i; LLCullResult::increment_iterator(i, end); + if (!pparams) + { + continue; + } if (pparams->mMaterialSlotList.size() > 1) { // multi-material legacy batch -- drawn by pushMaskBatchesIndexed continue; @@ -546,8 +557,12 @@ void LLRenderPass::pushRiggedMaskBatches(U32 type, bool texture, bool batch_text LLCullResult::increment_iterator(i, end); - llassert(pparams); + llassert(pparams); // figure out how null got here, it shouldn't be happening + if (!pparams) + { + continue; + } if (pparams->mMaterialSlotList.size() > 1) { // multi-material legacy batch -- drawn by pushMaskBatchesIndexed continue; diff --git a/indra/newview/lldrawpoolalpha.cpp b/indra/newview/lldrawpoolalpha.cpp index b38d1dc7a31..23417e49877 100644 --- a/indra/newview/lldrawpoolalpha.cpp +++ b/indra/newview/lldrawpoolalpha.cpp @@ -282,8 +282,8 @@ void LLDrawPoolAlpha::forwardRender(bool rigged) gGL.setColorMask(true, false); - if (!rigged && getType() == LLDrawPoolAlpha::POOL_ALPHA_POST_WATER) - { //render "highlight alpha" on final non-rigged pass + if (!rigged && (LLPipeline::sRenderingHUDs || getType() == LLDrawPoolAlpha::POOL_ALPHA_POST_WATER)) + { //render "highlight alpha" on final non-rigged pass for non-HUDs (HUDs only run pre-water alpha pass) // NOTE -- hacky call here protected by !rigged instead of alongside "forwardRender" // so renderDebugAlpha is executed while gls_pipeline_alpha and depth GL state // variables above are still in scope diff --git a/indra/newview/lldrawpoolavatar.cpp b/indra/newview/lldrawpoolavatar.cpp index e828d779762..2619d7963bc 100644 --- a/indra/newview/lldrawpoolavatar.cpp +++ b/indra/newview/lldrawpoolavatar.cpp @@ -642,7 +642,15 @@ void LLDrawPoolAvatar::beginSkinned() { LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR; - // used for preview only + // Used for preview only, like LLVisualParamHint + // Uses deprecated logic!!! + if (!gAvatarProgram.isComplete()) + { + llassert(false); // Avatar shader shouldn't have failed if deferred shader loaded + sVertexProgram = nullptr; + LLGLSLShader::unbind(); + return; + } sVertexProgram = &gAvatarProgram; @@ -650,30 +658,25 @@ void LLDrawPoolAvatar::beginSkinned() sVertexProgram->bind(); sVertexProgram->setMinimumAlpha(LLDrawPoolAvatar::sMinimumAlpha); + sDiffuseChannel = sVertexProgram->enableTexture(LLViewerShaderMgr::DIFFUSE_MAP); } void LLDrawPoolAvatar::endSkinned() { LL_PROFILE_ZONE_SCOPED_CATEGORY_AVATAR; - // if we're in software-blending, remember to set the fence _after_ we draw so we wait till this rendering is done - if (sShaderLevel > 0) - { - sRenderingSkinned = false; - sVertexProgram->disableTexture(LLViewerShaderMgr::BUMP_MAP); - sVertexProgram->unbind(); - sShaderLevel = mShaderLevel; - } - else + if (sVertexProgram == nullptr) { - if(gPipeline.shadersLoaded()) - { - // software skinning, use a basic shader for windlight. - // TODO: find a better fallback method for software skinning. - sVertexProgram->unbind(); - } + return; } + // if we're in software-blending, remember to set the fence _after_ we draw so we wait till this rendering is done + sRenderingSkinned = false; + sVertexProgram->disableTexture(LLViewerShaderMgr::DIFFUSE_MAP); + sVertexProgram->disableTexture(LLViewerShaderMgr::BUMP_MAP); + sVertexProgram->unbind(); + sShaderLevel = mShaderLevel; + } void LLDrawPoolAvatar::beginDeferredSkinned() diff --git a/indra/newview/lleventpoll.cpp b/indra/newview/lleventpoll.cpp index 9eddec55c58..836ff634297 100644 --- a/indra/newview/lleventpoll.cpp +++ b/indra/newview/lleventpoll.cpp @@ -331,26 +331,51 @@ namespace Details { // LLSD is too smart for it's own good and may act like a smart // pointer for the content of (*i), so instead of passing (*i) - // pass a prepared name and move ownership of "body", - // as we are not going to need "body" anywhere else. + // pass a prepared name and copy the body std::string msg_name = (*i)["message"].asString(); - // WARNING: This is a shallow copy! - // If something still retains the data (like in httpAdapter?) this might still - // result in a crash, if it does appear to be the case, make a deep copy or - // convert data to string and pass that string. - const LLSD body = (*i)["body"]; - (*i)["body"].clear(); - work = [this, msg_name, body]() + // Create a deep copy using binary serialization + try { - handleMessage(msg_name, body); - }; + std::stringstream body_stream; + LLSDSerialize::toBinary((*i)["body"], body_stream); + std::string body_str = body_stream.str(); + (*i)["body"].clear(); + work = [this, msg_name, body_str]() + { + try + { + LLSD body; + std::istringstream istr(body_str); + LLSDSerialize::fromBinary(body, istr, body_str.size()); + handleMessage(msg_name, body); + } + catch (std::bad_alloc&) + { + LLError::LLUserWarningMsg::showOutOfMemory(); + LL_ERRS("LLCoros") << "Bad memory allocation in handleMessage() for " << msg_name << LL_ENDL; + } + }; + } + catch (std::bad_alloc&) + { + LLError::LLUserWarningMsg::showOutOfMemory(); + LL_ERRS("LLCoros") << "Bad memory allocation in eventPollCoro for " << msg_name << LL_ENDL; + } } main_queue->post(work); } else { - handleMessage(*i); + try + { + handleMessage(*i); + } + catch (std::bad_alloc&) + { + LLError::LLUserWarningMsg::showOutOfMemory(); + LL_ERRS("LLCoros") << "Bad memory allocation in handleMessage() for " << (*i)["message"].asString() << LL_ENDL; + } } } } diff --git a/indra/newview/llfeaturemanager.cpp b/indra/newview/llfeaturemanager.cpp index b95f08ac48b..e896c099943 100644 --- a/indra/newview/llfeaturemanager.cpp +++ b/indra/newview/llfeaturemanager.cpp @@ -377,6 +377,83 @@ F32 gpu_benchmark(); #if LL_WINDOWS +bool gGPUBenchmarkMode = false; + +// Runs gpu_benchmark() in a subprocess (exe with --gpubenchmark). +static F32 subprocess_gpu_benchmark() +{ + LLProcess::Params params; + params.executable = gDirUtilp->getExecutablePathAndName(); + params.args.add("--gpubenchmark"); + params.desc = "GPU benchmark"; + params.autokill = true; // killed via job object if parent crashes + params.attached = true; // killed on LLProcessPtr destruction (timeout) + params.files.add(LLProcess::FileParam()); // stdin: default + params.files.add(LLProcess::FileParam().type("pipe")); // stdout: pipe + params.files.add(LLProcess::FileParam()); // stderr: default + + LLProcessPtr child; + try + { + child = LLProcess::create(params); + } + catch (const std::exception& e) + { + LL_WARNS("RenderInit") << "subprocess_gpu_benchmark: failed to launch: " + << e.what() << LL_ENDL; + return -1.f; + } + + if (!child) + { + LL_WARNS("RenderInit") << "subprocess_gpu_benchmark: LLProcess::create returned null." << LL_ENDL; + return -1.f; + } + + LLProcess::ReadPipe& out = child->getReadPipe(LLProcess::STDOUT); + + const F32 POLL_INTERVAL_S = 0.25f; + const F32 TOTAL_TIMEOUT_S = 120.f; // covers full viewer init + benchmark + LLTimer timer; + timer.start(); + + while (timer.getElapsedTimeF32() < TOTAL_TIMEOUT_S) + { + child->pump(); + + // Result is a single float followed by '\n' + if (out.contains('\n')) + { + std::string line = out.getline(); + float parsed = 0.f; + if (sscanf_s(line.c_str(), "%f", &parsed) == 1 && parsed > 0.f) + { + LL_INFOS("RenderInit") << "subprocess_gpu_benchmark: result = " + << parsed << " GB/sec" << LL_ENDL; + // Let LLProcessPtr destructor handle cleanup + return parsed; + } + LL_WARNS("RenderInit") << "subprocess_gpu_benchmark: unparseable output: '" + << line << "'" << LL_ENDL; + return -1.f; + } + + // If child already exited without writing anything, bail + if (!child->isRunning()) + { + LL_WARNS("RenderInit") << "subprocess_gpu_benchmark: process exited without result." << LL_ENDL; + return -1.f; + } + + ms_sleep((U32)(POLL_INTERVAL_S * 1000)); + } + + // Timeout: attached=true means LLProcessPtr destructor kills it + LL_WARNS("RenderInit") << "GPU benchmark subprocess timed out after " + << (int)TOTAL_TIMEOUT_S << " seconds; killing." << LL_ENDL; + return -1.f; +} + F32 logExceptionBenchmark() { // FIXME: gpu_benchmark uses many C++ classes on the stack to control state. @@ -515,7 +592,27 @@ bool LLFeatureManager::loadGPUClass() try { #if LL_WINDOWS - gbps = logExceptionBenchmark(); + if (gGPUBenchmarkMode) + { + // We ARE the benchmark subprocess; run directly in-process. + // logExceptionBenchmark wraps with SEH so structured exceptions + // (e.g. access violations inside the driver) are still caught. + gbps = logExceptionBenchmark(); + } + else + { + // Normal path: run benchmark in an isolated subprocess so a + // driver hang can be killed without freezing the main viewer. + gbps = subprocess_gpu_benchmark(); + if (gbps == -1.f + && gGLManager.getRawGLString().find("Radeon") != std::string::npos + && checkRDNA35()) + { + // Certain AMD GPUs have known issues with shader profiling and occlusion queries that can lead to hangs. + // If test returned -1, we are likely on a bad driver. + gSavedSettings.setBOOL("UseOcclusion", false); + } + } #else gbps = gpu_benchmark(); #endif @@ -526,6 +623,21 @@ bool LLFeatureManager::loadGPUClass() LL_WARNS("RenderInit") << "GPU benchmark failed: " << e.what() << LL_ENDL; } +#if LL_WINDOWS + // If we are the benchmark subprocess, write the raw result to stdout + // so the parent process can read it, then exit immediately. + if (gGPUBenchmarkMode) + { + LL_WARNS("RenderInit") << "Passing " << gbps << " to parent" << LL_ENDL; + char buf[64]; + int len = snprintf(buf, sizeof(buf), "%.6f\n", gbps); + DWORD written = 0; + WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), buf, (DWORD)len, &written, NULL); + FlushFileBuffers(GetStdHandle(STD_OUTPUT_HANDLE)); + ExitProcess(0); + } +#endif + mGPUMemoryBandwidth = gbps; // bias by CPU speed diff --git a/indra/newview/llfilepicker_mac.mm b/indra/newview/llfilepicker_mac.mm index a74d5225ecd..3a0bc4005ea 100644 --- a/indra/newview/llfilepicker_mac.mm +++ b/indra/newview/llfilepicker_mac.mm @@ -26,18 +26,28 @@ #ifdef LL_DARWIN #import +#import #include #include "llfilepicker_mac.h" -// For setAllowedFileTypes deprecation -#pragma clang diagnostic ignored "-Wdeprecated-declarations" +// Convert a file extension or UTI string into a UTType for use with +// NSOpenPanel/NSSavePanel's allowedContentTypes. +static UTType *contentTypeForString(NSString *typeString) +{ + UTType *type = [UTType typeWithFilenameExtension:typeString]; + if (!type) + { + type = [UTType typeWithIdentifier:typeString]; + } + return type; +} NSOpenPanel *init_panel(const std::vector* allowed_types, unsigned int flags) { int i; NSOpenPanel *panel = [NSOpenPanel openPanel]; - NSMutableArray *fileTypes = nil; + NSMutableArray *fileTypes = nil; if ( allowed_types && !allowed_types->empty()) @@ -46,9 +56,13 @@ for (i=0;isize();++i) { - [fileTypes addObject: - [NSString stringWithCString:(*allowed_types)[i].c_str() - encoding:[NSString defaultCStringEncoding]]]; + NSString *typeString = [NSString stringWithCString:(*allowed_types)[i].c_str() + encoding:[NSString defaultCStringEncoding]]; + UTType *type = contentTypeForString(typeString); + if (type) + { + [fileTypes addObject:type]; + } } } @@ -60,9 +74,9 @@ [panel setCanChooseFiles: ( (flags & F_FILE)?true:false )]; [panel setTreatsFilePackagesAsDirectories: ( flags & F_NAV_SUPPORT ) ]; - if (fileTypes) + if (fileTypes && fileTypes.count > 0) { - [panel setAllowedFileTypes:fileTypes]; + [panel setAllowedContentTypes:fileTypes]; } else { @@ -199,12 +213,25 @@ void doLoadDialogModeless(const std::vector* allowed_types, NSSavePanel *panel = [NSSavePanel savePanel]; NSString *extensionns = [NSString stringWithCString:extension->c_str() encoding:[NSString defaultCStringEncoding]]; - NSArray *fileType = [extensionns componentsSeparatedByString:@","]; + NSArray *extensions = [extensionns componentsSeparatedByString:@","]; + + NSMutableArray *fileType = [[NSMutableArray alloc] init]; + for (NSString *ext in extensions) + { + UTType *type = contentTypeForString(ext); + if (type) + { + [fileType addObject:type]; + } + } //[panel setMessage:@"Save Image File"]; [panel setTreatsFilePackagesAsDirectories: ( flags & F_NAV_SUPPORT ) ]; [panel setCanSelectHiddenExtension:true]; - [panel setAllowedFileTypes:fileType]; + if (fileType.count > 0) + { + [panel setAllowedContentTypes:fileType]; + } NSString *fileName = [NSString stringWithCString:file->c_str() encoding:[NSString defaultCStringEncoding]]; NSURL* url = [NSURL fileURLWithPath:fileName]; @@ -234,12 +261,25 @@ void doSaveDialogModeless(const std::string* file, NSSavePanel *panel = [NSSavePanel savePanel]; NSString *extensionns = [NSString stringWithCString:extension->c_str() encoding:[NSString defaultCStringEncoding]]; - NSArray *fileType = [extensionns componentsSeparatedByString:@","]; + NSArray *extensions = [extensionns componentsSeparatedByString:@","]; + + NSMutableArray *fileType = [[NSMutableArray alloc] init]; + for (NSString *ext in extensions) + { + UTType *type = contentTypeForString(ext); + if (type) + { + [fileType addObject:type]; + } + } //[panel setMessage:@"Save Image File"]; [panel setTreatsFilePackagesAsDirectories: ( flags & F_NAV_SUPPORT ) ]; [panel setCanSelectHiddenExtension:true]; - [panel setAllowedFileTypes:fileType]; + if (fileType.count > 0) + { + [panel setAllowedContentTypes:fileType]; + } NSString *fileName = [NSString stringWithCString:file->c_str() encoding:[NSString defaultCStringEncoding]]; NSURL* url = [NSURL fileURLWithPath:fileName]; diff --git a/indra/newview/llfloaterbuycurrency.cpp b/indra/newview/llfloaterbuycurrency.cpp index e41f893c433..506cff31f41 100644 --- a/indra/newview/llfloaterbuycurrency.cpp +++ b/indra/newview/llfloaterbuycurrency.cpp @@ -316,16 +316,23 @@ void LLFloaterBuyCurrency::handleBuyCurrency(bool has_piof, bool has_target, con if (has_piof) { LLFloaterBuyCurrencyUI* ui = LLFloaterReg::showTypedInstance("buy_currency"); - if (has_target) + if (ui) { - ui->target(name, price); + if (has_target) + { + ui->target(name, price); + } + else + { + ui->noTarget(); + } + ui->updateUI(); + ui->collapsePanels(!has_target); } else { - ui->noTarget(); + LL_WARNS() << "Cannot instantiate buy_currency floater" << LL_ENDL; } - ui->updateUI(); - ui->collapsePanels(!has_target); } else { diff --git a/indra/newview/llfloatercolorpicker.cpp b/indra/newview/llfloatercolorpicker.cpp index ef3aeac0196..2cd7ab07220 100644 --- a/indra/newview/llfloatercolorpicker.cpp +++ b/indra/newview/llfloatercolorpicker.cpp @@ -675,6 +675,44 @@ void LLFloaterColorPicker::drawPalette () } } +////////////////////////////////////////////////////////////////////////////// +// Boolean test if input string is a valid hex color string +bool LLFloaterColorPicker::isValidHexColor(std::string& hex_color, F32& hr, F32& hg, F32& hb) +{ + // Strip any whitespace and a leading # character if present + // (Often included in hex color strings from other places) + LLStringUtil::trim(hex_color); + if (!hex_color.empty() && hex_color.front() == '#') + { + hex_color = hex_color.substr(1); + } + + // Make sure it's a real string and valid hex - we can't use the + // hex to dec code because that coerces invalid hex into decimal 0 + if (hex_color.length() ==0 || hex_color.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos) + { + return false; + } + + // Convert the hex string to a decimal number + std::stringstream oss; + oss << std::hex << hex_color; + unsigned int dec_val; + oss >> dec_val; + + // Break out the RGB values in 0..1.0 range and pass back + // (They will only be used if this function returns true.) + hr = F32(((dec_val >> 16) & 0xff)) / 255.0f; + hg = F32(((dec_val >> 8) & 0xff)) / 255.0f; + hb = F32(((dec_val >> 0) & 0xff)) / 255.0f; + + // The max chars in the field is more than 6 now so we can paste in + // poorly formatted color strings and reformat them on commit. + // TODO: there must be a way to to the preformat when the string + // is pasted into the line editor control. + return dec_val > 0xffffff ? false : true; +} + ////////////////////////////////////////////////////////////////////////////// // update text entry values for RGB/HSL (can't be done in ::draw () since this overwrites input void LLFloaterColorPicker::updateTextEntry () @@ -756,28 +794,16 @@ void LLFloaterColorPicker::onTextEntryChanged ( LLUICtrl* ctrl ) } else if ( name == "hex_value" ) { - // get current RGB - S32 r, g, b; F32 rVal, gVal, bVal; - getCurRgb ( rVal, gVal, bVal ); - std::string hex_string = ctrl->getValue().asString(); - - static const boost::regex pattern("[[:xdigit:]]{6}"); - if (!ll_regex_match(hex_string, pattern)) + if (isValidHexColor(hex_string, rVal, gVal, bVal)) { - return; + // update current RGB (and implicitly HSL) + selectCurRgb ( rVal, gVal, bVal ); } - sscanf(hex_string.c_str(), "%02x%02x%02x", &r, &g, &b); - - rVal = F32(r) / 255.f; - gVal = F32(g) / 255.f; - bVal = F32(b) / 255.f; - - // update current RGB (and implicitly HSL) - selectCurRgb ( rVal, gVal, bVal ); - + // Either way: on a good entry this reformats it, on a bad one it puts + // the current colour back. updateTextEntry (); } // value in HSL boxes changed diff --git a/indra/newview/llfloatercolorpicker.h b/indra/newview/llfloatercolorpicker.h index 9d784263250..a753a91cca0 100644 --- a/indra/newview/llfloatercolorpicker.h +++ b/indra/newview/llfloatercolorpicker.h @@ -128,6 +128,10 @@ class LLFloaterColorPicker // mutators for color values, can raise event to preview changes at object void selectCurRgb ( F32 curRIn, F32 curGIn, F32 curBIn ); void selectCurHsl ( F32 curHIn, F32 curSIn, F32 curLIn ); + + // utility functions for manipulating hex colors + bool isValidHexColor(std::string& hex_color, F32& hr, F32& hg, F32& hb); + // draws color selection palette void drawPalette (); @@ -193,6 +197,7 @@ class LLFloaterColorPicker LLButton* mPipetteBtn; + F32 mContextConeOpacity; F32 mContextConeInAlpha; F32 mContextConeOutAlpha; diff --git a/indra/newview/llfloateremojipicker.cpp b/indra/newview/llfloateremojipicker.cpp index d6355ab9e94..4f250175e71 100644 --- a/indra/newview/llfloateremojipicker.cpp +++ b/indra/newview/llfloateremojipicker.cpp @@ -1820,6 +1820,9 @@ bool LLFloaterEmojiPicker::moveFocusedIconNext() if (mHoveredIcon) return false; + if (mFocusedIconRow < 0 || static_cast(mFocusedIconRow) >= mEmojiGrid->getPanelList().size()) + return false; + LLScrollingPanel* panel = mEmojiGrid->getPanelList()[mFocusedIconRow]; LLEmojiGridRow* row = dynamic_cast(panel); S32 colCount = row ? static_cast(row->mList->getPanelList().size()) : 0; diff --git a/indra/newview/llfloatergestureautocompletepicker.cpp b/indra/newview/llfloatergestureautocompletepicker.cpp new file mode 100644 index 00000000000..c7c4e464594 --- /dev/null +++ b/indra/newview/llfloatergestureautocompletepicker.cpp @@ -0,0 +1,154 @@ +/** + * @file llfloatergestureautocompletepicker.cpp + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "llfloatergestureautocompletepicker.h" + +#include "llgestureautocompletehelper.h" +#include "llscrolllistctrl.h" +#include "llscrolllistitem.h" + +LLFloaterGestureAutocompletePicker::LLFloaterGestureAutocompletePicker(const LLSD& key) +: LLFloater(key), mGestureList(NULL) +{ + setFocusStealsFrontmost(false); + setBackgroundVisible(false); + setAutoFocus(false); +} + +bool LLFloaterGestureAutocompletePicker::postBuild() +{ + mGestureList = getChild("gesture_list"); + mGestureList->setCommitOnKeyboardMovement(false); + mGestureList->setCommitCallback(boost::bind(&LLFloaterGestureAutocompletePicker::commitSelected, this)); + + return LLFloater::postBuild(); +} + +void LLFloaterGestureAutocompletePicker::onOpen(const LLSD& key) +{ + LLGestureAutocompleteHelper& helper = LLGestureAutocompleteHelper::instance(); + mGestureList->clearRows(); + + const std::vector& rows = helper.rows(); + + for (const auto& row : rows) + { + LLSD element; + element["value"] = row.value; + element["columns"][0]["column"] = "trigger"; + element["columns"][0]["value"] = row.trigger; + element["columns"][1]["column"] = "name"; + element["columns"][1]["value"] = row.name; + mGestureList->addElement(element); + } + + if (helper.total() > rows.size()) + { + LLSD element; + element["enabled"] = false; + element["columns"][0]["column"] = "trigger"; + element["columns"][0]["value"] = LLStringUtil::null; + element["columns"][1]["column"] = "name"; + + LLStringUtil::format_map_t args; + args["[COUNT]"] = llformat("%d", (S32)rows.size()); + args["[TOTAL]"] = llformat("%d", (S32)helper.total()); + element["columns"][1]["value"] = getString("showing_count", args); + + mGestureList->addElement(element); + } + + mGestureList->selectFirstItem(); + gFloaterView->adjustToFitScreen(this, false); +} + +bool LLFloaterGestureAutocompletePicker::handleKey(KEY key, MASK mask, bool called_from_parent) +{ + if (mask == MASK_NONE) + { + switch (key) + { + case KEY_UP: + mGestureList->selectPrevItem(); + mGestureList->scrollToShowSelected(); + return true; + case KEY_DOWN: + mGestureList->selectNextItem(); + mGestureList->scrollToShowSelected(); + return true; + case KEY_RETURN: + case KEY_TAB: + commitSelected(); + return true; + case KEY_ESCAPE: + LLGestureAutocompleteHelper::instance().hideHelper(); + return true; + case KEY_LEFT: + case KEY_RIGHT: + return true; + default: + break; + } + } + + return LLFloater::handleKey(key, mask, called_from_parent); +} + +void LLFloaterGestureAutocompletePicker::onClose(bool app_quitting) +{ + if (!app_quitting) + { + LLGestureAutocompleteHelper::instance().hideHelper(); + } +} + +void LLFloaterGestureAutocompletePicker::goneFromFront() +{ + LLGestureAutocompleteHelper::instance().hideHelper(); +} + +bool LLFloaterGestureAutocompletePicker::commitSelected() +{ + LLScrollListItem* item = mGestureList->getFirstSelected(); + + if (!item || !item->getEnabled()) + { + return false; + } + + const std::string value = mGestureList->getSelectedValue().asString(); + + if (value.empty()) + { + return false; + } + + setValue(value); + onCommit(); + + return true; +} diff --git a/indra/newview/llfloaterhowto.h b/indra/newview/llfloatergestureautocompletepicker.h similarity index 58% rename from indra/newview/llfloaterhowto.h rename to indra/newview/llfloatergestureautocompletepicker.h index 9d7793817a6..71f754138d0 100644 --- a/indra/newview/llfloaterhowto.h +++ b/indra/newview/llfloatergestureautocompletepicker.h @@ -1,10 +1,9 @@ /** - * @file llfloaterhowto.h - * @brief A variant of web floater meant to open guidebook + * @file llfloatergestureautocompletepicker.h * - * $LicenseInfo:firstyear=2021&license=viewerlgpl$ + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2021, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -24,35 +23,25 @@ * $/LicenseInfo$ */ -#ifndef LL_LLFLOATERHOWTO_H -#define LL_LLFLOATERHOWTO_H +#pragma once -#include "llfloaterwebcontent.h" +#include "llfloater.h" -class LLMediaCtrl; +class LLScrollListCtrl; - -class LLFloaterHowTo : - public LLFloaterWebContent +class LLFloaterGestureAutocompletePicker : public LLFloater { public: - LOG_CLASS(LLFloaterHowTo); - - typedef LLFloaterWebContent::Params Params; - - LLFloaterHowTo(const Params& key); + LLFloaterGestureAutocompletePicker(const LLSD& key); + bool postBuild() override; void onOpen(const LLSD& key) override; - - bool handleKeyHere(KEY key, MASK mask) override; - - static LLFloaterHowTo* getInstance(); - - bool matchesKey(const LLSD& key) override { return true; /*single instance*/ }; + bool handleKey(KEY key, MASK mask, bool called_from_parent) override; + void onClose(bool app_quitting) override; + void goneFromFront() override; private: - bool postBuild() override; -}; - -#endif // LL_LLFLOATERHOWTO_H + bool commitSelected(); + LLScrollListCtrl* mGestureList; +}; diff --git a/indra/newview/llfloaterhowto.cpp b/indra/newview/llfloaterhowto.cpp deleted file mode 100644 index 6a9f113d533..00000000000 --- a/indra/newview/llfloaterhowto.cpp +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @file llfloaterhowto.cpp - * @brief A variant of web floater meant to open guidebook - * - * $LicenseInfo:firstyear=2021&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2021, Linden Research, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; - * version 2.1 of the License only. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -#include "llviewerprecompiledheaders.h" - -#include "llfloaterhowto.h" - -#include "llfloaterreg.h" -#include "llviewercontrol.h" -#include "llweb.h" - - -constexpr S32 STACK_WIDTH = 300; -constexpr S32 STACK_HEIGHT = 505; // content will be 500 - -LLFloaterHowTo::LLFloaterHowTo(const Params& key) : - LLFloaterWebContent(key) -{ - mShowPageTitle = false; -} - -bool LLFloaterHowTo::postBuild() -{ - LLFloaterWebContent::postBuild(); - - return true; -} - -void LLFloaterHowTo::onOpen(const LLSD& key) -{ - LLFloaterWebContent::Params p(key); - if (!p.url.isProvided() || p.url.getValue().empty()) - { - std::string url = gSavedSettings.getString("GuidebookURL"); - p.url = LLWeb::expandURLSubstitutions(url, LLSD()); - } - p.show_chrome = false; - - LLFloaterWebContent::onOpen(p); - - if (p.preferred_media_size().isEmpty()) - { - // Elements from LLFloaterWebContent did not pick up restored size (save_rect) of LLFloaterHowTo - // set the stack size and position (alternative to preferred_media_size) - LLLayoutStack *stack = getChild("stack1"); - LLRect stack_rect = stack->getRect(); - stack->reshape(STACK_WIDTH, STACK_HEIGHT); - stack->setOrigin(stack_rect.mLeft, stack_rect.mTop - STACK_HEIGHT); - stack->updateLayout(); - } -} - -LLFloaterHowTo* LLFloaterHowTo::getInstance() -{ - return LLFloaterReg::getTypedInstance("guidebook"); -} - -bool LLFloaterHowTo::handleKeyHere(KEY key, MASK mask) -{ - bool handled = false; - - if (KEY_F1 == key ) - { - closeFloater(); - handled = true; - } - - return handled; -} diff --git a/indra/newview/llfloaterimcontainer.cpp b/indra/newview/llfloaterimcontainer.cpp index defd64fbbdd..65c0f7325ac 100644 --- a/indra/newview/llfloaterimcontainer.cpp +++ b/indra/newview/llfloaterimcontainer.cpp @@ -169,16 +169,24 @@ void LLFloaterIMContainer::sessionIDUpdated(const LLUUID& old_session_id, const // Note however that the LLFloaterIMSession has its session id updated through a call to sessionInitReplyReceived() // and do not need to be deleted and recreated (trying this creates loads of problems). We do need however to suppress // its related mSessions record as it's indexed with the wrong id. - // Grabbing the updated LLFloaterIMSession and readding it in mSessions will eventually be done by addConversationListItem(). mSessions.erase(old_session_id); - // Delete the model and participants related to the old session - bool change_focus = removeConversationListItem(old_session_id); + // Remove the old conversation widget without changing focus - we'll immediately re-add with + // the new id and select it, avoiding an unnecessary focus switch to an adjacent conversation. + bool was_selected = removeConversationListItem(old_session_id, false); // Create a new conversation with the new id - addConversationListItem(new_session_id, change_focus); + addConversationListItem(new_session_id, was_selected); LLFloaterIMSessionTab::addToHost(new_session_id); + // addToHost is a no-op for already-hosted floaters, so mSessions won't be + // updated by addFloater. Re-register manually so message flash works. + LLFloaterIMSessionTab* conversp = LLFloaterIMSessionTab::findConversation(new_session_id); + if (conversp && mSessions.find(new_session_id) == mSessions.end()) + { + mSessions[new_session_id] = conversp; + } + if (was_active) { selectConversationPair(new_session_id, true, false, true); diff --git a/indra/newview/llfloaterimnearbychat.cpp b/indra/newview/llfloaterimnearbychat.cpp index de5c719bd6c..a19c4bf8a8a 100644 --- a/indra/newview/llfloaterimnearbychat.cpp +++ b/indra/newview/llfloaterimnearbychat.cpp @@ -56,6 +56,7 @@ #include "llfloaterimnearbychatlistener.h" #include "llagent.h" // gAgent #include "llgesturemgr.h" +#include "llgestureautocompletehelper.h" #include "llmultigesture.h" #include "llkeyboard.h" #include "llanimationstates.h" @@ -76,6 +77,8 @@ // [/RLVa:KB] #include "alchatcommand.h" +#include + S32 LLFloaterIMNearbyChat::sLastSpecialChatChannel = 0; static LLFloaterIMNearbyChatListener sChatListener; @@ -83,6 +86,67 @@ static LLFloaterIMNearbyChatListener sChatListener; constexpr S32 EXPANDED_HEIGHT = 266; constexpr S32 COLLAPSED_HEIGHT = 60; constexpr S32 EXPANDED_MIN_HEIGHT = 150; +constexpr size_t MAX_GESTURE_AUTOCOMPLETE_ROWS = 50; + +namespace +{ +bool buildGestureAutocompleteRows( + const std::string& prefix, + std::vector& rows, + size_t& total) +{ + rows.clear(); + total = 0; + + // Wait for at least one character after the slash before offering matches. + if (prefix.size() < 2 || prefix[0] != '/' || prefix.find_first_of(" \t") != std::string::npos) + { + return false; + } + + std::string lower_prefix = prefix; + LLStringUtil::toLower(lower_prefix); + + std::map unique; + const LLGestureMgr::item_map_t& active = LLGestureMgr::instance().getActiveGestures(); + + for (const auto& entry : active) + { + LLMultiGesture* gesture = entry.second; + + if (!gesture || gesture->getTrigger().empty() || gesture->getTrigger()[0] != '/') + { + continue; + } + + std::string lower_trigger = gesture->getTrigger(); + LLStringUtil::toLower(lower_trigger); + + if (lower_trigger.compare(0, lower_prefix.size(), lower_prefix) != 0) + { + continue; + } + + unique.emplace( + gesture->getTrigger(), + gesture->mName); + } + + for (const auto& [trigger, name] : unique) + { + if (rows.size() >= MAX_GESTURE_AUTOCOMPLETE_ROWS) + { + break; + } + + rows.push_back({ trigger, trigger, name }); + } + + total = unique.size(); + + return total > 0; +} +} // legacy callback glue //void send_chat_from_viewer(const std::string& utf8_out_text, EChatType type, S32 channel); @@ -592,8 +656,32 @@ void LLFloaterIMNearbyChat::onChatBoxKeystroke() KEY key = gKeyboard->currentKey(); + static LLCachedControl autocomplete_gestures(gSavedSettings, "ChatAutocompleteGestures", true); + + if (autocomplete_gestures) + { + std::vector rows; + size_t total = 0; + const std::string utf8_trigger = wstring_to_utf8str(raw_text); + + if (buildGestureAutocompleteRows(utf8_trigger, rows, total)) + { + LLGestureAutocompleteHelper::instance().showHelper( + mInputEditor, + rows, + total, + [this](std::string trigger) + { + mInputEditor->setText(trigger + " "); + mInputEditor->endOfDoc(); + }); + return; + } + + LLGestureAutocompleteHelper::instance().hideHelper(mInputEditor); + } // Ignore "special" keys, like backspace, arrows, etc. - if (gSavedSettings.getBOOL("ChatAutocompleteGestures") + if (autocomplete_gestures && length > 1 && raw_text[0] == '/' && key < KEY_SPECIAL) @@ -681,6 +769,9 @@ void LLFloaterIMNearbyChat::sendChat( EChatType type ) LLWString text = mInputEditor->getConvertedText(); LLWStringUtil::trim(text); LLWStringUtil::replaceChar(text,182,'\n'); // Convert paragraph symbols back into newlines. + + LLGestureAutocompleteHelper::instance().hideHelper(); + if (!text.empty()) { // Check if this is destined for another channel diff --git a/indra/newview/llfloaterimnearbychathandler.cpp b/indra/newview/llfloaterimnearbychathandler.cpp index 0bfe6abceed..64abd3021ed 100644 --- a/indra/newview/llfloaterimnearbychathandler.cpp +++ b/indra/newview/llfloaterimnearbychathandler.cpp @@ -188,6 +188,8 @@ class LLFloaterIMNearbyChatScreenChannel: public LLScreenChannelBase void updateToastFadingTime(); + LLToast* getToastFromPool(); + create_toast_panel_callback_t m_create_toast_panel_callback_t; bool createPoolToast(); @@ -285,9 +287,19 @@ void LLFloaterIMNearbyChatScreenChannel::updateToastsLifetime() S32 seconds = gSavedSettings.getS32("NearbyToastLifeTime"); toast_list_t::iterator it; - for(it = m_toast_pool.begin(); it != m_toast_pool.end(); ++it) + for(it = m_toast_pool.begin(); it != m_toast_pool.end();) { - (*it).get()->setLifetime(seconds); + LLToast* toast = it->get(); + if (toast) + { + toast->setLifetime(seconds); + ++it; + } + else + { + LL_WARNS("NearbyChat") << "Discarding destroyed toast from pool" << LL_ENDL; + it = m_toast_pool.erase(it); + } } } @@ -296,10 +308,36 @@ void LLFloaterIMNearbyChatScreenChannel::updateToastFadingTime() S32 seconds = gSavedSettings.getS32("NearbyToastFadingTime"); toast_list_t::iterator it; - for(it = m_toast_pool.begin(); it != m_toast_pool.end(); ++it) + for(it = m_toast_pool.begin(); it != m_toast_pool.end();) { - (*it).get()->setFadingTime(seconds); + LLToast* toast = it->get(); + if (toast) + { + toast->setFadingTime(seconds); + ++it; + } + else + { + LL_WARNS("NearbyChat") << "Discarding destroyed toast from pool" << LL_ENDL; + it = m_toast_pool.erase(it); + } + } +} + +LLToast* LLFloaterIMNearbyChatScreenChannel::getToastFromPool() +{ + while (!m_toast_pool.empty()) + { + LLToast* toast = m_toast_pool.back().get(); + m_toast_pool.pop_back(); + + if (toast) + return toast; + + LL_WARNS("NearbyChat") << "Discarding destroyed toast from pool" << LL_ENDL; } + + return nullptr; } bool LLFloaterIMNearbyChatScreenChannel::createPoolToast() @@ -397,9 +435,18 @@ void LLFloaterIMNearbyChatScreenChannel::addChat(LLSD& chat) //take 1st element from pool, (re)initialize it, put it in active toasts LL_DEBUGS("NearbyChat") << "Getting toast from pool" << LL_ENDL; - LLToast* toast = m_toast_pool.back().get(); + LLToast* toast = getToastFromPool(); + if (!toast) + { + // A toast can be destroyed while its handle remains in the pool. + // Replenish the pool instead of dereferencing the dead handle. + if (!createPoolToast()) + return; - m_toast_pool.pop_back(); + toast = getToastFromPool(); + if (!toast) + return; + } LLFloaterIMNearbyChatToastPanel* panel = dynamic_cast(toast->getPanel()); @@ -707,6 +754,23 @@ void LLFloaterIMNearbyChatHandler::processChat(const LLChat& chat_msg, return; } + if (LLScriptEditorWSServer::isEnabled() && + gSavedSettings.getBOOL("ExternalWebsocketForwardDebug") && + (chat_msg.mChatType == CHAT_TYPE_DEBUG_MSG || + chat_msg.mChatType == CHAT_TYPE_OWNER)) + { + LLScriptEditorWSServer::ptr_t server = + LLScriptEditorWSServer::getServer(); + if (server) + { + const auto channel = + chat_msg.mChatType == CHAT_TYPE_OWNER + ? LLPublishedObjectMgr::RuntimeEventAggregator::Channel::OWNER_SAY + : LLPublishedObjectMgr::RuntimeEventAggregator::Channel::DEBUG; + server->forwardChatToIDE(chat_msg, channel); + } + } + // don't show toast and add message to chat history on receive debug message // with disabled setting showing script errors or enabled setting to show script // errors in separate window. @@ -718,15 +782,6 @@ void LLFloaterIMNearbyChatHandler::processChat(const LLChat& chat_msg, if (!gSavedSettings.getBOOL("ShowScriptErrors")) return; - if (LLScriptEditorWSServer::isEnabled() && gSavedSettings.getBOOL("ExternalWebsocketForwardDebug")) - { - LLScriptEditorWSServer::ptr_t server = LLScriptEditorWSServer::getServer(); - if (server) - { - server->forwardChatToIDE(chat_msg); - } - } - // don't process debug messages from not owned objects, see EXT-7762 if (gAgentID != chat_msg.mOwnerID) { @@ -746,16 +801,6 @@ void LLFloaterIMNearbyChatHandler::processChat(const LLChat& chat_msg, return; } } - else if ((chat_msg.mChatType == CHAT_TYPE_OWNER) && LLScriptEditorWSServer::isEnabled() && - gSavedSettings.getBOOL("ExternalWebsocketForwardDebug")) - { - LLScriptEditorWSServer::ptr_t server = LLScriptEditorWSServer::getServer(); - if (server) - { - server->forwardChatToIDE(chat_msg); - } - } - nearby_chat->addMessage(chat_msg, true, args); if (chat_msg.mSourceType == CHAT_SOURCE_AGENT diff --git a/indra/newview/llfloaterimsessiontab.h b/indra/newview/llfloaterimsessiontab.h index a89de3ceaf1..c5c32219f3f 100644 --- a/indra/newview/llfloaterimsessiontab.h +++ b/indra/newview/llfloaterimsessiontab.h @@ -124,6 +124,8 @@ class LLFloaterIMSessionTab virtual void sessionVoiceOrIMStarted(const LLUUID& session_id) override {}; // Stub virtual void sessionIDUpdated(const LLUUID& old_session_id, const LLUUID& new_session_id) override {}; // Stub + bool isP2PSessionType() { return mIsP2PChat; } + protected: // callback for click on any items of the visual states menu diff --git a/indra/newview/llfloaterinspect.cpp b/indra/newview/llfloaterinspect.cpp index 40a95206e48..acdd615826a 100644 --- a/indra/newview/llfloaterinspect.cpp +++ b/indra/newview/llfloaterinspect.cpp @@ -147,18 +147,21 @@ LLFloaterInspect::~LLFloaterInspect(void) { mInspectColumnConfigConnection.disconnect(); } - if(!LLFloaterReg::instanceVisible("build")) + if (!LLApp::isExiting()) { - if(LLToolMgr::getInstance()->getBaseTool() == LLToolCompInspect::getInstance()) + if (!LLFloaterReg::instanceVisible("build")) { - LLToolMgr::getInstance()->clearTransientTool(); + if (LLToolMgr::getInstance()->getBaseTool() == LLToolCompInspect::getInstance()) + { + LLToolMgr::getInstance()->clearTransientTool(); + } + // Switch back to basic toolset + LLToolMgr::getInstance()->setCurrentToolset(gBasicToolset); + } + else + { + LLFloaterReg::showInstance("build", LLSD(), true); } - // Switch back to basic toolset - LLToolMgr::getInstance()->setCurrentToolset(gBasicToolset); - } - else - { - LLFloaterReg::showInstance("build", LLSD(), true); } } diff --git a/indra/newview/llfloaterland.cpp b/indra/newview/llfloaterland.cpp index 3c7f4d8c792..a8762e8dd7c 100644 --- a/indra/newview/llfloaterland.cpp +++ b/indra/newview/llfloaterland.cpp @@ -454,9 +454,6 @@ bool LLPanelLandGeneral::postBuild() mContentRating = getChild("ContentRatingText"); mLandType = getChild("LandTypeText"); - mBtnProfile = getChild("Profile..."); - mBtnProfile->setClickedCallback(boost::bind(&LLPanelLandGeneral::onClickProfile, this)); - mTextGroupLabel = getChild("Group:"); mTextGroup = getChild("GroupText"); @@ -587,8 +584,6 @@ void LLPanelLandGeneral::refresh() mTextOwner->setText(LLStringUtil::null); mContentRating->setText(LLStringUtil::null); mLandType->setText(LLStringUtil::null); - mBtnProfile->setLabel(getString("profile_text")); - mBtnProfile->setEnabled(false); mTextClaimDate->setText(LLStringUtil::null); mTextGroup->setText(LLStringUtil::null); @@ -670,7 +665,6 @@ void LLPanelLandGeneral::refresh() mTextSalePending->setEnabled(false); mTextOwner->setText(getString("public_text")); mTextOwner->setEnabled(false); - mBtnProfile->setEnabled(false); mTextClaimDate->setText(LLStringUtil::null); mTextClaimDate->setEnabled(false); mTextGroup->setText(getString("none_text")); @@ -699,21 +693,14 @@ void LLPanelLandGeneral::refresh() //refreshNames(); mTextOwner->setEnabled(true); - // We support both group and personal profiles - mBtnProfile->setEnabled(true); - if (parcel->getGroupID().isNull()) { - // Not group owned, so "Profile" - mBtnProfile->setLabel(getString("profile_text")); mTextGroup->setText(getString("none_text")); mTextGroup->setEnabled(false); } else { - // Group owned, so "Info" - mBtnProfile->setLabel(getString("info_text")); //mTextGroup->setText("HIPPOS!");//parcel->getGroupName()); mTextGroup->setEnabled(true); @@ -947,23 +934,6 @@ void LLPanelLandGeneral::onClickSetGroup() } } -void LLPanelLandGeneral::onClickProfile() -{ - LLParcel* parcel = mParcel->getParcel(); - if (!parcel) return; - - if (parcel->getIsGroupOwned()) - { - const LLUUID& group_id = parcel->getGroupID(); - LLGroupActions::show(group_id); - } - else - { - const LLUUID& avatar_id = parcel->getOwnerID(); - LLAvatarActions::showProfile(avatar_id); - } -} - // public void LLPanelLandGeneral::setGroup(const LLUUID& group_id) { diff --git a/indra/newview/llfloaterland.h b/indra/newview/llfloaterland.h index 8af0caab337..79e5da042f2 100644 --- a/indra/newview/llfloaterland.h +++ b/indra/newview/llfloaterland.h @@ -146,7 +146,6 @@ class LLPanelLandGeneral virtual void draw(); void setGroup(const LLUUID& group_id); - void onClickProfile(); void onClickSetGroup(); static void onClickDeed(void*); static void onClickBuyLand(void* data); @@ -193,7 +192,6 @@ class LLPanelLandGeneral LLTextBox* mTextOwnerLabel; LLTextBox* mTextOwner; - LLButton* mBtnProfile; LLTextBox* mContentRating; LLTextBox* mLandType; diff --git a/indra/newview/llfloaterobjectweights.cpp b/indra/newview/llfloaterobjectweights.cpp index 11f58cac510..6247fff6183 100644 --- a/indra/newview/llfloaterobjectweights.cpp +++ b/indra/newview/llfloaterobjectweights.cpp @@ -34,8 +34,10 @@ #include "llagent.h" #include "llappviewer.h" +#include "llcallbacklist.h" #include "llviewerparcelmgr.h" #include "llviewerregion.h" +#include "llmeshrepository.h" static const std::string lod_strings[4] = { @@ -73,6 +75,14 @@ bool LLCrossParcelFunctor::apply(LLViewerObject* obj) LLFloaterObjectWeights::LLFloaterObjectWeights(const LLSD& key) : LLFloater(key), + mWeightsDirty(false), + mSelectionDirty(true), + mSelectionMeshDirty(true), + mSelectionLastLOD(-1), + mSelectionLastTris(0), + mSelectionLastArea(0), + mLastActiveLODRequests(0), + mSelectionRefreshTime(0.), mSelectedObjects(NULL), mSelectedPrims(NULL), mSelectedDownloadWeight(NULL), @@ -87,10 +97,20 @@ LLFloaterObjectWeights::LLFloaterObjectWeights(const LLSD& key) mTrianglesShown(nullptr), mPixelArea(nullptr) { + mSelectionConnection = LLSelectMgr::getInstance()->mUpdateSignal.connect( + [this]() + { + mSelectionDirty = true; + mSelectionMeshDirty = true; // assume that mesh data will arrive with a delay. + } + ); } LLFloaterObjectWeights::~LLFloaterObjectWeights() { + // onClose() normally does this, but the floater can also be destroyed + // outright, and gIdleCallbacks would keep calling into freed memory. + gIdleCallbacks.deleteFunction(onIdleRefresh, this); } // virtual @@ -113,6 +133,11 @@ bool LLFloaterObjectWeights::postBuild() mTrianglesShown = getChild("triangles_shown"); mPixelArea = getChild("pixel_area"); + mHighLodTris = getChild("high_lod_tris"); + mMediumLodTris = getChild("medium_lod_tris"); + mLowLodTris = getChild("low_lod_tris"); + mLowestLodTris = getChild("lowest_lod_tris"); + return true; } @@ -121,6 +146,14 @@ void LLFloaterObjectWeights::onOpen(const LLSD& key) { refresh(); updateLandImpacts(LLViewerParcelMgr::getInstance()->getFloatingParcelSelection()->getParcel()); + + onIdleRefresh(this); + gIdleCallbacks.addFunction(onIdleRefresh, this); +} + +void LLFloaterObjectWeights::onClose(bool app_quitting) +{ + gIdleCallbacks.deleteFunction(onIdleRefresh, this); } // virtual @@ -140,8 +173,9 @@ void LLFloaterObjectWeights::onWeightsUpdate(const SelectionCost& selection_cost mSelectedPhysicsWeight->setText(llformat("%.1f", selection_cost.mPhysicsCost)); mSelectedServerWeight->setText(llformat("%.1f", selection_cost.mSimulationCost)); - S32 render_cost = LLSelectMgr::getInstance()->getSelection()->getSelectedObjectRenderCost(); - mSelectedDisplayWeight->setText(llformat("%d", render_cost)); + // Postpone LLSelectMgr operations: getSelectedObjectRenderCost is + // not coroutine-safe, and the idle refresh reads this flag. + mWeightsDirty = true; toggleWeightsLoadingIndicators(false); }); @@ -162,20 +196,13 @@ void LLFloaterObjectWeights::setErrorStatus(S32 status, const std::string& reaso void LLFloaterObjectWeights::draw() { - // Normally it's a bad idea to set text and visibility inside draw - // since it can cause rect updates go to different, already drawn elements, - // but floater is very simple and these elements are supposed to be isolated + // This logic might be a bit too expensive for draw(), + // but this floater needs to react fast, even if it's + // detrimental to performance. Having callbacks like + // 'tris changed' in every selected object might be + // more impactful on performance. LLObjectSelectionHandle selection = LLSelectMgr::getInstance()->getSelection(); - if (selection->isEmpty()) - { - const std::string text = getString("nothing_selected"); - mLodLevel->setText(text); - mTrianglesShown->setText(text); - mPixelArea->setText(text); - - toggleRenderLoadingIndicators(false); - } - else + if (!selection->isEmpty() && !mSelectionDirty) { S32 object_lod = -1; bool multiple_lods = false; @@ -202,27 +229,71 @@ void LLFloaterObjectWeights::draw() } } - if (multiple_lods) + if (mSelectionLastTris != total_tris) { - mLodLevel->setText(getString("multiple_lods")); - toggleRenderLoadingIndicators(false); + mSelectionDirty = true; } - else if (object_lod < 0) + else if (mSelectionLastArea != pixel_area) { - // nodes are waiting for data - toggleRenderLoadingIndicators(true); + mSelectionDirty = true; } - else + else if (multiple_lods) { - mLodLevel->setText(getString(lod_strings[object_lod])); - toggleRenderLoadingIndicators(false); + mSelectionDirty |= mSelectionLastLOD != -1; + } + else if (mSelectionLastLOD != object_lod) + { + mSelectionDirty = true; } - mTrianglesShown->setText(llformat("%d", total_tris)); - mPixelArea->setText(llformat("%ld", (S64)pixel_area)); // value capped at 10M } LLFloater::draw(); } +void LLFloaterObjectWeights::onIdleRefresh(void* user_data) +{ + LLFloaterObjectWeights* self = (LLFloaterObjectWeights*)user_data; + + F64 current_time = LLTimer::getTotalSeconds(); + S32 current_lod_requests = LLMeshRepoThread::sActiveLODRequests.load(); + // Can't look for a specific mesh (no callback mechanics and we + // need multiple ones), so just update periodically if something loads + self->mSelectionMeshDirty |= (self->mLastActiveLODRequests > current_lod_requests); + bool throttle_elapsed = (current_time - self->mSelectionRefreshTime) >= 2.0; + + if (self->mLastActiveLODRequests < current_lod_requests) + { + // we are interested in finished downloads, + // so don't refresh if more requests are getting added. + self->mLastActiveLODRequests = current_lod_requests; + } + + if (self->mSelectionDirty) + { + // Always refresh on selection changes + self->refreshDataFromSelection(); + self->mSelectionDirty = false; + self->mSelectionRefreshTime = current_time; + // refreshDataFromSelection could have indirectly initiated more requests + self->mLastActiveLODRequests = LLMeshRepoThread::sActiveLODRequests.load(); + } + else if (self->mSelectionMeshDirty && throttle_elapsed) + { + // LOD requests count changed or mesh needs lod data reload + self->refreshDataFromSelection(); + self->mSelectionRefreshTime = current_time; + self->mSelectionMeshDirty = false; + // refreshDataFromSelection could have indirectly initiated more requests + self->mLastActiveLODRequests = LLMeshRepoThread::sActiveLODRequests.load(); + } + else if (self->mWeightsDirty) // also done in refreshDataFromSelection + { + LLObjectSelectionHandle selection = LLSelectMgr::getInstance()->getSelection(); + S32 render_cost = selection->getSelectedObjectRenderCost(); + self->mSelectedDisplayWeight->setText(llformat("%d", render_cost)); + self->mWeightsDirty = false; + } +} + void LLFloaterObjectWeights::updateLandImpacts(const LLParcel* parcel) { if (!parcel || LLSelectMgr::getInstance()->getSelection()->isEmpty()) @@ -250,6 +321,101 @@ void LLFloaterObjectWeights::updateLandImpacts(const LLParcel* parcel) } } +void LLFloaterObjectWeights::refreshDataFromSelection() +{ + LLObjectSelectionHandle selection = LLSelectMgr::getInstance()->getSelection(); + if (selection->isEmpty()) + { + const std::string text = getString("nothing_selected"); + mLodLevel->setText(text); + mTrianglesShown->setText(text); + mPixelArea->setText(text); + + mHighLodTris->setText(text); + mMediumLodTris->setText(text); + mLowLodTris->setText(text); + mLowestLodTris->setText(text); + + toggleRenderLoadingIndicators(false); + toggleLODLoadingIndicators(false); + } + else + { + S32 object_lod = -1; + bool multiple_lods = false; + S32 total_tris = 0; + F32 pixel_area = 0; + S32 high_tris = 0; + S32 medium_tris = 0; + S32 low_tris = 0; + S32 lowest_tris = 0; + for (LLObjectSelection::valid_root_iterator iter = selection->valid_root_begin(); + iter != selection->valid_root_end(); ++iter) + { + LLViewerObject* object = (*iter)->getObject(); + S32 lod = object->getLOD(); + if (object_lod < 0) + { + object_lod = lod; + } + else if (object_lod != lod) + { + multiple_lods = true; + } + + if (object->isRootEdit()) + { + total_tris += object->recursiveGetTriangleCount(); + pixel_area += object->getPixelArea(); + object->recursiveGetLODTriangleCount(high_tris, medium_tris, low_tris, lowest_tris); + } + } + + mSelectionLastTris = total_tris; + mSelectionLastArea = pixel_area; + if (multiple_lods) + { + mSelectionLastLOD = -1; + } + else + { + mSelectionLastLOD = object_lod; + } + + if (multiple_lods) + { + mLodLevel->setText(getString("multiple_lods")); + toggleRenderLoadingIndicators(false); + toggleLODLoadingIndicators(false); + } + else if (object_lod < 0) + { + // nodes are waiting for data + toggleRenderLoadingIndicators(true); + toggleLODLoadingIndicators(true); + } + else + { + mLodLevel->setText(getString(lod_strings[object_lod])); + toggleRenderLoadingIndicators(false); + toggleLODLoadingIndicators(false); + } + mTrianglesShown->setText(llformat("%d", total_tris)); + mPixelArea->setText(llformat("%ld", (S64)pixel_area)); // value capped at 10M + mHighLodTris->setText(llformat("%d", high_tris)); + mMediumLodTris->setText(llformat("%d", medium_tris)); + mLowLodTris->setText(llformat("%d", low_tris)); + mLowestLodTris->setText(llformat("%d", lowest_tris)); + } + + if (mWeightsDirty) + { + S32 render_cost = selection->getSelectedObjectRenderCost(); + mSelectedDisplayWeight->setText(llformat("%d", render_cost)); + mWeightsDirty = false; + } +} + void LLFloaterObjectWeights::refresh() { LLSelectMgr* sel_mgr = LLSelectMgr::getInstance(); @@ -377,6 +543,19 @@ void LLFloaterObjectWeights::toggleRenderLoadingIndicators(bool visible) mPixelArea->setVisible(!visible); } +void LLFloaterObjectWeights::toggleLODLoadingIndicators(bool visible) +{ + childSetVisible("high_lod_tris_loading_indicator", visible); + childSetVisible("medium_lod_tris_loading_indicator", visible); + childSetVisible("low_lod_tris_loading_indicator", visible); + childSetVisible("lowest_lod_tris_loading_indicator", visible); + + mHighLodTris->setVisible(!visible); + mMediumLodTris->setVisible(!visible); + mLowLodTris->setVisible(!visible); + mLowestLodTris->setVisible(!visible); +} + void LLFloaterObjectWeights::updateIfNothingSelected() { const std::string text = getString("nothing_selected"); diff --git a/indra/newview/llfloaterobjectweights.h b/indra/newview/llfloaterobjectweights.h index bda625564ba..dac87d2a106 100644 --- a/indra/newview/llfloaterobjectweights.h +++ b/indra/newview/llfloaterobjectweights.h @@ -61,13 +61,17 @@ class LLFloaterObjectWeights : public LLFloater, LLAccountingCostObserver bool postBuild() override; void onOpen(const LLSD& key) override; + void onClose(bool app_quitting) override; void onWeightsUpdate(const SelectionCost& selection_cost) override; void setErrorStatus(S32 status, const std::string& reason) override; void draw() override; + static void onIdleRefresh(void* user_data); + void updateLandImpacts(const LLParcel* parcel); + void refreshDataFromSelection(); void refresh() override; private: @@ -76,9 +80,19 @@ class LLFloaterObjectWeights : public LLFloater, LLAccountingCostObserver void toggleWeightsLoadingIndicators(bool visible); void toggleLandImpactsLoadingIndicators(bool visible); void toggleRenderLoadingIndicators(bool visible); + void toggleLODLoadingIndicators(bool visible); void updateIfNothingSelected(); + bool mWeightsDirty; + bool mSelectionDirty; + bool mSelectionMeshDirty; + F64 mSelectionRefreshTime; + S32 mSelectionLastLOD; + S32 mSelectionLastTris; + F32 mSelectionLastArea; + S32 mLastActiveLODRequests; + LLTextBox *mSelectedObjects; LLTextBox *mSelectedPrims; @@ -95,6 +109,13 @@ class LLFloaterObjectWeights : public LLFloater, LLAccountingCostObserver LLTextBox *mLodLevel; LLTextBox *mTrianglesShown; LLTextBox *mPixelArea; + + LLTextBox *mHighLodTris; + LLTextBox *mMediumLodTris; + LLTextBox *mLowLodTris; + LLTextBox *mLowestLodTris; + + boost::signals2::scoped_connection mSelectionConnection; }; #endif //LL_LLFLOATEROBJECTWEIGHTS_H diff --git a/indra/newview/llfloaterreporter.cpp b/indra/newview/llfloaterreporter.cpp index 3c2c92973c3..a549cdc26ae 100644 --- a/indra/newview/llfloaterreporter.cpp +++ b/indra/newview/llfloaterreporter.cpp @@ -668,9 +668,9 @@ void LLFloaterReporter::showFromAvatar(const LLUUID& avatar_id, const std::strin } // static -void LLFloaterReporter::showFromChat(const LLUUID& avatar_id, const std::string& avatar_name, const std::string& time, const std::string& description) +void LLFloaterReporter::showFromChatObj(const LLUUID& object_id, const std::string& time, const std::string& description) { - show(avatar_id, avatar_name); + show(object_id, LLStringUtil::null, LLUUID::null); LLStringUtil::format_map_t args; args["[MSG_TIME]"] = time; @@ -679,8 +679,25 @@ void LLFloaterReporter::showFromChat(const LLUUID& avatar_id, const std::string& LLFloaterReporter *self = LLFloaterReg::findTypedInstance("reporter"); if (self) { - std::string description = self->getString("chat_report_format", args); - self->getChild("details_edit")->setValue(description); + std::string description_frmt = self->getString("chat_report_format", args); + self->getChild("details_edit")->setValue(description_frmt); + } +} + +// static +void LLFloaterReporter::showFromChatAv(const LLUUID& avatar_id, const std::string& avatar_name, const std::string& time, const std::string& description) +{ + show(avatar_id, avatar_name); + + LLStringUtil::format_map_t args; + args["[MSG_TIME]"] = time; + args["[MSG_DESCRIPTION]"] = description; + + LLFloaterReporter* self = LLFloaterReg::findTypedInstance("reporter"); + if (self) + { + std::string description_frmt = self->getString("chat_report_format", args); + self->getChild("details_edit")->setValue(description_frmt); } } diff --git a/indra/newview/llfloaterreporter.h b/indra/newview/llfloaterreporter.h index 31b05235a65..446e7d63b61 100644 --- a/indra/newview/llfloaterreporter.h +++ b/indra/newview/llfloaterreporter.h @@ -93,7 +93,8 @@ class LLFloaterReporter static void showFromObject(const LLUUID& object_id, const LLUUID& experience_id = LLUUID::null); static void showFromAvatar(const LLUUID& avatar_id, const std::string avatar_name); - static void showFromChat(const LLUUID& avatar_id, const std::string& avatar_name, const std::string& time, const std::string& description); + static void showFromChatObj(const LLUUID& object_id, const std::string& time, const std::string& description); + static void showFromChatAv(const LLUUID& avatar_id, const std::string& avatar_name, const std::string& time, const std::string& description); static void showFromExperience(const LLUUID& experience_id); static void onClickSend (void *userdata); diff --git a/indra/newview/llfloatersnapshot.cpp b/indra/newview/llfloatersnapshot.cpp index 45a4cbab919..7c7d2be05d5 100644 --- a/indra/newview/llfloatersnapshot.cpp +++ b/indra/newview/llfloatersnapshot.cpp @@ -96,13 +96,13 @@ LLSnapshotModel::ESnapshotFormat LLFloaterSnapshot::Impl::getImageFormat(LLFloat LLSpinCtrl* LLFloaterSnapshot::Impl::getWidthSpinner(LLFloaterSnapshotBase* floater) { LLPanelSnapshot* active_panel = getActivePanel(floater); - return active_panel ? active_panel->getWidthSpinner() : floater->getChild("snapshot_width"); + return active_panel ? active_panel->getWidthSpinner() : floater->findChild("snapshot_width"); } LLSpinCtrl* LLFloaterSnapshot::Impl::getHeightSpinner(LLFloaterSnapshotBase* floater) { LLPanelSnapshot* active_panel = getActivePanel(floater); - return active_panel ? active_panel->getHeightSpinner() : floater->getChild("snapshot_height"); + return active_panel ? active_panel->getHeightSpinner() : floater->findChild("snapshot_height"); } void LLFloaterSnapshot::Impl::enableAspectRatioCheckbox(LLFloaterSnapshotBase* floater, bool enable) @@ -280,7 +280,7 @@ void LLFloaterSnapshot::Impl::updateControls(LLFloaterSnapshotBase* floater) LLSpinCtrl* height_ctrl = getHeightSpinner(floater); // Initialize spinners. - if (width_ctrl->getValue().asInteger() == 0) + if (width_ctrl && width_ctrl->getValue().asInteger() == 0) { S32 w = gViewerWindow->getWindowWidthRaw(); LL_DEBUGS() << "Initializing width spinner (" << width_ctrl->getName() << "): " << w << LL_ENDL; @@ -290,7 +290,7 @@ void LLFloaterSnapshot::Impl::updateControls(LLFloaterSnapshotBase* floater) width_ctrl->setIncrement((F32)(w >> 1)); } } - if (height_ctrl->getValue().asInteger() == 0) + if (height_ctrl && height_ctrl->getValue().asInteger() == 0) { S32 h = gViewerWindow->getWindowHeightRaw(); LL_DEBUGS() << "Initializing height spinner (" << height_ctrl->getName() << "): " << h << LL_ENDL; @@ -688,8 +688,8 @@ void LLFloaterSnapshot::Impl::updateResolution(LLUICtrl* ctrl, void* data, bool LLSnapshotLivePreview* previewp = getPreviewView(); if (previewp && combobox->getCurrentIndex() >= 0) { - S32 original_width = 0 , original_height = 0 ; - previewp->getSize(original_width, original_height) ; + S32 original_width = 0, original_height = 0; + previewp->getSize(original_width, original_height); if (gSavedSettings.getBOOL("RenderUIInSnapshot") || gSavedSettings.getBOOL("RenderHUDInSnapshot")) { //clamp snapshot resolution to window size when showing UI or HUD in snapshot @@ -739,24 +739,31 @@ void LLFloaterSnapshot::Impl::updateResolution(LLUICtrl* ctrl, void* data, bool previewp->setSize(width, height); } - checkAspectRatio(view, width) ; + checkAspectRatio(view, width); previewp->getSize(width, height); - // We use the height spinner here because we come here via the aspect ratio - // checkbox as well and we want height always changing to width by default. - // If we use the width spinner we would change width according to height by - // default, that is not what we want. - updateSpinners(view, previewp, width, height, !getHeightSpinner(view)->isDirty()); // may change width and height + LLSpinCtrl* height_ctrl = getHeightSpinner(view); + if (height_ctrl) + { + // We use the height spinner here because we come here via the aspect ratio + // checkbox as well and we want height always changing to width by default. + // If we use the width spinner we would change width according to height by + // default, that is not what we want. + updateSpinners(view, previewp, width, height, !height_ctrl->isDirty()); // may change width and height + } - if(getWidthSpinner(view)->getValue().asInteger() != width || getHeightSpinner(view)->getValue().asInteger() != height) + LLSpinCtrl* width_ctrl = getWidthSpinner(view); + if (width_ctrl + && height_ctrl + && (width_ctrl->getValue().asInteger() != width || height_ctrl->getValue().asInteger() != height)) { - getWidthSpinner(view)->setValue(width); - getHeightSpinner(view)->setValue(height); + width_ctrl->setValue(width); + height_ctrl->setValue(height); if (getActiveSnapshotType(view) == LLSnapshotModel::SNAPSHOT_TEXTURE) { - getWidthSpinner(view)->setIncrement((F32)(width >> 1)); - getHeightSpinner(view)->setIncrement((F32)(height >> 1)); + width_ctrl->setIncrement((F32)(width >> 1)); + height_ctrl->setIncrement((F32)(height >> 1)); } } @@ -826,7 +833,7 @@ void LLFloaterSnapshot::Impl::comboSetCustom(LLFloaterSnapshotBase* floater, con } // Update supplied width and height according to the constrain proportions flag; limit them by max_val. -bool LLFloaterSnapshot::Impl::checkImageSize(LLSnapshotLivePreview* previewp, S32& width, S32& height, bool isWidthChanged, S32 max_value) +bool LLFloaterSnapshot::Impl::checkImageSize(LLSnapshotLivePreview* previewp, S32& width, S32& height, bool isWidthChanged, S32 max_value) const { S32 w = width ; S32 h = height ; @@ -872,19 +879,31 @@ bool LLFloaterSnapshot::Impl::checkImageSize(LLSnapshotLivePreview* previewp, S3 void LLFloaterSnapshot::Impl::setImageSizeSpinnersValues(LLFloaterSnapshotBase* view, S32 width, S32 height) { - getWidthSpinner(view)->forceSetValue(width); - getHeightSpinner(view)->forceSetValue(height); + LLSpinCtrl* width_ctrl = getWidthSpinner(view); + LLSpinCtrl* height_ctrl = getHeightSpinner(view); + if (!height_ctrl || !width_ctrl) + { + return; + } + width_ctrl->forceSetValue(width); + height_ctrl->forceSetValue(height); if (getActiveSnapshotType(view) == LLSnapshotModel::SNAPSHOT_TEXTURE) { - getWidthSpinner(view)->setIncrement((F32)(width >> 1)); - getHeightSpinner(view)->setIncrement((F32)(height >> 1)); + width_ctrl->setIncrement((F32)(width >> 1)); + height_ctrl->setIncrement((F32)(height >> 1)); } } void LLFloaterSnapshot::Impl::updateSpinners(LLFloaterSnapshotBase* view, LLSnapshotLivePreview* previewp, S32& width, S32& height, bool is_width_changed) { - getWidthSpinner(view)->resetDirty(); - getHeightSpinner(view)->resetDirty(); + LLSpinCtrl* width_ctrl = getWidthSpinner(view); + LLSpinCtrl* height_ctrl = getHeightSpinner(view); + if (!height_ctrl || !width_ctrl) + { + return; + } + width_ctrl->resetDirty(); + height_ctrl->resetDirty(); if (checkImageSize(previewp, width, height, is_width_changed, previewp->getMaxImageSize())) { setImageSizeSpinnersValues(view, width, height); @@ -905,7 +924,15 @@ void LLFloaterSnapshot::Impl::applyCustomResolution(LLFloaterSnapshotBase* view, if (w != curw || h != curh) { //if to upload a snapshot, process spinner input in a special way. - previewp->setMaxImageSize((S32) getWidthSpinner(view)->getMaxValue()) ; + LLSpinCtrl* width_ctrl = getWidthSpinner(view); + if (width_ctrl) + { + previewp->setMaxImageSize((S32)width_ctrl->getMaxValue()); + } + else + { + previewp->setMaxImageSize((S32)2048); + } previewp->setSize(w,h); checkAutoSnapshot(previewp, false); diff --git a/indra/newview/llfloatersnapshot.h b/indra/newview/llfloatersnapshot.h index 186d9c41cf0..703ce9e57b5 100644 --- a/indra/newview/llfloatersnapshot.h +++ b/indra/newview/llfloatersnapshot.h @@ -194,7 +194,7 @@ class LLFloaterSnapshot::Impl : public LLFloaterSnapshotBase::ImplBase void onImageFormatChange(LLFloaterSnapshotBase* view); void applyCustomResolution(LLFloaterSnapshotBase* view, S32 w, S32 h); static void onSendingPostcardFinished(LLFloaterSnapshotBase* floater, bool status); - bool checkImageSize(LLSnapshotLivePreview* previewp, S32& width, S32& height, bool isWidthChanged, S32 max_value); + bool checkImageSize(LLSnapshotLivePreview* previewp, S32& width, S32& height, bool isWidthChanged, S32 max_value) const; void setImageSizeSpinnersValues(LLFloaterSnapshotBase *view, S32 width, S32 height); void updateSpinners(LLFloaterSnapshotBase* view, LLSnapshotLivePreview* previewp, S32& width, S32& height, bool is_width_changed); static void onSnapshotUploadFinished(LLFloaterSnapshotBase* floater, bool status); diff --git a/indra/newview/llfloaterworldmap.cpp b/indra/newview/llfloaterworldmap.cpp index a2b1b68a81c..c7b2a0d4712 100644 --- a/indra/newview/llfloaterworldmap.cpp +++ b/indra/newview/llfloaterworldmap.cpp @@ -426,7 +426,7 @@ bool LLFloaterWorldMap::postBuild() mEventsMatureCheck = getChild("events_mature_chk"); mEventsAdultCheck = getChild("events_adult_chk"); - mAvatarIcon = getChild("avatar_icon"); + mFriendAvatarIcon = getChild("friends_icon"); mLandmarkIcon = getChild("landmark_icon"); mLocationIcon = getChild("location_icon"); @@ -617,11 +617,11 @@ void LLFloaterWorldMap::draw() LLTracker::ETrackingStatus tracking_status = LLTracker::getTrackingStatus(); if (LLTracker::TRACKING_AVATAR == tracking_status) { - mAvatarIcon->setColor( map_track_color); + mFriendAvatarIcon->setColor( map_track_color); } else { - mAvatarIcon->setColor( map_track_disabled_color); + mFriendAvatarIcon->setColor( map_track_disabled_color); } if (LLTracker::TRACKING_LANDMARK == tracking_status) diff --git a/indra/newview/llfloaterworldmap.h b/indra/newview/llfloaterworldmap.h index 826d7464863..b68a8a528ef 100644 --- a/indra/newview/llfloaterworldmap.h +++ b/indra/newview/llfloaterworldmap.h @@ -236,7 +236,7 @@ class LLFloaterWorldMap : public LLFloater LLCheckBoxCtrl* mEventsMatureCheck = nullptr; LLCheckBoxCtrl* mEventsAdultCheck = nullptr; - LLUICtrl* mAvatarIcon = nullptr; + LLUICtrl* mFriendAvatarIcon = nullptr; LLUICtrl* mLandmarkIcon = nullptr; LLUICtrl* mLocationIcon = nullptr; diff --git a/indra/newview/llgltfmateriallist.cpp b/indra/newview/llgltfmateriallist.cpp index 0d545c24f53..fa79e0d238e 100644 --- a/indra/newview/llgltfmateriallist.cpp +++ b/indra/newview/llgltfmateriallist.cpp @@ -544,7 +544,7 @@ void LLGLTFMaterialList::onAssetLoadComplete(const LLUUID& id, LLAssetType::ETyp if (status != LL_ERR_NOERR) { - LL_WARNS("GLTF") << "Error getting material asset data: " << LLAssetStorage::getErrorString(status) << " (" << status << ")" << LL_ENDL; + LL_WARNS("GLTF") << "Error getting material asset data: " << LLAssetStorage::getErrorString(status) << " (" << status << ") for asset " << id << LL_ENDL; asset_data->mMaterial->materialComplete(false); delete asset_data; } diff --git a/indra/newview/llgroupmgr.cpp b/indra/newview/llgroupmgr.cpp index 74077737e6a..5c30f43ab60 100644 --- a/indra/newview/llgroupmgr.cpp +++ b/indra/newview/llgroupmgr.cpp @@ -868,19 +868,15 @@ void LLGroupMgr::removeObserver(LLGroupMgrObserver* observer) { return; } - observer_multimap_t::iterator it; - it = mObservers.find(observer->getID()); - while (it != mObservers.end()) + observer_multimap_t::iterator it = mObservers.lower_bound(observer->getID()); + observer_multimap_t::iterator end = mObservers.upper_bound(observer->getID()); + for (; it != end; ++it) { if (it->second == observer) { mObservers.erase(it); break; } - else - { - ++it; - } } } diff --git a/indra/newview/llhudnametag.cpp b/indra/newview/llhudnametag.cpp index 6fa49ee2601..c10cf0555eb 100644 --- a/indra/newview/llhudnametag.cpp +++ b/indra/newview/llhudnametag.cpp @@ -41,9 +41,9 @@ #include "llhudrender.h" #include "llui.h" #include "llviewercamera.h" +#include "llviewerregion.h" #include "llviewertexturelist.h" #include "llviewerobject.h" -#include "llvovolume.h" #include "llviewerwindow.h" #include "llstatusbar.h" #include "llmenugl.h" @@ -291,7 +291,22 @@ void LLHUDNameTag::renderText() + (x_pixel_vec * screen_offset.mV[VX]) + (y_pixel_vec * screen_offset.mV[VY]); - LLGLDepthTest gls_depth(GL_TRUE, GL_FALSE); + // Check if an underwater name tag should be rendered over the water (while camera is above the water) + bool render_over_water = false; + static LLCachedControl nametag_over_water(gSavedSettings, "NametagOverWater", true); + if (nametag_over_water && + mSourceObject && + mSourceObject->getRegion() && + LLPipeline::sRenderTransparentWater && + !LLViewerCamera::getInstance()->cameraUnderWater()) + { + if (mSourceObject->getPositionAgent().mV[VZ] < mSourceObject->getRegion()->getWaterHeight()) + { + render_over_water = true; + } + } + LLGLDepthTest gls_depth(GL_TRUE, GL_FALSE, render_over_water ? GL_ALWAYS : GL_LEQUAL); + LLRect screen_rect; screen_rect.setCenterAndSize(0, static_cast(lltrunc(-mHeight / 2 + mOffsetY)), static_cast(lltrunc(mWidth)), static_cast(lltrunc(mHeight))); mRoundedRectImgp->draw3D(render_position, x_pixel_vec, y_pixel_vec, screen_rect, bg_color); diff --git a/indra/newview/llimprocessing.cpp b/indra/newview/llimprocessing.cpp index 8a4b2276515..57035a7d0bf 100644 --- a/indra/newview/llimprocessing.cpp +++ b/indra/newview/llimprocessing.cpp @@ -1881,7 +1881,8 @@ void LLIMProcessing::requestOfflineMessagesCoro(std::string url) if (!contents.size()) { - LL_WARNS("Messaging") << "No contents received for offline messages via capability " << url << LL_ENDL; + // Received no offline messages on login. + LL_INFOS("Messaging") << "No contents received for offline messages via capability " << url << LL_ENDL; return; } diff --git a/indra/newview/llimview.cpp b/indra/newview/llimview.cpp index 69fa695af6a..9286e262634 100644 --- a/indra/newview/llimview.cpp +++ b/indra/newview/llimview.cpp @@ -3293,8 +3293,9 @@ void LLIMMgr::addMessage( return; } - // Fetch group chat history, enabled by default. - if (gSavedPerAccountSettings.getBOOL("FetchGroupChatHistory")) + // Fetch group chat or ad-hoc history, enabled by default. + static LLCachedControl fetch_chat_history(gSavedPerAccountSettings, "FetchGroupChatHistory", true); + if (fetch_chat_history && !session->isP2PSessionType()) { std::string chat_url = gAgent.getRegionCapability("ChatSessionRequest"); if (!chat_url.empty()) @@ -4213,8 +4214,9 @@ class LLViewerChatterBoxSessionStartReply : public LLHTTPNode { im_floater->processSessionUpdate(body["session_info"]); - // Send request for chat history, if enabled. - if (gSavedPerAccountSettings.getBOOL("FetchGroupChatHistory")) + // Send request for chat history, if enabled. Skip for peer-to-peer IMs. + static LLCachedControl fetch_chat_history(gSavedPerAccountSettings, "FetchGroupChatHistory", true); + if (fetch_chat_history && !im_floater->isP2PSessionType()) { std::string url = gAgent.getRegionCapability("ChatSessionRequest"); if (!url.empty()) diff --git a/indra/newview/llinventorybridge.cpp b/indra/newview/llinventorybridge.cpp index e5ae04c1c47..e54fd212ec9 100644 --- a/indra/newview/llinventorybridge.cpp +++ b/indra/newview/llinventorybridge.cpp @@ -259,11 +259,27 @@ const std::string& LLInvFVBridge::getName() const const std::string& LLInvFVBridge::getDisplayName() const { - if(mDisplayName.empty()) + if(mSearchableName.empty()) { - buildDisplayName(); + // first request of display name, build search string and cache it for later use + buildSearchableName(); } - return mDisplayName; + + LLInventoryModel* model = getInventoryModel(); + if (model) + { + LLViewerInventoryCategory* cat = model->getCategory(mUUID); + if (cat) + { + return cat->getDisplayName(); + } + LLViewerInventoryItem* item = model->getItem(mUUID); + if (item) + { + return item->getName(); + } + } + return LLStringUtil::null; } std::string LLInvFVBridge::getSearchableDescription() const @@ -2119,22 +2135,22 @@ PermissionMask LLItemBridge::getPermissionMask() const } // virtual -void LLItemBridge::buildDisplayName() const +void LLItemBridge::buildSearchableName() const { - if (getItem()) + LLViewerInventoryItem* item = getItem(); + if (item) { - mDisplayName.assign(getItem()->getName()); + // for items, display name matches item name + mSearchableName.assign(item->getName()); } else { - mDisplayName.assign(LLStringUtil::null); + mSearchableName.assign(LLStringUtil::null); } - - mSearchableName.assign(mDisplayName); mSearchableName.append(getLabelSuffix()); LLStringUtil::toUpper(mSearchableName); - // Name set, so trigger a sort + // Searchable and name set, so trigger a sort LLInventorySort sorter = static_cast(mRootViewModel).getSorter(); if (mParent && !sorter.isByDate()) { @@ -2462,39 +2478,17 @@ void LLFolderBridge::selectItem() } } -void LLFolderBridge::buildDisplayName() const +void LLFolderBridge::buildSearchableName() const { - LLFolderType::EType preferred_type = getPreferredType(); - - // *TODO: to be removed when database supports multi language. This is a - // temporary attempt to display the inventory folder in the user locale. - // mantipov: *NOTE: be sure this code is synchronized with LLFriendCardsManager::findChildFolderUUID - // it uses the same way to find localized string - - // HACK: EXT - 6028 ([HARD CODED]? Inventory > Library > "Accessories" folder) - // Translation of Accessories folder in Library inventory folder - bool accessories = false; - if(getName() == "Accessories") + LLViewerInventoryCategory* cat = gInventory.getCategory(getUUID()); + if (cat) { - //To ensure that Accessories folder is in Library we have to check its parent folder. - //Due to parent LLFolderViewFloder is not set to this item yet we have to check its parent via Inventory Model - LLInventoryCategory* cat = gInventory.getCategory(getUUID()); - if(cat) - { - const LLUUID& parent_folder_id = cat->getParentUUID(); - accessories = (parent_folder_id == gInventory.getLibraryRootFolderID()); - } + mSearchableName.assign(cat->getDisplayName()); } - - //"Accessories" inventory category has folder type FT_NONE. So, this folder - //can not be detected as protected with LLFolderType::lookupIsProtectedType - mDisplayName.assign(getName()); - if (accessories || LLFolderType::lookupIsProtectedType(preferred_type)) + else { - LLTrans::findString(mDisplayName, std::string("InvFolder ") + getName(), LLSD()); + mSearchableName.assign(LLStringUtil::null); } - - mSearchableName.assign(mDisplayName); mSearchableName.append(getLabelSuffix()); LLStringUtil::toUpper(mSearchableName); @@ -2508,6 +2502,8 @@ void LLFolderBridge::buildDisplayName() const std::string LLFolderBridge::getLabelSuffix() const { + // Folders, unlike items, have context dependent suffixes + // that may change as the folder is loaded static LLCachedControl xui_debug(gSavedSettings, "DebugShowXUINames", 0); if (mIsLoading && mTimeSinceRequestStart.getElapsedTimeF32() >= FOLDER_LOADING_MESSAGE_DELAY) @@ -4245,6 +4241,17 @@ void LLFolderBridge::perform_pasteFromClipboard() } else { + // Check that no folder is being pasted into itself or into one of its descendants + for (const LLUUID& item_id : objects) + { + LLInventoryCategory* cat = model->getCategory(item_id); + if (cat && (item_id == mUUID || model->isObjectDescendentOf(mUUID, item_id))) + { + LLNotificationsUtil::add("CannotPasteFolderIntoSelf"); + return; + } + } + // Check that all items can be moved into that folder : for the moment, only stock folder mismatch is checked for (std::vector::const_iterator iter = objects.begin(); iter != objects.end(); ++iter) { @@ -6729,18 +6736,29 @@ void LLCallingCardBridge::refreshFolderViewItem() void LLCallingCardBridge::checkSearchBySuffixChanges() { - if (!mDisplayName.empty()) + if (!mSearchableName.empty()) { - // changes in mDisplayName are processed by rename function and here it will be always same + LLViewerInventoryItem* item = getItem(); + if (!item) + { + // checkSearchBySuffixChanges is only used by friend list + // so if item is not found, we removed the calling card or + // are no longer friends. + mSearchableName.clear(); + return; + } + + // changes in display name are processed by rename function and here it will be always same // suffixes are also of fixed length, and we are processing change of one at a time, // so it should be safe to use length (note: mSearchableName is capitalized) - auto old_length = mSearchableName.length(); - auto new_length = mDisplayName.length() + getLabelSuffix().length(); + size_t old_length = mSearchableName.length(); + const std::string& display_name = item->getName(); + size_t new_length = display_name.length() + getLabelSuffix().length(); if (old_length == new_length) { return; } - mSearchableName.assign(mDisplayName); + mSearchableName.assign(display_name); mSearchableName.append(getLabelSuffix()); LLStringUtil::toUpper(mSearchableName); if (new_length replace_item(gSavedSettings, "InventoryAddAttachmentBehavior", false); - rez_attachment(item, NULL, ("attach" == action) ? replace_item() : true); // Replace if "Wear"ing. + static LLCachedControl add_attachment_behavior(gSavedSettings, "InventoryAddAttachmentBehavior", 0); + rez_attachment(item, NULL, ("attach" == action) ? (add_attachment_behavior() == 1) : true); // Replace if "Wear"ing. } else if(item && item->isFinished()) { @@ -7692,7 +7710,7 @@ bool LLObjectBridge::renameItem(const std::string& new_name) new_item->updateServer(false); model->updateItem(new_item); model->notifyObservers(); - buildDisplayName(); + buildSearchableName(); if (isAgentAvatarValid()) { @@ -8599,8 +8617,8 @@ void LLObjectBridgeAction::attachOrDetach() } else { - static LLCachedControl inventory_linking(gSavedSettings, "InventoryAddAttachmentBehavior", false); - LLAppearanceMgr::instance().wearItemOnAvatar(mUUID, true, inventory_linking()); // Don't replace if adding. + static LLCachedControl add_attachment_behavior(gSavedSettings, "InventoryAddAttachmentBehavior", 0); + LLAppearanceMgr::instance().wearItemOnAvatar(mUUID, true, add_attachment_behavior() == 1); // Don't replace if adding. } } diff --git a/indra/newview/llinventorybridge.h b/indra/newview/llinventorybridge.h index fc4289bcdf5..7740b11351e 100644 --- a/indra/newview/llinventorybridge.h +++ b/indra/newview/llinventorybridge.h @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -87,7 +87,7 @@ class LLInvFVBridge : public LLFolderViewModelItemInventory virtual const LLUUID& getUUID() const { return mUUID; } virtual const LLUUID& getThumbnailUUID() const { return LLUUID::null; } virtual bool isFavorite() const { return false; } - virtual void clearDisplayName() { mDisplayName.clear(); } + virtual void clearSearchableName() { mSearchableName.clear(); } virtual void restoreItem() {} virtual void restoreToWorld() {} @@ -201,12 +201,12 @@ class LLInvFVBridge : public LLFolderViewModelItemInventory const LLUUID mUUID; // item id LLInventoryType::EType mInvType; bool mIsLink; - mutable std::string mDisplayName; + LLTimer mTimeSinceRequestStart; mutable std::string mSearchableName; void purgeItem(LLInventoryModel *model, const LLUUID &uuid); void removeObject(LLInventoryModel *model, const LLUUID &uuid); - virtual void buildDisplayName() const {} + virtual void buildSearchableName() const {} }; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -265,7 +265,7 @@ class LLItemBridge : public LLInvFVBridge protected: bool confirmRemoveItem(const LLSD& notification, const LLSD& response); virtual bool isItemPermissive() const; - virtual void buildDisplayName() const; + virtual void buildSearchableName() const; void doActionOnCurSelectedLandmark(LLLandmarkList::loaded_callback_t cb); private: @@ -286,7 +286,7 @@ class LLFolderBridge : public LLInvFVBridge void callback_dropItemIntoFolder(const LLSD& notification, const LLSD& response, LLInventoryItem* inv_item); void callback_dropCategoryIntoFolder(const LLSD& notification, const LLSD& response, LLInventoryCategory* inv_category); - virtual void buildDisplayName() const; + virtual void buildSearchableName() const; virtual void performAction(LLInventoryModel* model, std::string action); virtual void openItem(); diff --git a/indra/newview/llinventoryfilter.h b/indra/newview/llinventoryfilter.h index c0164e04e48..6ff1c4839f5 100644 --- a/indra/newview/llinventoryfilter.h +++ b/indra/newview/llinventoryfilter.h @@ -208,12 +208,14 @@ class LLInventoryFilter : public LLFolderViewFilter Optional filter_ops; Optional substring; Optional since_logoff; + Optional order; Params() : name("name"), filter_ops(""), substring("substring"), - since_logoff("since_logoff") + since_logoff("since_logoff"), + order("order") {} }; diff --git a/indra/newview/llinventorymodel.cpp b/indra/newview/llinventorymodel.cpp index bc1ceb01ea9..2edfe94de7a 100644 --- a/indra/newview/llinventorymodel.cpp +++ b/indra/newview/llinventorymodel.cpp @@ -28,6 +28,7 @@ #include #include +#include #include "llinventorymodel.h" @@ -86,6 +87,7 @@ const S32 LLInventoryModel::sCurrentInvCacheVersion = 5; bool LLInventoryModel::sFirstTimeInViewer2 = true; S32 LLInventoryModel::sPendingSystemFolders = 0; +static std::vector sPendingCacheThreads; ///---------------------------------------------------------------------------- /// Local function declarations, constants, enums, and typedefs @@ -104,45 +106,6 @@ struct InventoryIDPtrLess } }; -class LLCanCache : public LLInventoryCollectFunctor -{ -public: - LLCanCache(LLInventoryModel* model) : mModel(model) {} - virtual ~LLCanCache() {} - virtual bool operator()(LLInventoryCategory* cat, LLInventoryItem* item); -protected: - LLInventoryModel* mModel; - std::set mCachedCatIDs; -}; - -bool LLCanCache::operator()(LLInventoryCategory* cat, LLInventoryItem* item) -{ - bool rv = false; - if(item) - { - if(mCachedCatIDs.find(item->getParentUUID()) != mCachedCatIDs.end()) - { - rv = true; - } - } - else if(cat) - { - // HACK: downcast - LLViewerInventoryCategory* c = (LLViewerInventoryCategory*)cat; - if(c->getVersion() != LLViewerInventoryCategory::VERSION_UNKNOWN) - { - S32 descendents_server = c->getDescendentCount(); - S32 descendents_actual = c->getViewerDescendentCount(); - if(descendents_server == descendents_actual) - { - mCachedCatIDs.insert(c->getUUID()); - rv = true; - } - } - } - return rv; -} - struct InventoryCallbackInfo { InventoryCallbackInfo(U32 callback, const LLUUID& inv_id) : @@ -471,30 +434,33 @@ LLInventoryModel::~LLInventoryModel() void LLInventoryModel::cleanupInventory() { + LL_PROFILE_ZONE_SCOPED; empty(); - // Deleting one observer might erase others from the list, so always pop off the front - while (!mObservers.empty()) + // Deleting one observer might trigger removeObserver, so use a local copy + if (!mObservers.empty()) { - observer_list_t::iterator iter = mObservers.begin(); - LLInventoryObserver* observer = *iter; - mObservers.erase(iter); - delete observer; + observer_list_t observers_to_delete; + observers_to_delete.swap(mObservers); + + for (LLInventoryObserver* observer : observers_to_delete) + { + delete observer; + } } if (mBulkFecthCallbackSlot.connected()) { mBulkFecthCallbackSlot.disconnect(); } - mObservers.clear(); // Run down HTTP transport mHttpHeaders.reset(); mHttpOptions.reset(); delete mHttpRequestFG; - mHttpRequestFG = NULL; + mHttpRequestFG = nullptr; delete mHttpRequestBG; - mHttpRequestBG = NULL; + mHttpRequestBG = nullptr; } // This is a convenience function to check if one object has a parent @@ -2473,22 +2439,101 @@ void LLInventoryModel::cache( const LLUUID& parent_folder_id, const LLUUID& agent_id) { + LL_PROFILE_ZONE_SCOPED; LL_DEBUGS(LOG_INV) << "Caching " << parent_folder_id << " for " << agent_id << LL_ENDL; + LLViewerInventoryCategory* root_cat = getCategory(parent_folder_id); - if(!root_cat) return; + if (!root_cat) + { + LL_WARNS(LOG_INV) << "Root category not found for " << parent_folder_id << LL_ENDL; + return; + } + cat_array_t categories; categories.push_back(root_cat); item_array_t items; - LLCanCache can_cache(this); - can_cache(root_cat, NULL); - collectDescendentsIf( - parent_folder_id, - categories, - items, - INCLUDE_TRASH, - can_cache); + // Lambda to check if a category should be cached + // Only cache if it has known version and matching descendent counts + auto should_cache_category = [](LLViewerInventoryCategory* cat) -> bool { + if (!cat || cat->getVersion() == LLViewerInventoryCategory::VERSION_UNKNOWN) + { + return false; + } + S32 descendents_server = cat->getDescendentCount(); + S32 descendents_actual = cat->getViewerDescendentCount(); + return (descendents_server == descendents_actual); + }; + + // Track which folders we've verified as cacheable descendants + std::unordered_set processed_folders; + processed_folders.insert(parent_folder_id); + + // First pass: identify all cacheable descendant folders + // Use pair of (folder_id, should_save_children) + std::deque> folders_to_check; + folders_to_check.push_back(std::make_pair(parent_folder_id, should_cache_category(root_cat))); + + while (!folders_to_check.empty()) + { + auto [current_id, save_children] = folders_to_check.front(); + folders_to_check.pop_front(); + + if (save_children) // else incorrect count or version + { + auto item_it = mParentChildItemTree.find(current_id); + if (item_it != mParentChildItemTree.end() && item_it->second) + { + for (LLViewerInventoryItem* item : *(item_it->second)) + { + if (item) + { + items.push_back(item); + } + } + } + } + + // Get child categories directly from the parent-child tree + auto cat_it = mParentChildCategoryTree.find(current_id); + if (cat_it != mParentChildCategoryTree.end() && cat_it->second) + { + for (LLViewerInventoryCategory* child_cat : *(cat_it->second)) + { + if (!child_cat) + { + continue; + } + + // Verify ownership matches (library vs agent inventory) + if (child_cat->getOwnerID() != root_cat->getOwnerID()) + { + LL_WARNS(LOG_INV) << "Owner mismatch in category tree: expected " + << root_cat->getOwnerID() << " got " + << child_cat->getOwnerID() << " for category " + << child_cat->getName() << LL_ENDL; + continue; + } + + const LLUUID& child_id = child_cat->getUUID(); + + // Only process each folder once + if (processed_folders.insert(child_id).second) + { + if (should_cache_category(child_cat)) + { + categories.push_back(child_cat); + folders_to_check.push_back(std::make_pair(child_id, true)); + } + else + { + folders_to_check.push_back(std::make_pair(child_id, false)); + } + } + } + } + } if (categories.empty() && items.empty()) { @@ -2507,17 +2552,70 @@ void LLInventoryModel::cache( } std::string gzip_filename = getInvCacheAddres(agent_id); gzip_filename.append(".gz"); - if(gzip_file(temp_file, gzip_filename)) + + if (sPendingCacheThreads.empty()) { - LL_DEBUGS(LOG_INV) << "Successfully compressed " << temp_file << " to " << gzip_filename << LL_ENDL; - LLFile::remove(temp_file); + LL_INFOS(LOG_INV) << "Inventory cache compression started" << LL_ENDL; } - else + + // Launch background packing thread + // Main thread is the only one modifying sPendingCacheThreads + auto compress_cache = [temp_file, gzip_filename]() { - LL_WARNS(LOG_INV) << "Unable to compress " << temp_file << " into " << gzip_filename << LL_ENDL; + LL_PROFILE_ZONE_NAMED("inv cache compression"); + LLTimer gzip_timer; + + if (gzip_file(temp_file, gzip_filename)) + { + F32 gzip_time = gzip_timer.getElapsedTimeF32(); + LL_DEBUGS(LOG_INV) << "Successfully compressed " << temp_file + << " to " << gzip_filename + << " in " << gzip_time << "s" << LL_ENDL; + LLFile::remove(temp_file); + } + else + { + LL_WARNS(LOG_INV) << "Unable to compress " << temp_file + << " into " << gzip_filename << LL_ENDL; + } + }; + + try + { + sPendingCacheThreads.emplace_back(compress_cache); + } + catch (...) + { + LL_WARNS(LOG_INV) << "Failed to start inventory cache compression thread; running compression synchronously" << LL_ENDL; + compress_cache(); } } +void LLInventoryModel::waitForPendingCacheWrites() +{ + if (sPendingCacheThreads.empty()) + { + return; + } + LL_PROFILE_ZONE_SCOPED; + + // By this point all threads should have already been added, + // viewer is shutting down, main thread is the only one to + // modify sPendingCacheThreads + LL_DEBUGS(LOG_INV) << "Waiting for " << sPendingCacheThreads.size() + << " inventory cache compression thread(s) to complete..." << LL_ENDL; + + for (auto& thread : sPendingCacheThreads) + { + if (thread.joinable()) + { + thread.join(); + } + } + sPendingCacheThreads.clear(); + + LL_INFOS(LOG_INV) << "Inventory cache compression completed" << LL_ENDL; +} void LLInventoryModel::addCategory(LLViewerInventoryCategory* category) { @@ -2825,10 +2923,11 @@ bool LLInventoryModel::loadSkeleton( for(LLSD::array_const_iterator it = options.beginArray(), end = options.endArray(); it != end; ++it) { - LLSD name = (*it)["name"]; - LLSD folder_id = (*it)["folder_id"]; - LLSD parent_id = (*it)["parent_id"]; - LLSD version = (*it)["version"]; + const LLSD &folder = *it; + const LLSD &name = folder["name"]; + const LLSD &folder_id = folder["folder_id"]; + const LLSD &parent_id = folder["parent_id"]; + const LLSD &version = folder["version"]; if(name.isDefined() && folder_id.isDefined() && parent_id.isDefined() @@ -2842,7 +2941,7 @@ bool LLInventoryModel::loadSkeleton( cat->setParent(parent_id.asUUID()); LLFolderType::EType preferred_type = LLFolderType::FT_NONE; - LLSD type_default = (*it)["type_default"]; + const LLSD &type_default = folder["type_default"]; if(type_default.isDefined()) { preferred_type = (LLFolderType::EType)type_default.asInteger(); @@ -3570,6 +3669,8 @@ bool LLInventoryModel::loadFromFile(const std::string& filename, const LLSD& llsd_cats = inventory["categories"]; if (llsd_cats.isArray()) { + size_t cats_count = llsd_cats.size(); + categories.reserve(cats_count); LLSD::array_const_iterator iter = llsd_cats.beginArray(); LLSD::array_const_iterator end = llsd_cats.endArray(); for (; iter != end; ++iter) @@ -3588,6 +3689,8 @@ bool LLInventoryModel::loadFromFile(const std::string& filename, const LLSD& llsd_items = inventory["items"]; if (llsd_items.isArray()) { + size_t items_count = llsd_items.size(); + items.reserve(items_count); LLSD::array_const_iterator iter = llsd_items.beginArray(); LLSD::array_const_iterator end = llsd_items.endArray(); for (; iter != end; ++iter) diff --git a/indra/newview/llinventorymodel.h b/indra/newview/llinventorymodel.h index 847716c8e17..3e9ef2c3e9a 100644 --- a/indra/newview/llinventorymodel.h +++ b/indra/newview/llinventorymodel.h @@ -199,6 +199,9 @@ class LLInventoryModel // Call on logout to save a terse representation. void cache(const LLUUID& parent_folder_id, const LLUUID& agent_id); + + // Wait for any pending async cache operations to complete + static void waitForPendingCacheWrites(); private: // Information for tracking the actual inventory. We index this // information in a lot of different ways so we can access diff --git a/indra/newview/llinventoryobserver.h b/indra/newview/llinventoryobserver.h index 99cb9ec8118..dbafc6fcb6f 100644 --- a/indra/newview/llinventoryobserver.h +++ b/indra/newview/llinventoryobserver.h @@ -183,7 +183,7 @@ class LLInventoryAddItemByAssetObserver : public LLInventoryObserver item_ref_t mAddedItems; item_ref_t mWatchedAssets; -private: +protected: bool mIsDirty; }; diff --git a/indra/newview/llinventorypanel.cpp b/indra/newview/llinventorypanel.cpp index 09c6930cc4a..7efc3c77253 100644 --- a/indra/newview/llinventorypanel.cpp +++ b/indra/newview/llinventorypanel.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -202,7 +202,6 @@ LLFolderView * LLInventoryPanel::createFolderRoot(LLUUID root_id ) p.title = getLabel(); p.rect = LLRect(0, 0, getRect().getWidth(), 0); p.parent_panel = this; - p.tool_tip = p.name; p.listener = mInvFVBridgeBuilder->createBridge( LLAssetType::AT_CATEGORY, LLAssetType::AT_CATEGORY, LLInventoryType::IT_CATEGORY, @@ -585,8 +584,9 @@ void LLInventoryPanel::itemChanged(const LLUUID& item_id, U32 mask, const LLInve LLInvFVBridge* bridge = (LLInvFVBridge*)view_item->getViewModelItem(); if(bridge) { - // Clear the display name first, so it gets properly re-built during refresh() - bridge->clearDisplayName(); + // Clear the searchable name first, so it gets + // properly re-built during refresh() + bridge->clearSearchableName(); view_item->refresh(); } @@ -1067,10 +1067,20 @@ LLFolderViewFolder * LLInventoryPanel::createFolderViewFolder(LLInvFVBridge * br { LLFolderViewFolder::Params params(mParams.folder); - params.name = bridge->getDisplayName(); +#ifndef LL_RELEASE_FOR_DOWNLOAD + // Only usable for debug and first call has a large + // overhead from search string construction. + // As inventory names aren't unique and can change, + // there is little we can use them for in release builds. + params.name = bridge->getName(); +#else + // We don't have a source of unique names and inventory + // items can reach millions in quantity, just use + // a short descriptor + params.name = "fld"; +#endif params.root = mFolderRoot.get(); params.listener = bridge; - params.tool_tip = params.name; params.allow_drop = allow_drop; params.font_color = (bridge->isLibraryItem() ? sLibraryColor : sDefaultColor); @@ -1083,12 +1093,23 @@ LLFolderViewItem * LLInventoryPanel::createFolderViewItem(LLInvFVBridge * bridge { LLFolderViewItem::Params params(mParams.item); - params.name = bridge->getDisplayName(); +#ifndef LL_RELEASE_FOR_DOWNLOAD + // Only usable for debug and first call has a large + // overhead from search string construction. + // As inventory names aren't unique, are large and can change, + // there is little we can use them for in release builds. + // Prefer shorter + params.name = bridge->getName(); +#else + // We don't have a source of unique names and inventory + // items can reach millions in quantity, just use + // a short descriptor + params.name = "itm"; +#endif params.creation_date = bridge->getCreationDate(); params.root = mFolderRoot.get(); params.listener = bridge; params.rect = LLRect (0, 0, 0, 0); - params.tool_tip = params.name; params.font_color = (bridge->isLibraryItem() ? sLibraryColor : sDefaultColor); params.font_highlight_color = (bridge->isLibraryItem() ? sLibraryColor : sDefaultHighlightColor); @@ -1680,7 +1701,7 @@ void LLInventoryPanel::onSelectionChange(const std::deque& it LLFolderBridge* prev_bridge = (LLFolderBridge*)prev_folder_item->getViewModelItem(); if(prev_bridge) { - prev_bridge->clearDisplayName(); + prev_bridge->clearSearchableName(); prev_bridge->setShowDescendantsCount(false); prev_folder_item->refresh(); } @@ -1689,7 +1710,7 @@ void LLInventoryPanel::onSelectionChange(const std::deque& it LLFolderBridge* bridge = (LLFolderBridge*)folder_item->getViewModelItem(); if(bridge) { - bridge->clearDisplayName(); + bridge->clearSearchableName(); bridge->setShowDescendantsCount(true); folder_item->refresh(); mPreviousSelectedFolder = bridge->getUUID(); @@ -1704,7 +1725,7 @@ void LLInventoryPanel::onSelectionChange(const std::deque& it LLFolderBridge* prev_bridge = (LLFolderBridge*)prev_folder_item->getViewModelItem(); if(prev_bridge) { - prev_bridge->clearDisplayName(); + prev_bridge->clearSearchableName(); prev_bridge->setShowDescendantsCount(false); prev_folder_item->refresh(); } @@ -1724,7 +1745,7 @@ void LLInventoryPanel::updateFolderLabel(const LLUUID& folder_id) LLFolderBridge* bridge = (LLFolderBridge*)folder_item->getViewModelItem(); if(bridge) { - bridge->clearDisplayName(); + bridge->clearSearchableName(); bridge->setShowDescendantsCount(true); folder_item->refresh(); } diff --git a/indra/newview/lllogininstance.cpp b/indra/newview/lllogininstance.cpp index 9f066efc9f1..0b0f69b7446 100644 --- a/indra/newview/lllogininstance.cpp +++ b/indra/newview/lllogininstance.cpp @@ -96,6 +96,8 @@ LLLoginInstance::LLLoginInstance() : mDispatcher.add("connect", "", boost::bind(&LLLoginInstance::handleLoginSuccess, this, _1)); mDispatcher.add("disconnect", "", boost::bind(&LLLoginInstance::handleDisconnect, this, _1)); mDispatcher.add("indeterminate", "", boost::bind(&LLLoginInstance::handleIndeterminate, this, _1)); + // Todo, implement "authenticating"? + mDispatcher.add("authenticating", "", boost::bind(&LLLoginInstance::handleIndeterminate, this, _1)); } void LLLoginInstance::setPlatformInfo(const std::string platform, diff --git a/indra/newview/llmachineid.cpp b/indra/newview/llmachineid.cpp index 784a8a26a9a..35eb5eeae2f 100644 --- a/indra/newview/llmachineid.cpp +++ b/indra/newview/llmachineid.cpp @@ -34,7 +34,7 @@ #include #elif LL_DARWIN #include -#include +#include "llwindowmacosx_iokit.h" #endif unsigned char static_unique_id[] = {0,0,0,0,0,0}; unsigned char static_legacy_id[] = {0,0,0,0,0,0}; @@ -362,7 +362,7 @@ bool LLWMIMethods::getGenericSerialNumber(const BSTR &select, const LPCWSTR &var bool getSerialNumber(unsigned char *unique_id, size_t len) { CFStringRef serial_cf_str = NULL; - io_service_t platformExpert = IOServiceGetMatchingService(kIOMainPortDefault, + io_service_t platformExpert = IOServiceGetMatchingService(kLLIOMainPort, IOServiceMatching("IOPlatformExpertDevice")); if (platformExpert) { diff --git a/indra/newview/llmanipscale.cpp b/indra/newview/llmanipscale.cpp index 3c399c700c7..cd03c19647b 100644 --- a/indra/newview/llmanipscale.cpp +++ b/indra/newview/llmanipscale.cpp @@ -1311,6 +1311,12 @@ void LLManipScale::updateSnapGuides(const LLBBox& bbox) LLVector3 grid_scale; LLQuaternion grid_rotation; LLSelectMgr::getInstance()->getGrid(grid_origin, grid_rotation, grid_scale); + LLViewerCamera* camera = LLViewerCamera::getInstance(); + if (!camera) + { + // can be null on shutdown + return; + } bool uniform = LLManipScale::getUniform(); diff --git a/indra/newview/llmaterialeditor.cpp b/indra/newview/llmaterialeditor.cpp index 1cce55a5af1..44fb8b725a7 100644 --- a/indra/newview/llmaterialeditor.cpp +++ b/indra/newview/llmaterialeditor.cpp @@ -177,6 +177,65 @@ void LLFloaterComboOptions::onCancel() closeFloater(); } +class LLMaterialEditorTaskMoveObserver : public LLInventoryAddItemByAssetObserver +{ +public: + LLMaterialEditorTaskMoveObserver( + const LLUUID& asset_id, + const LLUUID& dest_folder_id, + LLPointer cb) + : mDestFolderID(dest_folder_id), + mCallback(cb) + { + watchAsset(asset_id); + } + + virtual ~LLMaterialEditorTaskMoveObserver() + { + gInventory.removeObserver(this); + } + + virtual void changed(U32 mask) override + { + // Call base class to populate mAddedItems + LLInventoryAddItemByAssetObserver::changed(mask); + + // If base class completed (which calls done() and clears mAddedItems), + // and we set mIsDirty, that means we're finished + if (mIsDirty) + { + gInventory.removeObserver(this); + delete this; + } + } + +protected: + virtual void done() override + { + // Find the moved item + // There must be only one since we are watching only one item, + // changed wouldn't fire otherwise. But just in case. + if (mAddedItems.size() == 1) + { + LLUUID item_id = mAddedItems[0]; + + // Fire the callback + if (mCallback) + { + mCallback->fire(item_id); + } + } + + // Todo: gInventoryMoveObserver normally opens inventory + // on completion, should this do the same? + } + +private: + LLUUID mDestFolderID; + std::string mNewName; + LLPointer mCallback; +}; + class LLMaterialEditorCopiedCallback : public LLInventoryCallback { public: @@ -189,6 +248,18 @@ class LLMaterialEditorCopiedCallback : public LLInventoryCallback mHasUnsavedChanges(has_unsaved_changes) {} + LLMaterialEditorCopiedCallback( + const std::string & buffer, + const LLSD & old_key, + const std::string & new_name, + bool has_unsaved_changes) + : mBuffer(buffer), + mOldKey(old_key), + mNewName(new_name), + mHasUnsavedChanges(has_unsaved_changes) + { + } + LLMaterialEditorCopiedCallback( const LLSD &old_key, const std::string &new_name) @@ -201,7 +272,10 @@ class LLMaterialEditorCopiedCallback : public LLInventoryCallback { if (!mNewName.empty()) { - // making a copy from a notecard doesn't change name, do it now + // making a copy from a task inventory (object, notecard) + // doesn't change name, do it now + // Todo: Can calling update_inventory_item and finishSaveAs + // cause a race condition? LLViewerInventoryItem* item = gInventory.getItem(inv_item_id); if (item->getName() != mNewName) { @@ -1811,6 +1885,52 @@ void LLMaterialEditor::onSaveAsMsgCallback(const LLSD& notification, const LLSD& mNotecardInventoryID, mAuxItem.get(), gInventoryCallbacks.registerCB(cb)); + + mAssetStatus = PREVIEW_ASSET_LOADING; + setEnabled(false); + } + else if (mObjectUUID.notNull()) + { + // Item is in task (object) inventory - must move to agent + // inventory first. + // Note: If this is too flimsy, just disable the 'save as' + // button when in object inventory and create a server ticket, + // as we need a copy with callback variant. + LLViewerObject* object = gObjectList.findObject(mObjectUUID); + if (object) + { + LLPermissions perm(item->getPermissions()); + if (perm.allowCopyBy(gAgent.getID(), gAgent.getGroupID()) + && perm.allowTransferTo(gAgent.getID())) + { + // Create a callback for after the item is copied/moved + std::string buffer = getEncodedAsset(); + LLPointer cb = new LLMaterialEditorCopiedCallback( + buffer, + getKey(), + new_name, + mUnsavedChanges); + + // Create observer to watch for the item arriving in inventory + // The observer will fire our callback when the move completes + LLMaterialEditorTaskMoveObserver* observer = new LLMaterialEditorTaskMoveObserver( + item->getAssetUUID(), + parent_id, + cb + ); + gInventory.addObserver(observer); + + // Copies(not moves) item from object to agent inventory + object->moveInventory(parent_id, item->getUUID()); + + mAssetStatus = PREVIEW_ASSET_LOADING; + setEnabled(false); + } + else + { + LL_WARNS("MaterialEditor") << "Insufficient permissions to copy material from object inventory" << LL_ENDL; + } + } } else { @@ -1823,10 +1943,10 @@ void LLMaterialEditor::onSaveAsMsgCallback(const LLSD& notification, const LLSD& parent_id, new_name, cb); - } - mAssetStatus = PREVIEW_ASSET_LOADING; - setEnabled(false); + mAssetStatus = PREVIEW_ASSET_LOADING; + setEnabled(false); + } } else { diff --git a/indra/newview/llmediactrl.cpp b/indra/newview/llmediactrl.cpp index 897236cb745..85cc08e5f1b 100644 --- a/indra/newview/llmediactrl.cpp +++ b/indra/newview/llmediactrl.cpp @@ -62,9 +62,15 @@ #include "lllineeditor.h" #include "llfloaterwebcontent.h" #include "llwindowshade.h" +#include "lleventapi.h" +#include "llui.h" extern bool gRestoreGL; +const std::string PAGE_TEXT_EXTRACT_MARKER = "PAGE_TEXT_EXTRACT:"; + +class LLMediaCtrlListener; + static LLDefaultChildRegistry::Register r("web_browser"); LLMediaCtrl::Params::Params() @@ -100,6 +106,7 @@ LLMediaCtrl::LLMediaCtrl( const Params& p) : mUpdateScrolls( false ), mTextureWidth ( 1024 ), mTextureHeight ( 1024 ), + mLoadingState( LOADING_STATE_INITIALIZING ), mClearCache(false), mHomePageMimeType(p.initial_mime_type), mErrorPageURL(p.error_page_url), @@ -1081,12 +1088,14 @@ void LLMediaCtrl::handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event) { LL_DEBUGS("Media") << "Media event: MEDIA_EVENT_NAVIGATE_BEGIN, url is " << self->getNavigateURI() << LL_ENDL; hideNotification(); + mLoadingState = LOADING_STATE_LOADING; }; break; case MEDIA_EVENT_NAVIGATE_COMPLETE: { LL_DEBUGS("Media") << "Media event: MEDIA_EVENT_NAVIGATE_COMPLETE, result string is: " << self->getNavigateResultString() << LL_ENDL; + mLoadingState = LOADING_STATE_LOADED; }; break; @@ -1115,6 +1124,7 @@ void LLMediaCtrl::handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event) { navigateTo(mErrorPageURL, HTTP_CONTENT_TEXT_HTML); }; + mLoadingState = LOADING_STATE_ERROR; }; break; @@ -1159,12 +1169,14 @@ void LLMediaCtrl::handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event) case MEDIA_EVENT_PLUGIN_FAILED: { LL_DEBUGS("Media") << "Media event: MEDIA_EVENT_PLUGIN_FAILED" << LL_ENDL; + mLoadingState = LOADING_STATE_ERROR; }; break; case MEDIA_EVENT_PLUGIN_FAILED_LAUNCH: { LL_DEBUGS("Media") << "Media event: MEDIA_EVENT_PLUGIN_FAILED_LAUNCH" << LL_ENDL; + mLoadingState = LOADING_STATE_ERROR; }; break; @@ -1247,6 +1259,33 @@ void LLMediaCtrl::handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event) case MEDIA_EVENT_DEBUG_MESSAGE: { LL_INFOS("media") << self->getDebugMessageText() << LL_ENDL; + + // Handle text extraction responses + std::string debug_text = self->getDebugMessageText(); + if (debug_text.find(PAGE_TEXT_EXTRACT_MARKER) != std::string::npos) + { + if (LLPluginClassMedia* plugin = getMediaPlugin()) + { + // Disable plugin debugging if it was used just for text extraction + static LLCachedControl media_debugging(gSavedSettings, "MediaPluginDebugging", false); + plugin->enableMediaPluginDebugging(media_debugging); + } + // Extract the pump name and page text + size_t marker_pos = debug_text.find(PAGE_TEXT_EXTRACT_MARKER); + if (marker_pos != std::string::npos) + { + std::string remaining = debug_text.substr(marker_pos + PAGE_TEXT_EXTRACT_MARKER.length()); + size_t colon_pos = remaining.find(':'); + if (colon_pos != std::string::npos) + { + std::string pump_name = remaining.substr(0, colon_pos); + std::string page_text = remaining.substr(colon_pos + 1); + + // Send the response directly to the specified pump + LLEventPumps::instance().obtain(pump_name).post(LLSD().with("text", page_text)); + } + } + } }; break; @@ -1337,3 +1376,182 @@ bool LLMediaCtrl::wantsReturnKey() const { return true; } + +std::string LLMediaCtrl::getMediaMimeType() +{ + return mMediaSource ? mMediaSource->getMimeType() : "unknown"; +} + +std::string LLMediaCtrl::getMediaLoadingStatus() +{ + if (!mMediaSource) + { + return "error"; + } + + switch (mLoadingState) + { + case LOADING_STATE_INITIALIZING: + return "initializing"; + case LOADING_STATE_LOADING: + return "loading"; + case LOADING_STATE_LOADED: + return "loaded"; + case LOADING_STATE_ERROR: + default: + return "error"; + } +} + +std::string LLMediaCtrl::getMediaTitle() +{ + if (mMediaSource) + { + if (LLPluginClassMedia* plugin = mMediaSource->getMediaPlugin()) + { + return plugin->getMediaName(); + } + } + return "unknown"; +} + +bool LLMediaCtrl::executeJavaScript(const std::string& script) +{ + if (mMediaSource && mMediaSource->hasMedia()) + { + mMediaSource->executeJavaScript(script); + return true; + } + return false; +} + +class LLMediaCtrlListener: public LLEventAPI +{ +public: + LLMediaCtrlListener(); + +private: + void getMediaInfo(const LLSD& request); + void getMediaText(const LLSD& request); + void getPluginsList(const LLSD& request); + void replyError(const LLSD& request, const std::string& error); + LLMediaCtrl* findMediaCtrl(const std::string& path); +}; + +LLMediaCtrlListener::LLMediaCtrlListener(): + LLEventAPI("LLMediaAPI", "Acces to LLMediaCtrl(web_browse widget) info") +{ + add("getMediaInfo", + "Get information about the web_browser widget specified by [\"path\"].\n" + "Returns URL, MIME type, and loading status of the widget.", + &LLMediaCtrlListener::getMediaInfo, + llsd::map("path", LLSD(), "reply", LLSD())); + + add("getMediaText", + "Get text content from the web_browser widget specified by [\"path\"].\n" + "Returns the text content of the page or a portion of it.", + &LLMediaCtrlListener::getMediaText, + llsd::map("path", LLSD(), "reply", LLSD())); + + add("getPluginsList", + "Enumerate the active media plugin (SLPlugin) instances.\n" + "Reply contains [\"plugins\"] an array of { pid, url, mime_type, remote_debugging_port } entries.", + &LLMediaCtrlListener::getPluginsList, + llsd::map("reply", LLSD())); +} + +LLMediaCtrl* LLMediaCtrlListener::findMediaCtrl(const std::string& path) +{ + LLView* view = LLUI::getInstance()->resolvePath(LLUI::getInstance()->getRootView(), path); + if (!view) + { + return nullptr; + } + return dynamic_cast(view); +} + +void LLMediaCtrlListener::getMediaInfo(const LLSD& request) +{ + Response reply(LLSD(), request); + std::string path = request["path"]; + + LLMediaCtrl* media_ctrl = findMediaCtrl(path); + if (!media_ctrl) + { + reply["error"] = "Could not find web_browser widget at path: " + path; + return; + } + + reply["url"] = media_ctrl->getCurrentNavUrl(); + reply["mime_type"] = media_ctrl->getMediaMimeType(); + reply["status"] = media_ctrl->getMediaLoadingStatus(); + reply["title"] = media_ctrl->getMediaTitle(); +} + +void LLMediaCtrlListener::replyError(const LLSD& request, const std::string& error) +{ + Response reply(LLSD(), request); + reply["error"] = error; +} + +void LLMediaCtrlListener::getMediaText(const LLSD& request) +{ + std::string path = request["path"]; + + LLMediaCtrl* media_ctrl = findMediaCtrl(path); + if (!media_ctrl) + { + replyError(request, "Could not find web_browser widget at path: " + path); + return; + } + + LLPluginClassMedia* plugin = media_ctrl->getMediaPlugin(); + if (!plugin) + { + replyError(request, "Media plugin is not available for widget at path: " + path); + return; + } + + // Enable plugin debugging to capture console messages + plugin->enableMediaPluginDebugging(true); + std::string pump_name = request["reply"].asString(); + + // Execute JavaScript to extract page text, embedding pump name in the marker + const std::string text_extract_script = "console.log('" + PAGE_TEXT_EXTRACT_MARKER + pump_name + ":' + " + "(document.body ? (document.body.innerText ? document.body.innerText.substring(0, 1000).replace(/\\s+/g, ' ').trim() : " + "'No text content') : 'Document body not ready'));"; + + if (!media_ctrl->executeJavaScript(text_extract_script)) + { + replyError(request, "Failed to execute JavaScript for text extraction"); + } +} + +void LLMediaCtrlListener::getPluginsList(const LLSD& request) +{ + Response reply(LLSD(), request); + + LLSD plugins; + + LLViewerMedia::impl_list& impls = LLViewerMedia::getInstance()->getPriorityList(); + for (LLViewerMedia::impl_list::iterator it = impls.begin(); it != impls.end(); ++it) + { + LLViewerMediaImpl* impl = *it; + if (!impl) + { + continue; + } + LLPluginClassMedia* plugin = impl->getMediaPlugin(); + + LLSD entry; + entry["url"] = impl->getCurrentMediaURL(); + entry["mime_type"] = impl->getMimeType(); + entry["pid"] = plugin ? plugin->getProcessID() : 0; + entry["remote_debugging_port"] = plugin ? (S32)plugin->getCefRemoteDebuggingPort() : 0; + + plugins.append(entry); + } + reply["plugins"] = plugins; +} + +static LLMediaCtrlListener sMediaCtrlListener; diff --git a/indra/newview/llmediactrl.h b/indra/newview/llmediactrl.h index 6bf61bf47ba..6398b6c0aa4 100644 --- a/indra/newview/llmediactrl.h +++ b/indra/newview/llmediactrl.h @@ -77,6 +77,14 @@ class LLMediaCtrl : public: virtual ~LLMediaCtrl(); + enum ELoadingState + { + LOADING_STATE_INITIALIZING = 0, + LOADING_STATE_LOADING = 1, + LOADING_STATE_LOADED = 2, + LOADING_STATE_ERROR = 3 + }; + void setBorderVisible( bool border_visible ); // For the tutorial window, we don't want to take focus on clicks, @@ -187,6 +195,11 @@ class LLMediaCtrl : virtual bool acceptsTextInput() const { return true; } + std::string getMediaMimeType(); + std::string getMediaLoadingStatus(); + std::string getMediaTitle(); + bool executeJavaScript(const std::string& script); + protected: void convertInputCoords(S32& x, S32& y); @@ -222,6 +235,7 @@ class LLMediaCtrl : viewer_media_t mMediaSource; S32 mTextureWidth, mTextureHeight; + ELoadingState mLoadingState; class LLWindowShade* mWindowShade; LLHandle mContextMenuHandle; diff --git a/indra/newview/llmutelist.cpp b/indra/newview/llmutelist.cpp index e3b94f6733c..325d485d638 100644 --- a/indra/newview/llmutelist.cpp +++ b/indra/newview/llmutelist.cpp @@ -167,7 +167,8 @@ LLMuteList::LLMuteList() : mLoadState(ML_INITIAL), mLoadSource(MLS_NONE), mRequestStartTime(0.f), - mTriedCacheFallback(false) + mTriedCacheFallback(false), + mTriedRegionChangeRetry(false) { gGenericDispatcher.addHandler("emptymutelist", &sDispatchEmptyMuteList); @@ -866,6 +867,9 @@ void LLMuteList::requestFromServer(const LLUUID& agent_id) // Guard against potentially writing back to disk since we're not recovering our connection mLoadState = ML_LOADED; mLoadSource = MLS_FALLBACK_CACHE; + // This code path means we have disconnected/crashed before our request has been sent. + // As a result we do not NEED to do anything more than set these state values. + // cache() is liable to be called on shutdown, but since we've set a dirty state it will avoid writing to disk. return; } if (!gAgent.getRegion()) @@ -888,7 +892,7 @@ void LLMuteList::requestFromServer(const LLUUID& agent_id) void LLMuteList::cache(const LLUUID& agent_id) { // Write to disk even if empty, but never from degraded fallback state. - if (isLoaded() && mLoadSource != MLS_FALLBACK_CACHE) + if (isLoaded() && !isLoadedDegraded()) { const std::string filename = getCacheFilename(agent_id); saveToFile(filename); diff --git a/indra/newview/llmutelist.h b/indra/newview/llmutelist.h index ee6c475166d..762ffb2e1c3 100644 --- a/indra/newview/llmutelist.h +++ b/indra/newview/llmutelist.h @@ -135,9 +135,9 @@ class LLMuteList : public LLSingleton // Load state accessors. bool isLoaded() const { return mLoadState == ML_LOADED; } // Loaded, but not necessarily from server. bool isFailed() const { return mLoadState == ML_FAILED; } // Unable to load any mute list. Server did not reply. - // Loaded from server, which is the only source we consider authoritative. - bool isLoadedFromServer() const { return isLoaded() && (mLoadSource == MLS_SERVER || mLoadSource == MLS_SERVER_EMPTY); } - // Loaded, but from cache. Would be nice to upgrade to a server load from here if possible. + // Loaded from an authoritative server response, including when the server directs us to use our cached copy. + bool isLoadedFromServer() const { return isLoaded() && (mLoadSource == MLS_SERVER || mLoadSource == MLS_SERVER_EMPTY || mLoadSource == MLS_SERVER_CACHE); } + // Loaded without an authoritative server response. Would be nice to upgrade to a server load from here if possible. bool isLoadedDegraded() const { return isLoaded() && !isLoadedFromServer(); } // Advance the load state machine, trying cache fallback if necessary. diff --git a/indra/newview/llnotificationstorage.cpp b/indra/newview/llnotificationstorage.cpp index 5cec35fc880..3fb922f93f5 100644 --- a/indra/newview/llnotificationstorage.cpp +++ b/indra/newview/llnotificationstorage.cpp @@ -108,7 +108,10 @@ bool LLNotificationStorage::readNotifications(LLSD& pNotificationData, bool is_n { std::string filename = is_new_filename? mFileName : mOldFileName; - LL_INFOS("LLNotificationStorage") << "starting read '" << filename << "'" << LL_ENDL; + if (!filename.empty()) + { + LL_INFOS("LLNotificationStorage") << "starting read '" << filename << "'" << LL_ENDL; + } bool didFileRead; @@ -118,7 +121,17 @@ bool LLNotificationStorage::readNotifications(LLSD& pNotificationData, bool is_n didFileRead = notifyFile.is_open(); if (!didFileRead) { - LL_WARNS("LLNotificationStorage") << "Failed to open file '" << filename << "'" << LL_ENDL; + if (!filename.empty()) + { + if (LLFile::isfile(filename)) + { + LL_WARNS("LLNotificationStorage") << "Failed to open file '" << filename << "'" << LL_ENDL; + } + else + { + LL_INFOS("LLNotificationStorage") << "File '" << filename << "' doesn't exist" << LL_ENDL; + } + } } else { @@ -144,7 +157,10 @@ bool LLNotificationStorage::readNotifications(LLSD& pNotificationData, bool is_n if(didFileRead) { writeNotifications(pNotificationData); - LLFile::remove(mOldFileName); + if (!mOldFileName.empty()) + { + LLFile::remove(mOldFileName, ENOENT); + } } } } diff --git a/indra/newview/lloutfitslist.cpp b/indra/newview/lloutfitslist.cpp index 7db79c70106..d49d1d662ab 100644 --- a/indra/newview/lloutfitslist.cpp +++ b/indra/newview/lloutfitslist.cpp @@ -519,6 +519,22 @@ void LLOutfitsList::resetItemSelection(LLWearableItemsList* list, const LLUUID& list->resetSelection(); mItemSelected = false; signalSelectionOutfitUUID(category_id); + + // If filtering was applied while tab was collapsed, item visibility is updated but the parent tab height might not be updated. + // Force rearrange to recompute the height, when tab is expanded. + static LLCachedControl show_all_items(gSavedSettings, "OutfitListFilterFullList", 1); + if (!show_all_items) + { + outfits_map_t::const_iterator tab_iter = mOutfitsMap.find(category_id); + if (tab_iter != mOutfitsMap.end()) + { + LLOutfitAccordionCtrlTab* tab = tab_iter->second; + if (tab && tab->getDisplayChildren()) + { + list->notify(LLSD().with("rearrange", true)); + } + } + } } void LLOutfitsList::onChangeOutfitSelection(LLWearableItemsList* list, const LLUUID& category_id) @@ -664,6 +680,8 @@ void LLOutfitsList::applyFilterToTab( } else { + // We got a match + tab->setVisible(true); // Try restoring the tab selection. restoreOutfitSelection(tab, category_id); } @@ -1648,7 +1666,7 @@ bool LLOutfitListSortMenu::onEnable(LLSD::String param) } else if ("show_entire_outfit" == param) { - static LLCachedControl filter_mode(gSavedSettings, "OutfitListFilterFullList", 0); + static LLCachedControl filter_mode(gSavedSettings, "OutfitListFilterFullList", 1); return filter_mode; } diff --git a/indra/newview/llpaneldirgroups.cpp b/indra/newview/llpaneldirgroups.cpp index 992d92091cf..324d84efde9 100644 --- a/indra/newview/llpaneldirgroups.cpp +++ b/indra/newview/llpaneldirgroups.cpp @@ -51,6 +51,12 @@ bool LLPanelDirGroups::postBuild() childSetAction("Search", &LLPanelDirBrowser::onClickSearchCore, this); setDefaultBtn( "Search" ); + if (gAgent.isTeen()) + { + childSetEnabled("incmature", false); + gSavedSettings.setBOOL("ShowMatureGroups", false); + } + return true; } @@ -72,11 +78,25 @@ void LLPanelDirGroups::performQuery() U32 scope = DFQ_GROUPS; // Check group mature filter. - if ( !gSavedSettings.getBOOL("ShowMatureGroups") || gAgent.isTeen() ) + if ( gSavedSettings.getBOOL("ShowMatureGroups") && !gAgent.isTeen() ) { + // Supposed behavior: + // if nothing is set will search for <= mature + // if DFQ_INC_PG is set, will look for <= PG + // if DFQ_INC_MATURE is set, will look for == mature + // if DFQ_INC_ADULT is set, will look for >= adult + // Not compatible with legacy DFQ_FILTER_MATURE. + // But there appears to be a server bug, so we only use + // this to show all and use legacy setting for 'pg only' + scope |= DFQ_INC_PG; + scope |= DFQ_INC_MATURE; + scope |= DFQ_INC_ADULT; + } + else + { + // DFQ_FILTER_MATURE is a legacy setting scope |= DFQ_FILTER_MATURE; } - mCurrentSortColumn = "score"; mCurrentSortAscending = false; diff --git a/indra/newview/llpanelgrouproles.cpp b/indra/newview/llpanelgrouproles.cpp index 61c0843506d..9de2ccd67e3 100644 --- a/indra/newview/llpanelgrouproles.cpp +++ b/indra/newview/llpanelgrouproles.cpp @@ -1993,9 +1993,11 @@ LLPanelGroupRolesSubTab::LLPanelGroupRolesSubTab() mRolesList(NULL), mAssignedMembersList(NULL), mAllowedActionsList(NULL), + mActionDescription(NULL), mRoleName(NULL), mRoleTitle(NULL), mRoleDescription(NULL), + mMembersNotLoadedLbl(NULL), mMemberVisibleCheck(NULL), mDeleteRoleButton(NULL), mCopyRoleButton(NULL), @@ -2024,6 +2026,7 @@ bool LLPanelGroupRolesSubTab::postBuildSubTab(LLView* root) mAssignedMembersList = parent->getChild("role_assigned_members"); mAllowedActionsList = parent->getChild("role_allowed_actions"); mActionDescription = parent->getChild("role_action_description"); + mMembersNotLoadedLbl = parent->getChild("members_not_loaded"); mRoleName = parent->getChild("role_name"); mRoleTitle = parent->getChild("role_title"); @@ -2254,12 +2257,18 @@ void LLPanelGroupRolesSubTab::update(LLGroupChange gc) } } - if ((GC_ROLE_MEMBER_DATA == gc || GC_MEMBER_DATA == gc) - && gdatap - && gdatap->isMemberDataComplete() - && gdatap->isRoleMemberDataComplete()) + if (gdatap && gdatap->isMemberDataComplete()) { - buildMembersList(); + if ((GC_ROLE_MEMBER_DATA == gc || GC_MEMBER_DATA == gc) + && gdatap->isRoleMemberDataComplete()) + { + buildMembersList(); + } + mMembersNotLoadedLbl->setVisible(false); + } + else + { + mMembersNotLoadedLbl->setVisible(true); } } @@ -3279,6 +3288,12 @@ void LLPanelGroupBanListSubTab::setBanCount(U32 ban_count) void LLPanelGroupBanListSubTab::populateBanList() { + if (mGroupID.isNull()) + { + mBanList->deleteAllItems(); + return; + } + LLGroupMgrGroupData* gdatap = LLGroupMgr::getInstance()->getGroupData(mGroupID); if(!gdatap) { diff --git a/indra/newview/llpanelgrouproles.h b/indra/newview/llpanelgrouproles.h index 24891af8563..f0a6dc33316 100644 --- a/indra/newview/llpanelgrouproles.h +++ b/indra/newview/llpanelgrouproles.h @@ -300,6 +300,7 @@ class LLPanelGroupRolesSubTab : public LLPanelGroupSubTab LLLineEditor* mRoleTitle; LLTextEditor* mRoleDescription; + LLUICtrl* mMembersNotLoadedLbl; LLCheckBoxCtrl* mMemberVisibleCheck; LLButton* mDeleteRoleButton; LLButton* mCreateRoleButton; diff --git a/indra/newview/llpanellandmedia.cpp b/indra/newview/llpanellandmedia.cpp index 294bd4021dd..feea97c4a90 100644 --- a/indra/newview/llpanellandmedia.cpp +++ b/indra/newview/llpanellandmedia.cpp @@ -135,8 +135,6 @@ void LLPanelLandMedia::refresh() mMediaURLEdit->setText(parcel->getMediaURL()); mMediaURLEdit->setEnabled( false ); - getChild("current_url")->setValue(parcel->getMediaCurrentURL()); - mMediaDescEdit->setText(parcel->getMediaDesc()); mMediaDescEdit->setEnabled( can_change_media ); @@ -234,12 +232,9 @@ void LLPanelLandMedia::setMediaURL(const std::string& media_url) LLParcel *parcel = mParcel->getParcel(); if(parcel) parcel->setMediaCurrentURL(media_url); - // LLViewerMedia::navigateHome(); mMediaURLEdit->onCommit(); - // LLViewerParcelMedia::sendMediaNavigateMessage(media_url); - getChild("current_url")->setValue(media_url); } std::string LLPanelLandMedia::getMediaURL() { @@ -322,8 +317,6 @@ void LLPanelLandMedia::onResetBtn(void *userdata) LLParcel* parcel = self->mParcel->getParcel(); // LLViewerMedia::navigateHome(); self->refresh(); - self->getChild("current_url")->setValue(parcel->getMediaURL()); - // LLViewerParcelMedia::sendMediaNavigateMessage(parcel->getMediaURL()); } diff --git a/indra/newview/llpanellogin.cpp b/indra/newview/llpanellogin.cpp index b68599dae91..9b5a3399587 100644 --- a/indra/newview/llpanellogin.cpp +++ b/indra/newview/llpanellogin.cpp @@ -808,6 +808,8 @@ void LLPanelLogin::onUpdateStartSLURL(const LLSLURL& new_start_slurl) case LLSLURL::HOME_LOCATION: //location_combo->setCurrentByIndex(0); // home location break; + case LLSLURL::LAST_LOCATION: + break; default: LL_WARNS("AppInit")<<"invalid login slurl, using home"<getFilter().fromParams(p); - mRecentPanel->setSortOrder(gSavedSettings.getU32(LLInventoryPanel::RECENTITEMS_SORT_ORDER)); + + // Restore sort order if it was saved + if (p.order.isProvided()) + { + mRecentPanel->setSortOrder(p.order()); + } + else + { + mRecentPanel->setSortOrder(gSavedSettings.getU32(LLInventoryPanel::RECENTITEMS_SORT_ORDER)); + } } } if(mActivePanel) { + // 'all items' tab if(savedFilterState.has(mActivePanel->getFilter().getName())) { LLSD items = savedFilterState.get(mActivePanel->getFilter().getName()); diff --git a/indra/newview/llpanelmarketplaceinboxinventory.cpp b/indra/newview/llpanelmarketplaceinboxinventory.cpp index 557c7bbd7ba..e0dbd9acd21 100644 --- a/indra/newview/llpanelmarketplaceinboxinventory.cpp +++ b/indra/newview/llpanelmarketplaceinboxinventory.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2009&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -82,10 +82,20 @@ LLFolderViewFolder * LLInboxInventoryPanel::createFolderViewFolder(LLInvFVBridge LLInboxFolderViewFolder::Params params; - params.name = bridge->getDisplayName(); +#ifndef LL_RELEASE_FOR_DOWNLOAD + // Only usable for debug and first call has a large + // overhead from search string construction. + // As inventory names aren't unique and can change, + // there is little we can use them for in release builds. + params.name = bridge->getName(); +#else + // We don't have a source of unique names and inventory + // items can reach millions in quantity, just use + // a short descriptor + params.name = "fld"; +#endif params.root = mFolderRoot.get(); params.listener = bridge; - params.tool_tip = params.name; params.font_color = item_color; params.font_highlight_color = item_color; params.allow_drop = allow_drop; @@ -99,12 +109,22 @@ LLFolderViewItem * LLInboxInventoryPanel::createFolderViewItem(LLInvFVBridge * b LLInboxFolderViewItem::Params params; - params.name = bridge->getDisplayName(); +#ifndef LL_RELEASE_FOR_DOWNLOAD + // Only usable for debug and first call has a large + // overhead from search string construction. + // As inventory names aren't unique and can change, + // there is little we can use them for in release builds. + params.name = bridge->getName(); +#else + // We don't have a source of unique names and inventory + // items can reach millions in quantity, just use + // a short descriptor + params.name = "itm"; +#endif params.creation_date = bridge->getCreationDate(); params.root = mFolderRoot.get(); params.listener = bridge; params.rect = LLRect (0, 0, 0, 0); - params.tool_tip = params.name; params.font_color = item_color; params.font_highlight_color = item_color; diff --git a/indra/newview/llpanelobjectinventory.cpp b/indra/newview/llpanelobjectinventory.cpp index a15a8229a60..8bc9057f9c0 100644 --- a/indra/newview/llpanelobjectinventory.cpp +++ b/indra/newview/llpanelobjectinventory.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2002&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -1724,7 +1724,6 @@ void LLPanelObjectInventory::createViewsForCategory(LLInventoryObject::object_li params.name = obj->getName(); params.root = mFolders; params.listener = bridge; - params.tool_tip = params.name; params.font_color = item_color; params.font_highlight_color = item_color; view = LLUICtrlFactory::create(params); @@ -1738,7 +1737,6 @@ void LLPanelObjectInventory::createViewsForCategory(LLInventoryObject::object_li params.listener = bridge; params.creation_date = bridge->getCreationDate(); params.rect = LLRect(); - params.tool_tip = params.name; params.font_color = item_color; params.font_highlight_color = item_color; view = LLUICtrlFactory::create(params); diff --git a/indra/newview/llpanelplaces.cpp b/indra/newview/llpanelplaces.cpp index 5e97d923d70..2ad45118013 100644 --- a/indra/newview/llpanelplaces.cpp +++ b/indra/newview/llpanelplaces.cpp @@ -88,6 +88,7 @@ static const std::string LANDMARK_INFO_TYPE = "landmark"; static const std::string REMOTE_PLACE_INFO_TYPE = "remote_place"; static const std::string TELEPORT_HISTORY_INFO_TYPE = "teleport_history"; static const std::string LANDMARK_TAB_INFO_TYPE = "open_landmark_tab"; +static const std::string TELEPORT_HISTORY_TAB_INFO_TYPE = "open_teleport_history_tab"; // Support for secondlife:///app/parcel/{UUID}/about SLapps class LLParcelHandler : public LLCommandHandler @@ -415,6 +416,15 @@ void LLPanelPlaces::onOpen(const LLSD& key) // Update the buttons at the bottom of the panel updateVerbs(); } + else if (key_type == TELEPORT_HISTORY_TAB_INFO_TYPE) + { + // toggle twice, similar to LANDMARK_TAB_INFO_TYPE + togglePlaceInfoPanel(false); + mPlaceInfoType = key_type; + togglePlaceInfoPanel(false); + onTabSelected(); + updateVerbs(); + } else if (key_type == CREATE_PICK_TYPE) { LLUUID item_id = key["item_id"]; @@ -1108,6 +1118,18 @@ void LLPanelPlaces::togglePlaceInfoPanel(bool visible) } } } + else if (mPlaceInfoType == TELEPORT_HISTORY_TAB_INFO_TYPE) + { + mLandmarkInfo->setVisible(false); + mPlaceProfile->setVisible(false); + if (!visible) + { + if (LLPanel* teleport_history_panel = mTabContainer->getPanelByName("Teleport History")) + { + mTabContainer->selectTabPanel(teleport_history_panel); + } + } + } } // virtual diff --git a/indra/newview/llpanelprofilepicks.cpp b/indra/newview/llpanelprofilepicks.cpp index a622ef3b8b5..d8190ab5902 100644 --- a/indra/newview/llpanelprofilepicks.cpp +++ b/indra/newview/llpanelprofilepicks.cpp @@ -627,6 +627,7 @@ bool LLPanelProfilePick::postBuild() { mPickName = getChild("pick_name"); mPickDescription = getChild("pick_desc"); + mPickLocation = getChild("pick_location"); mSaveButton = getChild("save_changes_btn"); mCreateButton = getChild("create_changes_btn"); mCancelButton = getChild("cancel_changes_btn"); @@ -651,11 +652,20 @@ bool LLPanelProfilePick::postBuild() mPickDescription->setKeystrokeCallback(boost::bind(&LLPanelProfilePick::onPickChanged, this, _1)); mPickDescription->setFocusReceivedCallback(boost::bind(&LLPanelProfilePick::onDescriptionFocusReceived, this)); - getChild("pick_location")->setEnabled(false); + mPickLocation->setEnabled(false); return true; } +void LLPanelProfilePick::reshape(S32 width, S32 height, bool called_from_parent) +{ + LLPanelProfilePropertiesProcessorTab::reshape(width, height, called_from_parent); + if (mPickLocation) + { + mPickLocation->setCursor(0); + } +} + void LLPanelProfilePick::onDescriptionFocusReceived() { if (!mIsEditing && getSelfProfile()) @@ -754,7 +764,14 @@ void LLPanelProfilePick::setPickLocation(const LLUUID &parcel_id, const std::str void LLPanelProfilePick::setPickLocation(const std::string& location) { - getChild("pick_location")->setValue(location); + mPickLocation->setValue(location); + // Pick location can be set with a long 'substitute' value or + // just a long value. + // If user sets cursor at the end, application of the substitute + // value can shift text from visible are to the left. When text + // gets restored or set, text position isn't, so just drop cursor + // position. + mPickLocation->setCursor(0); mPickLocationStr = location; mLastRequestTimer.reset(); } @@ -901,6 +918,10 @@ void LLPanelProfilePick::sendParcelInfoRequest() void LLPanelProfilePick::processParcelInfo(const LLParcelData& parcel_data) { + // Region might have moved since the pick was saved; refresh the stored global position + // using the parcel info so map/teleport use the current location. + setPosGlobal(LLVector3d(parcel_data.global_x, parcel_data.global_y, parcel_data.global_z)); + setPickLocation(createLocationText(LLStringUtil::null, parcel_data.name, parcel_data.sim_name, getPosGlobal())); // We have received parcel info for the requested ID so clear it now. diff --git a/indra/newview/llpanelprofilepicks.h b/indra/newview/llpanelprofilepicks.h index 614d31c40c2..f7fb601ee63 100644 --- a/indra/newview/llpanelprofilepicks.h +++ b/indra/newview/llpanelprofilepicks.h @@ -113,6 +113,8 @@ class LLPanelProfilePick void setAvatarId(const LLUUID& avatar_id) override; + void reshape(S32 width, S32 height, bool called_from_parent = true) override; + void setPickId(const LLUUID& id) { mPickId = id; } virtual LLUUID& getPickId() { return mPickId; } @@ -229,13 +231,14 @@ class LLPanelProfilePick protected: - LLTextureCtrl* mSnapshotCtrl; - LLLineEditor* mPickName; - LLTextEditor* mPickDescription; - LLButton* mSetCurrentLocationButton; - LLButton* mSaveButton; - LLButton* mCreateButton; - LLButton* mCancelButton; + LLTextureCtrl* mSnapshotCtrl = nullptr; + LLLineEditor* mPickName = nullptr; + LLTextEditor* mPickDescription = nullptr; + LLLineEditor* mPickLocation = nullptr; + LLButton* mSetCurrentLocationButton = nullptr; + LLButton* mSaveButton = nullptr; + LLButton* mCreateButton = nullptr; + LLButton* mCancelButton = nullptr; LLVector3d mPosGlobal; LLUUID mParcelId; diff --git a/indra/newview/llpanelsnapshot.cpp b/indra/newview/llpanelsnapshot.cpp index 56c0294dbe6..daf19865225 100644 --- a/indra/newview/llpanelsnapshot.cpp +++ b/indra/newview/llpanelsnapshot.cpp @@ -64,7 +64,12 @@ bool LLPanelSnapshot::postBuild() { S32 w = getTypedPreviewWidth(); S32 h = getTypedPreviewHeight(); - getChild("save_btn")->setLabelArg("[UPLOAD_COST]", std::to_string(LLAgentBenefitsMgr::current().getTextureUploadCost(w, h))); + LLUICtrl *save_btn = findChild("save_btn"); + if (save_btn) + { + // Not all snapshot floaters have a save button + save_btn->setLabelArg("[UPLOAD_COST]", std::to_string(LLAgentBenefitsMgr::current().getTextureUploadCost(w, h))); + } getChild(getImageSizeComboName())->setCommitCallback(boost::bind(&LLPanelSnapshot::onResolutionComboCommit, this, _1)); if (!getWidthSpinnerName().empty()) { @@ -193,7 +198,11 @@ void LLPanelSnapshot::updateImageQualityLevel() quality_lvl = LLTrans::getString("snapshot_quality_very_high"); } - getChild("image_quality_level")->setTextArg("[QLVL]", quality_lvl); + LLTextBox* quality_lvl_ctrl = findChild("image_quality_level"); + if (quality_lvl_ctrl) + { + quality_lvl_ctrl->setTextArg("[QLVL]", quality_lvl); + } } void LLPanelSnapshot::goBack() diff --git a/indra/newview/llpanelvoicedevicesettings.cpp b/indra/newview/llpanelvoicedevicesettings.cpp index 5aaa53b732b..609177046e0 100644 --- a/indra/newview/llpanelvoicedevicesettings.cpp +++ b/indra/newview/llpanelvoicedevicesettings.cpp @@ -149,7 +149,7 @@ void LLPanelVoiceDeviceSettings::draw() LLColor4 color; if (power_bar_idx < discrete_power) { - color = (power_bar_idx >= 3) ? LLUIColorTable::instance().getColor("OverdrivenColor") : LLUIColorTable::instance().getColor("SpeakingColor"); + color = (power_bar_idx >= 3) ? LLUIColorTable::instance().getColor("OverdrivenColor") : LLUIColorTable::instance().getColor("OutfitGalleryItemSelected"); } else { diff --git a/indra/newview/llpreviewscript.cpp b/indra/newview/llpreviewscript.cpp index f4ad87c8547..f57b1d5c9a0 100644 --- a/indra/newview/llpreviewscript.cpp +++ b/indra/newview/llpreviewscript.cpp @@ -43,7 +43,6 @@ #include "llinventorymodel.h" #include "llkeyboard.h" #include "lllineeditor.h" -#include "llmd5.h" #include "llhelp.h" #include "llnotificationsutil.h" #include "llresmgr.h" @@ -1677,14 +1676,7 @@ std::string LLScriptEdContainer::getUniqueHash() const // Take script inventory item id (within the object inventory) // to consideration so that it's possible to edit multiple scripts // in the same object inventory simultaneously (STORM-781). - std::string script_id = mObjectUUID.asString() + "_" + mItemUUID.asString(); - - // Use MD5 sum to make the file name shorter and not exceed maximum path length. - char script_id_hash_str[33]; /* Flawfinder: ignore */ - LLMD5 script_id_hash((const U8*)script_id.c_str()); - script_id_hash.hex_digest(script_id_hash_str); - - return std::string(script_id_hash_str); + return LLScriptEditorWSServer::buildScriptSubscriptionId(mObjectUUID, mItemUUID); } std::string LLScriptEdContainer::getErrorLogFileName(const std::string& script_path) diff --git a/indra/newview/llpreviewtexture.cpp b/indra/newview/llpreviewtexture.cpp index 6d5ea0ff371..41e9186b534 100644 --- a/indra/newview/llpreviewtexture.cpp +++ b/indra/newview/llpreviewtexture.cpp @@ -306,7 +306,8 @@ void LLPreviewTexture::saveAs() if( mLoadingFullImage ) return; - std::string filename = getItem() ? LLDir::getScrubbedFileName(getItem()->getName()) : LLStringUtil::null; + // startPicker will sanitize the name + std::string filename = getItem() ? getItem()->getName() : LLStringUtil::null; LLFilePickerReplyThread::startPicker(boost::bind(&LLPreviewTexture::saveTextureToFile, this, _1), LLFilePicker::FFSAVE_TGAPNG, filename); } diff --git a/indra/newview/llpublishedobjectmgr.cpp b/indra/newview/llpublishedobjectmgr.cpp new file mode 100644 index 00000000000..d92bb382dee --- /dev/null +++ b/indra/newview/llpublishedobjectmgr.cpp @@ -0,0 +1,1309 @@ +/** + * @file llpublishedobjectmgr.cpp + * @brief Published object state/logic manager extracted from llscripteditorws + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" +#include "llpublishedobjectmgr.h" + +#include "llscripteditorws.h" + +#include "llchat.h" +#include "llinventorydefines.h" +#include "llregex.h" +#include "llselectmgr.h" +#include "llviewerinventory.h" +#include "llviewerobject.h" +#include "llviewerobjectlist.h" +#include "llviewerregion.h" +#include "llvoinventorylistener.h" + +namespace +{ + static const boost::regex LUAU_LOCATION_PATTERN( + R"(^([^:]*):([0-9]+):\s*(.*)$)"); + + static const boost::regex LSL_LOCATION_PATTERN( + R"(\((\d+), (\d+)\) : ([^:]+) : (.+))"); + + std::string nv_string(LLViewerObject* obj, const char* key) + { + if (!obj) + { + return std::string(); + } + LLNameValue* nv = obj->getNVPair(key); + if (!nv) + { + return std::string(); + } + const char* s = nv->getString(); + if (!s || s[0] == '\0') + { + return std::string(); + } + return std::string(s); + } + + std::string get_prim_name(LLViewerObject* obj) + { + std::string name = nv_string(obj, "Name"); + if (!name.empty()) + { + return name; + } + + if (!obj) + { + return std::string(); + } + + LLSelectNode* node = LLSelectMgr::instance().getSelection()->findNode(obj); + if (node && !node->mName.empty()) + { + return node->mName; + } + + // Never emit an empty prim/object name to downstream tooling. + return obj->getID().asString(); + } +} + +class LLPublishedPrimListener : public LLVOInventoryListener +{ +public: + LLPublishedPrimListener(LLScriptEditorWSServer* server, const LLUUID& object_id, const LLUUID& prim_id, + LLViewerObject* object) + : mServer(server) + , mObjectID(object_id) + , mPrimID(prim_id) + { + registerVOInventoryListener(object, nullptr); + } + + ~LLPublishedPrimListener() override = default; + + void inventoryChanged(LLViewerObject* object, + LLInventoryObject::object_list_t* inventory, + S32 serial_num, void* user_data) override + { + if (mServer) + { + if (mServer->isObjectPublished(mObjectID)) + { + mServer->onPrimInventoryChanged(mObjectID, mPrimID); + } + else + { + mServer->onPrimInventoryReady(mObjectID, mPrimID); + } + } + } + + const LLUUID& getObjectID() const { return mObjectID; } + const LLUUID& getPrimID() const { return mPrimID; } + +private: + LLScriptEditorWSServer* mServer; + LLUUID mObjectID; + LLUUID mPrimID; +}; + +LLPublishedObjectMgr::LLPublishedObjectMgr( + LLScriptEditorWSServer* server, + RuntimeEventCallback runtime_event_callback): + mServer(server), + mRuntimeEventCallback(std::move(runtime_event_callback)), + mRuntimeEventAggregator(std::make_unique( + [this](const RuntimeEventAggregator::RuntimeEvent& runtime_event) + { + std::optional event = + buildRuntimeChatEvent(runtime_event); + if (event && mRuntimeEventCallback) + { + mRuntimeEventCallback(*event); + } + })), + mRuntimeFlushTimer( + std::unique_ptr( + LLEventTimer::run_every( + RUNTIME_FLUSH_INTERVAL, + [this]() + { + flushExpiredRuntimeFragments(); + }))) +{ +} + +LLPublishedObjectMgr::~LLPublishedObjectMgr() = default; + +void LLPublishedObjectMgr::ingestRuntimeChat( + const LLChat& chat_msg, + RuntimeEventAggregator::Channel channel) +{ + static const boost::regex runtime_error_header( + R"(^(.+?)\s+\[script:([^\]]+)\]\s+Script run-time error)"); + + std::vector lines = + LLStringUtil::getTokens(chat_msg.mText, "\n"); + boost::smatch match; + bool is_error_header = + !lines.empty() && + boost::regex_match(lines.front(), match, runtime_error_header); + + if (!is_error_header && !mRuntimeEventAggregator->hasPending()) + { + RuntimeEventAggregator::RuntimeEvent runtime_event; + runtime_event.mVM = RuntimeEventAggregator::VM::LSL2; + runtime_event.mChannel = channel; + + RuntimeEventAggregator::Fragment fragment; + fragment.mFromID = chat_msg.mFromID; + fragment.mFromName = chat_msg.mFromName; + fragment.mText = chat_msg.mText; + runtime_event.mFragments.push_back(std::move(fragment)); + + std::optional event = + buildRuntimeChatEvent(runtime_event); + if (event && mRuntimeEventCallback) + { + mRuntimeEventCallback(*event); + } + return; + } + + RuntimeEventAggregator::VM vm = RuntimeEventAggregator::VM::LSL2; + LLViewerObject* prim = gObjectList.findObject(chat_msg.mFromID); + if (prim) + { + std::vector lines = + LLStringUtil::getTokens(chat_msg.mText, "\n"); + static const boost::regex runtime_error_header( + R"(^(.+?)\s+\[script:([^\]]+)\]\s+Script run-time error)"); + boost::smatch match; + + if (!lines.empty() && + boost::regex_match(lines.front(), match, runtime_error_header)) + { + const std::string script_name = match[2].str(); + LLInventoryObject::object_list_t inventory; + prim->getInventoryContents(inventory); + for (const auto& inventory_object : inventory) + { + LLInventoryItem* item = + dynamic_cast(inventory_object.get()); + if (item && item->getName() == script_name && + item->getRuntime() == "luau") + { + vm = RuntimeEventAggregator::VM::LUAU; + break; + } + } + } + } + + mRuntimeEventAggregator->ingest( + chat_msg.mFromID, + chat_msg.mFromName, + chat_msg.mText, + vm, + channel); +} + +void LLPublishedObjectMgr::flushExpiredRuntimeFragments() +{ + mRuntimeEventAggregator->flushExpired(); +} + +std::optional +LLPublishedObjectMgr::buildRuntimeChatEvent( + const RuntimeEventAggregator::RuntimeEvent& runtime_event) const +{ + if (runtime_event.mFragments.empty()) + { + return std::nullopt; + } + + const RuntimeEventAggregator::Fragment& source = + runtime_event.mFragments.front(); + LLViewerObject* prim = gObjectList.findObject(source.mFromID); + if (!prim) + { + return std::nullopt; + } + + LLViewerObject* root = prim->getRootEdit(); + if (!root) + { + return std::nullopt; + } + + RuntimeChatEvent event; + event.mChannel = runtime_event.mChannel; + event.mVM = runtime_event.mVM; + event.mRootID = root->getID(); + event.mPrimID = prim->getID(); + event.mObjectName = source.mFromName; + for (const auto& fragment : runtime_event.mFragments) + { + if (!event.mMessage.empty()) + { + event.mMessage += "\n"; + } + event.mMessage += fragment.mText; + } + + std::vector lines = + LLStringUtil::getTokens(event.mMessage, "\n"); + static const std::string runtime_error_marker = "Script run-time error"; + static const boost::regex runtime_error_header( + R"(^(.+?)\s+\[script:([^\]]+)\]\s+Script run-time error)"); + + auto ends_with = [](const std::string& value, const std::string& suffix) + { + return value.size() >= suffix.size() && + std::equal(suffix.rbegin(), suffix.rend(), value.rbegin()); + }; + + if (!lines.empty() && ends_with(lines.front(), runtime_error_marker)) + { + event.mIsError = true; + boost::smatch match; + if (boost::regex_match(lines.front(), match, runtime_error_header)) + { + event.mObjectName = match[1].str(); + event.mScriptName = match[2].str(); + lines.erase(lines.begin()); + } + else + { + lines.clear(); + } + } + + LLInventoryObject::object_list_t inventory; + prim->getInventoryContents(inventory); + for (const auto& inventory_object : inventory) + { + LLInventoryItem* item = + dynamic_cast(inventory_object.get()); + if (item && !event.mScriptName.empty() && + item->getName() == event.mScriptName) + { + event.mItemID = item->getUUID(); + break; + } + } + + if (event.mIsError && !lines.empty()) + { + RuntimeEventAggregator::VM vm = runtime_event.mVM; + if (event.mItemID.notNull()) + { + LLInventoryItem* item = + dynamic_cast(prim->getInventoryObject(event.mItemID)); + if (item && item->getRuntime() == "luau") + { + vm = RuntimeEventAggregator::VM::LUAU; + } + } + + RuntimeEventAggregator::ParsedError parsed = + mRuntimeEventAggregator->parseError(vm, runtime_event.mFragments); + event.mError = parsed.mError; + event.mLine = parsed.mLine; + event.mColumn = parsed.mColumn; + event.mStack = lines; + } + + return event; +} + +LLPublishedObjectMgr::RuntimeEventAggregator::RuntimeEventAggregator( + FlushCallback flush_callback): + mFlushCallback(std::move(flush_callback)) +{ +} + +void LLPublishedObjectMgr::RuntimeEventAggregator::ingest( + const LLUUID& from_id, + const std::string& from_name, + const std::string& text, + VM vm, + Channel channel) +{ + if (mPending && + isNewBurst(from_id, from_name, vm, channel)) + { + flushPending(); + } + + if (!mPending) + { + mPending = std::make_unique(); + mPending->mFromID = from_id; + mPending->mFromName = from_name; + mPending->mVM = vm; + mPending->mChannel = channel; + } + + Fragment fragment; + fragment.mFromID = from_id; + fragment.mFromName = from_name; + fragment.mText = text; + mPending->mFragments.push_back(std::move(fragment)); + mPending->mTimer.setTimerExpirySec(FRAGMENT_TIMEOUT); +} + +void LLPublishedObjectMgr::RuntimeEventAggregator::flushExpired() +{ + if (mPending && mPending->mTimer.hasExpired()) + { + flushPending(); + } +} + +void LLPublishedObjectMgr::RuntimeEventAggregator::flush() +{ + flushPending(); +} + +bool LLPublishedObjectMgr::RuntimeEventAggregator::hasPending() const +{ + return mPending != nullptr; +} + +LLPublishedObjectMgr::RuntimeEventAggregator::ParsedError +LLPublishedObjectMgr::RuntimeEventAggregator::parseError( + VM vm, + const fragments_t& fragments) const +{ + ParsedError parsed; + const boost::regex* location_pattern = nullptr; + bool lsl_coordinates = false; + + switch (vm) + { + case VM::LUAU: + location_pattern = &LUAU_LOCATION_PATTERN; + break; + case VM::LSL2: + location_pattern = &LSL_LOCATION_PATTERN; + lsl_coordinates = true; + break; + } + + for (const auto& fragment : fragments) + { + std::vector lines = + LLStringUtil::getTokens(fragment.mText, "\n"); + for (const auto& line : lines) + { + boost::smatch match; + if (!boost::regex_match(line, match, *location_pattern)) + { + continue; + } + + if (lsl_coordinates) + { + parsed.mLine = static_cast( + std::strtol(match[1].str().c_str(), nullptr, 10)) + 1; + parsed.mColumn = static_cast( + std::strtol(match[2].str().c_str(), nullptr, 10)) + 1; + parsed.mError = match[4].str(); + } + else + { + parsed.mSource = match[1].str(); + parsed.mLine = static_cast( + std::strtol(match[2].str().c_str(), nullptr, 10)); + parsed.mColumn = 0; + parsed.mError = match[3].str(); + } + return parsed; + } + } + + // LSL runtime errors commonly arrive as plain text without source + // location metadata, for example: "Math Error". + if (vm == VM::LSL2) + { + for (const auto& fragment : fragments) + { + const std::vector lines = + LLStringUtil::getTokens(fragment.mText, "\n"); + for (const auto& line : lines) + { + if (line.empty() || + line.find("Script run-time error") != std::string::npos) + { + continue; + } + + parsed.mError = line; + return parsed; + } + } + } + + return parsed; +} + +void LLPublishedObjectMgr::RuntimeEventAggregator::flushPending() +{ + if (!mPending) + { + return; + } + + if (!mFlushCallback) + { + mPending.reset(); + return; + } + + RuntimeEvent event; + event.mVM = mPending->mVM; + event.mChannel = mPending->mChannel; + event.mFragments = mPending->mFragments; + event.mError = parseError(mPending->mVM, mPending->mFragments); + mFlushCallback(event); + + mPending.reset(); +} + +bool LLPublishedObjectMgr::RuntimeEventAggregator::isNewBurst( + const LLUUID& from_id, + const std::string& from_name, + VM vm, + Channel channel) const +{ + return mPending->mFromID != from_id || + mPending->mFromName != from_name || + mPending->mVM != vm || + mPending->mChannel != channel; +} + +LLPublishedObjectMgr::PublishedObjectInfo::PublishedObjectInfo() = default; +LLPublishedObjectMgr::PublishedObjectInfo::~PublishedObjectInfo() = default; +LLPublishedObjectMgr::PublishedObjectInfo::PublishedObjectInfo(PublishedObjectInfo&&) noexcept = default; +LLPublishedObjectMgr::PublishedObjectInfo& LLPublishedObjectMgr::PublishedObjectInfo::operator=(PublishedObjectInfo&&) noexcept = default; + +LLPublishedObjectMgr::PendingPublish::PendingPublish() = default; +LLPublishedObjectMgr::PendingPublish::~PendingPublish() = default; +LLPublishedObjectMgr::PendingPublish::PendingPublish(PendingPublish&&) noexcept = default; +LLPublishedObjectMgr::PendingPublish& LLPublishedObjectMgr::PendingPublish::operator=(PendingPublish&&) noexcept = default; + +void LLPublishedObjectMgr::beginPendingPublish(const LLUUID& object_id, const std::vector& prims) +{ + PendingPublish pending; + pending.mObjectID = object_id; + for (LLViewerObject* prim : prims) + { + pending.mPendingPrims.insert(prim->getID()); + auto listener = std::make_unique( + mServer, object_id, prim->getID(), prim); + pending.mListeners.push_back(std::move(listener)); + } + mPendingPublishes[object_id] = std::move(pending); +} + +bool LLPublishedObjectMgr::hasPendingPublish(const LLUUID& object_id) const +{ + return mPendingPublishes.find(object_id) != mPendingPublishes.end(); +} + +bool LLPublishedObjectMgr::markPendingPublishPrimReady(const LLUUID& object_id, const LLUUID& prim_id) +{ + auto it = mPendingPublishes.find(object_id); + if (it == mPendingPublishes.end()) + { + return false; + } + + it->second.mPendingPrims.erase(prim_id); + return it->second.mPendingPrims.empty(); +} + +void LLPublishedObjectMgr::recordPendingPropertyChange( + const LLUUID& root_id, + const LLUUID& prim_id, + const std::string& name, + const std::string& desc) +{ + auto it = mPendingPublishes.find(root_id); + if (it == mPendingPublishes.end()) + { + return; + } + + PendingPublish& pending = it->second; + if (prim_id == root_id) + { + pending.mHasRootProperties = true; + pending.mObjectDescription = desc; + if (!name.empty()) + { + pending.mObjectName = name; + } + return; + } + + if (!name.empty()) + { + pending.mPrimNames[prim_id] = name; + } + pending.mPrimDescriptions[prim_id] = desc; +} + +std::vector> LLPublishedObjectMgr::takePendingPublishListeners(const LLUUID& object_id) +{ + auto it = mPendingPublishes.find(object_id); + if (it == mPendingPublishes.end()) + { + return {}; + } + + auto listeners = std::move(it->second.mListeners); + mPendingPublishes.erase(it); + return listeners; +} + +void LLPublishedObjectMgr::cancelPendingPublish(const LLUUID& object_id) +{ + mPendingPublishes.erase(object_id); +} + +void LLPublishedObjectMgr::cancelPendingPublishWithCleanup(const LLUUID& object_id) +{ + auto it = mPendingPublishes.find(object_id); + if (it == mPendingPublishes.end()) + { + return; + } + + it->second.mListeners.clear(); + mPendingPublishes.erase(it); +} + +LLPublishedObjectMgr::PublishedObjectInfo* LLPublishedObjectMgr::getPublished(const LLUUID& object_id) +{ + auto it = mPublishedObjects.find(object_id); + if (it == mPublishedObjects.end()) + { + return nullptr; + } + + return &it->second; +} + +const LLPublishedObjectMgr::PublishedObjectInfo* LLPublishedObjectMgr::getPublished(const LLUUID& object_id) const +{ + auto it = mPublishedObjects.find(object_id); + if (it == mPublishedObjects.end()) + { + return nullptr; + } + + return &it->second; +} + +bool LLPublishedObjectMgr::reservePendingItemCreate(const LLUUID& prim_id, std::string&& pump_name) +{ + auto it = mPendingItemCreates.find(prim_id); + if (it != mPendingItemCreates.end()) + { + return false; + } + + mPendingItemCreates[prim_id] = std::move(pump_name); + return true; +} + +bool LLPublishedObjectMgr::consumePendingItemCreate(const LLUUID& prim_id, std::string& pump_name) +{ + auto it = mPendingItemCreates.find(prim_id); + if (it == mPendingItemCreates.end()) + { + return false; + } + + pump_name = it->second; + mPendingItemCreates.erase(it); + return true; +} + +void LLPublishedObjectMgr::clearPendingItemCreate(const LLUUID& prim_id) +{ + mPendingItemCreates.erase(prim_id); +} + +LLSD LLPublishedObjectMgr::buildPrimInventoryLLSD(LLViewerObject* object) const +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; + LLSD items = LLSD::emptyArray(); + if (!object) + { + return items; + } + + LLInventoryObject::object_list_t contents; + object->getInventoryContents(contents); + + for (const auto& obj : contents) + { + LLInventoryItem* item = dynamic_cast(obj.get()); + if (!item) + { + continue; + } + + LLAssetType::EType type = item->getType(); + if (type != LLAssetType::AT_LSL_TEXT && type != LLAssetType::AT_NOTECARD) + { + continue; + } + + LLSD entry; + entry["item_id"] = item->getUUID(); + entry["name"] = item->getName(); + entry["description"] = item->getDescription(); + entry["type"] = (type == LLAssetType::AT_LSL_TEXT) ? "script" : "notecard"; + + if (type == LLAssetType::AT_LSL_TEXT) + { + U8 subtype = item->getInventorySubType(); + entry["subtype"] = static_cast(subtype); + + const std::string& runtime = item->getRuntime(); + if (!runtime.empty()) + { + entry["vm"] = runtime; + } + + LLViewerInventoryItem* viewer_item = dynamic_cast(item); + if (viewer_item) + { + entry["running"] = viewer_item->getIsRunning(); + entry["faulted"] = viewer_item->getIsFaulted(); + } + } + + const LLPermissions& perms = item->getPermissions(); + LLSD perm_entry; + perm_entry["owner"] = static_cast(perms.getMaskOwner()); + perm_entry["next_owner"] = static_cast(perms.getMaskNextOwner()); + entry["permissions"] = perm_entry; + + entry["creator_id"] = perms.getCreator(); + + items.append(entry); + } + + return items; +} + +LLSD LLPublishedObjectMgr::buildPublishedObjectLLSD(LLViewerObject* root) const +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; + LLSD pub; + pub["object_id"] = root->getID(); + pub["object_name"] = get_prim_name(root); + pub["object_description"] = nv_string(root, "Desc"); + pub["owner_id"] = root->mOwnerID; + if (root->getRegion()) + { + pub["region"] = root->getRegion()->getName(); + } + pub["inventory"] = buildPrimInventoryLLSD(root); + + LLSD linked_objects = LLSD::emptyArray(); + S32 link_number = 2; + for (LLViewerObject* child : root->getChildren()) + { + LLSD link; + link["link_id"] = child->getID(); + link["link_number"] = link_number++; + link["link_name"] = get_prim_name(child); + link["link_description"] = nv_string(child, "Desc"); + link["inventory"] = buildPrimInventoryLLSD(child); + linked_objects.append(link); + } + if (linked_objects.size() > 0) + { + pub["linked_objects"] = linked_objects; + } + + return pub; +} + +LLSD LLPublishedObjectMgr::buildObjectListLLSD() const +{ + LLSD objects = LLSD::emptyArray(); + for (const auto& [object_id, info] : mPublishedObjects) + { + LLViewerObject* root = gObjectList.findObject(object_id); + if (!root) + { + LL_DEBUGS("ScriptEditorWS") << "object.list: skipping " << object_id + << " (no longer in scene)" << LL_ENDL; + continue; + } + + LLSD pub; + pub["object_id"] = info.mObjectID; + pub["object_name"] = info.mObjectName; + pub["object_description"] = info.mObjectDescription; + pub["owner_id"] = info.mOwnerID; + if (!info.mRegionName.empty()) + { + pub["region"] = info.mRegionName; + } + pub["can_save_back"] = info.mCanSaveBackToContents; + pub["inventory"] = buildPrimInventoryLLSD(root); + + LLSD linked_objects = LLSD::emptyArray(); + for (const auto& prim_info : info.mPrims) + { + if (prim_info.mLinkNumber == 1) + { + continue; + } + + LLViewerObject* child = gObjectList.findObject(prim_info.mPrimID); + if (!child) + { + continue; + } + + LLSD link; + link["link_id"] = prim_info.mPrimID; + link["link_number"] = prim_info.mLinkNumber; + link["link_name"] = prim_info.mPrimName; + link["link_description"] = prim_info.mPrimDescription; + link["inventory"] = buildPrimInventoryLLSD(child); + linked_objects.append(link); + } + if (linked_objects.size() > 0) + { + pub["linked_objects"] = linked_objects; + } + + objects.append(pub); + } + + return objects; +} + +bool LLPublishedObjectMgr::buildLinksetUpdateLLSD( + const LLUUID& root_id, LLSD& update) const +{ + const PublishedObjectInfo* info = getPublished(root_id); + if (!info) + { + return false; + } + + LLSD linked_objects = LLSD::emptyArray(); + for (const PublishedPrimInfo& prim_info : info->mPrims) + { + if (prim_info.mPrimID == root_id) + { + continue; + } + + LLSD entry; + entry["link_id"] = prim_info.mPrimID; + entry["link_number"] = prim_info.mLinkNumber; + + LLViewerObject* prim = gObjectList.findObject(prim_info.mPrimID); + std::string link_name = prim ? get_prim_name(prim) : std::string(); + if (link_name.empty()) + { + link_name = prim_info.mPrimName; + } + std::string link_desc = prim ? nv_string(prim, "Desc") : std::string(); + if (link_desc.empty()) + { + link_desc = prim_info.mPrimDescription; + } + entry["link_name"] = link_name; + entry["link_description"] = link_desc; + entry["inventory"] = prim ? buildPrimInventoryLLSD(prim) : LLSD::emptyArray(); + + linked_objects.append(entry); + } + + update = LLSD(); + update["object_id"] = root_id; + update["linked_objects"] = linked_objects; + return true; +} + +bool LLPublishedObjectMgr::reconcileLinksetChildAdded( + const LLUUID& root_id, + LLViewerObject* child, + F64 request_start_sec) +{ + PublishedObjectInfo* info = getPublished(root_id); + if (!info || !child) + { + return false; + } + + const LLUUID child_id = child->getID(); + + info->mPrims.erase( + std::remove_if( + info->mPrims.begin(), + info->mPrims.end(), + [&](const PublishedPrimInfo& p) { return p.mPrimID == child_id; }), + info->mPrims.end()); + + PublishedPrimInfo prim_info; + prim_info.mPrimID = child_id; + prim_info.mPrimName = get_prim_name(child); + prim_info.mPrimDescription = nv_string(child, "Desc"); + prim_info.mLinkNumber = static_cast(info->mPrims.size()) + 1; + prim_info.mInventorySerial = -1; + info->mPrims.push_back(prim_info); + + auto listener = std::make_unique( + mServer, root_id, child_id, child); + info->mListeners.push_back(std::move(listener)); + + mInventoryRequestStartSec[child_id] = request_start_sec; + mNewChildPrims[root_id].insert(child_id); + return true; +} + +bool LLPublishedObjectMgr::reconcileLinksetChildRemoved( + const LLUUID& root_id, const LLUUID& child_id) +{ + PublishedObjectInfo* info = getPublished(root_id); + if (!info) + { + return false; + } + + info->mPrims.erase( + std::remove_if( + info->mPrims.begin(), + info->mPrims.end(), + [&](const PublishedPrimInfo& p) { return p.mPrimID == child_id; }), + info->mPrims.end()); + + info->mListeners.erase( + std::remove_if( + info->mListeners.begin(), + info->mListeners.end(), + [&](const std::unique_ptr& l) + { + return l->getPrimID() == child_id; + }), + info->mListeners.end()); + + bool root_empty_after_remove = false; + consumePendingNewChild(root_id, child_id, root_empty_after_remove); + + mInventoryRequestStartSec.erase(child_id); + + S32 link_num = 2; + for (auto& p : info->mPrims) + { + if (p.mPrimID != root_id) + { + p.mLinkNumber = link_num++; + } + } + + return true; +} + +bool LLPublishedObjectMgr::handlePrimInventoryReadyEvent( + const LLUUID& object_id, const LLUUID& prim_id) +{ + return markPendingPublishPrimReady(object_id, prim_id); +} + +LLPublishedObjectMgr::PrimInventoryEventResult +LLPublishedObjectMgr::handlePrimInventoryChangedEvent( + const LLUUID& object_id, + const LLUUID& prim_id, + LLViewerObject* prim, + F64 now_sec) +{ + PrimInventoryEventResult result; + if (!hasPublished(object_id) || !prim) + { + return result; + } + + F64 request_start_sec = 0.0; + if (consumeInventoryRequestStart(prim_id, request_start_sec)) + { + result.mTimingConsumed = true; + result.mTimingElapsedSec = llmax(0.0, now_sec - request_start_sec); + } + + InventoryChangeResult inv_result = reconcileInventoryChanged(object_id, prim_id, prim); + result.mKind = inv_result.mKind; + result.mUpdate = inv_result.mUpdate; + + if (result.mKind == InventoryChangeKind::ROOT_INVENTORY_UPDATE || + result.mKind == InventoryChangeKind::CHILD_INVENTORY_UPDATE) + { + std::string pending_item_create_pump; + if (consumePendingItemCreate(prim_id, pending_item_create_pump)) + { + result.mHasPendingItemCreate = true; + result.mPendingItemCreatePump = pending_item_create_pump; + } + } + + return result; +} + +LLPublishedObjectMgr::InventoryChangeResult +LLPublishedObjectMgr::reconcileInventoryChanged( + const LLUUID& object_id, + const LLUUID& prim_id, + LLViewerObject* prim) +{ + InventoryChangeResult result; + PublishedObjectInfo* pub_info = getPublished(object_id); + if (!pub_info || !prim) + { + return result; + } + + bool root_empty_after_remove = false; + if (consumePendingNewChild(object_id, prim_id, root_empty_after_remove)) + { + for (auto& p : pub_info->mPrims) + { + if (p.mPrimID == prim_id) + { + p.mPrimName = get_prim_name(prim); + p.mPrimDescription = nv_string(prim, "Desc"); + p.mInventorySerial = 0; + break; + } + } + result.mKind = root_empty_after_remove + ? InventoryChangeKind::CHILD_READY_FLUSH_NOW + : InventoryChangeKind::CHILD_READY_WAIT; + return result; + } + + result.mUpdate = LLSD(); + result.mUpdate["object_id"] = object_id; + LLSD inv = buildPrimInventoryLLSD(prim); + if (prim_id == object_id) + { + result.mUpdate["inventory"] = inv; + result.mKind = InventoryChangeKind::ROOT_INVENTORY_UPDATE; + } + else + { + LLSD modified_entry; + modified_entry["link_id"] = prim_id; + modified_entry["inventory"] = inv; + LLSD modified_arr = LLSD::emptyArray(); + modified_arr.append(modified_entry); + result.mUpdate["changes"]["linked_objects"]["modified"] = modified_arr; + result.mKind = InventoryChangeKind::CHILD_INVENTORY_UPDATE; + } + return result; +} + +bool LLPublishedObjectMgr::applyPropertyChange( + const LLUUID& root_id, + const LLUUID& prim_id, + const std::string& name, + const std::string& desc, + LLSD& update) +{ + PublishedObjectInfo* pub_info = getPublished(root_id); + if (!pub_info) + { + return false; + } + + update = LLSD(); + update["object_id"] = root_id; + + if (prim_id == root_id) + { + bool has_name = !name.empty(); + bool name_changed = has_name && (pub_info->mObjectName != name); + bool desc_changed = (pub_info->mObjectDescription != desc); + if (!name_changed && !desc_changed) + { + return false; + } + + if (name_changed) + { + pub_info->mObjectName = name; + update["object_name"] = name; + } + if (desc_changed) + { + pub_info->mObjectDescription = desc; + update["object_description"] = desc; + } + return true; + } + + auto prim_it = std::find_if(pub_info->mPrims.begin(), pub_info->mPrims.end(), + [&](const PublishedPrimInfo& p) { return p.mPrimID == prim_id; }); + if (prim_it == pub_info->mPrims.end()) + { + return false; + } + const bool name_changed = !name.empty() && prim_it->mPrimName != name; + const bool desc_changed = prim_it->mPrimDescription != desc; + if (!name_changed && !desc_changed) + { + return false; + } + + LLSD modified_entry; + modified_entry["link_id"] = prim_id; + if (name_changed) + { + prim_it->mPrimName = name; + modified_entry["link_name"] = name; + } + if (desc_changed) + { + prim_it->mPrimDescription = desc; + modified_entry["link_description"] = desc; + } + LLSD modified_arr = LLSD::emptyArray(); + modified_arr.append(modified_entry); + update["changes"]["linked_objects"]["modified"] = modified_arr; + return true; +} + +bool LLPublishedObjectMgr::hasActiveLinksetFlushTimer(const LLUUID& root_id) const +{ + auto it = mLinksetFlushTimers.find(root_id); + if (it == mLinksetFlushTimers.end()) + { + return false; + } + + return !it->second.expired(); +} + +void LLPublishedObjectMgr::setLinksetFlushTimer( + const LLUUID& root_id, const std::weak_ptr& timer) +{ + mLinksetFlushTimers[root_id] = timer; +} + +bool LLPublishedObjectMgr::cancelLinksetFlushTimer(const LLUUID& root_id) +{ + auto it = mLinksetFlushTimers.find(root_id); + if (it == mLinksetFlushTimers.end()) + { + return false; + } + + if (auto locked = it->second.lock()) + { + delete locked.get(); + } + + mLinksetFlushTimers.erase(it); + return true; +} + +void LLPublishedObjectMgr::clearLinksetFlushTimer(const LLUUID& root_id) +{ + mLinksetFlushTimers.erase(root_id); +} + +bool LLPublishedObjectMgr::consumeInventoryRequestStart( + const LLUUID& prim_id, F64& start_sec) +{ + auto it = mInventoryRequestStartSec.find(prim_id); + if (it == mInventoryRequestStartSec.end()) + { + return false; + } + + start_sec = it->second; + mInventoryRequestStartSec.erase(it); + return true; +} + +bool LLPublishedObjectMgr::markPrimInventorySerialAndDetectChange( + const LLUUID& root_id, const LLUUID& prim_id, S16 inventory_serial) +{ + if (inventory_serial < 0) + { + return false; + } + + PublishedObjectInfo* info = getPublished(root_id); + if (!info) + { + return false; + } + + auto it = std::find_if( + info->mPrims.begin(), + info->mPrims.end(), + [&](const PublishedPrimInfo& p) + { + return p.mPrimID == prim_id; + }); + if (it == info->mPrims.end()) + { + return false; + } + + if (it->mInventorySerial == inventory_serial) + { + return false; + } + + it->mInventorySerial = inventory_serial; + return true; +} + +bool LLPublishedObjectMgr::consumePendingNewChild( + const LLUUID& root_id, const LLUUID& child_id, bool& root_empty_after_remove) +{ + root_empty_after_remove = false; + auto root_it = mNewChildPrims.find(root_id); + if (root_it == mNewChildPrims.end()) + { + return false; + } + + auto child_it = root_it->second.find(child_id); + if (child_it == root_it->second.end()) + { + return false; + } + + root_it->second.erase(child_it); + if (root_it->second.empty()) + { + root_empty_after_remove = true; + mNewChildPrims.erase(root_it); + } + + return true; +} + +LLPublishedObjectMgr::PublishedObjectInfo& LLPublishedObjectMgr::finalizePendingPublish( + const LLUUID& object_id, PublishedObjectInfo&& info) +{ + PublishedObjectInfo& published_info = mPublishedObjects[object_id]; + auto pending_it = mPendingPublishes.find(object_id); + published_info = std::move(info); + + if (pending_it != mPendingPublishes.end()) + { + PendingPublish& pending = pending_it->second; + if (pending.mHasRootProperties) + { + if (!pending.mObjectName.empty()) + { + published_info.mObjectName = pending.mObjectName; + } + published_info.mObjectDescription = pending.mObjectDescription; + } + + for (PublishedPrimInfo& prim_info : published_info.mPrims) + { + auto name_it = pending.mPrimNames.find(prim_info.mPrimID); + if (name_it != pending.mPrimNames.end() && !name_it->second.empty()) + { + prim_info.mPrimName = name_it->second; + } + + auto desc_it = pending.mPrimDescriptions.find(prim_info.mPrimID); + if (desc_it != pending.mPrimDescriptions.end()) + { + prim_info.mPrimDescription = desc_it->second; + } + } + + published_info.mListeners = std::move(pending.mListeners); + mPendingPublishes.erase(pending_it); + } + else + { + published_info.mListeners = takePendingPublishListeners(object_id); + } + + return published_info; +} + +bool LLPublishedObjectMgr::cleanupObjectStateForUnpublish(const LLUUID& object_id) +{ + const bool was_published = hasPublished(object_id); + + cancelPendingPublishWithCleanup(object_id); + clearPublishedListeners(object_id); + erasePublished(object_id); + + cancelLinksetFlushTimer(object_id); + clearPendingNewChildren(object_id); + + return was_published; +} + +void LLPublishedObjectMgr::clearPublishedListeners(const LLUUID& object_id) +{ + auto pub_info = getPublished(object_id); + if (!pub_info) + { + return; + } + + pub_info->mListeners.clear(); +} + +void LLPublishedObjectMgr::clearAllStateWithListenerCleanup() +{ + for (auto& [id, pending] : mPendingPublishes) + { + pending.mListeners.clear(); + } + mPendingPublishes.clear(); + + for (auto& [id, info] : mPublishedObjects) + { + info.mListeners.clear(); + } + mPublishedObjects.clear(); +} diff --git a/indra/newview/llpublishedobjectmgr.h b/indra/newview/llpublishedobjectmgr.h new file mode 100644 index 00000000000..51af129dcba --- /dev/null +++ b/indra/newview/llpublishedobjectmgr.h @@ -0,0 +1,334 @@ +/** + * @file llpublishedobjectmgr.h + * @brief Published object state/logic manager extracted from llscripteditorws + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "llsd.h" +#include "lltimer.h" +#include "lluuid.h" +#include "lleventtimer.h" +#include "stdtypes.h" + +#include +#include +#include +#include +#include +#include +#include + +class LLScriptEditorWSServer; +class LLChat; +class LLPublishedPrimListener; +class LLViewerObject; + +class LLPublishedObjectMgr +{ +public: + struct PublishedPrimInfo + { + LLUUID mPrimID; + std::string mPrimName; + S32 mLinkNumber; + std::string mPrimDescription; + S16 mInventorySerial; + }; + + struct PublishedObjectInfo + { + PublishedObjectInfo(); + ~PublishedObjectInfo(); + PublishedObjectInfo(PublishedObjectInfo&&) noexcept; + PublishedObjectInfo& operator=(PublishedObjectInfo&&) noexcept; + PublishedObjectInfo(const PublishedObjectInfo&) = delete; + PublishedObjectInfo& operator=(const PublishedObjectInfo&) = delete; + + LLUUID mObjectID; + LLUUID mOwnerID; + std::string mObjectName; + std::string mObjectDescription; + std::string mRegionName; + bool mCanSaveBackToContents{ false }; + LLUUID mSourceTaskID; + std::vector mPrims; + std::vector> mListeners; + }; + + struct PendingPublish + { + PendingPublish(); + ~PendingPublish(); + PendingPublish(PendingPublish&&) noexcept; + PendingPublish& operator=(PendingPublish&&) noexcept; + PendingPublish(const PendingPublish&) = delete; + PendingPublish& operator=(const PendingPublish&) = delete; + + LLUUID mObjectID; + std::set mPendingPrims; + std::vector> mListeners; + bool mHasRootProperties{ false }; + std::string mObjectName; + std::string mObjectDescription; + std::map mPrimNames; + std::map mPrimDescriptions; + }; + + enum class InventoryChangeKind + { + NOT_PUBLISHED, + CHILD_READY_WAIT, + CHILD_READY_FLUSH_NOW, + ROOT_INVENTORY_UPDATE, + CHILD_INVENTORY_UPDATE + }; + + struct InventoryChangeResult + { + InventoryChangeKind mKind{ InventoryChangeKind::NOT_PUBLISHED }; + LLSD mUpdate; + }; + + struct PrimInventoryEventResult + { + InventoryChangeKind mKind{ InventoryChangeKind::NOT_PUBLISHED }; + LLSD mUpdate; + bool mTimingConsumed{ false }; + F64 mTimingElapsedSec{ 0.0 }; + bool mHasPendingItemCreate{ false }; + std::string mPendingItemCreatePump; + }; + + class RuntimeEventAggregator + { + public: + enum class Channel + { + DEBUG, + OWNER_SAY + }; + + enum class VM + { + LSL2, + LUAU + }; + + struct StackFrame + { + S32 mLine{ 0 }; + std::string mFunction; + std::string mSource; + }; + + struct ParsedError + { + std::string mSource; + std::string mError; + S32 mLine{ 0 }; + S32 mColumn{ 0 }; + std::vector mStack; + }; + + struct Fragment + { + LLUUID mFromID; + std::string mFromName; + std::string mText; + }; + + using fragments_t = std::vector; + + struct RuntimeEvent + { + VM mVM; + Channel mChannel; + fragments_t mFragments; + ParsedError mError; + }; + + using FlushCallback = std::function; + + explicit RuntimeEventAggregator(FlushCallback flush_callback); + + void ingest(const LLUUID& from_id, + const std::string& from_name, + const std::string& text, + VM vm, + Channel channel); + void flushExpired(); + void flush(); + ParsedError parseError(VM vm, + const fragments_t& fragments) const; + bool hasPending() const; + + private: + struct PendingBurst + { + LLUUID mFromID; + std::string mFromName; + VM mVM{ VM::LSL2 }; + Channel mChannel{ Channel::DEBUG }; + fragments_t mFragments; + LLTimer mTimer; + }; + + static constexpr F32 FRAGMENT_TIMEOUT = 1.0f; + + void flushPending(); + bool isNewBurst(const LLUUID& from_id, + const std::string& from_name, + VM vm, + Channel channel) const; + + FlushCallback mFlushCallback; + std::unique_ptr mPending; + }; + + struct RuntimeChatEvent + { + RuntimeEventAggregator::Channel mChannel; + RuntimeEventAggregator::VM mVM; + LLUUID mRootID; + LLUUID mPrimID; + LLUUID mItemID; + std::string mObjectName; + std::string mScriptName; + std::string mMessage; + std::string mError; + S32 mLine{ 0 }; + S32 mColumn{ 0 }; + std::vector mStack; + bool mIsError{ false }; + }; + + using RuntimeEventCallback = + std::function; + + explicit LLPublishedObjectMgr( + LLScriptEditorWSServer* server = nullptr, + RuntimeEventCallback runtime_event_callback = {}); + ~LLPublishedObjectMgr(); + + void flushExpiredRuntimeFragments(); + void ingestRuntimeChat( + const LLChat& chat_msg, + RuntimeEventAggregator::Channel channel); + std::optional buildRuntimeChatEvent( + const RuntimeEventAggregator::RuntimeEvent& event) const; + + bool hasPublished(const LLUUID& object_id) const { return mPublishedObjects.find(object_id) != mPublishedObjects.end(); } + void erasePublished(const LLUUID& object_id) { mPublishedObjects.erase(object_id); } + PublishedObjectInfo* getPublished(const LLUUID& object_id); + const PublishedObjectInfo* getPublished(const LLUUID& object_id) const; + + template + void forEachPublished(Fn&& fn) const + { + for (const auto& [id, info] : mPublishedObjects) + { + fn(id, info); + } + } + + LLSD buildPrimInventoryLLSD(LLViewerObject* object) const; + LLSD buildPublishedObjectLLSD(LLViewerObject* root) const; + LLSD buildObjectListLLSD() const; + bool buildLinksetUpdateLLSD(const LLUUID& root_id, LLSD& update) const; + bool reconcileLinksetChildAdded(const LLUUID& root_id, LLViewerObject* child, F64 request_start_sec); + bool reconcileLinksetChildRemoved(const LLUUID& root_id, const LLUUID& child_id); + bool handlePrimInventoryReadyEvent(const LLUUID& object_id, const LLUUID& prim_id); + PrimInventoryEventResult handlePrimInventoryChangedEvent( + const LLUUID& object_id, + const LLUUID& prim_id, + LLViewerObject* prim, + F64 now_sec); + InventoryChangeResult reconcileInventoryChanged( + const LLUUID& object_id, + const LLUUID& prim_id, + LLViewerObject* prim); + bool applyPropertyChange( + const LLUUID& root_id, + const LLUUID& prim_id, + const std::string& name, + const std::string& desc, + LLSD& update); + + void beginPendingPublish(const LLUUID& object_id, const std::vector& prims); + bool hasPendingPublish(const LLUUID& object_id) const; + bool markPendingPublishPrimReady(const LLUUID& object_id, const LLUUID& prim_id); + std::vector> takePendingPublishListeners(const LLUUID& object_id); + void recordPendingPropertyChange( + const LLUUID& root_id, + const LLUUID& prim_id, + const std::string& name, + const std::string& desc); + void cancelPendingPublish(const LLUUID& object_id); + PublishedObjectInfo& finalizePendingPublish(const LLUUID& object_id, PublishedObjectInfo&& info); + bool cleanupObjectStateForUnpublish(const LLUUID& object_id); + void clearAllStateWithListenerCleanup(); + + bool reservePendingItemCreate(const LLUUID& prim_id, std::string&& pump_name); + bool consumePendingItemCreate(const LLUUID& prim_id, std::string& pump_name); + void clearPendingItemCreate(const LLUUID& prim_id); + + bool consumePendingNewChild(const LLUUID& root_id, const LLUUID& child_id, bool& root_empty_after_remove); + void clearPendingNewChildren(const LLUUID& root_id) { mNewChildPrims.erase(root_id); } + + bool hasActiveLinksetFlushTimer(const LLUUID& root_id) const; + void setLinksetFlushTimer(const LLUUID& root_id, const std::weak_ptr& timer); + bool cancelLinksetFlushTimer(const LLUUID& root_id); + void clearLinksetFlushTimer(const LLUUID& root_id); + + void setInventoryRequestStart(const LLUUID& prim_id, F64 start_sec) { mInventoryRequestStartSec[prim_id] = start_sec; } + bool hasInventoryRequestStart(const LLUUID& prim_id) const { return mInventoryRequestStartSec.find(prim_id) != mInventoryRequestStartSec.end(); } + bool consumeInventoryRequestStart(const LLUUID& prim_id, F64& start_sec); + bool markPrimInventorySerialAndDetectChange(const LLUUID& root_id, const LLUUID& prim_id, S16 inventory_serial); + +private: + LLScriptEditorWSServer* mServer{ nullptr }; + std::unique_ptr mRuntimeEventAggregator; + RuntimeEventCallback mRuntimeEventCallback; + std::unique_ptr mRuntimeFlushTimer; + + static constexpr F32 RUNTIME_FLUSH_INTERVAL = 0.25f; + + void cancelPendingPublishWithCleanup(const LLUUID& object_id); + void clearPublishedListeners(const LLUUID& object_id); + + using published_map_t = std::map; + using pending_publish_map_t = std::map; + using pending_item_create_map_t = std::map; + using new_child_prims_map_t = std::map>; + using linkset_flush_timer_map_t = std::map>; + using inventory_request_start_map_t = std::map; + + published_map_t mPublishedObjects; + pending_publish_map_t mPendingPublishes; + pending_item_create_map_t mPendingItemCreates; + new_child_prims_map_t mNewChildPrims; + linkset_flush_timer_map_t mLinksetFlushTimers; + inventory_request_start_map_t mInventoryRequestStartSec; +}; diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp index a21aa459308..56f5e8dc1f8 100644 --- a/indra/newview/llscripteditorws.cpp +++ b/indra/newview/llscripteditorws.cpp @@ -31,6 +31,7 @@ #include "llscripteditorws.h" #include "llagent.h" +#include "llagentcamera.h" #include "llappviewer.h" #include "llchat.h" #include "lldate.h" @@ -44,10 +45,12 @@ #include "llinventorytype.h" #include "llinventorydefines.h" #include "llnotecard.h" +#include "llnotificationsutil.h" #include "llpreviewnotecard.h" #include "llpreviewscript.h" #include "llprocess.h" #include "llregex.h" +#include "llmd5.h" #include "llsdjson.h" #include "llselectmgr.h" #include "lltrans.h" @@ -61,10 +64,13 @@ #include "llviewerobject.h" #include "llviewerobjectlist.h" #include "llviewerregion.h" +#include "llviewermenu.h" #include "llviewertexteditor.h" #include "llvoinventorylistener.h" #include "roles_constants.h" +#include + namespace { // Per-operation timeouts (seconds) for coroutine-based async RPC handlers. @@ -77,6 +83,12 @@ namespace constexpr F32 LINKSET_ADD_FLUSH_DELAY = 5.0f; constexpr F32 LINKSET_REMOVE_FLUSH_DELAY = 0.2f; + static const boost::regex LUAU_LOCATION_PATTERN( + R"(^([^:]*):([0-9]+):\s*(.*)$)"); + + static const boost::regex LSL_LOCATION_PATTERN( + R"(\((\d+), (\d+)\) : ([^:]+) : (.+))"); + // Creates a uniquely-named LLEventMailDrop under ".", passes // its name to kickoff (which arranges for one post to that pump), then // suspends the current coroutine up to imeout seconds for the result. @@ -159,52 +171,61 @@ namespace } -class LLPublishedPrimListener : public LLVOInventoryListener +//======================================================================== +LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only): + LLJSONRPCServer(name, port, local_only), + mPublishedObjectManager( + this, + [this](const LLPublishedObjectMgr::RuntimeChatEvent& event) + { + sendRuntimeEvent(event); + }) { -public: - LLPublishedPrimListener(LLScriptEditorWSServer* server, const LLUUID& object_id, const LLUUID& prim_id, - LLViewerObject* object) - : mServer(server) - , mObjectID(object_id) - , mPrimID(prim_id) - { - registerVOInventoryListener(object, nullptr); - } + LL_INFOS("ScriptEditorWS") << "Created JSON-RPC script editor server: " << name + << " on port " << port << LL_ENDL; - ~LLPublishedPrimListener() override = default; + registerCommand({ "viewer.teleport", "Teleport agent to an in-world object" }, + [](U32, const LLSD& p) -> LLSD + { + LLUUID object_id = p["object_id"].asUUID(); + if (object_id.isNull()) + throw LLJSONRPCConnection::InvalidParams("object_id is required"); - void inventoryChanged(LLViewerObject* object, - LLInventoryObject::object_list_t* inventory, - S32 serial_num, void* user_data) override - { - if (mServer) + LLViewerObject* object = gObjectList.findObject(object_id); + if (!object) + throw LLJSONRPCConnection::InvalidParams("object_id not found"); + + LLVector3d global_pos = object->getPositionGlobal(); + gAgent.teleportViaLocation(global_pos); + + LLSD response; + response["success"] = true; + return response; + }); + + registerCommand({ "viewer.camera.focus", "Zoom camera to an in-world object (same behavior as context menu Zoom In)" }, + [](U32, const LLSD& p) -> LLSD { - if (mServer->isObjectPublished(mObjectID)) - { - mServer->onPrimInventoryChanged(mObjectID, mPrimID); - } - else + LLUUID object_id = p["object_id"].asUUID(); + if (object_id.isNull()) + throw LLJSONRPCConnection::InvalidParams("object_id is required"); + + if (!handle_zoom_to_object(object_id)) { - mServer->onPrimInventoryReady(mObjectID, mPrimID); + throw LLJSONRPCConnection::InternalError( + "Object not found or not reachable"); } - } - } - - const LLUUID& getObjectID() const { return mObjectID; } - const LLUUID& getPrimID() const { return mPrimID; } -private: - LLScriptEditorWSServer* mServer; // non-owning; server always outlives listeners - LLUUID mObjectID; // root object this prim belongs to - LLUUID mPrimID; // this specific prim -}; + LLSD response; + response["success"] = true; + return response; + }); -//======================================================================== -LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only): - LLJSONRPCServer(name, port, local_only) -{ - LL_INFOS("ScriptEditorWS") << "Created JSON-RPC script editor server: " << name - << " on port " << port << LL_ENDL; + registerCommand({ "viewer.object.save_back_to_contents", "Save an in-world object back to source object contents" }, + [this](U32 connection_id, const LLSD& p) -> LLSD + { + return this->handleSaveBackToObjectContents(connection_id, p); + }); } LLScriptEditorWSServer::ptr_t LLScriptEditorWSServer::getServer() @@ -252,16 +273,35 @@ LLScriptEditorWSServer::ptr_t LLScriptEditorWSServer::ensureServerRunning() if (!server->isRunning()) { + U16 port = static_cast(gSavedSettings.getS32("ExternalWebsocketSyncPort")); + LLSD args; + args["PORT"] = static_cast(port); + if (!wsmgr.startServer(DEFAULT_SERVER_NAME)) { LL_WARNS("ScriptEditorWS") << "Failed to start script editor websocket server" << LL_ENDL; + LLNotificationsUtil::add("ExternalEditorServerFailed", args); return nullptr; } + + LLNotificationsUtil::add("ExternalEditorServerStarted", args); } return server; } +std::string LLScriptEditorWSServer::buildScriptSubscriptionId(const LLUUID& object_id, + const LLUUID& item_id) +{ + std::string script_id = object_id.asString() + "_" + item_id.asString(); + + std::array script_id_hash_str = {}; + LLMD5 script_id_hash((const U8*)script_id.c_str()); + script_id_hash.hex_digest(script_id_hash_str.data()); + + return std::string(script_id_hash_str.data()); +} + std::string LLScriptEditorWSServer::buildVSCodeURI(const LLUUID& object_id, const LLUUID& script_id) { @@ -362,22 +402,14 @@ void LLScriptEditorWSServer::onStopped() // Connections are already closed -- clean up all internal state silently. // Do not attempt to send notifications; the sockets are gone. - for (auto& [id, pending] : mPendingPublishes) - { - pending.mListeners.clear(); - } - mPendingPublishes.clear(); - - for (auto& [id, info] : mPublishedObjects) - { - info.mListeners.clear(); - } - mPublishedObjects.clear(); + mPublishedObjectManager.clearAllStateWithListenerCleanup(); mSubscriptions.clear(); mActiveConnections.clear(); LL_INFOS("ScriptEditorWS") << "Script editor WebSocket server stopped, all state cleaned up" << LL_ENDL; + + LLNotificationsUtil::add("ExternalEditorServerStopped"); } void LLScriptEditorWSServer::onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) @@ -422,8 +454,12 @@ bool LLScriptEditorWSServer::subscribeScriptEditor(const LLUUID& object_id, cons if (it == mSubscriptions.end()) { // New subscription - mSubscriptions.emplace(script_id, - EditorSubscription(object_id, item_id, script_name, editor_handle)); + ItemRef item_ref; + item_ref.mPrimID = object_id; + item_ref.mItemID = item_id; + item_ref.mScriptName = script_name; + mSubscriptions.emplace(script_id, + EditorSubscription(item_ref, editor_handle)); } else { @@ -442,8 +478,6 @@ void LLScriptEditorWSServer::unsubscribeEditor(const std::string &script_id) auto connection = it->second.mConnection.lock(); mSubscriptions.erase(it); - // Maintain per-connection count; erase entry when it hits zero. - bool last_for_connection = false; if (connection_id != 0) { auto cit = mConnectionSubscriptionCounts.find(connection_id); @@ -452,21 +486,8 @@ void LLScriptEditorWSServer::unsubscribeEditor(const std::string &script_id) if (--cit->second <= 0) { mConnectionSubscriptionCounts.erase(cit); - last_for_connection = true; } } - else - { - // No counter entry means no other subs referenced this connection. - last_for_connection = true; - } - } - - if (connection && last_for_connection) - { // We have removed the last subscription, close the connection - LL_DEBUGS("ScriptEditorWS") << "Closing connection ID " << connection_id << - " as last subscription was removed" << LL_ENDL; - connection->sendDisconnect(LLScriptEditorWSConnection::DisconnectReason::EDITOR_CLOSED, "Editor closed"); } } @@ -588,7 +609,7 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c return s.handleSyntaxCacheFileRequest(params); })); - script_connection->registerMethod("script.subscribe", + script_connection->registerAsyncMethod("script.subscribe", bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params) { return s.handleScriptSubscribe(connection_id, params); @@ -600,7 +621,7 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c return s.handleFileWatcherFileListRequest(); })); - script_connection->registerMethod("object.unpublish", + script_connection->registerAsyncMethod("object.unpublish", bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params) { return s.handleObjectUnpublish(connection_id, params); @@ -672,59 +693,25 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c { return s.handleObjectItemModify(connection_id, params); })); + + script_connection->registerAsyncMethod("command.execute", + bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params) + { + return s.handleCommandExecute(connection_id, params); + })); + + script_connection->registerMethod("command.list", + bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&) + { + return s.handleCommandList(); + })); } } LLSD LLScriptEditorWSServer::handleObjectList() const { - LLSD objects = LLSD::emptyArray(); - for (const auto& [object_id, info] : mPublishedObjects) - { - LLViewerObject* root = gObjectList.findObject(object_id); - if (!root) - { - LL_DEBUGS("ScriptEditorWS") << "object.list: skipping " << object_id - << " (no longer in scene)" << LL_ENDL; - continue; - } - - // Use cached names from PublishedObjectInfo, but fetch live inventory - LLSD pub; - pub["object_id"] = info.mObjectID; - pub["object_name"] = info.mObjectName; - pub["object_description"] = info.mObjectDescription; - pub["owner_id"] = info.mOwnerID; - if (!info.mRegionName.empty()) - { - pub["region"] = info.mRegionName; - } - pub["inventory"] = buildPrimInventoryLLSD(root); - - LLSD linked_objects = LLSD::emptyArray(); - for (const auto& prim_info : info.mPrims) - { - if (prim_info.mLinkNumber == 1) continue; // skip root - - LLViewerObject* child = gObjectList.findObject(prim_info.mPrimID); - if (!child) continue; - - LLSD link; - link["link_id"] = prim_info.mPrimID; - link["link_number"] = prim_info.mLinkNumber; - link["link_name"] = prim_info.mPrimName; // Cached name - link["inventory"] = buildPrimInventoryLLSD(child); - linked_objects.append(link); - } - if (linked_objects.size() > 0) - { - pub["linked_objects"] = linked_objects; - } - - objects.append(pub); - } - LLSD response; - response["objects"] = objects; + response["objects"] = mPublishedObjectManager.buildObjectListLLSD(); return response; } @@ -816,9 +803,7 @@ LLSD LLScriptEditorWSServer::handleObjectScriptReset(U32 connection_id, const LL LLSD LLScriptEditorWSServer::handleObjectModify(U32 connection_id, const LLSD& params) { - // ───────────────────────────────────────────────────────────── // Step 1: Parameter Validation - // ───────────────────────────────────────────────────────────── LLUUID prim_id = params["prim_id"].asUUID(); if (prim_id.isNull()) throw LLJSONRPCConnection::InvalidParams("prim_id is required"); @@ -831,9 +816,7 @@ LLSD LLScriptEditorWSServer::handleObjectModify(U32 connection_id, const LLSD& p throw LLJSONRPCConnection::InvalidParams( "At least one property (name, description, or permissions) must be specified"); - // ───────────────────────────────────────────────────────────── // Step 2: Find and Validate Object - // ───────────────────────────────────────────────────────────── LLViewerObject* prim = gObjectList.findObject(prim_id); if (!prim) throw LLJSONRPCConnection::InvalidParams("Prim not found"); @@ -845,9 +828,7 @@ LLSD LLScriptEditorWSServer::handleObjectModify(U32 connection_id, const LLSD& p if (!prim->permModify()) throw LLJSONRPCConnection::ForbiddenError("No modify permission on object"); - // ───────────────────────────────────────────────────────────── // Step 3: Send Property Update Messages - // ───────────────────────────────────────────────────────────── LLMessageSystem* msg = gMessageSystem; LLHost host = prim->getRegion()->getHost(); U32 local_id = prim->getLocalID(); @@ -895,9 +876,7 @@ LLSD LLScriptEditorWSServer::handleObjectModify(U32 connection_id, const LLSD& p msg->sendReliable(host); } - // ───────────────────────────────────────────────────────────── // Step 4: Return Success Response - // ───────────────────────────────────────────────────────────── LLSD response; response["success"] = true; response["prim_id"] = prim_id.asString(); @@ -906,9 +885,7 @@ LLSD LLScriptEditorWSServer::handleObjectModify(U32 connection_id, const LLSD& p LLSD LLScriptEditorWSServer::handleObjectItemModify(U32 connection_id, const LLSD& params) { - // ───────────────────────────────────────────────────────────── // Step 1: Parameter Validation - // ───────────────────────────────────────────────────────────── if (!params.has("prim_id") || !params.has("item_id")) throw LLJSONRPCConnection::InvalidParams("prim_id and item_id are required"); @@ -920,17 +897,13 @@ LLSD LLScriptEditorWSServer::handleObjectItemModify(U32 connection_id, const LLS throw LLJSONRPCConnection::InvalidParams( "At least one property (name, description, or permissions) must be specified"); - // ───────────────────────────────────────────────────────────── // Step 2: Validate Published Item (reuse existing helper) - // ───────────────────────────────────────────────────────────── ValidatedItem v = validatePublishedItem(params, PERM_MODIFY); LLUUID prim_id = params["prim_id"].asUUID(); LLUUID item_id = params["item_id"].asUUID(); - // ───────────────────────────────────────────────────────────── // Step 3: Create Modified Item Copy - // ───────────────────────────────────────────────────────────── LLPointer new_item = new LLViewerInventoryItem(static_cast(v.item)); @@ -952,14 +925,10 @@ LLSD LLScriptEditorWSServer::handleObjectItemModify(U32 connection_id, const LLS new_item->setPermissions(perm); } - // ───────────────────────────────────────────────────────────── // Step 4: Send UpdateTaskInventory Message - // ───────────────────────────────────────────────────────────── v.prim->updateInventory(new_item, TASK_INVENTORY_ITEM_KEY, false); - // ───────────────────────────────────────────────────────────── // Step 5: Return Success Response - // ───────────────────────────────────────────────────────────── LLSD response; response["success"] = true; response["prim_id"] = prim_id.asString(); @@ -967,6 +936,123 @@ LLSD LLScriptEditorWSServer::handleObjectItemModify(U32 connection_id, const LLS return response; } +void LLScriptEditorWSServer::registerCommand(const WSCommandInfo& info, WSCommandHandler handler) +{ + mCommandRegistry.emplace(info.command, std::make_pair(info, std::move(handler))); +} + +bool LLScriptEditorWSConnection::hasFeature(const std::string& feature) const +{ + return mFeatures.count(feature) > 0; +} + +LLSD LLScriptEditorWSServer::handleSaveBackToObjectContents(U32 connection_id, const LLSD& params) +{ + LLUUID object_id = params["object_id"].asUUID(); + if (object_id.isNull()) + { + throw LLJSONRPCConnection::InvalidParams("object_id is required"); + } + + const LLPublishedObjectMgr::PublishedObjectInfo* published_info = + mPublishedObjectManager.getPublished(object_id); + if (!published_info) + { + throw LLJSONRPCConnection::InvalidParams( + "Object is not published"); + } + + if (!published_info->mCanSaveBackToContents || published_info->mSourceTaskID.isNull()) + { + throw LLJSONRPCConnection::ForbiddenError( + "Save back is not available for this object"); + } + + LLViewerObject* root = gObjectList.findObject(object_id); + if (!root) + { + throw LLJSONRPCConnection::InvalidParams( + "object_id not found"); + } + + if (!save_object_back_to_contents(root, published_info->mSourceTaskID)) + { + throw LLJSONRPCConnection::InternalError( + "Failed to save object back to contents"); + } + + LL_DEBUGS("ScriptEditorWS") << "Save-back requested via command for object " + << object_id << " on connection " << connection_id << LL_ENDL; + + LLSD response; + response["success"] = true; + + LLSD result; + result["object_id"] = object_id; + response["result"] = result; + return response; +} + +LLSD LLScriptEditorWSServer::handleCommandExecute(U32 connection_id, const LLSD& params) +{ + const std::string command = params["command"].asString(); + if (command.empty()) + { + throw LLJSONRPCConnection::InvalidParams("command is required"); + } + + auto it = mCommandRegistry.find(command); + if (it == mCommandRegistry.end()) + { + throw LLJSONRPCConnection::InvalidParams( + "Unknown command: " + command); + } + + return it->second.second(connection_id, params["params"]); +} + +LLSD LLScriptEditorWSServer::handleCommandList() +{ + LLSD commands(LLSD::emptyArray()); + for (const auto& [name, entry] : mCommandRegistry) + { + LLSD info; + info["command"] = entry.first.command; + info["description"] = entry.first.description; + commands.append(info); + } + LLSD response; + response["commands"] = commands; + return response; +} + +void LLScriptEditorWSServer::sendCommandExecute( + U32 connection_id, const std::string& command, const LLSD& params) +{ + auto it = mActiveConnections.find(connection_id); + if (it == mActiveConnections.end()) + { + return; + } + + auto connection = it->second.lock(); + if (!connection || !connection->hasFeature("commands")) + { + return; + } + + LLSD call_params; + call_params["command"] = command; + call_params["params"] = params; + + connection->call("command.execute", call_params, + [command](const LLSD& result, const LLSD& error) + { + LL_WARNS_IF(!error.isUndefined() || !result["success"].asBoolean(), "WSCommand") + << "command.execute failed for " << command << LL_ENDL; + }); +} + void LLScriptEditorWSServer::broadcastLanguageChange() { LLUUID syntax_id = LLSyntaxDefCache::instance().getSyntaxID(); @@ -999,27 +1085,32 @@ LLSD LLScriptEditorWSServer::handleSyntaxRequest(const LLSD& params) const if (category.empty()) { - response["error"] = "No syntax category specified"; - response["success"] = false; - return response; + throw LLJSONRPCConnection::InvalidParams( + "No syntax category specified"); } response["id"] = mLastSyntaxId; if (category == "defs.lua") { response["defs"] = LLSyntaxDefCache::instance().getLuaKeywords(); - response["success"] = response["defs"].isDefined(); } else if (category == "defs.lsl") { response["defs"] = LLSyntaxDefCache::instance().getLSLKeywords(); - response["success"] = response["defs"].isDefined(); } else { - response["error"] = "Unknown syntax category requested"; - response["success"] = false; + throw LLJSONRPCConnection::InvalidParams( + "Unknown syntax category requested"); + } + + if (!response["defs"].isDefined()) + { + throw LLJSONRPCConnection::InternalError( + "Syntax definitions are unavailable"); } + + response["success"] = true; return response; } @@ -1047,28 +1138,25 @@ LLSD LLScriptEditorWSServer::handleSyntaxCacheFileRequest(const LLSD& params) co if (filename.empty()) { - response["error"] = "No filename specified"; - response["success"] = false; - return response; + throw LLJSONRPCConnection::InvalidParams( + "No filename specified"); } if (!cache.hasCacheFile(filename)) { - response["error"] = "Requested syntax cache file not found"; - response["success"] = false; - return response; + throw LLJSONRPCConnection::InvalidParams( + "Requested syntax cache file not found"); } - bool success = false; if (as_json) { LLSD file_content = cache.loadCacheFileAsLLSD(filename); if (file_content.isDefined()) { response["content"] = file_content; - success = true; } else { - response["error"] = "Failed to load and format syntax cache file."; + throw LLJSONRPCConnection::InternalError( + "Failed to load and format syntax cache file."); } } else @@ -1077,14 +1165,14 @@ LLSD LLScriptEditorWSServer::handleSyntaxCacheFileRequest(const LLSD& params) co if (!content.empty()) { response["content"] = content; - success = true; } else { - response["error"] = "Failed to load syntax cache file"; + throw LLJSONRPCConnection::InternalError( + "Failed to load syntax cache file"); } } - response["success"] = success; + response["success"] = true; return response; } @@ -1128,10 +1216,22 @@ LLSD LLScriptEditorWSServer::handleScriptSubscribe(U32 connection_id, const LLSD auto it = mSubscriptions.find(script_id); if (it != mSubscriptions.end()) { - LLViewerObject* object = gObjectList.findObject((*it).second.mObjectID); - response["object_id"] = (*it).second.mObjectID; + LLUUID prim_id = (*it).second.mItemRef.mPrimID; + LLUUID root_id = prim_id; + LLViewerObject* object = gObjectList.findObject(prim_id); + if (object) + { + LLViewerObject* root = object->getRootEdit(); + if (root) + { + root_id = root->getID(); + } + } + + response["object_id"] = prim_id; + response["root_id"] = root_id; //response["object_name"] = object ? object->getName() : "Unknown"; - response["item_id"] = (*it).second.mItemID; + response["item_id"] = (*it).second.mItemRef.mItemID; } } @@ -1176,35 +1276,37 @@ LLSD LLScriptEditorWSServer::handleObjectRequest(U32 connection_id, const LLSD& if (object_id.isNull()) { - response["success"] = false; - response["message"] = "No object_id specified"; - return response; + throw LLJSONRPCConnection::InvalidParams( + "No object_id specified"); } LLViewerObject* object = gObjectList.findObject(object_id); if (!object) { - response["success"] = false; - response["message"] = "Object not found"; - return response; + throw LLJSONRPCConnection::InvalidParams( + "Object not found"); } if (!object->permModify()) { - response["success"] = false; - response["message"] = "Permission denied"; - return response; + throw LLJSONRPCConnection::ForbiddenError( + "Permission denied"); } bool accepted = publishObject(object_id); - response["success"] = accepted; if (!accepted) { - response["message"] = "Failed to initiate publish"; + throw LLJSONRPCConnection::InternalError( + "Failed to initiate publish"); } + + response["success"] = true; return response; } +// Helper function to validate that the specified prim and +// item are valid, published, and have the required permissions. +// Throws JSON-RPC exceptions if validation fails. LLScriptEditorWSServer::ValidatedItem LLScriptEditorWSServer::validatePublishedItem( const LLSD& params, U32 permMask) const { @@ -1439,7 +1541,51 @@ LLSD LLScriptEditorWSServer::saveScript(LLViewerObject* prim, LLInventoryItem* i response["compiled"] = cb_result["compiled"]; if (!cb_result["compiled"].asBoolean() && cb_result.has("errors")) { - response["errors"] = cb_result["errors"]; + response["diagnostics"] = LLSD::emptyArray(); + + const bool is_lua = + compile_target == "luau" || + compile_target == "lsl-luau"; + + for (const auto& error : llsd::inArray(cb_result["errors"])) + { + boost::smatch match; + LLSD diagnostic; + diagnostic["level"] = "ERROR"; + + if (is_lua && + boost::regex_match( + error.asString(), + match, + LUAU_LOCATION_PATTERN)) + { + diagnostic["row"] = std::stoi(match[2].str()); + diagnostic["column"] = 0; + diagnostic["message"] = match[3].str(); + } + else if (!is_lua && + boost::regex_match( + error.asString(), + match, + LSL_LOCATION_PATTERN)) + { + diagnostic["row"] = + std::stoi(match[1].str()) + 1; + diagnostic["column"] = + std::stoi(match[2].str()) + 1; + diagnostic["level"] = match[3].str(); + diagnostic["message"] = match[4].str(); + diagnostic["format"] = "lsl"; + } + else + { + diagnostic["row"] = 0; + diagnostic["column"] = 0; + diagnostic["message"] = error.asString(); + } + + response["diagnostics"].append(diagnostic); + } } // If the script is open in the viewer's editor, update it @@ -1522,12 +1668,28 @@ LLSD LLScriptEditorWSServer::handleObjectItemDelete(U32 connection_id, const LLS { auto v = validatePublishedItem(params, PERM_MODIFY); - v.prim->removeInventory(v.item->getUUID()); + const LLUUID prim_id = v.prim->getID(); + const LLUUID root_id = v.root->getID(); + const LLUUID item_id = v.item->getUUID(); + + // Optimistic local delete then emit immediate update + // for published clients and request authoritative server refresh. + v.prim->removeInventory(item_id); + onPrimInventoryChanged(root_id, prim_id); + + if (!mPublishedObjectManager.hasInventoryRequestStart(prim_id)) + { + v.prim->dirtyInventory(); + mPublishedObjectManager.setInventoryRequestStart( + prim_id, + LLTimer::getTotalSeconds().value()); + v.prim->requestInventory(); + } LLSD response; response["success"] = true; - response["prim_id"] = params["prim_id"].asUUID(); - response["item_id"] = params["item_id"].asUUID(); + response["prim_id"] = prim_id; + response["item_id"] = item_id; return response; } @@ -1535,11 +1697,15 @@ LLSD LLScriptEditorWSServer::handleObjectUnpublish(U32 connection_id, const LLSD { LLUUID object_id = params["object_id"].asUUID(); if (object_id.isNull()) + { throw LLJSONRPCConnection::InvalidParams("object_id is required"); + } - auto it = mPublishedObjects.find(object_id); - if (it == mPublishedObjects.end()) + if (!mPublishedObjectManager.hasPublished(object_id)) + { throw LLJSONRPCConnection::InvalidParams("Object is not published"); + } + unpublishObject(object_id, "manual"); LLSD response; @@ -1650,25 +1816,24 @@ LLSD LLScriptEditorWSServer::handleObjectItemCreate(const std::string& method, c } } + // Set up event pump to wait for inventory change + LLEventMailDrop result_pump("objectItemCreate." + LLUUID::generateNewID().asString(), true); + // Reject if another item.create is already in flight for this prim; the // map keys by prim, so two concurrent creates would clobber one another. - if (mPendingItemCreates.find(prim_id) != mPendingItemCreates.end()) + if (!mPublishedObjectManager.reservePendingItemCreate(prim_id, result_pump.getName())) { throw LLJSONRPCConnection::InvalidRequest( "An item.create is already in flight for this prim"); } - // Set up event pump to wait for inventory change - LLEventMailDrop result_pump("objectItemCreate." + LLUUID::generateNewID().asString(), true); - mPendingItemCreates[prim_id] = result_pump.getName(); - // RAII: guarantee the pending entry is cleared on every exit path (throw // or normal return), so no exception between here and the erase-on-post // in onPrimInventoryChanged can leave a stale entry behind. Uses a // shared_ptr custom deleter as a lightweight scope guard. std::shared_ptr pending_guard(nullptr, [this, prim_id](void*) { - mPendingItemCreates.erase(prim_id); + mPublishedObjectManager.clearPendingItemCreate(prim_id); }); if (has_cap) @@ -1681,7 +1846,7 @@ LLSD LLScriptEditorWSServer::handleObjectItemCreate(const std::string& method, c } else { - // Fallback: legacy RezScript UDP (scripts only — notecards already rejected above) + // Fallback: legacy RezScript UDP (scripts only -- notecards already rejected above) LLPointer new_item = new LLViewerInventoryItem( LLUUID::null, LLUUID::null, perms, LLUUID::null, @@ -1823,12 +1988,10 @@ void LLScriptEditorWSServer::sendCompileResults(const std::string &script_id, co params["running"] = results["is_running"].asBoolean(); if (results.has("errors")) { - params["errors"] = LLSD::emptyArray(); + params["diagnostics"] = LLSD::emptyArray(); if (is_lua) { // lua errors: ":line: message", line is 1-based - const static boost::regex lua_err_regex(R"(^[^:]*:(\d+): (.+)$)"); - for (const auto& err : llsd::inArray(results["errors"])) { boost::smatch match; @@ -1837,10 +2000,10 @@ void LLScriptEditorWSServer::sendCompileResults(const std::string &script_id, co err_entry["column"] = 0; // TODO: Lua compiler does not provide column info err_entry["level"] = "ERROR"; - if (boost::regex_match(err.asString(), match, lua_err_regex)) + if (boost::regex_match(err.asString(), match, LUAU_LOCATION_PATTERN)) { - S32 line_number = std::stoi(match[1].str()); - std::string message = match[2].str(); + S32 line_number = std::stoi(match[2].str()); + std::string message = match[3].str(); err_entry["row"] = line_number; err_entry["message"] = message; @@ -1850,19 +2013,17 @@ void LLScriptEditorWSServer::sendCompileResults(const std::string &script_id, co err_entry["row"] = 0; err_entry["message"] = err.asString(); } - params["errors"].append(err_entry); + params["diagnostics"].append(err_entry); } } else { // lsl errors: "(line, column) : SEVERITY : message", line and column are 0-based - static const boost::regex lsl_err_regex(R"(\((\d+), (\d+)\) : ([^:]+) : (.+))"); - for (const auto& err : llsd::inArray(results["errors"])) { boost::smatch match; LLSD err_entry; - if (boost::regex_match(err.asString(), match, lsl_err_regex)) + if (boost::regex_match(err.asString(), match, LSL_LOCATION_PATTERN)) { S32 line_number = std::stoi(match[1].str()); S32 col_number = std::stoi(match[2].str()); @@ -1883,7 +2044,7 @@ void LLScriptEditorWSServer::sendCompileResults(const std::string &script_id, co err_entry["message"] = err.asString(); err_entry["format"] = "lsl"; } - params["errors"].append(err_entry); + params["diagnostics"].append(err_entry); } } } @@ -1891,104 +2052,72 @@ void LLScriptEditorWSServer::sendCompileResults(const std::string &script_id, co notifyScript(script_id, "script.compiled", params); } -void LLScriptEditorWSServer::forwardChatToIDE(const LLChat& chat_msg) const +void LLScriptEditorWSServer::forwardChatToIDE( + const LLChat& chat_msg, + LLPublishedObjectMgr::RuntimeEventAggregator::Channel channel) const { LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - auto it = std::find_if(mSubscriptions.begin(), mSubscriptions.end(), - [&chat_msg](const auto& pair) { return (pair.second.mObjectID == chat_msg.mFromID); }); - if (it == mSubscriptions.end()) - { // Not a script we are tracking - return; - } + mPublishedObjectManager.ingestRuntimeChat(chat_msg, channel); +} - bool is_error = false; - std::string error_message; - std::string object_name; - std::string script_name; - S32 line_number = 0; - // We have at least one script from this object, we will forward the message to the IDE - // but first we need to see if it is a runtime error - std::vector lines = LLStringUtil::getTokens(chat_msg.mText, "\n"); - // If this is a runtime error, the first line will look like: " [script: