Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce support for OS command execution #438

Merged
merged 27 commits into from
Jul 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2bbdd56
[Automated] Update the native jar versions
madhukaw Jul 4, 2022
5a94ffc
[Automated] Update the native jar versions
madhukaw Jul 4, 2022
6a7ee05
[Automated] Update the native jar versions
madhukaw Jul 5, 2022
3981f20
Implement os:exec function
madhukaw Jul 6, 2022
6eaae73
Add test cases for os:Exec function
madhukaw Jul 6, 2022
de0ca64
Update exec test cases with distribution path
madhukaw Jul 6, 2022
6e9230a
Add private constructors for native classes
madhukaw Jul 6, 2022
d68075a
Add test case for process exit
madhukaw Jul 6, 2022
3dcfd26
Add negative test case for exec function
madhukaw Jul 6, 2022
d58685c
Add test util function to check if windows system
madhukaw Jul 6, 2022
c569453
Fix formatting issues
madhukaw Jul 6, 2022
a62eab3
Add exec function proposal
madhukaw Jul 7, 2022
d2fe277
Update spec with exec function
madhukaw Jul 7, 2022
22a3741
Update changelog
madhukaw Jul 7, 2022
c8c9e7d
[Automated] Update the native jar versions
madhukaw Jul 7, 2022
360f9bb
Update module version to 1.4.0
madhukaw Jul 7, 2022
68f8dc6
Refactor exec native class
madhukaw Jul 7, 2022
23bd17b
Refactor waitforExit native class
madhukaw Jul 7, 2022
99e39a9
Refacror EnvProperties record
madhukaw Jul 7, 2022
bd6c1fa
Fix code review suggestions
madhukaw Jul 7, 2022
c6d1af4
Refactor Exec class
madhukaw Jul 7, 2022
409b5f5
Update windows test cases
madhukaw Jul 7, 2022
43e9ded
Refactor util function to check os type
madhukaw Jul 7, 2022
10506fd
Add exec function related test case
madhukaw Jul 7, 2022
1aaf2e3
Fix issues in spec and proposal
madhukaw Jul 7, 2022
7ee7bed
Remove unnecessary comment
madhukaw Jul 7, 2022
d1239d3
Re-structure exec functin specification
madhukaw Jul 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
org = "ballerina"
name = "os"
version = "1.3.0"
version = "1.4.0"
authors = ["Ballerina"]
keywords = ["environment"]
repository = "https://github.com/ballerina-platform/module-ballerina-os"
Expand All @@ -10,10 +10,10 @@ license = ["Apache-2.0"]
distribution = "2201.0.4"

[[platform.java11.dependency]]
path = "../native/build/libs/os-native-1.3.0.jar"
path = "../native/build/libs/os-native-1.4.0-SNAPSHOT.jar"

[[platform.java11.dependency]]
path = "./lib/io-native-1.2.2.jar"

[[platform.java11.dependency]]
path = "../test-utils/build/libs/os-test-utils-1.3.0.jar"
path = "../test-utils/build/libs/os-test-utils-1.4.0-SNAPSHOT.jar"
23 changes: 22 additions & 1 deletion ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@
[ballerina]
dependencies-toml-version = "2"

[[package]]
org = "ballerina"
name = "io"
version = "1.2.2"
dependencies = [
{org = "ballerina", name = "jballerina.java"},
{org = "ballerina", name = "lang.value"}
]
modules = [
{org = "ballerina", packageName = "io", moduleName = "io"}
]

[[package]]
org = "ballerina"
name = "jballerina.java"
Expand All @@ -14,11 +26,20 @@ modules = [
{org = "ballerina", packageName = "jballerina.java", moduleName = "jballerina.java"}
]

[[package]]
org = "ballerina"
name = "lang.value"
version = "0.0.0"
dependencies = [
{org = "ballerina", name = "jballerina.java"}
]

[[package]]
org = "ballerina"
name = "os"
version = "1.3.0"
version = "1.4.0"
dependencies = [
{org = "ballerina", name = "io"},
{org = "ballerina", name = "jballerina.java"},
{org = "ballerina", name = "test"}
]
Expand Down
14 changes: 14 additions & 0 deletions ballerina/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def packageOrg = "ballerina"
def tomlVersion = stripBallerinaExtensionVersion("${project.version}")
def ballerinaTomlFilePlaceHolder = new File("${project.rootDir}/build-config/resources/Ballerina.toml")
def ballerinaTomlFile = new File("$project.projectDir/Ballerina.toml")
def configTOMLFile = new File("$project.projectDir/tests/Config.toml")

def stripBallerinaExtensionVersion(String extVersion) {
if (extVersion.matches(project.ext.timestampedVersionRegex)) {
Expand Down Expand Up @@ -75,12 +76,25 @@ task updateTomlFiles {
doLast {
def stdlibNativeIoVersion = project.stdlibIoVersion
def newConfig = ballerinaTomlFilePlaceHolder.text.replace("@project.version@", project.version)
def distributionBinPath = "$project.rootDir/target/ballerina-runtime/bin"
newConfig = newConfig.replace("@toml.version@", tomlVersion)
newConfig = newConfig.replace("@io.native.version@", stdlibNativeIoVersion)
ballerinaTomlFile.text = newConfig
setExecPath(configTOMLFile,distributionBinPath)
}
}

def setExecPath(configTOMLFile, distributionBinPath) {
def distributionPath
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
distributionPath = (distributionBinPath + "/bal.bat").replace("/", "\\")
distributionPath = distributionPath.replace("\\", "\\\\")
} else {
distributionPath = distributionBinPath + "/bal"
}
configTOMLFile.text = configTOMLFile.text.replace("@exec.path@", distributionPath)
}

task commitTomlFiles {
doLast {
project.exec {
Expand Down
25 changes: 24 additions & 1 deletion ballerina/os.bal
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@

import ballerina/jballerina.java;

public type Command record {|
string value;
string[] arguments = [];
|};

public type EnvProperties record {|
never command?;
anydata...;
|};

# Returns the environment variable value associated with the provided name.
# ```ballerina
# string port = os:getEnv("HTTP_PORT");
Expand Down Expand Up @@ -71,7 +81,7 @@ public isolated function getUserHome() returns string = @java:Method {
public isolated function setEnv(string key, string value) returns Error? {
if key == "" {
return error Error("The parameter key cannot be an empty string");
} else if key == "==" {
} else if key == "==" {
return error Error("The parameter key cannot be == sign");
} else {
return setEnvExtern(key, value);
Expand Down Expand Up @@ -113,3 +123,16 @@ isolated function listEnvExtern() returns map<string> = @java:Method {
name: "listEnv",
'class: "io.ballerina.stdlib.os.nativeimpl.ListEnv"
} external;

# Executes an operating system command as a subprocess of the current process.
# ```ballerina
# os:Process|os:Error result = os:exec({value: "bal", arguments: ["run", filepath]}, BAL_CONFIG_FILE = "/abc/Config.toml");
# ```
#
# + command - The command to be executed
# + envProperties - The environment properties
# + return - Process object in success, or an Error if a failure occurs
public isolated function exec(Command command, *EnvProperties envProperties) returns Process|Error = @java:Method {
name: "exec",
'class: "io.ballerina.stdlib.os.nativeimpl.Exec"
} external;
3 changes: 3 additions & 0 deletions ballerina/os_errors.bal
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@

# Represents OS module related errors.
public type Error distinct error;

# Process Execution error that returns when the os:Exec function fails.
public type ProcessExecError distinct Error;
70 changes: 70 additions & 0 deletions ballerina/process.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) 2022 WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
//
// WSO2 Inc. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import ballerina/io;
import ballerina/jballerina.java;

# This object contains information on a process being created from Ballerina.
# This is returned from the `exec` function in the `os` module.
public class Process {

# Waits for the process to finish its work and exit.
# This will return 0 if successful, or a different value during failure depending on the operating system.
# ```ballerina
# int|os:Error exitCode = process.waitForExit();
# ```
#
# + return - Returns the exit code for the process, or else an `Error` if a failure occurs
public isolated function waitForExit() returns int|Error {
return nativeWaitForExit(self);
}

# Returns the standard output as default. Option provided to return standard error by providing file descriptor.
# If the process was not finished and exited explicitly by running process.waitForExit(), then process.output() will finish the work and exit and return the output.
# ```ballerina
# byte[]|os:Error err = process.output(io:stderr);
# ```
#
# + fileOutputStream - The output stream (`io:stdout` or `io:stderr`) content needs to be returned
# + return - The `byte[]`, which represents the process's 'standard error', or the 'standard out', or an Error
public isolated function output(io:FileOutputStream fileOutputStream = io:stdout) returns byte[]|Error {
return nativeOutput(self, fileOutputStream);
}

# Terminates the process.
# ```ballerina
# process.exit();
# ```
#
public isolated function exit() {
return nativeExit(self);
}
}

isolated function nativeWaitForExit(Process process) returns int|Error = @java:Method {
name: "waitForExit",
'class: "io.ballerina.stdlib.os.nativeimpl.WaitForExit"
} external;

isolated function nativeExit(Process process) = @java:Method {
name: "exit",
'class: "io.ballerina.stdlib.os.nativeimpl.Exit"
} external;

isolated function nativeOutput(Process process, int fileOutputStream) returns byte[]|Error = @java:Method {
name: "output",
'class: "io.ballerina.stdlib.os.nativeimpl.Output"
} external;
2 changes: 2 additions & 0 deletions ballerina/tests/Config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[os]
bal_exec_path = "@exec.path@"
117 changes: 117 additions & 0 deletions ballerina/tests/os-test.bal → ballerina/tests/os_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

import ballerina/jballerina.java;
import ballerina/test;
import ballerina/io;

configurable string bal_exec_path = ?;

@test:Config {}
function testGetEnv() {
Expand Down Expand Up @@ -154,3 +157,117 @@ function getSystemProperty(string key) returns string = @java:Method {
name: "getSystemProperty",
'class: "io.ballerina.stdlib.os.utils.OSUtils"
} external;

@test:Config {}
function testExec() returns error? {
Process process = check exec({value: "echo", arguments: ["hello world"]});
int exitCode = check process.waitForExit();
test:assertEquals(exitCode, 0);

byte[] outputBytes = check process.output();
string outputString = check string:fromBytes(outputBytes);
test:assertEquals(outputString.trim(), "hello world");
}

@test:Config {}
function testExecOutputWithoutWaitForExit() returns error? {
Process process = check exec({value: "echo", arguments: ["hello world"]});

byte[] outputBytes = check process.output();
string outputString = check string:fromBytes(outputBytes);
test:assertEquals(outputString.trim(), "hello world");
}

@test:Config {}
function testExecWithOutputStdOut() returns error? {
Process process = check exec({value: bal_exec_path, arguments: ["run", "tests/resources/hello1.bal"]});
int exitCode = check process.waitForExit();
test:assertEquals(exitCode, 0);

byte[] stdOutBytes = check process.output(io:stdout);
string stdOutString = check string:fromBytes(stdOutBytes);
test:assertTrue(stdOutString.includes("hello world"));

byte[] stdErrBytes = check process.output(io:stderr);
string stdErrString = check string:fromBytes(stdErrBytes);
test:assertFalse(stdErrString.includes("hello world"));
}

@test:Config {}
function testExecWithOutputStdErr() returns error? {
Process process = check exec({value: bal_exec_path, arguments: ["run", "tests/resources/hello2.bal"]});
int exitCode = check process.waitForExit();
test:assertEquals(exitCode, 0);

byte[] stdOutBytes = check process.output(io:stdout);
string stdOutString = check string:fromBytes(stdOutBytes);
test:assertFalse(stdOutString.includes("hello world"));

byte[] stdErrBytes = check process.output(io:stderr);
string stdErrString = check string:fromBytes(stdErrBytes);
test:assertTrue(stdErrString.includes("hello world"));
}

@test:Config {}
function testExecWithEnvironmentVariable() returns error? {
Process process = check exec({value: bal_exec_path, arguments: ["run", "tests/resources/hello3.bal"]}, BAL_CONFIG_FILES = "tests/resources/config/Config.toml");
int exitCode = check process.waitForExit();
test:assertEquals(exitCode, 0);

byte[] outputBytes = check process.output(io:stderr);
string outputString = check string:fromBytes(outputBytes);
test:assertTrue(outputString.includes("{\"time\":\""));
test:assertTrue(outputString.includes("\", \"level\":\"DEBUG\", \"module\":\"\", \"message\":\"debug message\"}"));
}

@test:Config {}
function testExecExit() returns error? {
Process process = check exec({value: "echo", arguments: ["hello world"]});
process.exit();

int _ = check process.waitForExit();

byte[]|Error outputBytes = process.output();
if isWindowsEnvironment() {
if outputBytes is error {
test:assertFail("Expected output does not match");
} else {
test:assertEquals(string:fromBytes(outputBytes), "");
}
} else {
if outputBytes is error {
test:assertEquals(outputBytes.message(), "Failed to read the output of the process: Stream closed");
} else {
test:assertFail("Expected error message does not match");
}
}
}

@test:Config {}
function testExecNegative() returns error? {
Process|Error process = exec({value: "foo"});
if process is Error {
if isWindowsEnvironment() {
test:assertEquals(process.message(), "Failed to retrieve the process object: Cannot run program \"foo\": CreateProcess error=2, " +
"The system cannot find the file specified");
} else {
test:assertEquals(process.message(), "Failed to retrieve the process object: Cannot run program \"foo\": error=2, No such file or directory");
}
} else {
test:assertFail("Expected error message does not match");
}
}

isolated function isWindowsEnvironment() returns boolean {
var osType = java:toString(nativeGetSystemPropery(java:fromString("os.name")));
if osType is string {
return osType.toLowerAscii().includes("win");
}
return false;
}

isolated function nativeGetSystemPropery(handle key) returns handle = @java:Method {
name: "getProperty",
'class: "java.lang.System",
paramTypes: ["java.lang.String"]
} external;
3 changes: 3 additions & 0 deletions ballerina/tests/resources/config/Config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[ballerina.log]
level = "DEBUG"
format = "json"
21 changes: 21 additions & 0 deletions ballerina/tests/resources/hello1.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) 2022 WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
//
// WSO2 Inc. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import ballerina/io;

public function main() {
io:fprintln(io:stdout, "hello world");
}
21 changes: 21 additions & 0 deletions ballerina/tests/resources/hello2.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) 2022 WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
//
// WSO2 Inc. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import ballerina/io;

public function main() {
io:fprintln(io:stderr, "hello world");
}
Loading