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