Skip to content
onepub-devPublic

About

The Self package provides utility classes to make it easy to create Self extracting executables (for Dart) that ship with resources or need to run on boot.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

overview

The Self package provides utility classes to make it easy to create Self extracting executables that ship with resources or need to run on boot.

The Self package utilises the DCli pack command to create the self extracting exe.

As well as creating a self extracting executable, Self includes a utility to add your executable as a cronjob so that the exectuable is launched each time the system boots.

You can also use the automatic restart service to relaunch your executable if it should crash (exits with a non-zero exit code)

Version 0.8.1 stops on exit code zero. launch(args: ..., maxRestarts: 5) permits five retries after the initial attempt, ten seconds apart, then throws StateError. A zero retry budget runs once. Launch mode must construct child arguments from parsed options: ['--run', if (debug) '--debug']. This handles both -l and --launch without forwarding either launcher flag.

Installation security contract

The caller must protect the destination directory and every ancestor from untrusted replacement or modification for the entire operation. A private directory below a correctly configured sticky /tmp is suitable for tests; an attacker-writable installation directory is not suitable for privileged installation. The installer source must also be trusted and immutable while copied. Path checks reject existing symlinks but cannot prevent concurrent ancestor replacement. Dart does not expose the directory-handle based, no-follow operations needed to eliminate that race in hostile trees.

Resources must use relative slash-separated paths without empty, dot or parent components. Windows drive paths, backslashes, alternate streams, reserved device names and trailing dots/spaces are rejected on every platform. Executable names must be one filename. Files are staged in unpredictable private directories on the destination filesystem (mode 0700 on POSIX), then renamed over regular file destinations. Existing POSIX resource permissions are preserved on replacement; new resources use the process umask. Normal write failures clean up staging and do not truncate existing files. Unexpected staging contents are not recursively removed and may require manual inspection and cleanup. Installation is atomic per file, not a transaction across all resources.

Windows callers must supply a private inherited ACL and exclude junctions and other reparse points; these guarantees are not verified portably here. Replacing open executables can fail; no delete-first or cross-filesystem copy fallback is attempted. Windows platform validation remains required before privileged use.

Cron executable and working-directory paths must be absolute POSIX paths. Each argument is separately shell-quoted; controls and % are rejected because cron interprets percent before the shell. Usernames use an ASCII whitelist. Cron files use the same trusted-ancestor contract and atomic replacement, with mode 0644 on POSIX. The selected destination is replaced, so choose a unique altCronPath when executable basenames collide. Cron arguments are persisted; do not pass secrets in them.

Release privileges immediately on startup. Wrap each scoped elevation in an outer try/finally, including when the dependency callback throws:

try {
  Shell.current.withPrivileges(() {
    self.addBootLauncher(args: ['--launch'], runAsUser: username);
  });
} finally {
  Shell.current.releasePrivileges();
}

install

To use the Self package you need to install the dcli_sdk

dart pub global activate dcli_sdk
sudo env PATH="$PATH" dcli install

You can ignore the warnings about running Flutter as root.

Now add the Self package to your project.

cd <my project>
dart pub add self

companion packages

We recommend installing the following companion packages.

package details
args process command line args
path work with file paths
dcli_core filesystem management tools
dcli process launchers and cli helper functions

implementing

The self package needs to be called from you apps 'main' function.

You will normally do this be adding a number of command line arguments to the startup logic.

The easiest way to do this is using the args package.

We recommend you add the following command line args.

  • install - extract the exectuable and any resources.
  • launch - lauches the executable and restart if it ever crashes.
  • run - runs the app.

Example:

See example/main.dart for the full source code.

  ..addFlag(
     'launch',
     abbr: 'l',
     negatable: false,
     help: '''
Launches the application as a sub-process and will restart it if it crashes.
''',
   )
   ..addFlag(
     'run',
     abbr: 'r',
     negatable: false,
     help: '''
Run the application.
''',
   )
   ..addFlag(
     'install',
     abbr: 'i',
     negatable: false,
     help: '''
Extracts and installs the application, adding a cronjob so it launches on boot.
''',
   )
   ..addFlag('debug', abbr: 'd', help: 'Enable fine logging')
   ..addFlag('help', help: 'Print the usage message');

 // for the moment we don't want to be running as sudo
 // as it screws up env vars and file permissions.
 Shell.current.releasePrivileges();

 // parse the command line arguments.
 ArgResults? results;
 try {
   results = parser.parse(args);
 } on FormatException catch (e) {
   print(red(e.message));
   print(parser.usage);
   await exitWithFlush(1);
 }

 final run = results!.flag('run');
 final install = results.flag('install');
 final launch = results.flag('launch');
 final debug = results.flag('debug');
 final help = results.flag('help');

 if (help) {
   print('''A description of what your app does''');
   print(parser.usage);
   await exitWithFlush(0);
 }

 // The user must pass exactly one of the flags.
 if (!_exactlyOne(install: install, launch: launch, run: run)) {
   printerr(red('You must pass one of --install, --launch, --run'));
   print(parser.usage);
   await exitWithFlush(1);
 }

 // MyLogger implements SelfLogger so [Self] can
 // log through your existing logging system.
 logger = await MyLogger.create(logFilePath: './logfile.log', debug: debug);
 final self = Self(
   logger: logger,
   installPath: '$HOME/myapp',
   executableName: 'myapp',
   resources: ResourceRegistry.resources,
 );

 if (install) {
   await _install(self);
 }

 if (launch) {
   await _launch(results, self);
 }

 if (run) {
   await _run(args, logger);
 }

 // Use a 0 exit code to tell the launcher not to restart us as we are 
 // shutting down intentionally.
 //
 // technically not required as if we return from main
 // the app exit with a non-zero exit code anyway.
 await exitWithFlush(0);
}

resources

Self with the help of the 'dcli pack' allows you to pack self extracting resources (including binary files) into your executable.

dcli pack documenation

To build your application you need to pack any resources before you compile your application.

Create a directory under the root of your project:

cd <my project>
mkdir resource

Place any assets (files) you want to ship with your executable into the resource directory.

Advanced packing

If you resources are external to your project ( or need to be located in other directories ) you can create a pack.yaml file in the resource directory that lists the files to be packed.

Advanced resources packing guide

building

We recommend creating a build script to pack and compile your executable:

touch <my project>/tool/build.dart

#! /usr/bin/dart 
# update the above path to whereever your dart exe is installed
# run `which dart` to find the path.


void main() {
  // run `dart pub get` to ensure everything is up to date.
  _runPubGet();
  print(green('packing resources'));
  'dcli pack'.run;

  print(green('compiling executable'));
  if (Platform.isWindows) {
    'dart compile exe bin/main.dart -o myapp_installer.exe'.run;
  } else {
    'dart compile exe bin/main.dart -o myapp_installer'.run;
  }
}

void _runPubGet() {
  print(green('running dart pub get'));
  DartSdk().runPubGet(DartProject.self.pathToProjectRoot);
}

To run you build script:

Linux/MacOs:

chmod +x tool/build.dart
tool/build.dart

Windows:

dart tool/build.dart

The build script will create an executable in you project root directory called <myapp>_instaler. On Windows it will end in .exe.

deploying

Once you have created your installer you need to deploy it.

Copy the installer to the target system and run:

Linux/MacOS

chmod +x my_app_installer
sudo env PATH="$PATH" ./my_app_installer

Window:

my_app_installer

sudo

On Linux and MacOs you may need to run the installer as sudo if it needs to create directories outside of you home directory.

NOTE: you shoud never run a .dart file as sudo as it is likely to break your dart SDK.

To run a dart app as sudo you MUST first compile the app. Once the app has been compiled you can run the app with:

sudo env PATH="$PATH" ./my_app_installer

The DCli SDK provides tools to help you iteract with the file system when running as sudo.

Elevated Privileges

About

The Self package provides utility classes to make it easy to create Self extracting executables (for Dart) that ship with resources or need to run on boot.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages