แอ็พพลิเคชันคอนโซล .NET เป็นบริการ Windows


145

ฉันมีแอปพลิเคชั่นคอนโซลและต้องการเรียกใช้เป็นบริการของ Windows VS2010 มีเท็มเพลตโครงการที่อนุญาตให้แนบโครงการคอนโซลและสร้างบริการ Windows ฉันไม่ต้องการเพิ่มโครงการบริการที่แยกออกจากกันและหากเป็นไปได้ให้รวมรหัสบริการลงในคอนโซลแอปพลิเคชันเพื่อให้แอปพลิเคชันคอนโซลเป็นหนึ่งโครงการที่สามารถเรียกใช้เป็นแอปพลิเคชันคอนโซลหรือบริการ windows ได้

อาจมีคนแนะนำคลาสไลบรารีหรือโค้ดขนาดสั้นที่สามารถแปลงแอปพลิเคชัน c # console เป็นบริการได้อย่างรวดเร็วและง่ายดาย?


ทำไมคุณไม่สร้างโครงการบริการชั่วคราวและคัดลอกบิตที่ทำให้เป็นบริการ
Gabe

4
คุณสามารถลอง Topshelf topshelf-project.com
Artem Koshelev

คุณสามารถลองใช้เทคนิคที่อธิบายไว้ที่นี่: einaregilsson.com/2007/08/15/…
โจ

ฮะ? ฉันไม่แน่ใจ. เกี่ยวกับเรื่องนี้.

2
ทางเลือกชั้นวางของที่ง่ายมาก: runasservice.com
Luis Perez

คำตอบ:


185

ฉันมักจะใช้ techinque ต่อไปนี้เพื่อเรียกใช้แอพเดียวกันกับแอปพลิเคชันคอนโซลหรือบริการ:

public static class Program
{
    #region Nested classes to support running as service
    public const string ServiceName = "MyService";

    public class Service : ServiceBase
    {
        public Service()
        {
            ServiceName = Program.ServiceName;
        }

        protected override void OnStart(string[] args)
        {
            Program.Start(args);
        }

        protected override void OnStop()
        {
            Program.Stop();
        }
    }
    #endregion

    static void Main(string[] args)
    {
        if (!Environment.UserInteractive)
            // running as service
            using (var service = new Service())
                ServiceBase.Run(service);
        else
        {
            // running as console app
            Start(args);

            Console.WriteLine("Press any key to stop...");
            Console.ReadKey(true);

            Stop();
        }
    }

    private static void Start(string[] args)
    {
        // onstart code here
    }

    private static void Stop()
    {
        // onstop code here
    }
}

Environment.UserInteractiveปกติแล้วจะเป็นจริงสำหรับแอปคอนโซลและเท็จสำหรับบริการ ในทางเทคนิคเป็นไปได้ที่จะเรียกใช้บริการในโหมดผู้ใช้แบบโต้ตอบดังนั้นคุณสามารถตรวจสอบสวิตช์บรรทัดคำสั่งแทน


3
คุณใช้คลาส ServiceInstaller ดูmsdn.microsoft.com/en-us/library/...
VladV

2
เป็นไปตามที่คาดไว้ - บริการของคุณจะทำงานเป็นกระบวนการที่แยกจากกัน (ดังนั้นมันจะแสดงในตัวจัดการงาน) แต่กระบวนการนี้จะถูกควบคุมโดยระบบ (เช่นเริ่มหยุดหยุดรีสตาร์ทตามการตั้งค่าบริการ)
VladV

2
หากคุณเรียกใช้เป็นแอปคอนโซลคุณจะไม่เห็นบริการ จุดประสงค์ทั้งหมดของรหัสนี้คือเพื่อให้คุณสามารถเรียกใช้ได้ทั้งในฐานะที่เป็นแอพคอนโซลหรือเป็นบริการ ในการเรียกใช้เป็นบริการคุณจะต้องติดตั้งก่อน (ใช้คลาส ServiceInstaller - ดูลิงก์ MSDN ด้านบน - หรือ installuitil.exe) และเรียกใช้บริการจากแผงควบคุม
VladV

2
ServiceInstaller เป็นเพียงคลาสยูทิลิตี้ที่จะจัดการกับบริการ Windows (เล็กน้อยเช่นยูทิลิตี้ installutil.exe หรือ sc.exe) คุณสามารถใช้มันเพื่อติดตั้งสิ่งที่คุณต้องการเป็นบริการระบบปฏิบัติการไม่สนใจประเภทโครงการที่คุณใช้
VladV

5
เพียงแค่เพิ่มการอ้างอิงในโครงการของคุณเพื่อSystem.ServiceProcessและคุณจะสามารถที่จะใช้โค้ดข้างต้น
danimal

59

ผมเคยประสบความสำเร็จที่ดีกับTopShelf

TopShelf เป็นแพคเกจ Nuget ที่ออกแบบมาเพื่อให้ง่ายต่อการสร้างแอพ. NET Windows ที่สามารถเรียกใช้เป็นแอพคอนโซลหรือเป็นบริการ Windows คุณสามารถเชื่อมต่อเหตุการณ์ต่างๆเช่นบริการเริ่มและหยุดเหตุการณ์ได้อย่างรวดเร็วกำหนดค่าโดยใช้รหัสเช่นเพื่อตั้งค่าบัญชีที่มันทำงานกำหนดค่าการอ้างอิงกับบริการอื่น ๆ และกำหนดวิธีการกู้คืนจากข้อผิดพลาด

จาก Package Manager Console (Nuget):

แพคเกจติดตั้ง Topshelf

อ้างถึงตัวอย่างรหัสเพื่อเริ่มต้น

ตัวอย่าง:

HostFactory.Run(x =>                                 
{
    x.Service<TownCrier>(s =>                        
    {
       s.ConstructUsing(name=> new TownCrier());     
       s.WhenStarted(tc => tc.Start());              
       s.WhenStopped(tc => tc.Stop());               
    });
    x.RunAsLocalSystem();                            

    x.SetDescription("Sample Topshelf Host");        
    x.SetDisplayName("Stuff");                       
    x.SetServiceName("stuff");                       
}); 

TopShelf ยังดูแลการติดตั้งบริการซึ่งสามารถประหยัดเวลาได้มากและลบรหัสสำเร็จรูปจากโซลูชันของคุณ ในการติดตั้ง. exe เป็นบริการคุณเพียงแค่เรียกใช้สิ่งต่อไปนี้จากพรอมต์คำสั่ง

myservice.exe install -servicename "MyService" -displayname "My Service" -description "This is my service."

คุณไม่จำเป็นต้องเชื่อมต่อ ServiceInstaller และสิ่งที่ TopShelf ทำเพื่อคุณ


1
สวัสดีฉันได้รับสิ่งนี้: - "ไม่สามารถติดตั้งแพคเกจ 'Topshelf 4.0.1' คุณกำลังพยายามติดตั้งแพคเกจนี้ในโครงการที่มีเป้าหมาย '.NETFramework, Version = v4.5' แต่แพ็คเกจไม่ได้มี การอ้างอิงแอสเซมบลีหรือไฟล์เนื้อหาที่เข้ากันได้กับเฟรมเวิร์กนั้น " เกิดอะไรขึ้นที่นี่?

3
ตรวจสอบให้แน่ใจว่าคุณกำหนดเป้าหมายรันไทม์. NET 4.5.2 เต็มไม่ใช่โปรไฟล์ไคลเอ็นต์
saille

ได้โปรดให้คุณเพิ่มแสงสว่างใน myservice.exe และคุณจะเปิดได
เร็กตอรี่

1
@Izuagbala myservice.exe เป็นแอปพลิเคชั่นคอนโซลที่คุณสร้างขึ้นโดยที่ TopShelf bootstrapped เข้าไปดังแสดงในตัวอย่างโค้ด
แล่นเรือ

สามารถเรียกใช้ myservice.exe เป็นคอนโซลหลังจากติดตั้งเป็นบริการได้หรือไม่ เอกสารไม่ชัดเจน: "เมื่อสร้างแอปพลิเคชันคอนโซลแล้วผู้พัฒนาจะสร้างคลาสบริการเดียว" docs.topshelf-project.com/en/latest/overview/ ......
Michael Freidgeim

27

ดังนั้นนี่คือคำแนะนำแบบสมบูรณ์:

  1. สร้างโครงการแอปพลิเคชันคอนโซลใหม่ (เช่น MyService)
  2. เพิ่มการอ้างอิงสองไลบรารี: System.ServiceProcess และ System.Configuration.Install
  3. เพิ่มไฟล์สามไฟล์ที่พิมพ์ด้านล่าง
  4. สร้างโครงการและเรียกใช้ "InstallUtil.exe c: \ path \ to \ MyService.exe"
  5. ตอนนี้คุณควรเห็น MyService ในรายการบริการ (run services.msc)

* InstallUtil.exe สามารถพบได้ที่นี่: C: \ windows \ Microsoft.NET \ Framework \ v4.0.30319 \ InstallUtil.ex‌ e

Program.cs

using System;
using System.IO;
using System.ServiceProcess;

namespace MyService
{
    class Program
    {
        public const string ServiceName = "MyService";

        static void Main(string[] args)
        {
            if (Environment.UserInteractive)
            {
                // running as console app
                Start(args);

                Console.WriteLine("Press any key to stop...");
                Console.ReadKey(true);

                Stop();
            }
            else
            {
                // running as service
                using (var service = new Service())
                {
                    ServiceBase.Run(service);
                }
            }
        }

        public static void Start(string[] args)
        {
            File.AppendAllText(@"c:\temp\MyService.txt", String.Format("{0} started{1}", DateTime.Now, Environment.NewLine));
        }

        public static void Stop()
        {
            File.AppendAllText(@"c:\temp\MyService.txt", String.Format("{0} stopped{1}", DateTime.Now, Environment.NewLine));
        }
    }
}

MyService.cs

using System.ServiceProcess;

namespace MyService
{
    class Service : ServiceBase
    {
        public Service()
        {
            ServiceName = Program.ServiceName;
        }

        protected override void OnStart(string[] args)
        {
            Program.Start(args);
        }

        protected override void OnStop()
        {
            Program.Stop();
        }
    }
}

MyServiceInstaller.cs

using System.ComponentModel;
using System.Configuration.Install;
using System.ServiceProcess;

namespace MyService
{
    [RunInstaller(true)]
    public class MyServiceInstaller : Installer
    {
        public MyServiceInstaller()
        {
            var spi = new ServiceProcessInstaller();
            var si = new ServiceInstaller();

            spi.Account = ServiceAccount.LocalSystem;
            spi.Username = null;
            spi.Password = null;

            si.DisplayName = Program.ServiceName;
            si.ServiceName = Program.ServiceName;
            si.StartType = ServiceStartMode.Automatic;

            Installers.Add(spi);
            Installers.Add(si);
        }
    }
}

1
หากคุณกำลังรวบรวมโครงการเป็น 64 บิตคุณจะต้องใช้ InstallUtil.exe เป็น 64 บิตซึ่งสามารถพบได้ที่นี่: C: \ windows \ Microsoft.NET \ Framework64 \ ... รุ่น 32 บิต (C: \ windows \ Microsoft.NET \ Framework) จะโยน BadImageFormatException ที่คุณ ...
snytek

วิธีนี้ใช้งานได้ดีมากโปรดทราบว่าตามที่ @snytek แจ้งว่าหากคุณใช้ฐาน 64 ตรวจสอบให้แน่ใจว่าคุณใช้ไดเรกทอรีที่ถูกต้อง นอกจากนี้หากคุณทำเช่นเดียวกันกับฉันและลืมเปลี่ยนชื่อบริการเป็นอื่นนอกเหนือจาก "MyService" ตรวจสอบให้แน่ใจว่าคุณได้ถอนการติดตั้งบริการก่อนทำการเปลี่ยนแปลงรหัส
dmoore1181

3

ฉันได้ยินจุดของคุณที่ต้องการให้แอสเซมบลีหนึ่งหยุดการทำซ้ำรหัส แต่มันจะง่ายที่สุดและลดการทำซ้ำรหัสและทำให้ง่ายต่อการใช้รหัสของคุณในวิธีอื่น ๆ ในอนาคตถ้า ...... คุณแบ่งออกเป็น 3 แอสเซมบลี

  1. แอสเซมบลีไลบรารีหนึ่งที่ทำงานทั้งหมด จากนั้นมีโครงการที่เรียบง่าย / เรียบง่ายสองโครงการ:
  2. หนึ่งซึ่งเป็น commandline
  3. หนึ่งซึ่งเป็นบริการ windows

1
นี่คือวิธีที่ผมเคยทำมันสำหรับปี - บริการสวยมากมีStart()และStop()วิธีการและแอพพลิเคคอนโซลมีห่วง ไม่ใช้เฟรมเวิร์กอย่างTopShelfนี่เป็นตัวเลือกที่ดีที่สุด
พื้นฐาน

เห็นด้วยกับคำตอบนั้นมากที่สุด การใช้เครื่องมือของบุคคลที่สามสำหรับการแก้ปัญหาอย่างง่ายทำให้การบำรุงรักษาในอนาคตไม่ซับซ้อนโดยไม่ต้องยุ่งยาก
tatigo

3

นี่เป็นวิธีที่ใหม่กว่าของวิธีเปลี่ยนแอปพลิเคชันคอนโซลเป็นบริการ Windows เป็นบริการผู้ปฏิบัติงานตาม. Net Core 3.1ล่าสุด

หากคุณสร้าง Worker Service จาก Visual Studio 2019 จะให้เกือบทุกอย่างที่คุณต้องการสำหรับการสร้างบริการ Windows นอกกรอบซึ่งเป็นสิ่งที่คุณต้องการเปลี่ยนเป็นแอปพลิเคชันคอนโซลเพื่อแปลงเป็นบริการของ Windows

นี่คือการเปลี่ยนแปลงที่คุณต้องทำ:

ติดตั้งแพ็คเกจ NuGet ต่อไปนี้

Install-Package Microsoft.Extensions.Hosting.WindowsServices -Version 3.1.0
Install-Package Microsoft.Extensions.Configuration.Abstractions -Version 3.1.0

เปลี่ยน Program.cs เพื่อให้มีการใช้งานดังนี้:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace ConsoleApp
{
    class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).UseWindowsService().Build().Run();
        }

        private static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<Worker>();
                });
    }
}

และเพิ่ม Worker.cs โดยที่คุณจะใส่รหัสที่จะเรียกใช้โดยการดำเนินการบริการ:

using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp
{
    public class Worker : BackgroundService
    {
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            //do some operation
        }

        public override Task StartAsync(CancellationToken cancellationToken)
        {
            return base.StartAsync(cancellationToken);
        }

        public override Task StopAsync(CancellationToken cancellationToken)
        {
            return base.StopAsync(cancellationToken);
        }
    }
}

เมื่อทุกอย่างพร้อมใช้งานและแอปพลิเคชันได้สร้างเสร็จเรียบร้อยแล้วคุณสามารถใช้sc.exeเพื่อติดตั้งคอนโซลแอปพลิเคชัน exe เป็นบริการ Windows ด้วยคำสั่งต่อไปนี้:

sc.exe create DemoService binpath= "path/to/your/file.exe"

2

คุณสามารถใช้ได้

reg add HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run /v ServiceName /d "c:\path\to\service\file\exe"

และจะปรากฏขึ้นภายในรายการบริการ ฉันไม่รู้ว่าทำงานได้ถูกต้องหรือไม่ บริการมักจะต้องฟังเหตุการณ์ต่าง ๆ

มี wrapper บริการหลายอย่างที่สามารถเรียกใช้แอปพลิเคชันใด ๆ เป็นบริการจริง ตัวอย่าง Microsofts SrvAnyจากKit ทรัพยากร Win2003


ดังที่คุณกล่าวไว้ exe บริการจะต้องสื่อสารกับ windows +1 สำหรับลิงก์ไปยัง SrvAny
Jodrell

5
ฉันคิดว่าวิธีนี้ไม่ปลอดภัย Windows มีไลบรารีและยูทิลิตี้พิเศษเพื่อจัดการบริการและมีแนวโน้มที่จะทำงานได้อย่างต่อเนื่องในระบบปฏิบัติการและสภาพแวดล้อมที่แตกต่างกัน สำหรับแอป. NET การสร้างตัวติดตั้ง MSI ใน VS นั้นค่อนข้างง่าย นอกจากนี้ยังเป็นไปได้ที่จะทำการติดตั้งแบบ progrmmatically โดยใช้วิธี ManagedInstallerClass.InstallHelper
VladV

1
ไม่จำเป็นต้องติดตั้งและสิ่งต่าง ๆ : เพียงแค่ใช้บรรทัดคำสั่งนี้: sc สร้าง MyServiceName binPath = "c: \ path \ to \ service \ file \ exe"
JDC

2

ประการแรกฉันฝังโซลูชันแอปพลิเคชันคอนโซลลงในโซลูชันบริการ windows และอ้างอิง

จากนั้นฉันทำให้แอปพลิเคชันคอนโซลคลาสโปรแกรมเป็นแบบสาธารณะ

/// <summary>
/// Hybrid service/console application
/// </summary>
public class Program
{
}

ฉันจะสร้างสองฟังก์ชันภายในแอปพลิเคชันคอนโซล

    /// <summary>
    /// Used to start as a service
    /// </summary>
    public void Start()
    {
        Main();
    }

    /// <summary>
    /// Used to stop the service
    /// </summary>
    public void Stop()
    {
       if (Application.MessageLoop)
            Application.Exit();   //windows app
        else
            Environment.Exit(1);  //console app
    }

จากนั้นในบริการ windows เองฉันสร้างอินสแตนซ์ของโปรแกรมและเรียกใช้ฟังก์ชัน Start และ Stop ที่เพิ่มไว้ใน OnStart และ OnStop ดูด้านล่าง

class WinService : ServiceBase
{
    readonly Program _application = new Program();

    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    static void Main()
    {
        ServiceBase[] servicesToRun = { new WinService() };
        Run(servicesToRun);
    }

    /// <summary>
    /// Set things in motion so your service can do its work.
    /// </summary>
    protected override void OnStart(string[] args)
    {
        Thread thread = new Thread(() => _application.Start());
        thread.Start();
    }

    /// <summary>
    /// Stop this service.
    /// </summary>
    protected override void OnStop()
    {
        Thread thread = new Thread(() => _application.Stop());
        thread.Start();
    }
}

วิธีนี้ยังสามารถใช้สำหรับไฮบริดแอปพลิเคชัน windows / windows service hybrid


นี่เป็นสิ่งที่ JonAlb ได้กล่าวไว้ในคำตอบก่อนหน้า แต่ขอบคุณสำหรับตัวอย่างโค้ด
tatigo

0

บางทีคุณควรกำหนดสิ่งที่คุณต้องการเท่าที่ฉันรู้คุณไม่สามารถเรียกใช้แอปของคุณในฐานะคอนโซลหรือบริการด้วยบรรทัดคำสั่งในเวลาเดียวกัน โปรดจำไว้ว่าบริการได้รับการติดตั้งและคุณต้องเริ่มบริการในตัวจัดการบริการคุณสามารถสร้างแอปพลิเคชันใหม่ซึ่งเริ่มบริการหรือเริ่มกระบวนการใหม่ที่เรียกใช้แอปคอนโซลของคุณ แต่ตามที่คุณเขียน

"เก็บแอปพลิเคชันคอนโซลเป็นหนึ่งโครงการ"

เมื่อฉันอยู่ในตำแหน่งของคุณเปลี่ยนแอปพลิเคชันคอนโซลเป็นบริการ ก่อนอื่นคุณต้องใช้เทมเพลตในกรณีที่คุณใช้งาน VS Express Edition นี่คือลิงค์ที่คุณสามารถทำตามขั้นตอนแรกของคุณ: บริการC # Windowsนี้มีประโยชน์มากสำหรับฉัน จากนั้นใช้เทมเพลตนั้นเพิ่มรหัสของคุณไปยังกิจกรรมที่ต้องการของบริการ

เพื่อปรับปรุงการบริการของคุณมีสิ่งอื่นที่คุณสามารถทำได้ แต่สิ่งนี้ไม่รวดเร็วและ / หรือง่ายดายใช้ appdomains และสร้าง DLLs เพื่อโหลด / ไม่โหลด ในหนึ่งคุณสามารถเริ่มกระบวนการใหม่ด้วยแอปคอนโซลและใน dll อื่นคุณเพียงแค่ใส่ฟังก์ชั่นที่บริการต้องทำ

โชคดี.


0

คุณต้องแยกหน้าที่การใช้งานเป็นคลาสหรือคลาสและเปิดใช้งานผ่านหนึ่งในสองสตับ ตอคอนโซลหรือส่วนบริการ

เป็นธรรมดาที่จะเห็นเมื่อใช้ windows บริการ myriad ที่ประกอบโครงสร้างพื้นฐานไม่ (และไม่สามารถโดยตรง) นำเสนอ windows console ให้กับผู้ใช้ บริการต้องสื่อสารกับผู้ใช้ในรูปแบบที่ไม่ใช่กราฟิก: ผ่าน SCM; ในบันทึกเหตุการณ์ไปยังไฟล์บันทึก ฯลฯ บริการจะต้องสื่อสารกับ windows ผ่าน SCM ไม่เช่นนั้นจะหยุดทำงาน

เห็นได้ชัดว่ามันจะเป็นที่ยอมรับได้ที่จะมีแอพคอนโซลที่สามารถสื่อสารกับบริการได้ แต่บริการจำเป็นต้องทำงานอย่างอิสระโดยไม่ต้องมีการโต้ตอบกับ GUI

ส่วนต้นของ Console มีประโยชน์อย่างมากสำหรับการแก้ไขข้อบกพร่องของพฤติกรรมการบริการ แต่ไม่ควรใช้ในสภาพแวดล้อม "ที่ได้รับการผลิต" ซึ่งวัตถุประสงค์ทั้งหมดคือการสร้างบริการ

ฉันยังไม่ได้อ่านอย่างเต็มที่ แต่บทความนี้ดูเหมือนจะไพน์ไปในทิศทางที่ถูกต้อง


0

ฉันใช้คลาสบริการที่เป็นไปตามรูปแบบมาตรฐานที่กำหนดโดยServiceBaseและช่วยให้การดีบัก F5 ง่ายขึ้น ซึ่งจะเก็บข้อมูลบริการที่กำหนดไว้ในบริการทำให้ง่ายต่อการค้นหาและอายุการใช้งานที่ง่ายต่อการจัดการ

ปกติฉันจะสร้างแอพพลิเคชั่น Windows ด้วยโครงสร้างด้านล่าง ฉันไม่ได้สร้างแอปพลิเคชันคอนโซล ด้วยวิธีนี้ฉันจะไม่ได้รับกล่องดำขนาดใหญ่โผล่เข้ามาในหน้าของฉันทุกครั้งที่เรียกใช้แอพ ฉันอยู่ในดีบักเกอร์ซึ่งการกระทำทั้งหมดอยู่ ฉันใช้Debug.WriteLineเพื่อให้ข้อความไปที่หน้าต่างผลลัพธ์ซึ่งท่าเทียบเรือเป็นอย่างดีและยังคงมองเห็นได้หลังจากที่แอปสิ้นสุดลง

ฉันมักจะไม่รำคาญเพิ่มรหัส debug สำหรับการหยุด; ฉันใช้ตัวดีบักแทน ถ้าฉันไม่จำเป็นต้องแก้ปัญหาการหยุดผมทำโครงการแอปคอนโซลเพิ่มวิธีการส่งและเรียกมันว่าหลังจากที่โทรไปStopConsole.ReadKey

public class Service : ServiceBase
{
    protected override void OnStart(string[] args)
    {
        // Start logic here.
    }

    protected override void OnStop()
    {
        // Stop logic here.
    }

    static void Main(string[] args)
    {
        using (var service = new Service()) {
            if (Environment.UserInteractive) {
                service.Start();
                Thread.Sleep(Timeout.Infinite);
            } else
                Run(service);
        }
    }
    public void Start() => OnStart(null);
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.