Introduction
In this blog post, we will explore how to build a Service Manager CLI in Dart using the win32 package. By leveraging the Windows APIs provided by win32, we'll create a robust command-line tool that can enumerate services, start and stop services, and query service status.
Whether you're looking to enhance your development toolkit or simply learn more about integrating Dart with Windows system functionalities, this guide will provide you with the insights and steps necessary to build your own service manager from scratch.
Here's what we'll cover:
- Feature Overview
- Setting Up the Project
- Defining the Models
- Implementing Service Manager Logic
- Building the CLI
- Conclusion
- Source Code
Feature Overview
Our Service Manager CLI will include the following key features:
- Enumerating services: View a set of all available services on the system.
- Starting and stopping a service: Start or stop a service by its name.
- Querying service status: Retrieve the current operational status of a service by its name.
Setting Up the Project
Before we dive into coding, let’s set up our project.
Creating a New Dart Project
Open your terminal and run:
dart create service_manager_cli
cd service_manager_cli
Installing Dependencies
Add the ffi and win32 packages to your project with:
dart pub add ffi win32
Defining the Models
We'll start by defining the models responsible for storing service information and result details for start and stop operations.
Create a new file named models.dart
in the lib\src
directory and add the
following code:
import 'package:win32/win32.dart';
/// The result of an attempt to start a service.
enum ServiceStartResult {
/// The attempt to start the service was denied due to insufficient
/// permissions.
accessDenied,
/// The service is already running.
alreadyRunning,
/// The attempt to start the service failed for an unspecified reason.
failed,
/// The service was started successfully.
success,
/// The attempt to start the service timed out.
timedOut,
}
/// The various states a service can be in.
enum ServiceStatus {
/// The service is not running.
stopped,
/// The service is in the process of starting.
startPending,
/// The service is in the process of stopping.
stopPending,
/// The service is running.
running,
/// The service is in the process of resuming from a paused state.
continuePending,
/// The service is in the process of being paused.
pausePending,
/// The service is paused.
paused;
/// Converts an integer value to a corresponding [ServiceStatus] enum.
///
/// Throws an [ArgumentError] if the value does not correspond to a valid
/// value.
static ServiceStatus fromValue(int value) => switch (value) {
SERVICE_STOPPED => ServiceStatus.stopped,
SERVICE_START_PENDING => ServiceStatus.startPending,
SERVICE_STOP_PENDING => ServiceStatus.stopPending,
SERVICE_RUNNING => ServiceStatus.running,
SERVICE_CONTINUE_PENDING => ServiceStatus.continuePending,
SERVICE_PAUSE_PENDING => ServiceStatus.pausePending,
SERVICE_PAUSED => ServiceStatus.paused,
_ => throw ArgumentError('Invalid value: $value')
};
}
/// The result of an attempt to stop a service.
enum ServiceStopResult {
/// The attempt to stop the service was denied due to insufficient
/// permissions.
accessDenied,
/// The service is already stopped.
alreadyStopped,
/// The attempt to stop the service failed for an unspecified reason.
failed,
/// The service was stopped successfully.
success,
/// The attempt to stop the service timed out.
timedOut,
}
/// A Windows service with its name, display name, and current status.
class Service {
const Service({
required this.displayName,
required this.name,
required this.status,
});
/// The display name of the service.
final String displayName;
/// The name of the service.
final String name;
/// The current status of the service.
final ServiceStatus status;
String toString() =>
'Service(displayName: $displayName, name: $name, status: $status)';
}
Implementing Service Manager Logic
Next, we'll implement the core functionality for managing Windows services, including enumerating services, starting and stopping services, and querying service status.
Create a new file named service_manager.dart
in the lib\src
directory and
add the following code to set up the skeleton for managing Windows services:
import 'dart:collection';
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
import 'models.dart';
/// Provides functionality for managing Windows services, including:
/// - Enumerating available services
/// - Starting and stopping services
/// - Retrieving the current status of services
abstract class ServiceManager {
/// Whether to log informative messages to the console.
static bool log = false;
/// Retrieves a set of all services (sorted by display name).
static Set<Service> get services {
// TODO: Implement this method
throw UnimplementedError();
}
/// Starts a service defined by [serviceName].
static ServiceStartResult start(String serviceName) {
// TODO: Implement this method
throw UnimplementedError();
}
/// Stops a service defined by [serviceName].
static ServiceStopResult stop(String serviceName) {
// TODO: Implement this method
throw UnimplementedError();
}
/// Retrieves the status of a service defined by [serviceName].
static ServiceStatus? status(String serviceName) {
// TODO: Implement this method
throw UnimplementedError();
}
/// Logs a message to the console if [log] is `true`.
static void _log(String message) {
if (log) print(message);
}
}
With the skeleton in place, we can start implementing the service manager logic.
Enumerating Services
Now, let's implement the services
getter to enumerate all services on the
system.
/// Retrieves a set of all services (sorted by display name).
static Set<Service> get services {
final services =
SplayTreeSet<Service>((a, b) => a.displayName.compareTo(b.displayName));
// Get a handle to the SCM database.
final scmHandle =
OpenSCManager(nullptr, nullptr, SC_MANAGER_ENUMERATE_SERVICE);
if (scmHandle == NULL) return services;
return using((arena) {
try {
final bytesNeeded = arena<DWORD>();
final servicesReturned = arena<DWORD>();
final resumeHandle = arena<DWORD>();
_log('Getting service list...');
// First call to EnumServicesStatusEx to get the required buffer size.
EnumServicesStatusEx(
scmHandle,
SC_ENUM_PROCESS_INFO,
SERVICE_WIN32,
SERVICE_STATE_ALL,
nullptr,
0,
bytesNeeded,
servicesReturned,
resumeHandle,
nullptr,
);
final buffer = arena<BYTE>(bytesNeeded.value);
// Second call to EnumServicesStatusEx to get the actual data.
if (EnumServicesStatusEx(
scmHandle,
SC_ENUM_PROCESS_INFO,
SERVICE_WIN32,
SERVICE_STATE_ALL,
buffer,
bytesNeeded.value,
bytesNeeded,
servicesReturned,
resumeHandle,
nullptr,
) !=
FALSE) {
final enumBuffer = buffer.cast<ENUM_SERVICE_STATUS_PROCESS>();
for (var i = 0; i < servicesReturned.value; i++) {
final serviceStatus = (enumBuffer + i).ref;
final ENUM_SERVICE_STATUS_PROCESS(:lpServiceName, :lpDisplayName) =
serviceStatus;
final serviceName = lpServiceName.toDartString();
final displayName = lpDisplayName.toDartString();
final status = ServiceStatus.fromValue(
serviceStatus.ServiceStatusProcess.dwCurrentState,
);
final service = Service(
displayName: displayName,
name: serviceName,
status: status,
);
services.add(service);
}
}
} finally {
CloseServiceHandle(scmHandle);
}
return services;
});
}
We first obtain a handle to the Service Control Manager (SCM) database using
OpenSCManager, which allows us to interact with the SCM to query, start, stop,
and configure services. We then make an initial call to EnumServicesStatusEx
to determine the required buffer size for storing the service information. With
the necessary buffer allocated, we make a second call to EnumServicesStatusEx
to retrieve the actual service data. Iterating through the services, we convert
them into Service
objects and add them to a sorted set.
Throughout this process, we log informative messages to track progress and errors. If we fail to open the SCM or enumerate services, we ensure appropriate error handling and logging.