How CasaOS Integrates with the Message Bus for Event Publishing

CasaOS uses a generated OpenAPI client (message_bus.ClientWithResponses) to communicate with the external CasaOS-MessageBus service, wrapping event publishing in a NotifyServer helper that gracefully handles failures without crashing the system.

CasaOS integrates with the message bus through a lightweight abstraction layer that decouples internal services from the external messaging infrastructure. The implementation allows any component to broadcast events via a simple helper method while ensuring the system remains operational even if the message bus is unreachable. This architecture centers around three core components: a dynamic client factory, static event definitions, and a marshaling publish helper.

Message-Bus Client Factory

The integration begins in service/service.go, where the MessageBus() method acts as a factory for the OpenAPI client. This method discovers the message bus address at runtime using external.GetMessageBusAddress and returns a message_bus.ClientWithResponses instance configured with that endpoint.

According to the CasaOS source code in service/service.go:29-44, the client initialization looks like this:

func (c *store) MessageBus() *message_bus.ClientWithResponses {
    client, _ := message_bus.NewClientWithResponses(
        external.GetMessageBusAddress(runtimePath),
    )
    return client
}

The runtimePath variable typically resolves to CommonInfo.RuntimePath defined in pkg/config/config.go, allowing the system to locate the message bus socket or URL dynamically.

Event Type Definitions

Before publishing, components reference predefined event types declared in common/message.go:7-12. This file maintains a static registry of fully-qualified event names paired with their originating service identifiers, ensuring consistency across the distributed system.

For example, the file defines events like "casaos:system:utilization" and "casaos:file:operate" that services can reference by constant rather than string literal.

The Publish Helper Implementation

The actual publishing logic resides in service/notify.go, specifically within the NotifyServer.SendNotify method. This helper transforms generic Go maps into the string-only format required by the OpenAPI specification, then forwards the payload to the message bus.

As implemented in service/notify.go:55-68, the method performs three critical operations:

  1. Marshals the map[string]interface{} payload into map[string]string by JSON-encoding each value
  2. Invokes PublishEventWithResponse on the message bus client with the service name, event name, and marshaled message
  3. Handles errors gracefully by logging failures without returning panics or blocking the caller
func (i *notifyServer) SendNotify(name string, message map[string]interface{}) {
    // Convert interface{} values to JSON strings for OpenAPI compliance
    msg := make(map[string]string)
    for k, v := range message {
        b, _ := json.Marshal(v)
        msg[k] = string(b)
    }

    // Publish to the message bus with context
    resp, err := MyService.MessageBus().
        PublishEventWithResponse(context.Background(), common.SERVICENAME, name, msg)
    
    if err != nil {
        logger.Error("failed to publish event to message bus", zap.Error(err))
        return
    }
    if resp.StatusCode() != http.StatusOK {
        logger.Error("failed to publish event", zap.String("status", resp.Status()))
    }
}

Publishing Events from Application Components

When an internal component needs to broadcast state changes, it calls MyService.Notify().SendNotify() with the event name and a payload map. The payload can contain any serializable data structures, as the helper automatically handles conversion.

Example: Publishing a system utilization update

func reportSystemLoad(cpuPercent float64) {
    payload := map[string]interface{}{
        "timestamp": time.Now().Unix(),
        "cpu":       cpuPercent,
        "memory":    getMemoryStats(),
    }
    
    // Uses the pre-declared event type from common/message.go
    MyService.Notify().SendNotify("casaos:system:utilization", payload)
}

Example: File operation status updates

Periodic workers can emit progress events using the same pattern. The following excerpt shows how file operations broadcast their state:

model := notify.NotifyModel{State: "NORMAL"}
listMsg := map[string]interface{}{"file_operate": model}

// The helper converts the NotifyModel to JSON strings internally
MyService.Notify().SendNotify("casaos:file:operate", listMsg)

Error Handling and Graceful Degradation

A key architectural feature of CasaOS message bus integration is optional availability. If the PublishEventWithResponse call fails due to network issues, service unavailability, or configuration errors, the error is logged using zap.Error() but never propagated as a panic or returned error that would disrupt the calling operation.

This design ensures that CasaOS continues functioning as a local NAS operating system even when the message bus service is offline. The bus acts as a best-effort notification channel rather than a mandatory dependency for core functionality.

Summary

  • Dynamic client creation: The MessageBus() factory in service/service.go creates OpenAPI clients using runtime-discovered addresses via external.GetMessageBusAddress.
  • Type-safe events: Event names are centralized in common/message.go to prevent string duplication errors across services.
  • Automatic marshaling: The SendNotify method in service/notify.go converts map[string]interface{} to map[string]string by JSON-encoding values before transmission.
  • Graceful failures: Publishing errors are logged but never panic, allowing CasaOS to operate independently of the message bus availability.
  • Simple API: Components publish via MyService.Notify().SendNotify(eventName, payload) without managing HTTP clients or connection pools directly.

Frequently Asked Questions

What happens if the Message Bus is unreachable?

CasaOS treats the message bus as an optional dependency. If the PublishEventWithResponse call fails or returns a non-200 status code, the error is logged using the Zap logger and the method returns immediately. This prevents cascading failures and ensures that local file operations and system management continue working even when the messaging service is down.

Where are event types defined in CasaOS?

Event types are declared as constants in common/message.go at lines 7-12. This file contains the fully-qualified event strings (such as "casaos:system:utilization" and "casaos:file:operate") that services should reference when calling SendNotify. Using these constants ensures consistency and prevents typos in event names across the codebase.

How does CasaOS discover the Message Bus address?

The system uses the external.GetMessageBusAddress function from the CasaOS-Common library, which resolves the address based on CommonInfo.RuntimePath defined in pkg/config/config.go. This allows the message bus to run on a dynamic socket path or port while still being discoverable by services at runtime, supporting both development and production deployments without hardcoded URLs.

Can I publish custom events from my CasaOS extension?

Yes, extensions can publish custom events by calling MyService.Notify().SendNotify() with a custom event name string and a map[string]interface{} payload. The payload can contain any JSON-serializable values, as the helper automatically marshals them to strings. However, you should register your event names in common/message.go if building within the core repository, or follow the existing naming convention (casaos:service:action) for consistency.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →